Merge branch 'api-rewrite' into '3-1-0'

BETA API v1

See merge request !55
merge-requests/56/head
Mike Koch 7 years ago
commit e21c26689a

7
.gitignore vendored

@ -1,3 +1,8 @@
# Mods for HESK-specific files
api/vendor
api/Tests/integration_test_mfh_settings.php
# HESK Files
admin/admin_suggest_articles.php
admin/archive.php
admin/custom_statuses.php
@ -265,7 +270,7 @@ readme.html
robots.txt
.idea/
attachments/__latest.txt
attachments
/attachments
img/ban.png
img/banned.png
img/ico_tools.png

@ -97,6 +97,7 @@ require_once(HESK_PATH . 'inc/show_admin_nav.inc.php');
<th><?php echo $hesklang['user']; ?></th>
<th><?php echo $hesklang['custom_place']; ?></th>
<th><?php echo $hesklang['message']; ?></th>
<th><?php echo $hesklang['stack_trace_header']; ?></th>
</tr>
</thead>
<tbody></tbody>

@ -0,0 +1,133 @@
<?php
// Responsible for loading in all necessary classes. AKA a poor man's DI solution.
use BusinessLogic\Attachments\AttachmentHandler;
use BusinessLogic\Attachments\AttachmentRetriever;
use BusinessLogic\Categories\CategoryRetriever;
use BusinessLogic\Emails\BasicEmailSender;
use BusinessLogic\Emails\EmailSenderHelper;
use BusinessLogic\Emails\EmailTemplateParser;
use BusinessLogic\Emails\EmailTemplateRetriever;
use BusinessLogic\Emails\MailgunEmailSender;
use BusinessLogic\Security\BanRetriever;
use BusinessLogic\Security\UserContextBuilder;
use BusinessLogic\Security\UserToTicketChecker;
use BusinessLogic\Settings\ApiChecker;
use BusinessLogic\Settings\SettingsRetriever;
use BusinessLogic\Statuses\StatusRetriever;
use BusinessLogic\Tickets\Autoassigner;
use BusinessLogic\Tickets\TicketDeleter;
use BusinessLogic\Tickets\TicketEditor;
use BusinessLogic\Tickets\TicketRetriever;
use BusinessLogic\Tickets\TicketCreator;
use BusinessLogic\Tickets\NewTicketValidator;
use BusinessLogic\Tickets\TicketValidators;
use BusinessLogic\Tickets\TrackingIdGenerator;
use BusinessLogic\Tickets\VerifiedEmailChecker;
use DataAccess\Attachments\AttachmentGateway;
use DataAccess\Categories\CategoryGateway;
use DataAccess\Files\FileDeleter;
use DataAccess\Files\FileReader;
use DataAccess\Files\FileWriter;
use DataAccess\Logging\LoggingGateway;
use DataAccess\Security\BanGateway;
use DataAccess\Security\UserGateway;
use DataAccess\Settings\ModsForHeskSettingsGateway;
use DataAccess\Statuses\StatusGateway;
use DataAccess\Tickets\TicketGateway;
use DataAccess\Tickets\VerifiedEmailGateway;
class ApplicationContext {
public $get;
function __construct() {
$this->get = array();
// Settings
$this->get[ModsForHeskSettingsGateway::class] = new ModsForHeskSettingsGateway();
// API Checker
$this->get[ApiChecker::class] = new ApiChecker($this->get[ModsForHeskSettingsGateway::class]);
// Logging
$this->get[LoggingGateway::class] = new LoggingGateway();
// Verified Email Checker
$this->get[VerifiedEmailGateway::class] = new VerifiedEmailGateway();
$this->get[VerifiedEmailChecker::class] = new VerifiedEmailChecker($this->get[VerifiedEmailGateway::class]);
// Users
$this->get[UserGateway::class] = new UserGateway();
$this->get[UserContextBuilder::class] = new UserContextBuilder($this->get[UserGateway::class]);
// Categories
$this->get[CategoryGateway::class] = new CategoryGateway();
$this->get[CategoryRetriever::class] = new CategoryRetriever($this->get[CategoryGateway::class]);
// Bans
$this->get[BanGateway::class] = new BanGateway();
$this->get[BanRetriever::class] = new BanRetriever($this->get[BanGateway::class]);
// Statuses
$this->get[StatusGateway::class] = new StatusGateway();
// Email Sender
$this->get[EmailTemplateRetriever::class] = new EmailTemplateRetriever();
$this->get[EmailTemplateParser::class] = new EmailTemplateParser($this->get[StatusGateway::class],
$this->get[CategoryGateway::class],
$this->get[UserGateway::class],
$this->get[EmailTemplateRetriever::class]);
$this->get[BasicEmailSender::class] = new BasicEmailSender();
$this->get[MailgunEmailSender::class] = new MailgunEmailSender();
$this->get[EmailSenderHelper::class] = new EmailSenderHelper($this->get[EmailTemplateParser::class],
$this->get[BasicEmailSender::class],
$this->get[MailgunEmailSender::class]);
// Tickets
$this->get[UserToTicketChecker::class] = new UserToTicketChecker($this->get[UserGateway::class]);
$this->get[TicketGateway::class] = new TicketGateway();
$this->get[TicketRetriever::class] = new TicketRetriever($this->get[TicketGateway::class],
$this->get[UserToTicketChecker::class]);
$this->get[TicketValidators::class] = new TicketValidators($this->get[TicketGateway::class]);
$this->get[TrackingIdGenerator::class] = new TrackingIdGenerator($this->get[TicketGateway::class]);
$this->get[Autoassigner::class] = new Autoassigner($this->get[CategoryGateway::class], $this->get[UserGateway::class]);
$this->get[NewTicketValidator::class] = new NewTicketValidator($this->get[CategoryRetriever::class],
$this->get[BanRetriever::class],
$this->get[TicketValidators::class]);
$this->get[TicketCreator::class] = new TicketCreator($this->get[NewTicketValidator::class],
$this->get[TrackingIdGenerator::class],
$this->get[Autoassigner::class],
$this->get[StatusGateway::class],
$this->get[TicketGateway::class],
$this->get[VerifiedEmailChecker::class],
$this->get[EmailSenderHelper::class],
$this->get[UserGateway::class],
$this->get[ModsForHeskSettingsGateway::class]);
$this->get[FileWriter::class] = new FileWriter();
$this->get[FileReader::class] = new FileReader();
$this->get[FileDeleter::class] = new FileDeleter();
$this->get[AttachmentGateway::class] = new AttachmentGateway();
$this->get[AttachmentHandler::class] = new AttachmentHandler($this->get[TicketGateway::class],
$this->get[AttachmentGateway::class],
$this->get[FileWriter::class],
$this->get[UserToTicketChecker::class],
$this->get[FileDeleter::class]);
$this->get[AttachmentRetriever::class] = new AttachmentRetriever($this->get[AttachmentGateway::class],
$this->get[FileReader::class],
$this->get[TicketGateway::class],
$this->get[UserToTicketChecker::class]);
$this->get[TicketDeleter::class] =
new TicketDeleter($this->get[TicketGateway::class],
$this->get[UserToTicketChecker::class],
$this->get[AttachmentHandler::class]);
$this->get[TicketEditor::class] =
new TicketEditor($this->get[TicketGateway::class], $this->get[UserToTicketChecker::class]);
// Statuses
$this->get[StatusRetriever::class] = new StatusRetriever($this->get[StatusGateway::class]);
// Settings
$this->get[SettingsRetriever::class] = new SettingsRetriever($this->get[ModsForHeskSettingsGateway::class]);
}
}

@ -0,0 +1,21 @@
<?php
namespace BusinessLogic\Attachments;
class Attachment {
/* @var $id int */
public $id;
/* @var $savedName string */
public $savedName;
/* @var $displayName string */
public $displayName;
/* @var $id int */
public $fileSize;
/* @var $downloadCount int */
public $downloadCount;
}

@ -0,0 +1,475 @@
<?php
namespace BusinessLogic\Attachments;
use BusinessLogic\Exceptions\AccessViolationException;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Exceptions\ValidationException;
use BusinessLogic\Security\UserContext;
use BusinessLogic\Security\UserPrivilege;
use BusinessLogic\Security\UserToTicketChecker;
use BusinessLogic\Tickets\Attachment;
use BusinessLogic\Tickets\Ticket;
use BusinessLogic\ValidationModel;
use DataAccess\Attachments\AttachmentGateway;
use DataAccess\Files\FileDeleter;
use DataAccess\Files\FileWriter;
use DataAccess\Tickets\TicketGateway;
class AttachmentHandler {
/* @var $ticketGateway TicketGateway */
private $ticketGateway;
/* @var $attachmentGateway AttachmentGateway */
private $attachmentGateway;
/* @var $fileWriter FileWriter */
private $fileWriter;
/* @var $fileDeleter FileDeleter */
private $fileDeleter;
/* @var $userToTicketChecker UserToTicketChecker */
private $userToTicketChecker;
function __construct($ticketGateway, $attachmentGateway, $fileWriter, $userToTicketChecker, $fileDeleter) {
$this->ticketGateway = $ticketGateway;
$this->attachmentGateway = $attachmentGateway;
$this->fileWriter = $fileWriter;
$this->userToTicketChecker = $userToTicketChecker;
$this->fileDeleter = $fileDeleter;
}
/**
* @param $createAttachmentModel CreateAttachmentForTicketModel
* @param $userContext UserContext
* @param $heskSettings array
* @return TicketAttachment the newly created attachment
* @throws \Exception
*/
function createAttachmentForTicket($createAttachmentModel, $userContext, $heskSettings) {
$this->validate($createAttachmentModel, $heskSettings);
$decodedAttachment = base64_decode($createAttachmentModel->attachmentContents);
$ticket = $this->ticketGateway->getTicketById($createAttachmentModel->ticketId, $heskSettings);
if ($ticket === null) {
throw new ApiFriendlyException("Ticket {$createAttachmentModel->ticketId} not found", "Ticket Not Found", 404);
}
$extraPermissions = $createAttachmentModel->isEditing
? array(UserPrivilege::CAN_EDIT_TICKETS)
: array();
if (!$this->userToTicketChecker->isTicketAccessibleToUser($userContext, $ticket, $heskSettings, $extraPermissions)) {
throw new AccessViolationException("User does not have access to ticket {$ticket->id} being created / edited!");
}
$cleanedFileName = $this->cleanFileName($createAttachmentModel->displayName);
$fileParts = pathinfo($cleanedFileName);
$ticketAttachment = new TicketAttachment();
$ticketAttachment->savedName = $this->generateSavedName($ticket->trackingId,
$cleanedFileName, $fileParts['extension']);
$ticketAttachment->displayName = $cleanedFileName;
$ticketAttachment->ticketTrackingId = $ticket->trackingId;
$ticketAttachment->type = 0;
$ticketAttachment->downloadCount = 0;
$ticketAttachment->fileSize =
$this->fileWriter->writeToFile($ticketAttachment->savedName, $heskSettings['attach_dir'], $decodedAttachment);
$attachmentId = $this->attachmentGateway->createAttachmentForTicket($ticketAttachment, $heskSettings);
$this->updateAttachmentsOnTicket($ticket, $ticketAttachment, $attachmentId, $heskSettings);
$ticketAttachment->id = $attachmentId;
return $ticketAttachment;
}
/**
* Supports deleting attachments from both ticket messages AND replies
*
* @param $ticketId int The ticket ID
* @param $attachmentId int The attachment ID
* @param $userContext UserContext
* @param $heskSettings array
* @throws ApiFriendlyException
* @throws \Exception
*/
function deleteAttachmentFromTicket($ticketId, $attachmentId, $userContext, $heskSettings) {
$ticket = $this->ticketGateway->getTicketById($ticketId, $heskSettings);
if ($ticket === null) {
throw new ApiFriendlyException("Ticket {$ticketId} not found!", "Ticket Not Found", 404);
}
if (!$this->userToTicketChecker->isTicketAccessibleToUser($userContext, $ticket, $heskSettings, array(UserPrivilege::CAN_EDIT_TICKETS))) {
throw new AccessViolationException("User does not have access to ticket {$ticketId} being created / edited!");
}
$indexToRemove = -1;
$attachmentType = AttachmentType::MESSAGE;
$replyId = -1;
for ($i = 0; $i < count($ticket->attachments); $i++) {
$attachment = $ticket->attachments[$i];
if ($attachment->id === $attachmentId) {
$indexToRemove = $i;
$this->fileDeleter->deleteFile($attachment->savedName, $heskSettings['attach_dir']);
$this->attachmentGateway->deleteAttachment($attachment->id, $heskSettings);
}
}
foreach ($ticket->replies as $reply) {
for ($i = 0; $i < count($reply->attachments); $i++) {
$attachment = $reply->attachments[$i];
if ($attachment->id === $attachmentId) {
$indexToRemove = $i;
$replyId = $reply->id;
$attachmentType = AttachmentType::REPLY;
$this->fileDeleter->deleteFile($attachment->savedName, $heskSettings['attach_dir']);
$this->attachmentGateway->deleteAttachment($attachment->id, $heskSettings);
}
}
}
if ($indexToRemove === -1) {
throw new ApiFriendlyException("Attachment not found for ticket or reply! ID: {$attachmentId}", "Attachment not found", 404);
}
if ($attachmentType == AttachmentType::MESSAGE) {
$attachments = $ticket->attachments;
unset($attachments[$indexToRemove]);
$this->ticketGateway->updateAttachmentsForTicket($ticketId, $attachments, $heskSettings);
} else {
$attachments = $ticket->replies[$replyId]->attachments;
unset($attachments[$indexToRemove]);
$this->ticketGateway->updateAttachmentsForReply($replyId, $attachments, $heskSettings);
}
}
/**
* @param $createAttachmentModel CreateAttachmentForTicketModel
* @param $heskSettings array
* @throws ValidationException
*/
private function validate($createAttachmentModel, $heskSettings) {
$errorKeys = array();
if ($createAttachmentModel->attachmentContents === null ||
trim($createAttachmentModel->attachmentContents) === '') {
$errorKeys[] = 'CONTENTS_EMPTY';
}
if (base64_decode($createAttachmentModel->attachmentContents, true) === false) {
$errorKeys[] = 'CONTENTS_NOT_BASE_64';
}
if ($createAttachmentModel->displayName === null ||
trim($createAttachmentModel->displayName === '')) {
$errorKeys[] = 'DISPLAY_NAME_EMPTY';
}
if ($createAttachmentModel->ticketId === null ||
$createAttachmentModel->ticketId < 1) {
$errorKeys[] = 'TICKET_ID_MISSING';
}
$fileParts = pathinfo($createAttachmentModel->displayName);
if (!isset($fileParts['extension']) || !in_array(".{$fileParts['extension']}", $heskSettings['attachments']['allowed_types'])) {
$errorKeys[] = 'EXTENSION_NOT_PERMITTED';
}
$fileContents = base64_decode($createAttachmentModel->attachmentContents);
if (function_exists('mb_strlen')) {
$fileSize = mb_strlen($fileContents, '8bit');
} else {
$fileSize = strlen($fileContents);
}
if ($fileSize > $heskSettings['attachments']['max_size']) {
$errorKeys[] = 'FILE_SIZE_TOO_LARGE';
}
if (count($errorKeys) > 0) {
$validationModel = new ValidationModel();
$validationModel->errorKeys = $errorKeys;
throw new ValidationException($validationModel);
}
}
private function generateSavedName($trackingId, $displayName, $fileExtension) {
$fileExtension = ".{$fileExtension}";
$useChars = 'AEUYBDGHJLMNPQRSTVWXZ123456789';
$tmp = uniqid();
for ($j = 1; $j < 10; $j++) {
$tmp .= $useChars{mt_rand(0, 29)};
}
return substr($trackingId . '_' . md5($tmp . $displayName), 0, 200) . $fileExtension;
}
/**
* @param $displayName string original file name
* @return string The cleaned file name
*/
private function cleanFileName($displayName) {
$filename = str_replace(array('%20', '+'), '-', $displayName);
$filename = preg_replace('/[\s-]+/', '-', $filename);
$filename = $this->removeAccents($filename);
$filename = preg_replace('/[^A-Za-z0-9\.\-_]/', '', $filename);
$filename = trim($filename, '-_');
return $filename;
}
// The following code has been borrowed from Wordpress, and also from posting_functions.inc.php :P
// Credits: http://wordpress.org
private function removeAccents($string)
{
if (!preg_match('/[\x80-\xff]/', $string)) {
return $string;
}
if ($this->seemsUtf8($string)) {
$chars = array(
// Decompositions for Latin-1 Supplement
chr(194) . chr(170) => 'a', chr(194) . chr(186) => 'o',
chr(195) . chr(128) => 'A', chr(195) . chr(129) => 'A',
chr(195) . chr(130) => 'A', chr(195) . chr(131) => 'A',
chr(195) . chr(132) => 'A', chr(195) . chr(133) => 'A',
chr(195) . chr(134) => 'AE', chr(195) . chr(135) => 'C',
chr(195) . chr(136) => 'E', chr(195) . chr(137) => 'E',
chr(195) . chr(138) => 'E', chr(195) . chr(139) => 'E',
chr(195) . chr(140) => 'I', chr(195) . chr(141) => 'I',
chr(195) . chr(142) => 'I', chr(195) . chr(143) => 'I',
chr(195) . chr(144) => 'D', chr(195) . chr(145) => 'N',
chr(195) . chr(146) => 'O', chr(195) . chr(147) => 'O',
chr(195) . chr(148) => 'O', chr(195) . chr(149) => 'O',
chr(195) . chr(150) => 'O', chr(195) . chr(153) => 'U',
chr(195) . chr(154) => 'U', chr(195) . chr(155) => 'U',
chr(195) . chr(156) => 'U', chr(195) . chr(157) => 'Y',
chr(195) . chr(158) => 'TH', chr(195) . chr(159) => 's',
chr(195) . chr(160) => 'a', chr(195) . chr(161) => 'a',
chr(195) . chr(162) => 'a', chr(195) . chr(163) => 'a',
chr(195) . chr(164) => 'a', chr(195) . chr(165) => 'a',
chr(195) . chr(166) => 'ae', chr(195) . chr(167) => 'c',
chr(195) . chr(168) => 'e', chr(195) . chr(169) => 'e',
chr(195) . chr(170) => 'e', chr(195) . chr(171) => 'e',
chr(195) . chr(172) => 'i', chr(195) . chr(173) => 'i',
chr(195) . chr(174) => 'i', chr(195) . chr(175) => 'i',
chr(195) . chr(176) => 'd', chr(195) . chr(177) => 'n',
chr(195) . chr(178) => 'o', chr(195) . chr(179) => 'o',
chr(195) . chr(180) => 'o', chr(195) . chr(181) => 'o',
chr(195) . chr(182) => 'o', chr(195) . chr(184) => 'o',
chr(195) . chr(185) => 'u', chr(195) . chr(186) => 'u',
chr(195) . chr(187) => 'u', chr(195) . chr(188) => 'u',
chr(195) . chr(189) => 'y', chr(195) . chr(190) => 'th',
chr(195) . chr(191) => 'y', chr(195) . chr(152) => 'O',
// Decompositions for Latin Extended-A
chr(196) . chr(128) => 'A', chr(196) . chr(129) => 'a',
chr(196) . chr(130) => 'A', chr(196) . chr(131) => 'a',
chr(196) . chr(132) => 'A', chr(196) . chr(133) => 'a',
chr(196) . chr(134) => 'C', chr(196) . chr(135) => 'c',
chr(196) . chr(136) => 'C', chr(196) . chr(137) => 'c',
chr(196) . chr(138) => 'C', chr(196) . chr(139) => 'c',
chr(196) . chr(140) => 'C', chr(196) . chr(141) => 'c',
chr(196) . chr(142) => 'D', chr(196) . chr(143) => 'd',
chr(196) . chr(144) => 'D', chr(196) . chr(145) => 'd',
chr(196) . chr(146) => 'E', chr(196) . chr(147) => 'e',
chr(196) . chr(148) => 'E', chr(196) . chr(149) => 'e',
chr(196) . chr(150) => 'E', chr(196) . chr(151) => 'e',
chr(196) . chr(152) => 'E', chr(196) . chr(153) => 'e',
chr(196) . chr(154) => 'E', chr(196) . chr(155) => 'e',
chr(196) . chr(156) => 'G', chr(196) . chr(157) => 'g',
chr(196) . chr(158) => 'G', chr(196) . chr(159) => 'g',
chr(196) . chr(160) => 'G', chr(196) . chr(161) => 'g',
chr(196) . chr(162) => 'G', chr(196) . chr(163) => 'g',
chr(196) . chr(164) => 'H', chr(196) . chr(165) => 'h',
chr(196) . chr(166) => 'H', chr(196) . chr(167) => 'h',
chr(196) . chr(168) => 'I', chr(196) . chr(169) => 'i',
chr(196) . chr(170) => 'I', chr(196) . chr(171) => 'i',
chr(196) . chr(172) => 'I', chr(196) . chr(173) => 'i',
chr(196) . chr(174) => 'I', chr(196) . chr(175) => 'i',
chr(196) . chr(176) => 'I', chr(196) . chr(177) => 'i',
chr(196) . chr(178) => 'IJ', chr(196) . chr(179) => 'ij',
chr(196) . chr(180) => 'J', chr(196) . chr(181) => 'j',
chr(196) . chr(182) => 'K', chr(196) . chr(183) => 'k',
chr(196) . chr(184) => 'k', chr(196) . chr(185) => 'L',
chr(196) . chr(186) => 'l', chr(196) . chr(187) => 'L',
chr(196) . chr(188) => 'l', chr(196) . chr(189) => 'L',
chr(196) . chr(190) => 'l', chr(196) . chr(191) => 'L',
chr(197) . chr(128) => 'l', chr(197) . chr(129) => 'L',
chr(197) . chr(130) => 'l', chr(197) . chr(131) => 'N',
chr(197) . chr(132) => 'n', chr(197) . chr(133) => 'N',
chr(197) . chr(134) => 'n', chr(197) . chr(135) => 'N',
chr(197) . chr(136) => 'n', chr(197) . chr(137) => 'N',
chr(197) . chr(138) => 'n', chr(197) . chr(139) => 'N',
chr(197) . chr(140) => 'O', chr(197) . chr(141) => 'o',
chr(197) . chr(142) => 'O', chr(197) . chr(143) => 'o',
chr(197) . chr(144) => 'O', chr(197) . chr(145) => 'o',
chr(197) . chr(146) => 'OE', chr(197) . chr(147) => 'oe',
chr(197) . chr(148) => 'R', chr(197) . chr(149) => 'r',
chr(197) . chr(150) => 'R', chr(197) . chr(151) => 'r',
chr(197) . chr(152) => 'R', chr(197) . chr(153) => 'r',
chr(197) . chr(154) => 'S', chr(197) . chr(155) => 's',
chr(197) . chr(156) => 'S', chr(197) . chr(157) => 's',
chr(197) . chr(158) => 'S', chr(197) . chr(159) => 's',
chr(197) . chr(160) => 'S', chr(197) . chr(161) => 's',
chr(197) . chr(162) => 'T', chr(197) . chr(163) => 't',
chr(197) . chr(164) => 'T', chr(197) . chr(165) => 't',
chr(197) . chr(166) => 'T', chr(197) . chr(167) => 't',
chr(197) . chr(168) => 'U', chr(197) . chr(169) => 'u',
chr(197) . chr(170) => 'U', chr(197) . chr(171) => 'u',
chr(197) . chr(172) => 'U', chr(197) . chr(173) => 'u',
chr(197) . chr(174) => 'U', chr(197) . chr(175) => 'u',
chr(197) . chr(176) => 'U', chr(197) . chr(177) => 'u',
chr(197) . chr(178) => 'U', chr(197) . chr(179) => 'u',
chr(197) . chr(180) => 'W', chr(197) . chr(181) => 'w',
chr(197) . chr(182) => 'Y', chr(197) . chr(183) => 'y',
chr(197) . chr(184) => 'Y', chr(197) . chr(185) => 'Z',
chr(197) . chr(186) => 'z', chr(197) . chr(187) => 'Z',
chr(197) . chr(188) => 'z', chr(197) . chr(189) => 'Z',
chr(197) . chr(190) => 'z', chr(197) . chr(191) => 's',
// Decompositions for Latin Extended-B
chr(200) . chr(152) => 'S', chr(200) . chr(153) => 's',
chr(200) . chr(154) => 'T', chr(200) . chr(155) => 't',
// Euro Sign
chr(226) . chr(130) . chr(172) => 'E',
// GBP (Pound) Sign
chr(194) . chr(163) => '',
// Vowels with diacritic (Vietnamese)
// unmarked
chr(198) . chr(160) => 'O', chr(198) . chr(161) => 'o',
chr(198) . chr(175) => 'U', chr(198) . chr(176) => 'u',
// grave accent
chr(225) . chr(186) . chr(166) => 'A', chr(225) . chr(186) . chr(167) => 'a',
chr(225) . chr(186) . chr(176) => 'A', chr(225) . chr(186) . chr(177) => 'a',
chr(225) . chr(187) . chr(128) => 'E', chr(225) . chr(187) . chr(129) => 'e',
chr(225) . chr(187) . chr(146) => 'O', chr(225) . chr(187) . chr(147) => 'o',
chr(225) . chr(187) . chr(156) => 'O', chr(225) . chr(187) . chr(157) => 'o',
chr(225) . chr(187) . chr(170) => 'U', chr(225) . chr(187) . chr(171) => 'u',
chr(225) . chr(187) . chr(178) => 'Y', chr(225) . chr(187) . chr(179) => 'y',
// hook
chr(225) . chr(186) . chr(162) => 'A', chr(225) . chr(186) . chr(163) => 'a',
chr(225) . chr(186) . chr(168) => 'A', chr(225) . chr(186) . chr(169) => 'a',
chr(225) . chr(186) . chr(178) => 'A', chr(225) . chr(186) . chr(179) => 'a',
chr(225) . chr(186) . chr(186) => 'E', chr(225) . chr(186) . chr(187) => 'e',
chr(225) . chr(187) . chr(130) => 'E', chr(225) . chr(187) . chr(131) => 'e',
chr(225) . chr(187) . chr(136) => 'I', chr(225) . chr(187) . chr(137) => 'i',
chr(225) . chr(187) . chr(142) => 'O', chr(225) . chr(187) . chr(143) => 'o',
chr(225) . chr(187) . chr(148) => 'O', chr(225) . chr(187) . chr(149) => 'o',
chr(225) . chr(187) . chr(158) => 'O', chr(225) . chr(187) . chr(159) => 'o',
chr(225) . chr(187) . chr(166) => 'U', chr(225) . chr(187) . chr(167) => 'u',
chr(225) . chr(187) . chr(172) => 'U', chr(225) . chr(187) . chr(173) => 'u',
chr(225) . chr(187) . chr(182) => 'Y', chr(225) . chr(187) . chr(183) => 'y',
// tilde
chr(225) . chr(186) . chr(170) => 'A', chr(225) . chr(186) . chr(171) => 'a',
chr(225) . chr(186) . chr(180) => 'A', chr(225) . chr(186) . chr(181) => 'a',
chr(225) . chr(186) . chr(188) => 'E', chr(225) . chr(186) . chr(189) => 'e',
chr(225) . chr(187) . chr(132) => 'E', chr(225) . chr(187) . chr(133) => 'e',
chr(225) . chr(187) . chr(150) => 'O', chr(225) . chr(187) . chr(151) => 'o',
chr(225) . chr(187) . chr(160) => 'O', chr(225) . chr(187) . chr(161) => 'o',
chr(225) . chr(187) . chr(174) => 'U', chr(225) . chr(187) . chr(175) => 'u',
chr(225) . chr(187) . chr(184) => 'Y', chr(225) . chr(187) . chr(185) => 'y',
// acute accent
chr(225) . chr(186) . chr(164) => 'A', chr(225) . chr(186) . chr(165) => 'a',
chr(225) . chr(186) . chr(174) => 'A', chr(225) . chr(186) . chr(175) => 'a',
chr(225) . chr(186) . chr(190) => 'E', chr(225) . chr(186) . chr(191) => 'e',
chr(225) . chr(187) . chr(144) => 'O', chr(225) . chr(187) . chr(145) => 'o',
chr(225) . chr(187) . chr(154) => 'O', chr(225) . chr(187) . chr(155) => 'o',
chr(225) . chr(187) . chr(168) => 'U', chr(225) . chr(187) . chr(169) => 'u',
// dot below
chr(225) . chr(186) . chr(160) => 'A', chr(225) . chr(186) . chr(161) => 'a',
chr(225) . chr(186) . chr(172) => 'A', chr(225) . chr(186) . chr(173) => 'a',
chr(225) . chr(186) . chr(182) => 'A', chr(225) . chr(186) . chr(183) => 'a',
chr(225) . chr(186) . chr(184) => 'E', chr(225) . chr(186) . chr(185) => 'e',
chr(225) . chr(187) . chr(134) => 'E', chr(225) . chr(187) . chr(135) => 'e',
chr(225) . chr(187) . chr(138) => 'I', chr(225) . chr(187) . chr(139) => 'i',
chr(225) . chr(187) . chr(140) => 'O', chr(225) . chr(187) . chr(141) => 'o',
chr(225) . chr(187) . chr(152) => 'O', chr(225) . chr(187) . chr(153) => 'o',
chr(225) . chr(187) . chr(162) => 'O', chr(225) . chr(187) . chr(163) => 'o',
chr(225) . chr(187) . chr(164) => 'U', chr(225) . chr(187) . chr(165) => 'u',
chr(225) . chr(187) . chr(176) => 'U', chr(225) . chr(187) . chr(177) => 'u',
chr(225) . chr(187) . chr(180) => 'Y', chr(225) . chr(187) . chr(181) => 'y',
// Vowels with diacritic (Chinese, Hanyu Pinyin)
chr(201) . chr(145) => 'a',
// macron
chr(199) . chr(149) => 'U', chr(199) . chr(150) => 'u',
// acute accent
chr(199) . chr(151) => 'U', chr(199) . chr(152) => 'u',
// caron
chr(199) . chr(141) => 'A', chr(199) . chr(142) => 'a',
chr(199) . chr(143) => 'I', chr(199) . chr(144) => 'i',
chr(199) . chr(145) => 'O', chr(199) . chr(146) => 'o',
chr(199) . chr(147) => 'U', chr(199) . chr(148) => 'u',
chr(199) . chr(153) => 'U', chr(199) . chr(154) => 'u',
// grave accent
chr(199) . chr(155) => 'U', chr(199) . chr(156) => 'u',
);
$string = strtr($string, $chars);
} else {
// Assume ISO-8859-1 if not UTF-8
$chars['in'] = chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158)
. chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194)
. chr(195) . chr(196) . chr(197) . chr(199) . chr(200) . chr(201) . chr(202)
. chr(203) . chr(204) . chr(205) . chr(206) . chr(207) . chr(209) . chr(210)
. chr(211) . chr(212) . chr(213) . chr(214) . chr(216) . chr(217) . chr(218)
. chr(219) . chr(220) . chr(221) . chr(224) . chr(225) . chr(226) . chr(227)
. chr(228) . chr(229) . chr(231) . chr(232) . chr(233) . chr(234) . chr(235)
. chr(236) . chr(237) . chr(238) . chr(239) . chr(241) . chr(242) . chr(243)
. chr(244) . chr(245) . chr(246) . chr(248) . chr(249) . chr(250) . chr(251)
. chr(252) . chr(253) . chr(255);
$chars['out'] = "EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy";
$string = strtr($string, $chars['in'], $chars['out']);
$double_chars['in'] = array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254));
$double_chars['out'] = array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th');
$string = str_replace($double_chars['in'], $double_chars['out'], $string);
}
return $string;
}
private function seemsUtf8($str)
{
$length = strlen($str);
for ($i = 0; $i < $length; $i++) {
$c = ord($str[$i]);
if ($c < 0x80) $n = 0; # 0bbbbbbb
elseif (($c & 0xE0) == 0xC0) $n = 1; # 110bbbbb
elseif (($c & 0xF0) == 0xE0) $n = 2; # 1110bbbb
elseif (($c & 0xF8) == 0xF0) $n = 3; # 11110bbb
elseif (($c & 0xFC) == 0xF8) $n = 4; # 111110bb
elseif (($c & 0xFE) == 0xFC) $n = 5; # 1111110b
else return false; # Does not match any model
for ($j = 0; $j < $n; $j++) { # n bytes matching 10bbbbbb follow ?
if ((++$i == $length) || ((ord($str[$i]) & 0xC0) != 0x80))
return false;
}
}
return true;
}
/**
* @param $ticket Ticket
* @param $ticketAttachment TicketAttachment
* @param $attachmentId int
* @param $heskSettings array
*/
private function updateAttachmentsOnTicket($ticket, $ticketAttachment, $attachmentId, $heskSettings) {
$attachments = $ticket->attachments === null ? array() : $ticket->attachments;
$newAttachment = new Attachment();
$newAttachment->savedName = $ticketAttachment->savedName;
$newAttachment->fileName = $ticketAttachment->displayName;
$newAttachment->id = $attachmentId;
$attachments[] = $newAttachment;
$this->ticketGateway->updateAttachmentsForTicket($ticket->id, $attachments, $heskSettings);
}
}

@ -0,0 +1,50 @@
<?php
namespace BusinessLogic\Attachments;
use BusinessLogic\Exceptions\AccessViolationException;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Security\UserToTicketChecker;
use DataAccess\Attachments\AttachmentGateway;
use DataAccess\Files\FileReader;
use DataAccess\Tickets\TicketGateway;
class AttachmentRetriever {
/* @var $attachmentGateway AttachmentGateway */
private $attachmentGateway;
/* @var $fileReader FileReader */
private $fileReader;
/* @var $ticketGateway TicketGateway */
private $ticketGateway;
/* @var $userToTicketChecker UserToTicketChecker */
private $userToTicketChecker;
function __construct($attachmentGateway, $fileReader, $ticketGateway, $userToTicketChecker) {
$this->attachmentGateway = $attachmentGateway;
$this->fileReader = $fileReader;
$this->ticketGateway = $ticketGateway;
$this->userToTicketChecker = $userToTicketChecker;
}
function getAttachmentContentsForTicket($ticketId, $attachmentId, $userContext, $heskSettings) {
$ticket = $this->ticketGateway->getTicketById($ticketId, $heskSettings);
if ($ticket === null) {
throw new ApiFriendlyException("Ticket {$ticketId} not found!", "Ticket Not Found", 404);
}
if ($this->userToTicketChecker->isTicketAccessibleToUser($userContext, $ticket, $heskSettings)) {
throw new AccessViolationException("User does not have access to attachment {$attachmentId}!");
}
$attachment = $this->attachmentGateway->getAttachmentById($attachmentId, $heskSettings);
$contents = base64_encode($this->fileReader->readFromFile(
$attachment->savedName, $heskSettings['attach_dir']));
return $contents;
}
}

@ -0,0 +1,9 @@
<?php
namespace BusinessLogic\Attachments;
class AttachmentType {
const MESSAGE = 0;
const REPLY = 1;
}

@ -0,0 +1,9 @@
<?php
namespace BusinessLogic\Attachments;
class CreateAttachmentForTicketModel extends CreateAttachmentModel {
/* @var $ticketId int */
public $ticketId;
}

@ -0,0 +1,21 @@
<?php
namespace BusinessLogic\Attachments;
class CreateAttachmentModel {
/* @var $savedName string */
public $savedName;
/* @var $displayName string */
public $displayName;
/* @var $id int */
public $fileSize;
/* @var $attachmentContents string */
public $attachmentContents;
/* @var $isEditing bool */
public $isEditing;
}

@ -0,0 +1,12 @@
<?php
namespace BusinessLogic\Attachments;
class TicketAttachment extends Attachment {
/* @var $ticketTrackingId string */
public $ticketTrackingId;
/* @var $type int [use <code>AttachmentType</code>] */
public $type;
}

@ -0,0 +1,53 @@
<?php
namespace BusinessLogic\Categories;
class Category {
/**
* @var int The Categories ID
*/
public $id;
/* @var $name string */
public $name;
/**
* @var int Categories order number
*/
public $catOrder;
/**
* @var bool Tickets autoassigned in this Categories
*/
public $autoAssign;
/**
* @var int The type of Categories (1 = Private, 2 = Public)
*/
public $type;
/**
* @var int The Categories's usage (0 = Tickets and Events, 1 = Tickets, 2 = Events)
*/
public $usage;
/**
* @var string? The color of the Categories
*/
public $color;
/**
* @var int The default Tickets priority
*/
public $priority;
/**
* @var int|null The manager for the Categories, if applicable
*/
public $manager;
/**
* @var bool Indication if the user has access to the Categories
*/
public $accessible;
}

@ -0,0 +1,33 @@
<?php
namespace BusinessLogic\Categories;
use BusinessLogic\Security\UserContext;
use DataAccess\Categories\CategoryGateway;
class CategoryRetriever {
/**
* @var CategoryGateway
*/
private $categoryGateway;
function __construct($categoryGateway) {
$this->categoryGateway = $categoryGateway;
}
/**
* @param $heskSettings array
* @param $userContext UserContext
* @return array
*/
function getAllCategories($heskSettings, $userContext) {
$categories = $this->categoryGateway->getAllCategories($heskSettings);
foreach ($categories as $category) {
$category->accessible = $userContext->admin ||
in_array($category->id, $userContext->categories);
}
return $categories;
}
}

@ -0,0 +1,21 @@
<?php
namespace BusinessLogic\Emails;
class Addressees {
/**
* @var $to string[]
*/
public $to;
/**
* @var $cc string[]|null
*/
public $cc;
/**
* @var $bcc string[]|null
*/
public $bcc;
}

@ -0,0 +1,75 @@
<?php
namespace BusinessLogic\Emails;
use BusinessLogic\Tickets\Attachment;
use BusinessLogic\Tickets\Ticket;
use PHPMailer;
class BasicEmailSender implements EmailSender {
function sendEmail($emailBuilder, $heskSettings, $modsForHeskSettings, $sendAsHtml) {
$mailer = new PHPMailer();
if ($heskSettings['smtp']) {
$mailer->isSMTP();
$mailer->SMTPAuth = true;
if ($heskSettings['smtp_ssl']) {
$mailer->SMTPSecure = "ssl";
} elseif ($heskSettings['smtp_tls']) {
$mailer->SMTPSecure = "tls";
}
$mailer->Host = $heskSettings['smtp_host_name'];
$mailer->Port = $heskSettings['smtp_host_port'];
$mailer->Username = $heskSettings['smtp_user'];
$mailer->Password = $heskSettings['smtp_password'];
}
$mailer->FromName = $heskSettings['noreply_name'] !== null &&
$heskSettings['noreply_name'] !== '' ? $heskSettings['noreply_name'] : '';
$mailer->From = $heskSettings['noreply_mail'];
if ($emailBuilder->to !== null) {
foreach ($emailBuilder->to as $to) {
$mailer->addAddress($to);
}
}
if ($emailBuilder->cc !== null) {
foreach ($emailBuilder->cc as $cc) {
$mailer->addCC($cc);
}
}
if ($emailBuilder->bcc !== null) {
foreach ($emailBuilder->bcc as $bcc) {
$mailer->addBCC($bcc);
}
}
$mailer->Subject = $emailBuilder->subject;
if ($sendAsHtml) {
$mailer->Body = $emailBuilder->htmlMessage;
$mailer->AltBody = $emailBuilder->message;
} else {
$mailer->Body = $emailBuilder->message;
$mailer->isHTML(false);
}
$mailer->Timeout = $heskSettings['smtp_timeout'];
if ($emailBuilder->attachments !== null) {
foreach ($emailBuilder->attachments as $attachment) {
$mailer->addAttachment(__DIR__ . '/../../../' . $heskSettings['attach_dir'] . '/' . $attachment->savedName,
$attachment->fileName);
}
}
if ($mailer->send()) {
return true;
}
return $mailer->ErrorInfo;
}
}

@ -0,0 +1,43 @@
<?php
namespace BusinessLogic\Emails;
use BusinessLogic\Tickets\Attachment;
class EmailBuilder {
/**
* @var $to string[]
*/
public $to;
/**
* @var $cc string[]
*/
public $cc;
/**
* @var $bcc string[]
*/
public $bcc;
/**
* @var $subject string
*/
public $subject;
/**
* @var $message string
*/
public $message;
/**
* @var $htmlMessage string
*/
public $htmlMessage;
/**
* @var $attachments Attachment[]
*/
public $attachments;
}

@ -0,0 +1,21 @@
<?php
namespace BusinessLogic\Emails;
use BusinessLogic\Tickets\Attachment;
use BusinessLogic\Tickets\Ticket;
use PHPMailer;
interface EmailSender {
/**
* Use to send emails
*
* @param $emailBuilder EmailBuilder
* @param $heskSettings array
* @param $modsForHeskSettings array
* @param $sendAsHtml bool
* @return bool|string|\stdClass true if message sent successfully, string for PHPMail/Smtp error, stdClass for Mailgun error
*/
function sendEmail($emailBuilder, $heskSettings, $modsForHeskSettings, $sendAsHtml);
}

@ -0,0 +1,74 @@
<?php
namespace BusinessLogic\Emails;
use BusinessLogic\Tickets\Ticket;
class EmailSenderHelper {
/**
* @var $emailTemplateParser EmailTemplateParser
*/
private $emailTemplateParser;
/**
* @var $basicEmailSender BasicEmailSender
*/
private $basicEmailSender;
/**
* @var $mailgunEmailSender MailgunEmailSender
*/
private $mailgunEmailSender;
function __construct($emailTemplateParser, $basicEmailSender, $mailgunEmailSender) {
$this->emailTemplateParser = $emailTemplateParser;
$this->basicEmailSender = $basicEmailSender;
$this->mailgunEmailSender = $mailgunEmailSender;
}
/**
* @param $templateId int the EmailTemplateRetriever::TEMPLATE_NAME
* @param $language string the language name
* @param $addressees Addressees the addressees. **cc and bcc addresses from custom fields will be added here!**
* @param $ticket Ticket
* @param $heskSettings array
* @param $modsForHeskSettings array
*/
function sendEmailForTicket($templateId, $language, $addressees, $ticket, $heskSettings, $modsForHeskSettings) {
$languageCode = $heskSettings['languages'][$language]['folder'];
$parsedTemplate = $this->emailTemplateParser->getFormattedEmailForLanguage($templateId, $languageCode,
$ticket, $heskSettings, $modsForHeskSettings);
$emailBuilder = new EmailBuilder();
$emailBuilder->subject = $parsedTemplate->subject;
$emailBuilder->message = $parsedTemplate->message;
$emailBuilder->htmlMessage = $parsedTemplate->htmlMessage;
$emailBuilder->to = $addressees->to;
$emailBuilder->cc = $addressees->cc;
$emailBuilder->bcc = $addressees->bcc;
foreach ($heskSettings['custom_fields'] as $k => $v) {
$number = intval(str_replace('custom', '', $k));
if ($v['use'] && $v['type'] == 'email' && !empty($ticket->customFields[$number])) {
if ($v['value']['email_type'] == 'cc') {
$emailBuilder->cc[] = $ticket->customFields[$number];
} elseif ($v['value']['email_type'] == 'bcc') {
$emailBuilder->bcc[] = $ticket->customFields[$number];
}
}
}
if ($modsForHeskSettings['attachments']) {
$emailBuilder->attachments = $ticket->attachments;
}
if ($modsForHeskSettings['use_mailgun']) {
$this->mailgunEmailSender->sendEmail($emailBuilder, $heskSettings, $modsForHeskSettings, $modsForHeskSettings['html_emails']);
} else {
$this->basicEmailSender->sendEmail($emailBuilder, $heskSettings, $modsForHeskSettings, $modsForHeskSettings['html_emails']);
}
}
}

@ -0,0 +1,27 @@
<?php
namespace BusinessLogic\Emails;
class EmailTemplate {
/**
* @var $languageKey string
*/
public $languageKey;
/**
* @var $fileName string
*/
public $fileName;
/**
* @var $forStaff bool
*/
public $forStaff;
function __construct($forStaff, $fileName, $languageKey = null) {
$this->languageKey = $languageKey === null ? $fileName : $languageKey;
$this->fileName = $fileName;
$this->forStaff = $forStaff;
}
}

@ -0,0 +1,330 @@
<?php
namespace BusinessLogic\Emails;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Exceptions\EmailTemplateNotFoundException;
use BusinessLogic\Exceptions\InvalidEmailTemplateException;
use BusinessLogic\Statuses\DefaultStatusForAction;
use BusinessLogic\Tickets\Ticket;
use Core\Constants\Priority;
use DataAccess\Categories\CategoryGateway;
use DataAccess\Security\UserGateway;
use DataAccess\Statuses\StatusGateway;
class EmailTemplateParser {
/**
* @var $statusGateway StatusGateway
*/
private $statusGateway;
/**
* @var $categoryGateway CategoryGateway
*/
private $categoryGateway;
/**
* @var $userGateway UserGateway
*/
private $userGateway;
/**
* @var $emailTemplateRetriever EmailTemplateRetriever
*/
private $emailTemplateRetriever;
function __construct($statusGateway, $categoryGateway, $userGateway, $emailTemplateRetriever) {
$this->statusGateway = $statusGateway;
$this->categoryGateway = $categoryGateway;
$this->userGateway = $userGateway;
$this->emailTemplateRetriever = $emailTemplateRetriever;
}
/**
* @param $templateId int
* @param $languageCode string
* @param $ticket Ticket
* @param $heskSettings array
* @param $modsForHeskSettings array
* @return ParsedEmailProperties
* @throws InvalidEmailTemplateException
*/
function getFormattedEmailForLanguage($templateId, $languageCode, $ticket, $heskSettings, $modsForHeskSettings) {
global $hesklang;
$emailTemplate = $this->emailTemplateRetriever->getTemplate($templateId);
if ($emailTemplate === null) {
throw new InvalidEmailTemplateException($templateId);
}
$template = self::getFromFileSystem($emailTemplate->fileName, $languageCode, false);
$htmlTemplate = self::getFromFileSystem($emailTemplate->fileName, $languageCode, true);
$subject = $hesklang[$emailTemplate->languageKey];
$fullLanguageName = null;
foreach ($heskSettings['languages'] as $key => $value) {
if ($value['folder'] === $languageCode) {
$fullLanguageName = $key;
break;
}
}
if ($fullLanguageName === null) {
throw new \Exception("Language code {$languageCode} did not return any valid HESK languages!");
}
$subject = $this->parseSubject($subject, $ticket, $fullLanguageName, $heskSettings);
$message = $this->parseMessage($template, $ticket, $fullLanguageName, $emailTemplate->forStaff, $heskSettings, $modsForHeskSettings, false);
$htmlMessage = $this->parseMessage($htmlTemplate, $ticket, $fullLanguageName, $emailTemplate->forStaff, $heskSettings, $modsForHeskSettings, true);
return new ParsedEmailProperties($subject, $message, $htmlMessage);
}
/**
* @param $template string
* @param $language string
* @param $html bool
* @return string The template
* @throws EmailTemplateNotFoundException If the template was not found in the filesystem for the provided language
*/
private function getFromFileSystem($template, $language, $html)
{
$htmlFolder = $html ? 'html/' : '';
/* Get email template */
$file = "language/{$language}/emails/{$htmlFolder}{$template}.txt";
$absoluteFilePath = __DIR__ . '/../../../' . $file;
if (file_exists($absoluteFilePath)) {
return file_get_contents($absoluteFilePath);
} else {
throw new EmailTemplateNotFoundException($template, $language);
}
}
/**
* @param $subjectTemplate string
* @param $ticket Ticket
* @param $language string
* @param $heskSettings array
* @return string
* @throws \Exception if common.inc.php isn't loaded
*/
private function parseSubject($subjectTemplate, $ticket, $language, $heskSettings) {
global $hesklang;
if (!function_exists('hesk_msgToPlain')) {
throw new \Exception("common.inc.php not loaded!");
}
if ($ticket === null) {
return $subjectTemplate;
}
// Status name and category name
$defaultStatus = $this->statusGateway->getStatusForDefaultAction(DefaultStatusForAction::NEW_TICKET, $heskSettings);
$statusName = $defaultStatus->localizedNames[$language];
$category = $this->categoryGateway->getAllCategories($heskSettings)[$ticket->categoryId];
switch ($ticket->priorityId) {
case Priority::CRITICAL:
$priority = $hesklang['critical'];
break;
case Priority::HIGH:
$priority = $hesklang['high'];
break;
case Priority::MEDIUM:
$priority = $hesklang['medium'];
break;
case Priority::LOW:
$priority = $hesklang['low'];
break;
default:
$priority = 'PRIORITY NOT FOUND';
break;
}
// Special tags
$subject = str_replace('%%SUBJECT%%', $ticket->subject, $subjectTemplate);
$subject = str_replace('%%TRACK_ID%%', $ticket->trackingId, $subject);
$subject = str_replace('%%CATEGORY%%', $category->id, $subject);
$subject = str_replace('%%PRIORITY%%', $priority, $subject);
$subject = str_replace('%%STATUS%%', $statusName, $subject);
return $subject;
}
/**
* @param $messageTemplate string
* @param $ticket Ticket
* @param $language string
* @param $heskSettings array
* @return string
* @throws \Exception if common.inc.php isn't loaded
*/
private function parseMessage($messageTemplate, $ticket, $language, $admin, $heskSettings, $modsForHeskSettings, $html) {
global $hesklang;
if (!function_exists('hesk_msgToPlain')) {
throw new \Exception("common.inc.php not loaded!");
}
if ($ticket === null) {
return $messageTemplate;
}
$heskSettings['site_title'] = hesk_msgToPlain($heskSettings['site_title'], 1);
// Is email required to view ticket (for customers only)?
$heskSettings['e_param'] = $heskSettings['email_view_ticket'] ? '&e=' . rawurlencode($ticket->email) : '';
/* Generate the ticket URLs */
$trackingURL = $heskSettings['hesk_url'];
$trackingURL .= $admin ? '/' . $heskSettings['admin_dir'] . '/admin_ticket.php' : '/ticket.php';
$trackingURL .= '?track=' . $ticket->trackingId . ($admin ? '' : $heskSettings['e_param']) . '&Refresh=' . rand(10000, 99999);
// Status name and category name
$defaultStatus = $this->statusGateway->getStatusForDefaultAction(DefaultStatusForAction::NEW_TICKET, $heskSettings);
$statusName = hesk_msgToPlain($defaultStatus->localizedNames[$language]);
$category = hesk_msgToPlain($this->categoryGateway->getAllCategories($heskSettings)[$ticket->categoryId]->name);
$owner = hesk_msgToPlain($this->userGateway->getUserById($ticket->ownerId, $heskSettings)->name);
switch ($ticket->priorityId) {
case Priority::CRITICAL:
$priority = $hesklang['critical'];
break;
case Priority::HIGH:
$priority = $hesklang['high'];
break;
case Priority::MEDIUM:
$priority = $hesklang['medium'];
break;
case Priority::LOW:
$priority = $hesklang['low'];
break;
default:
$priority = 'PRIORITY NOT FOUND';
break;
}
// Special tags
$msg = str_replace('%%NAME%%', $ticket->name, $messageTemplate);
$msg = str_replace('%%SUBJECT%%', $ticket->subject, $msg);
$msg = str_replace('%%TRACK_ID%%', $ticket->trackingId, $msg);
$msg = str_replace('%%TRACK_URL%%', $trackingURL, $msg);
$msg = str_replace('%%SITE_TITLE%%', $heskSettings['site_title'], $msg);
$msg = str_replace('%%SITE_URL%%', $heskSettings['site_url'], $msg);
$msg = str_replace('%%CATEGORY%%', $category, $msg);
$msg = str_replace('%%PRIORITY%%', $priority, $msg);
$msg = str_replace('%%OWNER%%', $owner, $msg);
$msg = str_replace('%%STATUS%%', $statusName, $msg);
$msg = str_replace('%%EMAIL%%', $ticket->email, $msg);
$msg = str_replace('%%CREATED%%', $ticket->dateCreated, $msg);
$msg = str_replace('%%UPDATED%%', $ticket->lastChanged, $msg);
$msg = str_replace('%%ID%%', $ticket->id, $msg);
/* All custom fields */
for ($i=1; $i<=50; $i++) {
$k = 'custom'.$i;
if (isset($heskSettings['custom_fields'][$k]) && isset($ticket->customFields[$i])) {
$v = $heskSettings['custom_fields'][$k];
switch ($v['type']) {
case 'checkbox':
$ticket->customFields[$i] = str_replace("<br>","\n",$ticket->customFields[$i]);
break;
case 'date':
$ticket->customFields[$i] = hesk_custom_date_display_format($ticket->customFields[$i], $v['value']['date_format']);
break;
}
$msg = str_replace('%%'.strtoupper($k).'%%',stripslashes($ticket->customFields[$i]),$msg);
} else {
$msg = str_replace('%%'.strtoupper($k).'%%','',$msg);
}
}
// Is message tag in email template?
if (strpos($msg, '%%MESSAGE%%') !== false) {
// Replace message
if ($html) {
$htmlMessage = nl2br($ticket->message);
$msg = str_replace('%%MESSAGE%%', $htmlMessage, $msg);
} else {
$plainTextMessage = $ticket->message;
$messageHtml = $ticket->usesHtml;
if (count($ticket->replies) > 0) {
$lastReply = end($ticket->replies);
$messageHtml = $lastReply->usesHtml;
}
if ($messageHtml) {
if (!function_exists('convert_html_to_text')) {
require(__DIR__ . '/../../../inc/html2text/html2text.php');
}
$plainTextMessage = convert_html_to_text($plainTextMessage);
$plainTextMessage = fix_newlines($plainTextMessage);
}
$msg = str_replace('%%MESSAGE%%', $plainTextMessage, $msg);
}
// Add direct links to any attachments at the bottom of the email message
if ($heskSettings['attachments']['use'] && isset($ticket->attachments) && count($ticket->attachments) > 0) {
if (!$modsForHeskSettings['attachments']) {
if ($html) {
$msg .= "<br><br><br>" . $hesklang['fatt'];
} else {
$msg .= "\n\n\n" . $hesklang['fatt'];
}
foreach ($ticket->attachments as $attachment) {
if ($html) {
$msg .= "<br><br>{$attachment->fileName}<br>";
} else {
$msg .= "\n\n{$attachment->fileName}\n";
}
$msg .= "{$heskSettings['hesk_url']}/download_attachment.php?att_id={$attachment->id}&track={$ticket->trackingId}{$heskSettings['e_param']}";
}
}
}
// For customer notifications: if we allow email piping/pop 3 fetching and
// stripping quoted replies add an "reply above this line" tag
if (!$admin && ($heskSettings['email_piping'] || $heskSettings['pop3']) && $heskSettings['strip_quoted']) {
$msg = $hesklang['EMAIL_HR'] . "\n\n" . $msg;
}
} elseif (strpos($msg, '%%MESSAGE_NO_ATTACHMENTS%%') !== false) {
if ($html) {
$htmlMessage = nl2br($ticket->message);
$msg = str_replace('%%MESSAGE_NO_ATTACHMENTS%%', $htmlMessage, $msg);
} else {
$plainTextMessage = $ticket->message;
$messageHtml = $ticket->usesHtml;
if (count($ticket->replies) > 0) {
$lastReply = end($ticket->replies);
$messageHtml = $lastReply->usesHtml;
}
if ($messageHtml) {
if (!function_exists('convert_html_to_text')) {
require(__DIR__ . '/../../../inc/html2text/html2text.php');
}
$plainTextMessage = convert_html_to_text($plainTextMessage);
$plainTextMessage = fix_newlines($plainTextMessage);
}
$msg = str_replace('%%MESSAGE_NO_ATTACHMENTS%%', $plainTextMessage, $msg);
}
}
return $msg;
}
}

@ -0,0 +1,65 @@
<?php
namespace BusinessLogic\Emails;
class EmailTemplateRetriever {
/**
* @var $validTemplates EmailTemplate[]
*/
private $validTemplates;
function __construct() {
$this->validTemplates = array();
$this->initializeArray();
}
const FORGOT_TICKET_ID = 0;
const NEW_REPLY_BY_STAFF = 1;
const NEW_TICKET = 2;
const VERIFY_EMAIL = 3;
const TICKET_CLOSED = 4;
const CATEGORY_MOVED = 5;
const NEW_REPLY_BY_CUSTOMER = 6;
const NEW_TICKET_STAFF = 7;
const TICKET_ASSIGNED_TO_YOU = 8;
const NEW_PM = 9;
const NEW_NOTE = 10;
const RESET_PASSWORD = 11;
const CALENDAR_REMINDER = 12;
const OVERDUE_TICKET = 13;
function initializeArray() {
if (count($this->validTemplates) > 0) {
//-- Map already built
return;
}
$this->validTemplates[self::FORGOT_TICKET_ID] = new EmailTemplate(false, 'forgot_ticket_id');
$this->validTemplates[self::NEW_REPLY_BY_STAFF] = new EmailTemplate(false, 'new_reply_by_staff');
$this->validTemplates[self::NEW_TICKET] = new EmailTemplate(false, 'new_ticket', 'ticket_received');
$this->validTemplates[self::VERIFY_EMAIL] = new EmailTemplate(false, 'verify_email');
$this->validTemplates[self::TICKET_CLOSED] = new EmailTemplate(false, 'ticket_closed');
$this->validTemplates[self::CATEGORY_MOVED] = new EmailTemplate(true, 'category_moved');
$this->validTemplates[self::NEW_REPLY_BY_CUSTOMER] = new EmailTemplate(true, 'new_reply_by_customer');
$this->validTemplates[self::NEW_TICKET_STAFF] = new EmailTemplate(true, 'new_ticket_staff');
$this->validTemplates[self::TICKET_ASSIGNED_TO_YOU] = new EmailTemplate(true, 'ticket_assigned_to_you');
$this->validTemplates[self::NEW_PM] = new EmailTemplate(true, 'new_pm');
$this->validTemplates[self::NEW_NOTE] = new EmailTemplate(true, 'new_note');
$this->validTemplates[self::RESET_PASSWORD] = new EmailTemplate(true, 'reset_password');
$this->validTemplates[self::CALENDAR_REMINDER] = new EmailTemplate(true, 'reset_password');
$this->validTemplates[self::OVERDUE_TICKET] = new EmailTemplate(true, 'overdue_ticket');
}
/**
* @param $templateId
* @return EmailTemplate|null
*/
function getTemplate($templateId) {
if (isset($this->validTemplates[$templateId])) {
return $this->validTemplates[$templateId];
}
return null;
}
}

@ -0,0 +1,71 @@
<?php
namespace BusinessLogic\Emails;
use BusinessLogic\Tickets\Attachment;
use BusinessLogic\Tickets\Ticket;
use Mailgun\Mailgun;
class MailgunEmailSender implements EmailSender {
function sendEmail($emailBuilder, $heskSettings, $modsForHeskSettings, $sendAsHtml) {
$mailgunArray = array();
$mailgunArray['from'] = $heskSettings['noreply_mail']; // Email Address
if ($heskSettings['noreply_name'] !== null && $heskSettings['noreply_name'] !== '') {
$mailgunArray['from'] = "{$heskSettings['noreply_name']} <{$heskSettings['noreply_mail']}>"; // Name and address
}
$mailgunArray['to'] = implode(',', $emailBuilder->to);
if ($emailBuilder->cc !== null) {
$mailgunArray['cc'] = implode(',', $emailBuilder->cc);
}
if ($emailBuilder->bcc !== null) {
$mailgunArray['bcc'] = implode(',', $emailBuilder->bcc);
}
$mailgunArray['subject'] = $emailBuilder->subject;
$mailgunArray['text'] = $emailBuilder->message;
if ($sendAsHtml) {
$mailgunArray['html'] = $emailBuilder->htmlMessage;
}
$mailgunAttachments = array();
if ($emailBuilder->attachments !== null) {
foreach ($emailBuilder->attachments as $attachment) {
$mailgunAttachments[] = array(
'remoteName' => $attachment->fileName,
'filePath' => __DIR__ . '/../../../' . $heskSettings['attach_dir'] . '/' . $attachment->savedName
);
}
}
$result = $this->sendMessage($mailgunArray, $mailgunAttachments, $modsForHeskSettings);
if (isset($result->http_response_code)
&& $result->http_response_code === 200) {
return true;
}
return $result;
}
private function sendMessage($mailgunArray, $attachments, $modsForHeskSettings) {
$messageClient = new Mailgun($modsForHeskSettings['mailgun_api_key']);
$mailgunAttachments = array();
if (count($attachments) > 0) {
$mailgunAttachments = array(
'attachment' => $attachments
);
}
$result = $messageClient->sendMessage($modsForHeskSettings['mailgun_domain'], $mailgunArray, $mailgunAttachments);
return $result;
}
}

@ -0,0 +1,33 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 2/28/2017
* Time: 9:36 PM
*/
namespace BusinessLogic\Emails;
class ParsedEmailProperties {
function __construct($subject, $message, $htmlMessage) {
$this->subject = $subject;
$this->message = $message;
$this->htmlMessage = $htmlMessage;
}
/**
* @var $subject string
*/
public $subject;
/**
* @var $message string
*/
public $message;
/**
* @var $htmlMessage string
*/
public $htmlMessage;
}

@ -0,0 +1,10 @@
<?php
namespace BusinessLogic\Exceptions;
class AccessViolationException extends ApiFriendlyException {
function __construct($message) {
parent::__construct($message, 'Access Exception', 403);
}
}

@ -0,0 +1,25 @@
<?php
namespace BusinessLogic\Exceptions;
use Exception;
class ApiFriendlyException extends Exception {
public $title;
public $httpResponseCode;
/**
* ApiFriendlyException constructor.
* @param string $message
* @param string $title
* @param int $httpResponseCode
*/
function __construct($message, $title, $httpResponseCode) {
$this->title = $title;
$this->httpResponseCode = $httpResponseCode;
parent::__construct($message);
}
}

@ -0,0 +1,17 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 2/22/2017
* Time: 10:00 PM
*/
namespace BusinessLogic\Exceptions;
class EmailTemplateNotFoundException extends ApiFriendlyException {
function __construct($emailTemplate, $language) {
parent::__construct(sprintf("The email template '%s' was not found for the language '%s'", $emailTemplate, $language),
'Email Template Not Found!', 400);
}
}

@ -0,0 +1,12 @@
<?php
namespace BusinessLogic\Exceptions;
class InvalidAuthenticationTokenException extends ApiFriendlyException {
public function __construct() {
parent::__construct('The X-Auth-Token is invalid. The token must be for an active helpdesk user.',
'Security Exception',
401);
}
}

@ -0,0 +1,16 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 2/23/2017
* Time: 8:13 PM
*/
namespace BusinessLogic\Exceptions;
class InvalidEmailTemplateException extends ApiFriendlyException {
function __construct($template) {
parent::__construct(sprintf("The email template '%s' is invalid", $template), 'Invalid Email Template', 400);
}
}

@ -0,0 +1,11 @@
<?php
namespace BusinessLogic\Exceptions;
class MissingAuthenticationTokenException extends ApiFriendlyException {
function __construct() {
parent::__construct("An 'X-Auth-Token' is required for all requests",
'Security Exception',
400);
}
}

@ -0,0 +1,21 @@
<?php
namespace BusinessLogic\Exceptions;
use BusinessLogic\ValidationModel;
use Exception;
class ValidationException extends ApiFriendlyException {
/**
* ValidationException constructor.
* @param ValidationModel $validationModel The validation model
* @throws Exception If the validationModel's errorKeys is empty
*/
function __construct($validationModel) {
if (count($validationModel->errorKeys) === 0) {
throw new Exception('Tried to throw a ValidationException, but the validation model was valid or had 0 error keys!');
}
parent::__construct(implode(",", $validationModel->errorKeys), "Validation Failed. Error keys are available in the message section.", 400);
}
}

@ -0,0 +1,29 @@
<?php
namespace BusinessLogic;
class Helpers {
static function getHeader($key) {
$headers = getallheaders();
$uppercaseHeaders = array();
foreach ($headers as $header => $value) {
$uppercaseHeaders[strtoupper($header)] = $value;
}
return isset($uppercaseHeaders[$key])
? $uppercaseHeaders[$key]
: NULL;
}
static function hashToken($token) {
return hash('sha512', $token);
}
static function safeArrayGet($array, $key) {
return $array !== null && array_key_exists($key, $array)
? $array[$key]
: null;
}
}

@ -0,0 +1,52 @@
<?php
namespace BusinessLogic\Security;
use DataAccess\Security\BanGateway;
class BanRetriever {
/**
* @var BanGateway
*/
private $banGateway;
function __construct($banGateway) {
$this->banGateway = $banGateway;
}
/**
* @param $email
* @param $heskSettings
* @return bool
*/
function isEmailBanned($email, $heskSettings) {
$bannedEmails = $this->banGateway->getEmailBans($heskSettings);
foreach ($bannedEmails as $bannedEmail) {
if ($bannedEmail->email === $email) {
return true;
}
}
return false;
}
/**
* @param $ip int the IP address, converted beforehand using ip2long()
* @param $heskSettings
* @return bool
*/
function isIpAddressBanned($ip, $heskSettings) {
$bannedIps = $this->banGateway->getIpBans($heskSettings);
foreach ($bannedIps as $bannedIp) {
if ($bannedIp->ipFrom <= $ip && $bannedIp->ipTo >= $ip) {
return true;
}
}
return false;
}
}

@ -0,0 +1,26 @@
<?php
namespace BusinessLogic\Security;
class BannedEmail {
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $email;
/**
* @var int|null The user who banned the email, or null if the user was deleted
*/
public $bannedById;
/**
* @var string
*/
public $dateBanned;
}

@ -0,0 +1,36 @@
<?php
namespace BusinessLogic\Security;
class BannedIp {
/**
* @var int
*/
public $id;
/**
* @var int the lower bound of the IP address range
*/
public $ipFrom;
/**
* @var int the upper bound of the IP address range
*/
public $ipTo;
/**
* @var string the display of the IP ban to be shown to the user
*/
public $ipDisplay;
/**
* @var int|null The user who banned the IP, or null if the user was deleted
*/
public $bannedById;
/**
* @var string
*/
public $dateBanned;
}

@ -0,0 +1,106 @@
<?php
namespace BusinessLogic\Security;
class UserContext {
/* @var $id int */
public $id;
/* @var $username string */
public $username;
/* @var $admin bool */
public $admin;
/* @var $name string */
public $name;
/* @var $email string */
public $email;
/* @var $signature string */
public $signature;
/* @var $language string|null */
public $language;
/* @var $categories int[] */
public $categories;
/* @var $permissions string[] */
public $permissions;
/* @var UserContextPreferences */
public $preferences;
/* @var UserContextNotifications */
public $notificationSettings;
/* @var $autoAssign bool */
public $autoAssign;
/* @var $ratingNegative int */
public $ratingNegative;
/* @var $ratingPositive int */
public $ratingPositive;
/* @var $rating float */
public $rating;
/* @var $totalNumberOfReplies int */
public $totalNumberOfReplies;
/* @var $active bool */
public $active;
/**
* Builds a user context based on the current session. **The session must be active!**
* @param $dataRow array the $_SESSION superglobal or the hesk_users result set
* @return UserContext the built user context
*/
static function fromDataRow($dataRow) {
$userContext = new UserContext();
$userContext->id = $dataRow['id'];
$userContext->username = $dataRow['user'];
$userContext->admin = $dataRow['isadmin'] === '1';
$userContext->name = $dataRow['name'];
$userContext->email = $dataRow['email'];
$userContext->signature = $dataRow['signature'];
$userContext->language = $dataRow['language'];
$userContext->categories = explode(',', $dataRow['categories']);
$userContext->permissions = explode(',', $dataRow['heskprivileges']);
$userContext->autoAssign = $dataRow['autoassign'];
$userContext->ratingNegative = $dataRow['ratingneg'];
$userContext->ratingPositive = $dataRow['ratingpos'];
$userContext->rating = $dataRow['rating'];
$userContext->totalNumberOfReplies = $dataRow['replies'];
$userContext->active = $dataRow['active'];
$preferences = new UserContextPreferences();
$preferences->afterReply = $dataRow['afterreply'];
$preferences->autoStartTimeWorked = $dataRow['autostart'];
$preferences->autoreload = $dataRow['autoreload'];
$preferences->defaultNotifyCustomerNewTicket = $dataRow['notify_customer_new'];
$preferences->defaultNotifyCustomerReply = $dataRow['notify_customer_reply'];
$preferences->showSuggestedKnowledgebaseArticles = $dataRow['show_suggested'];
$preferences->defaultCalendarView = $dataRow['default_calendar_view'];
$preferences->defaultTicketView = $dataRow['default_list'];
$userContext->preferences = $preferences;
$notifications = new UserContextNotifications();
$notifications->newUnassigned = $dataRow['notify_new_unassigned'];
$notifications->newAssignedToMe = $dataRow['notify_new_my'];
$notifications->replyUnassigned = $dataRow['notify_reply_unassigned'];
$notifications->replyToMe = $dataRow['notify_reply_my'];
$notifications->ticketAssignedToMe = $dataRow['notify_assigned'];
$notifications->privateMessage = $dataRow['notify_pm'];
$notifications->noteOnTicketAssignedToMe = $dataRow['notify_note'];
$notifications->noteOnTicketNotAssignedToMe = $dataRow['notify_note_unassigned'];
$notifications->overdueTicketUnassigned = $dataRow['notify_overdue_unassigned'];
$userContext->notificationSettings = $notifications;
return $userContext;
}
}

@ -0,0 +1,87 @@
<?php
namespace BusinessLogic\Security;
use BusinessLogic\Exceptions\InvalidAuthenticationTokenException;
use BusinessLogic\Exceptions\MissingAuthenticationTokenException;
use BusinessLogic\Helpers;
use DataAccess\Security\UserGateway;
class UserContextBuilder {
/**
* @var UserGateway
*/
private $userGateway;
function __construct($userGateway) {
$this->userGateway = $userGateway;
}
function buildUserContext($authToken, $heskSettings) {
$NULL_OR_EMPTY_STRING = 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e';
$hashedToken = Helpers::hashToken($authToken);
if ($hashedToken === $NULL_OR_EMPTY_STRING) {
throw new MissingAuthenticationTokenException();
}
$userRow = $this->userGateway->getUserForAuthToken($hashedToken, $heskSettings);
if ($userRow === null) {
throw new InvalidAuthenticationTokenException();
}
return UserContext::fromDataRow($userRow);
}
/**
* Builds a user context based on the current session. **The session must be active!**
* @param $dataRow array the $_SESSION superglobal or the hesk_users result set
* @return UserContext the built user context
*/
function fromDataRow($dataRow) {
$userContext = new UserContext();
$userContext->id = $dataRow['id'];
$userContext->username = $dataRow['user'];
$userContext->admin = $dataRow['isadmin'];
$userContext->name = $dataRow['name'];
$userContext->email = $dataRow['email'];
$userContext->signature = $dataRow['signature'];
$userContext->language = $dataRow['language'];
$userContext->categories = explode(',', $dataRow['categories']);
$userContext->permissions = explode(',', $dataRow['heskprivileges']);
$userContext->autoAssign = $dataRow['autoassign'];
$userContext->ratingNegative = $dataRow['ratingneg'];
$userContext->ratingPositive = $dataRow['ratingpos'];
$userContext->rating = $dataRow['rating'];
$userContext->totalNumberOfReplies = $dataRow['replies'];
$userContext->active = $dataRow['active'];
$preferences = new UserContextPreferences();
$preferences->afterReply = $dataRow['afterreply'];
$preferences->autoStartTimeWorked = $dataRow['autostart'];
$preferences->autoreload = $dataRow['autoreload'];
$preferences->defaultNotifyCustomerNewTicket = $dataRow['notify_customer_new'];
$preferences->defaultNotifyCustomerReply = $dataRow['notify_customer_reply'];
$preferences->showSuggestedKnowledgebaseArticles = $dataRow['show_suggested'];
$preferences->defaultCalendarView = $dataRow['default_calendar_view'];
$preferences->defaultTicketView = $dataRow['default_list'];
$userContext->preferences = $preferences;
$notifications = new UserContextNotifications();
$notifications->newUnassigned = $dataRow['notify_new_unassigned'];
$notifications->newAssignedToMe = $dataRow['notify_new_my'];
$notifications->replyUnassigned = $dataRow['notify_reply_unassigned'];
$notifications->replyToMe = $dataRow['notify_reply_my'];
$notifications->ticketAssignedToMe = $dataRow['notify_assigned'];
$notifications->privateMessage = $dataRow['notify_pm'];
$notifications->noteOnTicketAssignedToMe = $dataRow['notify_note'];
$notifications->noteOnTicketNotAssignedToMe = $dataRow['notify_note_unassigned'];
$notifications->overdueTicketUnassigned = $dataRow['notify_overdue_unassigned'];
$userContext->notificationSettings = $notifications;
return $userContext;
}
}

@ -0,0 +1,16 @@
<?php
namespace BusinessLogic\Security;
class UserContextNotifications {
public $newUnassigned;
public $newAssignedToMe;
public $replyUnassigned;
public $replyToMe;
public $ticketAssignedToMe;
public $privateMessage;
public $noteOnTicketAssignedToMe;
public $noteOnTicketNotAssignedToMe;
public $overdueTicketUnassigned;
}

@ -0,0 +1,15 @@
<?php
namespace BusinessLogic\Security;
class UserContextPreferences {
public $afterReply;
public $autoStartTimeWorked;
public $autoreload;
public $defaultNotifyCustomerNewTicket;
public $defaultNotifyCustomerReply;
public $showSuggestedKnowledgebaseArticles;
public $defaultCalendarView;
public $defaultTicketView;
}

@ -0,0 +1,17 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 3/12/2017
* Time: 12:11 PM
*/
namespace BusinessLogic\Security;
class UserPrivilege {
const CAN_VIEW_TICKETS = 'can_view_tickets';
const CAN_REPLY_TO_TICKETS = 'can_reply_tickets';
const CAN_EDIT_TICKETS = 'can_edit_tickets';
const CAN_DELETE_TICKETS = 'can_del_tickets';
}

@ -0,0 +1,50 @@
<?php
namespace BusinessLogic\Security;
use BusinessLogic\Tickets\Ticket;
use DataAccess\Security\UserGateway;
class UserToTicketChecker {
/* @var $userGateway UserGateway */
private $userGateway;
function __construct($userGateway) {
$this->userGateway = $userGateway;
}
/**
* @param $user UserContext
* @param $ticket Ticket
* @param $heskSettings array
* @param $extraPermissions UserPrivilege[] additional privileges the user needs besides CAN_VIEW_TICKETS (if not an admin)
* for this to return true
* @return bool
*/
function isTicketAccessibleToUser($user, $ticket, $heskSettings, $extraPermissions = array()) {
if ($user->admin === true) {
return true;
}
if (!in_array($ticket->categoryId, $user->categories)) {
return false;
}
$categoryManagerId = $this->userGateway->getManagerForCategory($ticket->categoryId, $heskSettings);
if ($user->id === $categoryManagerId) {
return true;
}
$extraPermissions[] = UserPrivilege::CAN_VIEW_TICKETS;
foreach ($extraPermissions as $permission) {
if (!in_array($permission, $user->permissions)) {
return false;
}
}
return true;
}
}

@ -0,0 +1,21 @@
<?php
namespace BusinessLogic\Settings;
use DataAccess\Settings\ModsForHeskSettingsGateway;
class ApiChecker {
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */
private $modsForHeskSettingsGateway;
function __construct($modsForHeskSettingsGateway) {
$this->modsForHeskSettingsGateway = $modsForHeskSettingsGateway;
}
function isApiEnabled($heskSettings) {
$modsForHeskSettings = $this->modsForHeskSettingsGateway->getAllSettings($heskSettings);
return intval($modsForHeskSettings['public_api']) === 1;
}
}

@ -0,0 +1,86 @@
<?php
namespace BusinessLogic\Settings;
// TODO Test!
use DataAccess\Settings\ModsForHeskSettingsGateway;
class SettingsRetriever {
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */
private $modsForHeskSettingsGateway;
function __construct($modsForHeskSettingsGateway) {
$this->modsForHeskSettingsGateway = $modsForHeskSettingsGateway;
}
private static $settingsToNotReturn = array(
'webmaster_email',
'noreply_email',
'noreply_name',
'db_.*',
'admin_dir',
'attach_dir',
'cache_dir',
'autoclose',
'autologin',
'autoassign',
'secimg_.*',
'recaptcha_.*',
'question_.*',
'attempt_.*',
'reset_pass',
'x_frame_opt',
'force_ssl',
'imap.*',
'smtp.*',
'email_piping',
'pop3.*',
'loop.*',
'email_providers',
'notify_.*',
'alink',
'submit_notice',
'online',
'online_min',
'modsForHeskVersion',
'use_mailgun',
'mailgun.*',
'kb_attach_dir',
'public_api',
'custom_fields',
'hesk_version',
'hesk_license',
);
/**
* @param $heskSettings array
* @return array
*/
function getAllSettings($heskSettings) {
$modsForHeskSettings = $this->modsForHeskSettingsGateway->getAllSettings($heskSettings);
$settingsToReturn = array();
foreach ($heskSettings as $key => $value) {
if ($this->isPermittedKey($key)) {
$settingsToReturn[$key] = $value;
}
}
foreach ($modsForHeskSettings as $key => $value) {
if ($this->isPermittedKey($key)) {
$settingsToReturn[$key] = $value;
}
}
return $settingsToReturn;
}
private function isPermittedKey($key) {
foreach (self::$settingsToNotReturn as $setting) {
if (preg_match("/{$setting}/", $key)) {
return false;
}
}
return true;
}
}

@ -0,0 +1,11 @@
<?php
namespace BusinessLogic\Statuses;
class Closable {
const YES = "yes";
const STAFF_ONLY = "sonly";
const CUSTOMERS_ONLY = "conly";
const NO = "no";
}

@ -0,0 +1,30 @@
<?php
namespace BusinessLogic\Statuses;
class DefaultStatusForAction {
const NEW_TICKET = "IsNewTicketStatus";
const CLOSED_STATUS = "IsClosed";
const CLOSED_BY_CLIENT = "IsClosedByClient";
const CUSTOMER_REPLY = "IsCustomerReplyStatus";
const CLOSED_BY_STAFF = "IsStaffClosedOption";
const REOPENED_BY_STAFF = "IsStaffReopenedStatus";
const DEFAULT_STAFF_REPLY = "IsDefaultStaffReplyStatus";
const LOCKED_TICKET = "LockedTicketStatus";
const AUTOCLOSE_STATUS = "IsAutocloseOption";
static function getAll() {
return array(
self::NEW_TICKET,
self::CLOSED_STATUS,
self::CLOSED_BY_CLIENT,
self::CUSTOMER_REPLY,
self::CLOSED_BY_STAFF,
self::REOPENED_BY_STAFF,
self::DEFAULT_STAFF_REPLY,
self::LOCKED_TICKET,
self::AUTOCLOSE_STATUS
);
}
}

@ -0,0 +1,72 @@
<?php
namespace BusinessLogic\Statuses;
class Status {
static function fromDatabase($row, $languageRs) {
$status = new Status();
$status->id = intval($row['ID']);
$status->textColor = $row['TextColor'];
$status->defaultActions = array();
foreach (DefaultStatusForAction::getAll() as $defaultStatus) {
$status = self::addDefaultStatusIfSet($status, $row, $defaultStatus);
}
$status->closable = $row['Closable'];
$localizedLanguages = array();
while ($languageRow = hesk_dbFetchAssoc($languageRs)) {
$localizedLanguages[$languageRow['language']] = $languageRow['text'];
}
$status->localizedNames = $localizedLanguages;
$status->sort = intval($row['sort']);
return $status;
}
/**
* @param $status Status
* @param $row array
* @param $key string
* @return Status
*/
private static function addDefaultStatusIfSet($status, $row, $key) {
if ($row[$key]) {
$status->defaultActions[] = $key;
}
return $status;
}
/**
* @var $id int
*/
public $id;
/**
* @var $textColor string
*/
public $textColor;
/**
* @var $defaultActions DefaultStatusForAction[]
*/
public $defaultActions;
/**
* @var $closable Closable
*/
public $closable;
/**
* @var $sort int
*/
public $sort;
/**
* @var $name string[]
*/
public $localizedNames;
}

@ -0,0 +1,20 @@
<?php
namespace BusinessLogic\Statuses;
use DataAccess\Statuses\StatusGateway;
// TODO Test!
class StatusRetriever {
/* @var $statusGateway StatusGateway */
private $statusGateway;
function __construct($statusGateway) {
$this->statusGateway = $statusGateway;
}
function getAllStatuses($heskSettings) {
return $this->statusGateway->getStatuses($heskSettings);
}
}

@ -0,0 +1,21 @@
<?php
namespace BusinessLogic\Tickets;
class Attachment {
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $fileName;
/**
* @var string
*/
public $savedName;
}

@ -0,0 +1,47 @@
<?php
namespace BusinessLogic\Tickets;
use BusinessLogic\Security\UserContext;
use BusinessLogic\Security\UserPrivilege;
use DataAccess\Categories\CategoryGateway;
use DataAccess\Security\UserGateway;
class Autoassigner {
/* @var $categoryGateway CategoryGateway */
private $categoryGateway;
/* @var $userGateway UserGateway */
private $userGateway;
function __construct($categoryGateway, $userGateway) {
$this->categoryGateway = $categoryGateway;
$this->userGateway = $userGateway;
}
/**
* @param $categoryId int
* @param $heskSettings array
* @return UserContext the user who is assigned, or null if no user should be assigned
*/
function getNextUserForTicket($categoryId, $heskSettings) {
if (!$heskSettings['autoassign']) {
return null;
}
$potentialUsers = $this->userGateway->getUsersByNumberOfOpenTicketsForAutoassign($heskSettings);
foreach ($potentialUsers as $potentialUser) {
if ($potentialUser->admin ||
(in_array($categoryId, $potentialUser->categories) &&
in_array(UserPrivilege::CAN_VIEW_TICKETS, $potentialUser->permissions) &&
in_array(UserPrivilege::CAN_REPLY_TO_TICKETS, $potentialUser->permissions))) {
return $potentialUser;
}
}
return null;
}
}

@ -0,0 +1,80 @@
<?php
namespace BusinessLogic\Tickets;
class CreateTicketByCustomerModel {
/**
* @var string
*/
public $name;
/**
* @var string
*/
public $email;
/**
* @var integer
*/
public $priority;
/**
* @var integer
*/
public $category;
/**
* @var string
*/
public $subject;
/**
* @var string
*/
public $message;
/**
* @var bool
*/
public $html;
/**
* @var array
*/
public $customFields;
/**
* @var string[]|null The latitude/longitude pair, or relevant error code (E-#)
*/
public $location;
/**
* @var int[]|null
*/
public $suggestedKnowledgebaseArticleIds;
/**
* @var string|null
*/
public $userAgent;
/**
* @var int[]|null
*/
public $screenResolution;
/**
* @var int|null
*/
public $ipAddress;
/**
* @var string
*/
public $language;
/**
* @var $sendEmailToCustomer bool
*/
public $sendEmailToCustomer;
}

@ -0,0 +1,17 @@
<?php
namespace BusinessLogic\Tickets;
class CreatedTicketModel {
/* @var $ticket Ticket */
public $ticket;
/* @var $emailVerified bool */
public $emailVerified;
function __construct($ticket, $emailVerified) {
$this->ticket = $ticket;
$this->emailVerified = $emailVerified;
}
}

@ -0,0 +1,18 @@
<?php
namespace BusinessLogic\Tickets\CustomFields;
class CustomFieldValidator {
static function isCustomFieldInCategory($customFieldId, $categoryId, $staff, $heskSettings) {
$customField = $heskSettings['custom_fields']["custom{$customFieldId}"];
if (!$customField['use'] ||
(!$staff && $customField['use'] === 2)) {
return false;
}
return count($customField['category']) === 0 ||
in_array($categoryId, $customField['category']);
}
}

@ -0,0 +1,30 @@
<?php
namespace BusinessLogic\Tickets;
class EditTicketModel {
/* @var $id int */
public $id;
/* @var $language string */
public $language;
/* @var $subject string */
public $subject;
/* @var $name string */
public $name;
/* @var $emails string */
public $email;
/* @var $message string */
public $message;
/* @var $html bool */
public $html;
/* @var $customFields string[] */
public $customFields;
}

@ -0,0 +1,12 @@
<?php
namespace BusinessLogic\Tickets\Exceptions;
use Exception;
class UnableToGenerateTrackingIdException extends Exception {
public function __construct() {
parent::__construct("Error generating a unique ticket ID.");
}
}

@ -0,0 +1,132 @@
<?php
namespace BusinessLogic\Tickets;
use BusinessLogic\Categories\CategoryRetriever;
use BusinessLogic\Security\BanRetriever;
use BusinessLogic\Tickets\CustomFields\CustomFieldValidator;
use BusinessLogic\ValidationModel;
use BusinessLogic\Validators;
use Core\Constants\CustomField;
class NewTicketValidator {
/**
* @var $categoryRetriever CategoryRetriever
*/
private $categoryRetriever;
/**
* @var $banRetriever BanRetriever
*/
private $banRetriever;
/**
* @var $ticketValidators TicketValidators
*/
private $ticketValidators;
function __construct($categoryRetriever, $banRetriever, $ticketValidators) {
$this->categoryRetriever = $categoryRetriever;
$this->banRetriever = $banRetriever;
$this->ticketValidators = $ticketValidators;
}
/**
* @param $ticketRequest CreateTicketByCustomerModel
* @param $heskSettings array HESK settings
* @return ValidationModel If errorKeys is empty, validation successful. Otherwise invalid ticket
*/
function validateNewTicketForCustomer($ticketRequest, $heskSettings, $userContext) {
$TICKET_PRIORITY_CRITICAL = 0;
$validationModel = new ValidationModel();
if ($ticketRequest->name === NULL || $ticketRequest->name == '') {
$validationModel->errorKeys[] = 'NO_NAME';
}
if (!Validators::validateEmail($ticketRequest->email, $heskSettings['multi_eml'], false)) {
$validationModel->errorKeys[] = 'INVALID_OR_MISSING_EMAIL';
}
$categoryId = intval($ticketRequest->category);
if ($categoryId < 1) {
$validationModel->errorKeys[] = 'NO_CATEGORY';
} else {
$categoryExists = array_key_exists($categoryId, $this->categoryRetriever->getAllCategories($heskSettings, $userContext));
if (!$categoryExists) {
$validationModel->errorKeys[] = 'CATEGORY_DOES_NOT_EXIST';
}
}
//-- TODO assert priority exists
if ($heskSettings['cust_urgency'] && intval($ticketRequest->priority) === $TICKET_PRIORITY_CRITICAL) {
$validationModel->errorKeys[] = 'CRITICAL_PRIORITY_FORBIDDEN';
}
if ($heskSettings['require_subject'] === 1 &&
($ticketRequest->subject === NULL || $ticketRequest->subject === '')) {
$validationModel->errorKeys[] = 'SUBJECT_REQUIRED';
}
if ($heskSettings['require_message'] === 1 &&
($ticketRequest->message === NULL || $ticketRequest->message === '')) {
$validationModel->errorKeys[] = 'MESSAGE_REQUIRED';
}
foreach ($heskSettings['custom_fields'] as $key => $value) {
$customFieldNumber = intval(str_replace('custom', '', $key));
//TODO test this
if (!array_key_exists($customFieldNumber, $ticketRequest->customFields)) {
continue;
}
if ($value['use'] == 1 && CustomFieldValidator::isCustomFieldInCategory($customFieldNumber, intval($ticketRequest->category), false, $heskSettings)) {
$custom_field_value = $ticketRequest->customFields[$customFieldNumber];
if (empty($custom_field_value)) {
$validationModel->errorKeys[] = "CUSTOM_FIELD_{$customFieldNumber}_INVALID::NO_VALUE";
continue;
}
switch($value['type']) {
case CustomField::DATE:
if (!preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $custom_field_value)) {
$validationModel->errorKeys[] = 'CUSTOM_FIELD_' . $customFieldNumber . '_INVALID::INVALID_DATE';
} else {
// Actually validate based on range
$date = strtotime($custom_field_value . ' t00:00:00');
$dmin = strlen($value['value']['dmin']) ? strtotime($value['value']['dmin'] . ' t00:00:00') : false;
$dmax = strlen($value['value']['dmax']) ? strtotime($value['value']['dmax'] . ' t00:00:00') : false;
if ($dmin && $dmin > $date) {
$validationModel->errorKeys[] = 'CUSTOM_FIELD_' . $customFieldNumber . '_INVALID::DATE_BEFORE_MIN::MIN:' . date('Y-m-d', $dmin) . '::ENTERED:' . date('Y-m-d', $date);
} elseif ($dmax && $dmax < $date) {
$validationModel->errorKeys[] = 'CUSTOM_FIELD_' . $customFieldNumber . '_INVALID::DATE_AFTER_MAX::MAX:' . date('Y-m-d', $dmax) . '::ENTERED:' . date('Y-m-d', $date);
}
}
break;
case CustomField::EMAIL:
if (!Validators::validateEmail($custom_field_value, $value['value']['multiple'], false)) {
$validationModel->errorKeys[] = "CUSTOM_FIELD_{$customFieldNumber}_INVALID::INVALID_EMAIL";
}
break;
}
}
}
if ($this->banRetriever->isEmailBanned($ticketRequest->email, $heskSettings)) {
$validationModel->errorKeys[] = 'EMAIL_BANNED';
}
if ($this->ticketValidators->isCustomerAtMaxTickets($ticketRequest->email, $heskSettings)) {
$validationModel->errorKeys[] = 'EMAIL_AT_MAX_OPEN_TICKETS';
}
if ($ticketRequest->language === null ||
$ticketRequest->language === '') {
$validationModel->errorKeys[] = 'MISSING_LANGUAGE';
}
return $validationModel;
}
}

@ -0,0 +1,62 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 2/28/2017
* Time: 9:17 PM
*/
namespace BusinessLogic\Tickets;
class Reply {
/**
* @var $id int
*/
public $id;
/**
* @var $ticketId int
*/
public $ticketId;
/**
* @var $replierName string
*/
public $replierName;
/**
* @var $message string
*/
public $message;
/**
* @var $dateCreated string
*/
public $dateCreated;
/**
* @var $attachments Attachment[]
*/
public $attachments;
/**
* @var $staffId int|null
*/
public $staffId;
/**
* @var $rating int|null
*/
public $rating;
/**
* @var $isRead bool
*/
public $isRead;
/**
* @var $usesHtml bool
*/
public $usesHtml;
}

@ -0,0 +1,14 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 2/20/2017
* Time: 10:03 PM
*/
namespace BusinessLogic\Tickets;
class StageTicket extends Ticket {
//-- Nothing here, just an indicator that it is a StageTicket and not a regular Ticket
}

@ -0,0 +1,359 @@
<?php
namespace BusinessLogic\Tickets;
class Ticket {
static function fromDatabaseRow($row, $linkedTicketsRs, $repliesRs, $heskSettings) {
$ticket = new Ticket();
$ticket->id = intval($row['id']);
$ticket->trackingId = $row['trackid'];
$ticket->name = $row['name'];
if ($row['email'] !== null) {
$emails = str_replace(';', ',', $row['email']);
$emails = explode(',', strtolower($emails));
$ticket->email = array_filter($emails);
}
$ticket->categoryId = intval($row['category']);
$ticket->priorityId = intval($row['priority']);
$ticket->subject = $row['subject'];
$ticket->message = $row['message'];
$ticket->dateCreated = $row['dt'];
$ticket->lastChanged = $row['lastchange'];
$ticket->firstReplyDate = $row['firstreply'];
$ticket->closedDate = $row['closedat'];
if (trim($row['articles']) !== '') {
$suggestedArticles = explode(',', $row['articles']);
$articlesAsInts = array();
foreach ($suggestedArticles as $article) {
$articlesAsInts[] = intval($article);
}
$ticket->suggestedArticles = $articlesAsInts;
}
$ticket->ipAddress = $row['ip'];
$ticket->language = $row['language'];
$ticket->statusId = intval($row['status']);
$ticket->openedBy = intval($row['openedby']);
$ticket->firstReplyByUserId = $row['firstreplyby'] === null ? null : intval($row['firstreplyby']);
$ticket->closedByUserId = $row['closedby'] === null ? null : intval($row['closedby']);
$ticket->numberOfReplies = intval($row['replies']);
$ticket->numberOfStaffReplies = intval($row['staffreplies']);
$ticket->ownerId = intval($row['owner']);
$ticket->timeWorked = $row['time_worked'];
$ticket->lastReplyBy = intval($row['lastreplier']);
$ticket->lastReplier = $row['replierid'] === null ? null : intval($row['replierid']);
$ticket->archived = intval($row['archive']) === 1;
$ticket->locked = intval($row['locked']) === 1;
if (trim($row['attachments']) !== '') {
$attachments = explode(',', $row['attachments']);
$attachmentArray = array();
foreach ($attachments as $attachment) {
if (trim($attachment) === '') {
continue;
}
$attachmentRow = explode('#', $attachment);
$attachmentModel = new Attachment();
$attachmentModel->id = $attachmentRow[0];
$attachmentModel->fileName = $attachmentRow[1];
$attachmentModel->savedName = $attachmentRow[2];
$attachmentArray[] = $attachmentModel;
}
$ticket->attachments = $attachmentArray;
}
if (trim($row['merged']) !== '') {
$ticket->mergedTicketIds = explode(',', $row['merged']);
}
$ticket->auditTrailHtml = $row['history'];
$ticket->customFields = array();
foreach ($heskSettings['custom_fields'] as $key => $value) {
if ($value['use'] && hesk_is_custom_field_in_category($key, intval($ticket->categoryId))) {
$ticket->customFields[str_replace('custom', '', $key)] = $row[$key];
}
}
while ($linkedTicketsRow = hesk_dbFetchAssoc($linkedTicketsRs)) {
$ticket->linkedTicketIds[] = $linkedTicketsRow['id'];
}
if ($row['latitude'] !== '' && $row['longitude'] !== '') {
$ticket->location = array();
$ticket->location[0] = $row['latitude'];
$ticket->location[1] = $row['longitude'];
}
$ticket->usesHtml = intval($row['html']) === 1;
if ($row['user_agent'] !== null && trim($row['user_agent']) !== '') {
$ticket->userAgent = $row['user_agent'];
}
if ($row['screen_resolution_height'] !== null && $row['screen_resolution_width'] !== null){
$ticket->screenResolution = array();
$ticket->screenResolution[0] = $row['screen_resolution_width'];
$ticket->screenResolution[1] = $row['screen_resolution_height'];
}
$ticket->dueDate = $row['due_date'];
$ticket->dueDateOverdueEmailSent = $row['overdue_email_sent'] !== null && intval($row['overdue_email_sent']) === 1;
$replies = array();
while ($replyRow = hesk_dbFetchAssoc($repliesRs)) {
$reply = new Reply();
$reply->id = $replyRow['id'];
$reply->ticketId = $replyRow['replyto'];
$reply->replierName = $replyRow['name'];
$reply->message = $replyRow['message'];
$reply->dateCreated = $replyRow['dt'];
if (trim($replyRow['attachments']) !== '') {
$attachments = explode(',', $replyRow['attachments']);
$attachmentArray = array();
foreach ($attachments as $attachment) {
if (trim($attachment) === '') {
continue;
}
$attachmentRow = explode('#', $attachment);
$attachmentModel = new Attachment();
$attachmentModel->id = $attachmentRow[0];
$attachmentModel->fileName = $attachmentRow[1];
$attachmentModel->savedName = $attachmentRow[2];
$attachmentArray[] = $attachmentModel;
}
$reply->attachments = $attachmentArray;
}
$reply->staffId = $replyRow['staffid'] > 0 ? $replyRow['staffid'] : null;
$reply->rating = $replyRow['rating'];
$reply->isRead = $replyRow['read'] === '1';
$reply->usesHtml = $replyRow['html'] === '1';
$replies[$reply->id] = $reply;
}
$ticket->replies = $replies;
return $ticket;
}
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $trackingId;
/**
* @var string
*/
public $name;
/**
* @var array|null
*/
public $email;
/**
* @var int
*/
public $categoryId;
/**
* @var int
*/
public $priorityId;
/**
* @var string|null
*/
public $subject;
/**
* @var string|null
*/
public $message;
/**
* @var string
*/
public $dateCreated;
/**
* @var string
*/
public $lastChanged;
/**
* @var string|null
*/
public $firstReplyDate;
/**
* @var string|null
*/
public $closedDate;
/**
* @var int[]
*/
public $suggestedArticles = array();
/**
* @var string
*/
public $ipAddress;
/**
* @var string|null
*/
public $language;
/**
* @var int
*/
public $statusId;
/**
* @var int
*/
public $openedBy;
/**
* @var int|null
*/
public $firstReplyByUserId;
/**
* @var int|null
*/
public $closedByUserId;
/**
* @var int
*/
public $numberOfReplies;
/**
* @var int
*/
public $numberOfStaffReplies;
/**
* @var int|null
*/
public $ownerId;
/**
* @var string
*/
public $timeWorked;
/**
* @var int
*/
public $lastReplyBy;
/**
* @var int|null
*/
public $lastReplier;
/**
* @var bool
*/
public $archived;
/**
* @var bool
*/
public $locked;
/**
* @var Attachment[]
*/
public $attachments = array();
function getAttachmentsForDatabase() {
$attachmentArray = array();
if ($this->attachments !== null) {
foreach ($this->attachments as $attachment) {
$attachmentArray[] = $attachment->id . '#' . $attachment->fileName . '#' . $attachment->savedName;
}
}
return implode(',', $attachmentArray);
}
/**
* @var int[]
*/
public $mergedTicketIds = array();
/**
* @var string
*/
public $auditTrailHtml;
/**
* @var string[]
*/
public $customFields;
/**
* @var int[]
*/
public $linkedTicketIds = array();
/**
* @var float[]|null
*/
public $location;
/**
* @var bool
*/
public $usesHtml;
/**
* @var string|null
*/
public $userAgent;
/**
* 0 => width
* 1 => height
*
* @var int[]|null
*/
public $screenResolution;
/**
* @var string|null
*/
public $dueDate;
/**
* @var bool|null
*/
public $dueDateOverdueEmailSent;
/**
* @var Reply[]
*/
public $replies = array();
}

@ -0,0 +1,194 @@
<?php
namespace BusinessLogic\Tickets;
use BusinessLogic\Emails\Addressees;
use BusinessLogic\Emails\EmailSenderHelper;
use BusinessLogic\Emails\EmailTemplateRetriever;
use BusinessLogic\Exceptions\ValidationException;
use BusinessLogic\Statuses\DefaultStatusForAction;
use DataAccess\Security\UserGateway;
use DataAccess\Settings\ModsForHeskSettingsGateway;
use DataAccess\Statuses\StatusGateway;
use DataAccess\Tickets\TicketGateway;
class TicketCreator {
/**
* @var $newTicketValidator NewTicketValidator
*/
private $newTicketValidator;
/**
* @var $trackingIdGenerator TrackingIdGenerator
*/
private $trackingIdGenerator;
/**
* @var $autoassigner Autoassigner
*/
private $autoassigner;
/**
* @var $statusGateway StatusGateway
*/
private $statusGateway;
/**
* @var $ticketGateway TicketGateway
*/
private $ticketGateway;
/**
* @var $verifiedEmailChecker VerifiedEmailChecker
*/
private $verifiedEmailChecker;
/**
* @var $emailSenderHelper EmailSenderHelper
*/
private $emailSenderHelper;
/**
* @var $userGateway UserGateway
*/
private $userGateway;
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */
private $modsForHeskSettingsGateway;
function __construct($newTicketValidator, $trackingIdGenerator, $autoassigner, $statusGateway, $ticketGateway,
$verifiedEmailChecker, $emailSenderHelper, $userGateway, $modsForHeskSettingsGateway) {
$this->newTicketValidator = $newTicketValidator;
$this->trackingIdGenerator = $trackingIdGenerator;
$this->autoassigner = $autoassigner;
$this->statusGateway = $statusGateway;
$this->ticketGateway = $ticketGateway;
$this->verifiedEmailChecker = $verifiedEmailChecker;
$this->emailSenderHelper = $emailSenderHelper;
$this->userGateway = $userGateway;
$this->modsForHeskSettingsGateway = $modsForHeskSettingsGateway;
}
/**
* Ticket attachments are <b>NOT</b> handled here!
*
* @param $ticketRequest CreateTicketByCustomerModel
* @param $heskSettings array HESK settings
* @param $userContext
* @return CreatedTicketModel The newly created ticket along with if the email is verified or not
* @throws ValidationException When a required field in $ticket_request is missing
* @throws \Exception When the default status for new tickets is not found
*/
function createTicketByCustomer($ticketRequest, $heskSettings, $userContext) {
$modsForHeskSettings = $this->modsForHeskSettingsGateway->getAllSettings($heskSettings);
$validationModel = $this->newTicketValidator->validateNewTicketForCustomer($ticketRequest, $heskSettings, $userContext);
if (count($validationModel->errorKeys) > 0) {
// Validation failed
throw new ValidationException($validationModel);
}
$emailVerified = true;
if ($modsForHeskSettings['customer_email_verification_required']) {
$emailVerified = $this->verifiedEmailChecker->isEmailVerified($ticketRequest->email, $heskSettings);
}
// Create the ticket
$ticket = $emailVerified
? new Ticket()
: new StageTicket();
$ticket->trackingId = $this->trackingIdGenerator->generateTrackingId($heskSettings);
if ($heskSettings['autoassign']) {
$ticket->ownerId = $this->autoassigner->getNextUserForTicket($ticketRequest->category, $heskSettings)->id;
}
// Transform one-to-one properties
$ticket->name = $ticketRequest->name;
$ticket->email = $ticketRequest->email;
$ticket->priorityId = $ticketRequest->priority;
$ticket->categoryId = $ticketRequest->category;
$ticket->subject = $ticketRequest->subject;
$ticket->message = $ticketRequest->message;
$ticket->usesHtml = $ticketRequest->html;
$ticket->customFields = $ticketRequest->customFields;
$ticket->location = $ticketRequest->location;
$ticket->suggestedArticles = $ticketRequest->suggestedKnowledgebaseArticleIds;
$ticket->userAgent = $ticketRequest->userAgent;
$ticket->screenResolution = $ticketRequest->screenResolution;
$ticket->ipAddress = $ticketRequest->ipAddress;
$ticket->language = $ticketRequest->language;
$status = $this->statusGateway->getStatusForDefaultAction(DefaultStatusForAction::NEW_TICKET, $heskSettings);
if ($status === null) {
throw new \Exception("Could not find the default status for a new ticket!");
}
$ticket->statusId = $status->id;
$ticketGatewayGeneratedFields = $this->ticketGateway->createTicket($ticket, $emailVerified, $heskSettings);
$ticket->dateCreated = $ticketGatewayGeneratedFields->dateCreated;
$ticket->lastChanged = $ticketGatewayGeneratedFields->dateModified;
$ticket->archived = false;
$ticket->locked = false;
$ticket->id = $ticketGatewayGeneratedFields->id;
$ticket->openedBy = 0;
$ticket->numberOfReplies = 0;
$ticket->numberOfStaffReplies = 0;
$ticket->timeWorked = '00:00:00';
$ticket->lastReplier = 0;
$addressees = new Addressees();
$addressees->to = $this->getAddressees($ticket->email);
if ($ticketRequest->sendEmailToCustomer && $emailVerified) {
$this->emailSenderHelper->sendEmailForTicket(EmailTemplateRetriever::NEW_TICKET, $ticketRequest->language, $addressees, $ticket, $heskSettings, $modsForHeskSettings);
} else if ($modsForHeskSettings['customer_email_verification_required'] && !$emailVerified) {
$this->emailSenderHelper->sendEmailForTicket(EmailTemplateRetriever::VERIFY_EMAIL, $ticketRequest->language, $addressees, $ticket, $heskSettings, $modsForHeskSettings);
}
if ($ticket->ownerId !== null) {
$owner = $this->userGateway->getUserById($ticket->ownerId, $heskSettings);
if ($owner->notificationSettings->newAssignedToMe) {
$addressees = new Addressees();
$addressees->to = array($owner->email);
$this->emailSenderHelper->sendEmailForTicket(EmailTemplateRetriever::TICKET_ASSIGNED_TO_YOU, $ticketRequest->language, $addressees, $ticket, $heskSettings, $modsForHeskSettings);
}
} else {
// TODO Test
$usersToBeNotified = $this->userGateway->getUsersForNewTicketNotification($heskSettings);
foreach ($usersToBeNotified as $user) {
if ($user->admin || in_array($ticket->categoryId, $user->categories)) {
$this->sendEmailToStaff($user, $ticket, $heskSettings, $modsForHeskSettings);
}
}
}
return new CreatedTicketModel($ticket, $emailVerified);
}
private function getAddressees($emailAddress) {
if ($emailAddress === null) {
return null;
}
$emails = str_replace(';', ',', $emailAddress);
return explode(',', $emails);
}
private function sendEmailToStaff($user, $ticket, $heskSettings, $modsForHeskSettings) {
$addressees = new Addressees();
$addressees->to = array($user->email);
$language = $user->language !== null && trim($user->language) !== ''
? $user->language
: $heskSettings['language'];
$this->emailSenderHelper->sendEmailForTicket(EmailTemplateRetriever::NEW_TICKET_STAFF, $language,
$addressees, $ticket, $heskSettings, $modsForHeskSettings);
}
}

@ -0,0 +1,59 @@
<?php
namespace BusinessLogic\Tickets;
use BusinessLogic\Attachments\AttachmentHandler;
use BusinessLogic\Exceptions\AccessViolationException;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Security\UserPrivilege;
use BusinessLogic\Security\UserToTicketChecker;
use DataAccess\Tickets\TicketGateway;
class TicketDeleter {
/* @var $ticketGateway TicketGateway */
private $ticketGateway;
/* @var $userToTicketChecker UserToTicketChecker */
private $userToTicketChecker;
/* @var $attachmentHandler AttachmentHandler */
private $attachmentHandler;
function __construct($ticketGateway, $userToTicketChecker, $attachmentHandler) {
$this->ticketGateway = $ticketGateway;
$this->userToTicketChecker = $userToTicketChecker;
$this->attachmentHandler = $attachmentHandler;
}
function deleteTicket($ticketId, $userContext, $heskSettings) {
$ticket = $this->ticketGateway->getTicketById($ticketId, $heskSettings);
if ($ticket === null) {
throw new ApiFriendlyException("Ticket {$ticketId} not found!", "Ticket Not Found", 404);
}
if (!$this->userToTicketChecker->isTicketAccessibleToUser($userContext, $ticket, $heskSettings,
array(UserPrivilege::CAN_DELETE_TICKETS))) {
throw new AccessViolationException("User does not have access to ticket {$ticketId}");
}
foreach ($ticket->attachments as $attachment) {
$this->attachmentHandler->deleteAttachmentFromTicket($ticketId, $attachment->id, $userContext, $heskSettings);
}
foreach ($ticket->replies as $reply) {
foreach ($reply->attachments as $attachment) {
$this->attachmentHandler->deleteAttachmentFromTicket($ticketId, $attachment->id, $userContext, $heskSettings);
}
}
$this->ticketGateway->deleteReplyDraftsForTicket($ticketId, $heskSettings);
$this->ticketGateway->deleteRepliesForTicket($ticketId, $heskSettings);
$this->ticketGateway->deleteNotesForTicket($ticketId, $heskSettings);
$this->ticketGateway->deleteTicket($ticketId, $heskSettings);
}
}

@ -0,0 +1,137 @@
<?php
namespace BusinessLogic\Tickets;
use BusinessLogic\Exceptions\AccessViolationException;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Exceptions\ValidationException;
use BusinessLogic\Security\UserContext;
use BusinessLogic\Security\UserPrivilege;
use BusinessLogic\Security\UserToTicketChecker;
use BusinessLogic\Tickets\CustomFields\CustomFieldValidator;
use BusinessLogic\ValidationModel;
use BusinessLogic\Validators;
use Core\Constants\CustomField;
use DataAccess\Tickets\TicketGateway;
class TicketEditor {
/* @var $ticketGateway TicketGateway */
private $ticketGateway;
/* @var $userToTicketChecker UserToTicketChecker */
private $userToTicketChecker;
function __construct($ticketGateway, $userToTicketChecker) {
$this->ticketGateway = $ticketGateway;
$this->userToTicketChecker = $userToTicketChecker;
}
/**
* @param $editTicketModel EditTicketModel
* @param $userContext UserContext
* @param $heskSettings array
* @throws ApiFriendlyException When the ticket isn't found for the ID
* @throws \Exception When the user doesn't have access to the ticket
*/
// TODO Unit Tests
function editTicket($editTicketModel, $userContext, $heskSettings) {
$ticket = $this->ticketGateway->getTicketById($editTicketModel->id, $heskSettings);
if ($ticket === null) {
throw new ApiFriendlyException("Ticket with ID {$editTicketModel->id} not found!", "Ticket not found", 404);
}
if (!$this->userToTicketChecker->isTicketAccessibleToUser($userContext, $ticket, $heskSettings, array(UserPrivilege::CAN_EDIT_TICKETS))) {
throw new AccessViolationException("User does not have access to ticket {$editTicketModel->id}");
}
$this->validate($editTicketModel, $ticket->categoryId, $heskSettings);
$ticket->name = $editTicketModel->name;
$ticket->email = $editTicketModel->email;
$ticket->subject = $editTicketModel->subject;
$ticket->message = $editTicketModel->message;
$ticket->customFields = $editTicketModel->customFields;
$this->ticketGateway->updateBasicTicketInfo($ticket, $heskSettings);
}
/**
* @param $editTicketModel EditTicketModel
* @param $categoryId int
* @param $heskSettings array
* @throws ValidationException When validation fails
*/
private function validate($editTicketModel, $categoryId, $heskSettings) {
$validationModel = new ValidationModel();
if ($editTicketModel->name === null || trim($editTicketModel->name) === '') {
$validationModel->errorKeys[] = 'NO_NAME';
}
if (!Validators::validateEmail($editTicketModel->email, $heskSettings['multi_eml'], false)) {
$validationModel->errorKeys[] = 'INVALID_OR_MISSING_EMAIL';
}
if ($heskSettings['require_subject'] === 1 &&
($editTicketModel->subject === NULL || $editTicketModel->subject === '')) {
$validationModel->errorKeys[] = 'SUBJECT_REQUIRED';
}
if ($heskSettings['require_message'] === 1 &&
($editTicketModel->message === NULL || $editTicketModel->message === '')) {
$validationModel->errorKeys[] = 'MESSAGE_REQUIRED';
}
foreach ($heskSettings['custom_fields'] as $key => $value) {
$customFieldNumber = intval(str_replace('custom', '', $key));
//TODO test this
if ($editTicketModel->customFields === null || !array_key_exists($customFieldNumber, $editTicketModel->customFields)) {
continue;
}
if ($value['use'] == 1 && CustomFieldValidator::isCustomFieldInCategory($customFieldNumber, intval($categoryId), false, $heskSettings)) {
$custom_field_value = $editTicketModel->customFields[$customFieldNumber];
if (empty($custom_field_value)) {
$validationModel->errorKeys[] = "CUSTOM_FIELD_{$customFieldNumber}_INVALID::NO_VALUE";
continue;
}
switch($value['type']) {
case CustomField::DATE:
if (!preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $custom_field_value)) {
$validationModel->errorKeys[] = 'CUSTOM_FIELD_' . $customFieldNumber . '_INVALID::INVALID_DATE';
} else {
// Actually validate based on range
$date = strtotime($custom_field_value . ' t00:00:00');
$dmin = strlen($value['value']['dmin']) ? strtotime($value['value']['dmin'] . ' t00:00:00') : false;
$dmax = strlen($value['value']['dmax']) ? strtotime($value['value']['dmax'] . ' t00:00:00') : false;
if ($dmin && $dmin > $date) {
$validationModel->errorKeys[] = 'CUSTOM_FIELD_' . $customFieldNumber . '_INVALID::DATE_BEFORE_MIN::MIN:' . date('Y-m-d', $dmin) . '::ENTERED:' . date('Y-m-d', $date);
} elseif ($dmax && $dmax < $date) {
$validationModel->errorKeys[] = 'CUSTOM_FIELD_' . $customFieldNumber . '_INVALID::DATE_AFTER_MAX::MAX:' . date('Y-m-d', $dmax) . '::ENTERED:' . date('Y-m-d', $date);
}
}
break;
case CustomField::EMAIL:
if (!Validators::validateEmail($custom_field_value, $value['value']['multiple'], false)) {
$validationModel->errorKeys[] = "CUSTOM_FIELD_{$customFieldNumber}_INVALID::INVALID_EMAIL";
}
break;
}
}
}
if ($editTicketModel->language === null ||
$editTicketModel->language === '') {
$validationModel->errorKeys[] = 'MISSING_LANGUAGE';
}
if (count($validationModel->errorKeys) > 0) {
throw new ValidationException($validationModel);
}
}
}

@ -0,0 +1,10 @@
<?php
namespace BusinessLogic\Tickets;
class TicketGatewayGeneratedFields {
public $id;
public $dateCreated;
public $dateModified;
}

@ -0,0 +1,77 @@
<?php
namespace BusinessLogic\Tickets;
use BusinessLogic\Exceptions\AccessViolationException;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Exceptions\ValidationException;
use BusinessLogic\Security\UserToTicketChecker;
use BusinessLogic\ValidationModel;
use DataAccess\Tickets\TicketGateway;
class TicketRetriever {
/**
* @var $ticketGateway TicketGateway
*/
private $ticketGateway;
/* @var $userToTicketChecker UserToTicketChecker */
private $userToTicketChecker;
function __construct($ticketGateway, $userToTicketChecker) {
$this->ticketGateway = $ticketGateway;
$this->userToTicketChecker = $userToTicketChecker;
}
//TODO Properly test
function getTicketById($id, $heskSettings, $userContext) {
$ticket = $this->ticketGateway->getTicketById($id, $heskSettings);
if ($ticket === null) {
throw new ApiFriendlyException("Ticket {$id} not found!", "Ticket Not Found", 404);
}
if (!$this->userToTicketChecker->isTicketAccessibleToUser($userContext, $ticket, $heskSettings)) {
throw new AccessViolationException("User does not have access to ticket {$id}!");
}
return $ticket;
}
function getTicketByTrackingIdAndEmail($trackingId, $emailAddress, $heskSettings) {
$this->validate($trackingId, $emailAddress, $heskSettings);
$ticket = $this->ticketGateway->getTicketByTrackingId($trackingId, $heskSettings);
if ($ticket === null) {
$ticket = $this->ticketGateway->getTicketByMergedTrackingId($trackingId, $heskSettings);
if ($ticket === null) {
return null;
}
}
if ($heskSettings['email_view_ticket'] && !in_array($emailAddress, $ticket->email)) {
throw new ApiFriendlyException("Email '{$emailAddress}' entered in for ticket '{$trackingId}' does not match!",
"Email Does Not Match", 400);
}
return $ticket;
}
private function validate($trackingId, $emailAddress, $heskSettings) {
$validationModel = new ValidationModel();
if ($trackingId === null || trim($trackingId) === '') {
$validationModel->errorKeys[] = 'MISSING_TRACKING_ID';
}
if ($heskSettings['email_view_ticket'] && ($emailAddress === null || trim($emailAddress) === '')) {
$validationModel->errorKeys[] = 'EMAIL_REQUIRED_AND_MISSING';
}
if (count($validationModel->errorKeys) > 0) {
throw new ValidationException($validationModel);
}
}
}

@ -0,0 +1,30 @@
<?php
namespace BusinessLogic\Tickets;
use DataAccess\Tickets\TicketGateway;
class TicketValidators {
/**
* @var $ticketGateway TicketGateway
*/
private $ticketGateway;
function __construct($ticketGateway) {
$this->ticketGateway = $ticketGateway;
}
/**
* @param $customerEmail string The email address
* @param $heskSettings array HESK Settings
* @return bool true if the user is maxed out on open tickets, false otherwise
*/
function isCustomerAtMaxTickets($customerEmail, $heskSettings) {
if ($heskSettings['max_open'] === 0) {
return false;
}
return count($this->ticketGateway->getTicketsByEmail($customerEmail, $heskSettings)) >= $heskSettings['max_open'];
}
}

@ -0,0 +1,137 @@
<?php
namespace BusinessLogic\Tickets;
use BusinessLogic\Tickets\Exceptions\UnableToGenerateTrackingIdException;
use DataAccess\Tickets\TicketGateway;
class TrackingIdGenerator {
/**
* @var $ticketGateway TicketGateway
*/
private $ticketGateway;
function __construct($ticketGateway) {
$this->ticketGateway = $ticketGateway;
}
/**
* @param $heskSettings array
* @return string
* @throws UnableToGenerateTrackingIdException
*/
function generateTrackingId($heskSettings) {
$acceptableCharacters = 'AEUYBDGHJLMNPQRSTVWXZ123456789';
/* Generate raw ID */
$trackingId = '';
/* Let's avoid duplicate ticket ID's, try up to 3 times */
for ($i = 1; $i <= 3; $i++) {
for ($i = 0; $i < 10; $i++) {
$trackingId .= $acceptableCharacters[mt_rand(0, 29)];
}
$trackingId = $this->formatTrackingId($trackingId);
/* Check for duplicate IDs */
$goodId = !$this->ticketGateway->doesTicketExist($trackingId, $heskSettings);
if ($goodId) {
return $trackingId;
}
/* A duplicate ID has been found! Let's try again (up to 2 more) */
$trackingId = '';
}
/* No valid tracking ID, try one more time with microtime() */
$trackingId = $acceptableCharacters[mt_rand(0, 29)];
$trackingId .= $acceptableCharacters[mt_rand(0, 29)];
$trackingId .= $acceptableCharacters[mt_rand(0, 29)];
$trackingId .= $acceptableCharacters[mt_rand(0, 29)];
$trackingId .= $acceptableCharacters[mt_rand(0, 29)];
$trackingId .= substr(microtime(), -5);
/* Format the ID to the correct shape and check wording */
$trackingId = $this->formatTrackingId($trackingId);
$goodId = !$this->ticketGateway->doesTicketExist($trackingId, $heskSettings);
if ($goodId) {
return $trackingId;
}
throw new UnableToGenerateTrackingIdException();
}
/**
* @param $id string
* @return string
*/
private function formatTrackingId($id) {
$acceptableCharacters = 'AEUYBDGHJLMNPQRSTVWXZ123456789';
$replace = $acceptableCharacters[mt_rand(0, 29)];
$replace .= mt_rand(1, 9);
$replace .= $acceptableCharacters[mt_rand(0, 29)];
/*
Remove 3 letter bad words from ID
Possiblitiy: 1:27,000
*/
$remove = array(
'ASS',
'CUM',
'FAG',
'FUK',
'GAY',
'SEX',
'TIT',
'XXX',
);
$id = str_replace($remove, $replace, $id);
/*
Remove 4 letter bad words from ID
Possiblitiy: 1:810,000
*/
$remove = array(
'ANAL',
'ANUS',
'BUTT',
'CAWK',
'CLIT',
'COCK',
'CRAP',
'CUNT',
'DICK',
'DYKE',
'FART',
'FUCK',
'JAPS',
'JERK',
'JIZZ',
'KNOB',
'PISS',
'POOP',
'SHIT',
'SLUT',
'SUCK',
'TURD',
// Also, remove words that are known to trigger mod_security
'WGET',
);
$replace .= mt_rand(1, 9);
$id = str_replace($remove, $replace, $id);
/* Format the ID string into XXX-XXX-XXXX format for easier readability */
$id = $id[0] . $id[1] . $id[2] . '-' . $id[3] . $id[4] . $id[5] . '-' . $id[6] . $id[7] . $id[8] . $id[9];
return $id;
}
}

@ -0,0 +1,27 @@
<?php
/**
* Created by PhpStorm.
* User: cokoch
* Date: 2/20/2017
* Time: 12:40 PM
*/
namespace BusinessLogic\Tickets;
use DataAccess\Tickets\VerifiedEmailGateway;
class VerifiedEmailChecker {
/**
* @var $verifiedEmailGateway VerifiedEmailGateway
*/
private $verifiedEmailGateway;
function __construct($verifiedEmailGateway) {
$this->verifiedEmailGateway = $verifiedEmailGateway;
}
function isEmailVerified($emailAddress, $heskSettings) {
return $this->verifiedEmailGateway->isEmailVerified($emailAddress, $heskSettings);
}
}

@ -0,0 +1,14 @@
<?php
namespace BusinessLogic;
class ValidationModel {
/**
* @var array
*/
public $errorKeys;
function __construct() {
$this->errorKeys = [];
}
}

@ -0,0 +1,123 @@
<?php
namespace BusinessLogic;
class Validators {
/**
* @param string $address - the email address
* @param array $multiple_emails - true if HESK (or custom field) supports multiple emails
* @param bool $return_emails (def. true): return the email address(es). Otherwise a boolean is returned
*
* @return mixed|null|string - array if multiple valid emails, null if no email and not required, string if valid email
*/
static function validateEmail($address, $multiple_emails, $return_emails = true) {
/* Allow multiple emails to be used? */
if ($multiple_emails) {
/* Make sure the format is correct */
$address = preg_replace('/\s/', '', $address);
$address = str_replace(';', ',', $address);
/* Check if addresses are valid */
$all = explode(',', $address);
foreach ($all as $k => $v) {
if (!self::isValidEmail($v)) {
unset($all[$k]);
}
}
/* If at least one is found return the value */
if (count($all)) {
if ($return_emails) {
return implode(',', $all);
}
return true;
} elseif (!$return_emails) {
return false;
}
} else {
/* Make sure people don't try to enter multiple addresses */
$address = str_replace(strstr($address, ','), '', $address);
$address = str_replace(strstr($address, ';'), '', $address);
$address = trim($address);
/* Valid address? */
if (self::isValidEmail($address)) {
if ($return_emails) {
return $address;
}
return true;
} else {
return false;
}
}
//-- We shouldn't get here
return false;
} // END hesk_validateEmail()
/**
* @param $email
* @return bool
*/
static function isValidEmail($email) {
/* Check for header injection attempts */
if (preg_match("/\r|\n|%0a|%0d/i", $email)) {
return false;
}
/* Does it contain an @? */
$atIndex = strrpos($email, "@");
if ($atIndex === false) {
return false;
}
/* Get local and domain parts */
$domain = substr($email, $atIndex + 1);
$local = substr($email, 0, $atIndex);
$localLen = strlen($local);
$domainLen = strlen($domain);
/* Check local part length */
if ($localLen < 1 || $localLen > 64) {
return false;
}
/* Check domain part length */
if ($domainLen < 1 || $domainLen > 254) {
return false;
}
/* Local part mustn't start or end with a dot */
if ($local[0] == '.' || $local[$localLen - 1] == '.') {
return false;
}
/* Local part mustn't have two consecutive dots*/
if (strpos($local, '..') !== false) {
return false;
}
/* Check domain part characters */
if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
return false;
}
/* Domain part mustn't have two consecutive dots */
if (strpos($domain, '..') !== false) {
return false;
}
/* Character not valid in local part unless local part is quoted */
if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) /* " */ {
if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) /* " */ {
return false;
}
}
/* All tests passed, email seems to be OK */
return true;
} // END hesk_isValidEmail()
}

@ -0,0 +1,70 @@
<?php
namespace Controllers\Attachments;
use BusinessLogic\Attachments\AttachmentHandler;
use BusinessLogic\Attachments\AttachmentRetriever;
use BusinessLogic\Attachments\CreateAttachmentForTicketModel;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Helpers;
use BusinessLogic\Security\UserToTicketChecker;
use Controllers\JsonRetriever;
class StaffTicketAttachmentsController {
function get($ticketId, $attachmentId) {
global $hesk_settings, $applicationContext, $userContext;
$this->verifyAttachmentsAreEnabled($hesk_settings);
/* @var $attachmentRetriever AttachmentRetriever */
$attachmentRetriever = $applicationContext->get[AttachmentRetriever::class];
$contents = $attachmentRetriever->getAttachmentContentsForTicket($ticketId, $attachmentId, $userContext, $hesk_settings);
output(array('contents' => $contents));
}
private function verifyAttachmentsAreEnabled($heskSettings) {
if (!$heskSettings['attachments']['use']) {
throw new ApiFriendlyException('Attachments are disabled on this server', 'Attachments Disabled', 404);
}
}
function post($ticketId) {
global $hesk_settings, $applicationContext, $userContext;
$this->verifyAttachmentsAreEnabled($hesk_settings);
/* @var $attachmentHandler AttachmentHandler */
$attachmentHandler = $applicationContext->get[AttachmentHandler::class];
$createAttachmentForTicketModel = $this->createModel(JsonRetriever::getJsonData(), $ticketId);
$createdAttachment = $attachmentHandler->createAttachmentForTicket(
$createAttachmentForTicketModel, $userContext, $hesk_settings);
return output($createdAttachment, 201);
}
private function createModel($json, $ticketId) {
$model = new CreateAttachmentForTicketModel();
$model->attachmentContents = Helpers::safeArrayGet($json, 'data');
$model->displayName = Helpers::safeArrayGet($json, 'displayName');
$model->isEditing = Helpers::safeArrayGet($json, 'isEditing');
$model->ticketId = $ticketId;
return $model;
}
function delete($ticketId, $attachmentId) {
global $applicationContext, $hesk_settings, $userContext;
/* @var $attachmentHandler AttachmentHandler */
$attachmentHandler = $applicationContext->get[AttachmentHandler::class];
$attachmentHandler->deleteAttachmentFromTicket($ticketId, $attachmentId, $userContext, $hesk_settings);
return http_response_code(204);
}
}

@ -0,0 +1,31 @@
<?php
namespace Controllers\Categories;
use BusinessLogic\Categories\CategoryRetriever;
use BusinessLogic\Exceptions\ApiFriendlyException;
class CategoryController {
function get($id) {
$categories = self::getAllCategories();
if (!isset($categories[$id])) {
throw new ApiFriendlyException("Category {$id} not found!", "Category Not Found", 404);
}
output($categories[$id]);
}
static function printAllCategories() {
output(self::getAllCategories());
}
private static function getAllCategories() {
global $hesk_settings, $applicationContext, $userContext;
/* @var $categoryRetriever CategoryRetriever */
$categoryRetriever = $applicationContext->get[CategoryRetriever::class];
return $categoryRetriever->getAllCategories($hesk_settings, $userContext);
}
}

@ -0,0 +1,16 @@
<?php
namespace Controllers;
class JsonRetriever {
/**
* Support POST, PUT, and PATCH request (and possibly more)
*
* @return mixed
*/
static function getJsonData() {
$json = file_get_contents('php://input');
return json_decode($json, true);
}
}

@ -0,0 +1,17 @@
<?php
namespace Controllers\Settings;
use BusinessLogic\Settings\SettingsRetriever;
class SettingsController {
function get() {
global $applicationContext, $hesk_settings, $modsForHesk_settings;
/* @var $settingsRetriever SettingsRetriever */
$settingsRetriever = $applicationContext->get[SettingsRetriever::class];
output($settingsRetriever->getAllSettings($hesk_settings, $modsForHesk_settings));
}
}

@ -0,0 +1,17 @@
<?php
namespace Controllers\Statuses;
use BusinessLogic\Statuses\StatusRetriever;
class StatusController {
function get() {
global $applicationContext, $hesk_settings;
/* @var $statusRetriever StatusRetriever */
$statusRetriever = $applicationContext->get[StatusRetriever::class];
output($statusRetriever->getAllStatuses($hesk_settings));
}
}

@ -0,0 +1,71 @@
<?php
namespace Controllers\Tickets;
use BusinessLogic\Helpers;
use BusinessLogic\Tickets\CreateTicketByCustomerModel;
use BusinessLogic\Tickets\TicketCreator;
use BusinessLogic\Tickets\TicketRetriever;
use BusinessLogic\ValidationModel;
use Controllers\JsonRetriever;
class CustomerTicketController {
function get() {
global $applicationContext, $hesk_settings;
$trackingId = isset($_GET['trackingId']) ? $_GET['trackingId'] : null;
$emailAddress = isset($_GET['email']) ? $_GET['email'] : null;
/* @var $ticketRetriever TicketRetriever */
$ticketRetriever = $applicationContext->get[TicketRetriever::class];
output($ticketRetriever->getTicketByTrackingIdAndEmail($trackingId, $emailAddress, $hesk_settings));
}
function post() {
global $applicationContext, $hesk_settings, $userContext;
/* @var $ticketCreator TicketCreator */
$ticketCreator = $applicationContext->get[TicketCreator::class];
$jsonRequest = JsonRetriever::getJsonData();
$ticket = $ticketCreator->createTicketByCustomer($this->buildTicketRequestFromJson($jsonRequest), $hesk_settings, $userContext);
return output($ticket->ticket, $ticket->emailVerified ? 201 : 202);
}
/**
* @param $json array
* @return CreateTicketByCustomerModel
*/
private function buildTicketRequestFromJson($json) {
$ticketRequest = new CreateTicketByCustomerModel();
$ticketRequest->name = Helpers::safeArrayGet($json, 'name');
$ticketRequest->email = Helpers::safeArrayGet($json, 'email');
$ticketRequest->category = Helpers::safeArrayGet($json, 'category');
$ticketRequest->priority = Helpers::safeArrayGet($json, 'priority');
$ticketRequest->subject = Helpers::safeArrayGet($json, 'subject');
$ticketRequest->message = Helpers::safeArrayGet($json, 'message');
$ticketRequest->html = Helpers::safeArrayGet($json, 'html');
$ticketRequest->location = Helpers::safeArrayGet($json, 'location');
$ticketRequest->suggestedKnowledgebaseArticleIds = Helpers::safeArrayGet($json, 'suggestedArticles');
$ticketRequest->userAgent = Helpers::safeArrayGet($json, 'userAgent');
$ticketRequest->screenResolution = Helpers::safeArrayGet($json, 'screenResolution');
$ticketRequest->ipAddress = Helpers::safeArrayGet($json, 'ip');
$ticketRequest->language = Helpers::safeArrayGet($json, 'language');
$ticketRequest->sendEmailToCustomer = true;
$ticketRequest->customFields = array();
$jsonCustomFields = Helpers::safeArrayGet($json, 'customFields');
if ($jsonCustomFields !== null && !empty($jsonCustomFields)) {
foreach ($jsonCustomFields as $key => $value) {
$ticketRequest->customFields[intval($key)] = $value;
}
}
return $ticketRequest;
}
}

@ -0,0 +1,68 @@
<?php
namespace Controllers\Tickets;
use BusinessLogic\Helpers;
use BusinessLogic\Tickets\EditTicketModel;
use BusinessLogic\Tickets\TicketDeleter;
use BusinessLogic\Tickets\TicketEditor;
use BusinessLogic\Tickets\TicketRetriever;
use Controllers\JsonRetriever;
class StaffTicketController {
function get($id) {
global $applicationContext, $userContext, $hesk_settings;
/* @var $ticketRetriever TicketRetriever */
$ticketRetriever = $applicationContext->get[TicketRetriever::class];
output($ticketRetriever->getTicketById($id, $hesk_settings, $userContext));
}
function delete($id) {
global $applicationContext, $userContext, $hesk_settings;
/* @var $ticketDeleter TicketDeleter */
$ticketDeleter = $applicationContext->get[TicketDeleter::class];
$ticketDeleter->deleteTicket($id, $userContext, $hesk_settings);
http_response_code(204);
}
function put($id) {
global $applicationContext, $userContext, $hesk_settings;
/* @var $ticketEditor TicketEditor */
$ticketEditor = $applicationContext->get[TicketEditor::class];
$jsonRequest = JsonRetriever::getJsonData();
$ticketEditor->editTicket($this->getEditTicketModel($id, $jsonRequest), $userContext, $hesk_settings);
http_response_code(204);
return;
}
private function getEditTicketModel($id, $jsonRequest) {
$editTicketModel = new EditTicketModel();
$editTicketModel->id = $id;
$editTicketModel->language = Helpers::safeArrayGet($jsonRequest, 'language');
$editTicketModel->name = Helpers::safeArrayGet($jsonRequest, 'name');
$editTicketModel->subject = Helpers::safeArrayGet($jsonRequest, 'subject');
$editTicketModel->message = Helpers::safeArrayGet($jsonRequest, 'message');
$editTicketModel->html = Helpers::safeArrayGet($jsonRequest, 'html');
$editTicketModel->email = Helpers::safeArrayGet($jsonRequest, 'email');
$jsonCustomFields = Helpers::safeArrayGet($jsonRequest, 'customFields');
if ($jsonCustomFields !== null && !empty($jsonCustomFields)) {
foreach ($jsonCustomFields as $key => $value) {
$editTicketModel->customFields[intval($key)] = $value;
}
}
return $editTicketModel;
}
}

@ -0,0 +1,16 @@
<?php
namespace Core\Constants;
class CustomField {
const RADIO = 'radio';
const SELECT = 'select';
const CHECKBOX = 'checkbox';
const TEXTAREA = 'textarea';
const DATE = 'date';
const EMAIL = 'email';
const HIDDEN = 'hidden';
const READONLY = 'readonly';
const TEXT = 'text';
}

@ -0,0 +1,11 @@
<?php
namespace Core\Constants;
class Priority {
const CRITICAL = 0;
const HIGH = 1;
const MEDIUM = 2;
const LOW = 3;
}

@ -0,0 +1,18 @@
<?php
namespace Core\Exceptions;
use Exception;
class SQLException extends Exception {
/**
* @var $failingQuery string
*/
public $failingQuery;
function __construct($failingQuery) {
$this->failingQuery = $failingQuery;
parent::__construct('A SQL exception occurred. Check the logs for more information.');
}
}

@ -140,32 +140,15 @@ function hesk_dbConnect()
// Errors?
if ( ! $hesk_db_link)
{
if ($hesk_settings['debug_mode'])
{
$message = $hesklang['mysql_said'] . ': ' . mysql_error();
}
else
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_connect_db'], $message);
return http_response_code(500);
$message = $hesklang['mysql_said'] . ': ' . mysql_error();
throw new \Core\Exceptions\SQLException($message);
}
if ( ! @mysql_select_db($hesk_settings['db_name'], $hesk_db_link))
{
if ($hesk_settings['debug_mode'])
{
$message = $hesklang['mysql_said'] . ': ' . mysql_error();
}
else
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_connect_db'], $message);
die();
if ( ! @mysql_select_db($hesk_settings['db_name'], $hesk_db_link)) {
$message = $hesklang['mysql_said'] . ': ' . mysql_error();
throw new \Core\Exceptions\SQLException($message);
}
// Check MySQL/PHP version and set encoding to utf8
@ -201,22 +184,12 @@ function hesk_dbQuery($query)
$hesk_last_query = $query;
if ($res = @mysql_query($query, $hesk_db_link))
{
if ($res = @mysql_query($query, $hesk_db_link)) {
return $res;
}
elseif ($hesk_settings['debug_mode'])
{
$message = $hesklang['mysql_said'] . mysql_error();
}
else
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_sql'], $message);
die();
$message = $hesklang['mysql_said'] . mysql_error();
throw new \Core\Exceptions\SQLException($message);
} // END hesk_dbQuery()
@ -253,6 +226,7 @@ function hesk_dbInsertID()
return $lastid;
}
return null;
} // END hesk_dbInsertID()

@ -148,17 +148,9 @@ function hesk_dbConnect()
// Errors?
if ( ! $hesk_db_link)
{
if ($hesk_settings['debug_mode'])
{
$message = $hesklang['mysql_said'] . ': (' . mysqli_connect_errno() . ') ' . mysqli_connect_error();
}
else
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_connect_db'], $message);
http_response_code(500);
$message = $hesklang['mysql_said'] . ': (' . mysqli_connect_errno() . ') ' . mysqli_connect_error();
throw new \Core\Exceptions\SQLException($message);
}
// Check MySQL/PHP version and set encoding to utf8
@ -200,17 +192,9 @@ function hesk_dbQuery($query)
{
return $res;
}
elseif ($hesk_settings['debug_mode'])
{
$message = $hesklang['mysql_said'] . ': ' . mysqli_error($hesk_db_link);
}
else
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_sql'], $message);
die(http_response_code(500));
$message = $hesklang['mysql_said'] . ': ' . mysqli_error($hesk_db_link);
throw new \Core\Exceptions\SQLException($message);
} // END hesk_dbQuery()

@ -0,0 +1,13 @@
<?php
function print_error($title, $message, $response_code = 500) {
require_once(__DIR__ . '/output.php');
$error = array();
$error['type'] = 'ERROR';
$error['title'] = $title;
$error['message'] = $message;
print output($error, $response_code);
return;
}

@ -1,7 +1,7 @@
<?php
function output($data, $status_code = 200) {
http_response_code($status_code);
header('Content-Type: application/json');
print json_encode($data);
return http_response_code($status_code);
}

@ -0,0 +1,65 @@
<?php
namespace DataAccess\Attachments;
use BusinessLogic\Attachments\Attachment;
use BusinessLogic\Attachments\TicketAttachment;
use DataAccess\CommonDao;
class AttachmentGateway extends CommonDao {
/**
* @param $attachment TicketAttachment
* @param $heskSettings array
* @return int The inserted attachment ID
*/
function createAttachmentForTicket($attachment, $heskSettings) {
$this->init();
hesk_dbQuery("INSERT INTO `" . hesk_dbEscape($heskSettings['db_pfix']) . "attachments`
(`ticket_id`, `note_id`, `saved_name`, `real_name`, `size`, `type`, `download_count`)
VALUES ('" . hesk_dbEscape($attachment->ticketTrackingId) . "', NULL, '" . hesk_dbEscape($attachment->savedName) . "',
'" . hesk_dbEscape($attachment->displayName) . "', " . intval($attachment->fileSize) . ", '" . intval($attachment->type) . "', 0)");
$attachmentId = hesk_dbInsertID();
$this->close();
return $attachmentId;
}
function getAttachmentById($id, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT *
FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "attachments`
WHERE `att_id` = " . intval($id));
if (hesk_dbNumRows($rs) === 0) {
return null;
}
$row = hesk_dbFetchAssoc($rs);
$attachment = new Attachment();
$attachment->id = $row['att_id'];
$attachment->savedName = $row['saved_name'];
$attachment->displayName = $row['real_name'];
$attachment->downloadCount = $row['download_count'];
$attachment->fileSize = $row['size'];
$this->close();
return $attachment;
}
function deleteAttachment($attachmentId, $heskSettings) {
$this->init();
hesk_dbQuery("DELETE FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "attachments`
WHERE `att_id` = " . intval($attachmentId));
$this->close();
}
}

@ -0,0 +1,41 @@
<?php
namespace DataAccess\Categories;
use BusinessLogic\Categories\Category;
use DataAccess\CommonDao;
use Exception;
class CategoryGateway extends CommonDao {
/**
* @param $hesk_settings
* @return Category[]
*/
function getAllCategories($hesk_settings) {
$this->init();
$sql = 'SELECT * FROM `' . hesk_dbEscape($hesk_settings['db_pfix']) . 'categories`';
$response = hesk_dbQuery($sql);
$results = array();
while ($row = hesk_dbFetchAssoc($response)) {
$category = new Category();
$category->id = intval($row['id']);
$category->name = $row['name'];
$category->catOrder = intval($row['cat_order']);
$category->autoAssign = $row['autoassign'] == 1;
$category->type = intval($row['type']);
$category->usage = intval($row['usage']);
$category->color = $row['color'];
$category->priority = intval($row['priority']);
$category->manager = intval($row['manager']) == 0 ? NULL : intval($row['manager']);
$results[$category->id] = $category;
}
$this->close();
return $results;
}
}

@ -0,0 +1,22 @@
<?php
namespace DataAccess;
use Exception;
class CommonDao {
/**
* @throws Exception if the database isn't properly configured
*/
function init() {
if (!function_exists('hesk_dbConnect')) {
throw new Exception('Database not loaded!');
}
hesk_dbConnect();
}
function close() {
hesk_dbClose();
}
}

@ -0,0 +1,17 @@
<?php
namespace DataAccess\Files;
use BusinessLogic\Exceptions\ApiFriendlyException;
class FileDeleter {
function deleteFile($name, $folder) {
$path = __DIR__ . "/../../../{$folder}/{$name}";
if (!file_exists($path)) {
return;
}
@unlink($path);
}
}

@ -0,0 +1,31 @@
<?php
namespace DataAccess\Files;
use BusinessLogic\Exceptions\ApiFriendlyException;
class FileReader {
/**
* @param $name string - The file name (including extension)
* @param $folder - The folder name (relative to the ROOT of the helpdesk)
* @returns string - The contents of the file to write
* @throws \Exception When the file fails to save
*/
function readFromFile($name, $folder) {
// __DIR__ === '/{ROOT}/api/DataAccess/Files
$location = __DIR__ . "/../../../{$folder}/{$name}";
if (!file_exists($location)) {
throw new ApiFriendlyException("The file '{$name}' does not exist on the server", "File Not Found", 404);
}
$fileContents = file_get_contents($location);
if ($fileContents === false) {
throw new \Exception("Failed to read the file!");
}
return $fileContents;
}
}

@ -0,0 +1,25 @@
<?php
namespace DataAccess\Files;
class FileWriter {
/**
* @param $name string - The file name (including extension)
* @param $folder - The folder name (relative to the ROOT of the helpdesk)
* @param $contents string - The contents of the file to write
* @return int The file size, in bytes
* @throws \Exception When the file fails to save
*/
function writeToFile($name, $folder, $contents) {
// __DIR__ === '/{ROOT}/api/DataAccess/Files
$location = __DIR__ . "/../../../{$folder}/{$name}";
$fileSize = file_put_contents($location, $contents);
if ($fileSize === false) {
throw new \Exception("Failed to save the file!");
}
return $fileSize;
}
}

@ -0,0 +1,51 @@
<?php
namespace DataAccess\Logging;
use BusinessLogic\Security\UserContext;
use DataAccess\CommonDao;
class LoggingGateway extends CommonDao {
function logDebug($location, $message, $stackTrace, $userContext, $heskSettings) {
return $this->log(Severity::DEBUG, $location, $message, $stackTrace, $userContext, $heskSettings);
}
function logInfo($location, $message, $stackTrace, $userContext, $heskSettings) {
return $this->log(Severity::INFO, $location, $message, $stackTrace, $userContext, $heskSettings);
}
function logWarning($location, $message, $stackTrace, $userContext, $heskSettings) {
return $this->log(Severity::WARNING, $location, $message, $stackTrace, $userContext, $heskSettings);
}
function logError($location, $message, $stackTrace, $userContext, $heskSettings) {
return $this->log(Severity::ERROR, $location, $message, $stackTrace, $userContext, $heskSettings);
}
/**
* @param $severity int (from Severity)
* @param $location string
* @param $message string
* @param $userContext UserContext
* @param $heskSettings array
* @return int|null|string The inserted ID, or null on failure.
*/
private function log($severity, $location, $message, $stackTrace, $userContext, $heskSettings) {
$this->init();
hesk_dbQuery("INSERT INTO `" . hesk_dbEscape($heskSettings['db_pfix']) . "logging` (`username`, `message`, `severity`, `location`, `timestamp`, `stack_trace`)
VALUES ('" . hesk_dbEscape($userContext->username) . "',
'" . hesk_dbEscape(addslashes($message)) . "',
" . intval($severity) . ",
'" . hesk_dbEscape(addslashes($location)) . "',
NOW(),
'" . hesk_dbEscape(addslashes($stackTrace)) . "')");
$insertedId = hesk_dbInsertID();
$this->close();
return $insertedId;
}
}

@ -0,0 +1,11 @@
<?php
namespace DataAccess\Logging;
class Severity {
const DEBUG = 0;
const INFO = 1;
const WARNING = 2;
const ERROR = 3;
}

@ -0,0 +1,77 @@
<?php
namespace DataAccess\Security;
use BusinessLogic\Security\BannedEmail;
use BusinessLogic\Security\BannedIp;
use DataAccess\CommonDao;
use Exception;
class BanGateway extends CommonDao {
/**
* @param $heskSettings
* @return BannedEmail[]
*/
function getEmailBans($heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT `bans`.`id` AS `id`, `bans`.`email` AS `email`,
`users`.`id` AS `banned_by`, `bans`.`dt` AS `dt`
FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "banned_emails` AS `bans`
LEFT JOIN `" . hesk_dbEscape($heskSettings['db_pfix']) . "users` AS `users`
ON `bans`.`banned_by` = `users`.`id`
AND `users`.`active` = '1'");
$bannedEmails = array();
while ($row = hesk_dbFetchAssoc($rs)) {
$bannedEmail = new BannedEmail();
$bannedEmail->id = intval($row['id']);
$bannedEmail->email = $row['email'];
$bannedEmail->bannedById = $row['banned_by'] === null ? null : intval($row['banned_by']);
$bannedEmail->dateBanned = $row['dt'];
$bannedEmails[$bannedEmail->id] = $bannedEmail;
}
$this->close();
return $bannedEmails;
}
/**
* @param $heskSettings
* @return BannedIp[]
*/
function getIpBans($heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT `bans`.`id` AS `id`, `bans`.`ip_from` AS `ip_from`,
`bans`.`ip_to` AS `ip_to`, `bans`.`ip_display` AS `ip_display`,
`users`.`id` AS `banned_by`, `bans`.`dt` AS `dt`
FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "banned_ips` AS `bans`
LEFT JOIN `" . hesk_dbEscape($heskSettings['db_pfix']) . "users` AS `users`
ON `bans`.`banned_by` = `users`.`id`
AND `users`.`active` = '1'");
$bannedIps = array();
while ($row = hesk_dbFetchAssoc($rs)) {
$bannedIp = new BannedIp();
$bannedIp->id = intval($row['id']);
$bannedIp->ipFrom = intval($row['ip_from']);
$bannedIp->ipTo = intval($row['ip_to']);
$bannedIp->ipDisplay = $row['ip_display'];
$bannedIp->bannedById = $row['banned_by'] === null ? null : intval($row['banned_by']);
$bannedIp->dateBanned = $row['dt'];
$bannedIps[$bannedIp->id] = $bannedIp;
}
$this->close();
return $bannedIps;
}
}

@ -0,0 +1,123 @@
<?php
namespace DataAccess\Security;
use BusinessLogic\Security\UserContext;
use BusinessLogic\Security\UserContextBuilder;
use DataAccess\CommonDao;
use Exception;
class UserGateway extends CommonDao {
/**
* @param $hashedToken string The pre-hashed token from Helpers::hashToken
* @param $heskSettings
* @return array|null User ResultSet if an active user for the token is found, null otherwise
*/
function getUserForAuthToken($hashedToken, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "users` WHERE `id` = (
SELECT `user_id`
FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "user_api_tokens`
WHERE `token` = '" . hesk_dbEscape($hashedToken) . "'
) AND `active` = '1'");
if (hesk_dbNumRows($rs) === 0) {
$this->close();
return null;
}
$row = hesk_dbFetchAssoc($rs);
$this->close();
return $row;
}
function getUserById($id, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "users` WHERE `id` = " . intval($id));
if (hesk_dbNumRows($rs) === 0) {
$this->close();
return null;
}
$user = UserContext::fromDataRow(hesk_dbFetchAssoc($rs));
$this->close();
return $user;
}
/**
* @param $heskSettings array
* @return UserContext[]
*/
function getUsersByNumberOfOpenTicketsForAutoassign($heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT `t1`.*,
(SELECT COUNT(*) FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets`
WHERE `owner`=`t1`.`id`
AND `status` IN (
SELECT `ID` FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "statuses`
WHERE `IsClosed` = 0
)
) AS `open_tickets`
FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "users` AS `t1`
WHERE `t1`.`autoassign` = '1' ORDER BY `open_tickets` ASC, RAND()");
$users = array();
while ($row = hesk_dbFetchAssoc($rs)) {
$user = UserContext::fromDataRow($row);
$users[] = $user;
}
$this->close();
return $users;
}
/**
* @param $heskSettings array
* @return UserContext[]
*/
function getUsersForNewTicketNotification($heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "users` WHERE `notify_new_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();
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "users`
WHERE `id` = (
SELECT `manager`
FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "categories`
WHERE `id` = " . intval($categoryId) . ")");
if (hesk_dbNumRows($rs) === 0) {
$this->close();
return null;
}
$user = UserContext::fromDataRow(hesk_dbFetchAssoc($rs));
$this->close();
return $user;
}
}

@ -0,0 +1,23 @@
<?php
namespace DataAccess\Settings;
use DataAccess\CommonDao;
class ModsForHeskSettingsGateway extends CommonDao {
function getAllSettings($heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT `Key`, `Value` FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "settings` WHERE `Key` <> 'modsForHeskVersion'");
$settings = array();
while ($row = hesk_dbFetchAssoc($rs)) {
$settings[$row['Key']] = $row['Value'];
}
$this->close();
return $settings;
}
}

@ -0,0 +1,56 @@
<?php
namespace DataAccess\Statuses;
use BusinessLogic\Statuses\Status;
use DataAccess\CommonDao;
class StatusGateway extends CommonDao {
/**
* @param $defaultAction string
* @return Status
*/
function getStatusForDefaultAction($defaultAction, $heskSettings) {
$this->init();
$metaRs = hesk_dbQuery('SELECT * FROM `' . hesk_dbEscape($heskSettings['db_pfix']) . 'statuses`
WHERE `' . $defaultAction . '` = 1');
if (hesk_dbNumRows($metaRs) === 0) {
return null;
}
$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;
}
/**
* @param $heskSettings array
* @return Status[]
*/
function getStatuses($heskSettings) {
$this->init();
$metaRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "statuses`");
$statuses = array();
while ($row = hesk_dbFetchAssoc($metaRs)) {
$languageRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "text_to_status_xref`
WHERE `status_id` = " . intval($row['ID']));
$statuses[] = Status::fromDatabase($row, $languageRs);
}
$this->close();
return $statuses;
}
}

@ -0,0 +1,361 @@
<?php
namespace DataAccess\Tickets;
use BusinessLogic\Attachments\AttachmentType;
use BusinessLogic\Tickets\Attachment;
use BusinessLogic\Tickets\Ticket;
use BusinessLogic\Tickets\TicketGatewayGeneratedFields;
use DataAccess\CommonDao;
class TicketGateway extends CommonDao {
/**
* @param $id int
* @param $heskSettings array
* @return Ticket|null
*/
function getTicketById($id, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets` WHERE `id` = " . intval($id));
if (hesk_dbNumRows($rs) === 0) {
return null;
}
$row = hesk_dbFetchAssoc($rs);
$linkedTicketsRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets` WHERE `parent` = " . intval($id));
$repliesRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "replies` WHERE `replyto` = " . intval($id) . " ORDER BY `id` ASC");
$ticket = Ticket::fromDatabaseRow($row, $linkedTicketsRs, $repliesRs, $heskSettings);
$this->close();
return $ticket;
}
/**
* @param $emailAddress string
* @param $heskSettings array
* @return array|null
*/
function getTicketsByEmail($emailAddress, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets`
WHERE `email` = '" . hesk_dbEscape($emailAddress) . "'");
if (hesk_dbNumRows($rs) === 0) {
return null;
}
$tickets = array();
while ($row = hesk_dbFetchAssoc($rs)) {
$linkedTicketsRs =
hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets` WHERE `parent` = " . intval($row['id']));
$repliesRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "replies` WHERE `replyto` = " . intval($row['id']) . " ORDER BY `id` ASC");
$tickets[] = Ticket::fromDatabaseRow($row, $linkedTicketsRs, $repliesRs, $heskSettings);
}
$this->close();
return $tickets;
}
/**
* @param $trackingId string
* @param $heskSettings array
* @return bool
*/
function doesTicketExist($trackingId, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT 1 FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets`
WHERE `trackid` = '" . hesk_dbEscape($trackingId) . "'");
$ticketExists = hesk_dbNumRows($rs) > 0;
$this->close();
return $ticketExists;
}
/**
* @param $trackingId string
* @param $heskSettings array
* @return Ticket|null
*/
function getTicketByTrackingId($trackingId, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets` WHERE `trackid` = " . intval($trackingId));
if (hesk_dbNumRows($rs) === 0) {
return null;
}
$row = hesk_dbFetchAssoc($rs);
$linkedTicketsRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets` WHERE `parent` = " . intval($trackingId));
$repliesRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "replies` WHERE `replyto` = " . intval($row['id']) . " ORDER BY `id` ASC");
$ticket = Ticket::fromDatabaseRow($row, $linkedTicketsRs, $repliesRs, $heskSettings);
$this->close();
return $ticket;
}
/**
* @param $trackingId string
* @param $heskSettings array
* @return Ticket|null
*/
function getTicketByMergedTrackingId($trackingId, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT `trackid` FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets` WHERE `merged` LIKE '%#" . hesk_dbEscape($trackingId) . "#%'");
if (hesk_dbNumRows($rs) === 0) {
return null;
}
$row = hesk_dbFetchAssoc($rs);
$actualTrackingId = $row['trackid'];
$this->close();
return $this->getTicketByTrackingId($actualTrackingId, $heskSettings);
}
/**
* @param $ticket Ticket
* @param $isEmailVerified
* @param $heskSettings
* @return TicketGatewayGeneratedFields
*/
function createTicket($ticket, $isEmailVerified, $heskSettings) {
$this->init();
$dueDate = $ticket->dueDate ? "'{$ticket->dueDate}'" : "NULL";
// Prepare SQL for custom fields
$customWhere = '';
$customWhat = '';
for ($i=1; $i<=50; $i++)
{
$customWhere .= ", `custom{$i}`";
$customWhat .= ", '" . (isset($ticket->customFields[$i]) ? hesk_dbEscape($ticket->customFields[$i]) : '') . "'";
}
$suggestedArticles = 'NULL';
if ($ticket->suggestedArticles !== null && !empty($ticket->suggestedArticles)) {
$suggestedArticles = "'" .implode(',', $ticket->suggestedArticles) . "'";
}
$latitude = $ticket->location !== null
&& isset($ticket->location[0])
&& $ticket->location[0] !== null ? $ticket->location[0] : 'E-0';
$longitude = $ticket->location !== null
&& isset($ticket->location[1])
&& $ticket->location[1] !== null ? $ticket->location[1] : 'E-0';
$userAgent = $ticket->userAgent !== null ? $ticket->userAgent : '';
$screenResolutionWidth = $ticket->screenResolution !== null
&& isset($ticket->screenResolution[0])
&& $ticket->screenResolution[0] !== null ? intval($ticket->screenResolution[0]) : 'NULL';
$screenResolutionHeight = $ticket->screenResolution !== null
&& isset($ticket->screenResolution[1])
&& $ticket->screenResolution[1] !== null ? intval($ticket->screenResolution[1]) : 'NULL';
$ipAddress = $ticket->ipAddress !== null
&& $ticket->ipAddress !== '' ? $ticket->ipAddress : '';
$tableName = $isEmailVerified ? 'tickets' : 'stage_tickets';
$sql = "INSERT INTO `" . hesk_dbEscape($heskSettings['db_pfix']) . $tableName ."`
(
`trackid`,
`name`,
`email`,
`category`,
`priority`,
`subject`,
`message`,
`dt`,
`lastchange`,
`articles`,
`ip`,
`language`,
`openedby`,
`owner`,
`attachments`,
`merged`,
`status`,
`latitude`,
`longitude`,
`html`,
`user_agent`,
`screen_resolution_height`,
`screen_resolution_width`,
`due_date`,
`history`
{$customWhere}
)
VALUES
(
'" . hesk_dbEscape($ticket->trackingId) . "',
'" . hesk_dbEscape($ticket->name) . "',
'" . hesk_dbEscape($ticket->email) . "',
'" . intval($ticket->categoryId) . "',
'" . intval($ticket->priorityId) . "',
'" . hesk_dbEscape($ticket->subject) . "',
'" . hesk_dbEscape($ticket->message) . "',
NOW(),
NOW(),
" . $suggestedArticles . ",
'" . hesk_dbEscape($ipAddress) . "',
'" . hesk_dbEscape($ticket->language) . "',
'" . intval($ticket->openedBy) . "',
'" . intval($ticket->ownerId) . "',
'" . hesk_dbEscape($ticket->getAttachmentsForDatabase()) . "',
'',
" . intval($ticket->statusId) . ",
'" . hesk_dbEscape($latitude) . "',
'" . hesk_dbEscape($longitude) . "',
'" . hesk_dbEscape($ticket->usesHtml) . "',
'" . hesk_dbEscape($userAgent) . "',
" . hesk_dbEscape($screenResolutionHeight) . ",
" . hesk_dbEscape($screenResolutionWidth) . ",
{$dueDate},
'" . hesk_dbEscape($ticket->auditTrailHtml) . "'
{$customWhat}
)
";
hesk_dbQuery($sql);
$id = hesk_dbInsertID();
$rs = hesk_dbQuery('SELECT `dt`, `lastchange` FROM `' . hesk_dbEscape($heskSettings['db_pfix']) . $tableName .'` WHERE `id` = ' . intval($id));
$row = hesk_dbFetchAssoc($rs);
$generatedFields = new TicketGatewayGeneratedFields();
$generatedFields->id = $id;
$generatedFields->dateCreated = $row['dt'];
$generatedFields->dateModified = $row['lastchange'];
$this->close();
return $generatedFields;
}
/**
* @param $ticketId int
* @param $attachments Attachment[]
* @param $heskSettings array
*
* Crappy logic that should just be pulled from the attachments table, but using for backwards compatibility
*/
function updateAttachmentsForTicket($ticketId, $attachments, $heskSettings) {
$this->init();
$this->updateAttachmentsFor($ticketId, $attachments, AttachmentType::MESSAGE, $heskSettings);
$this->close();
}
private function updateAttachmentsFor($id, $attachments, $attachmentType, $heskSettings) {
$attachmentStrings = array();
foreach ($attachments as $attachment) {
$attachmentStrings[] = "{$attachment->id}#{$attachment->fileName}#{$attachment->savedName}";
}
$attachmentStringToSave = implode(',', $attachmentStrings);
$tableName = $attachmentType == AttachmentType::MESSAGE ? 'tickets' : 'replies';
hesk_dbQuery("UPDATE `" . hesk_dbEscape($heskSettings['db_pfix']) . $tableName . "`
SET `attachments` = '" . hesk_dbEscape($attachmentStringToSave) . "'
WHERE `id` = " . intval($id));
}
/**
* @param $replyId int
* @param $attachments Attachment[]
* @param $heskSettings array
*
* Crappy logic that should just be pulled from the attachments table, but using for backwards compatibility
*/
function updateAttachmentsForReply($replyId, $attachments, $heskSettings) {
$this->init();
$this->updateAttachmentsFor($replyId, $attachments, AttachmentType::REPLY, $heskSettings);
$this->close();
}
function deleteRepliesForTicket($ticketId, $heskSettings) {
$this->init();
hesk_dbQuery("DELETE FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "replies` WHERE `replyto` = " . intval($ticketId));
$this->close();
}
function deleteReplyDraftsForTicket($ticketId, $heskSettings) {
$this->init();
hesk_dbQuery("DELETE FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "reply_drafts` WHERE `ticket`=" . intval($ticketId));
$this->close();
}
function deleteNotesForTicket($ticketId, $heskSettings) {
$this->init();
hesk_dbQuery("DELETE FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "notes` WHERE `ticket`='" . intval($ticketId) . "'");
$this->close();
}
/**
* @param $ticketId int
* @param $heskSettings array
*/
function deleteTicket($ticketId, $heskSettings) {
$this->init();
hesk_dbQuery("DELETE FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets` WHERE `id` = " . intval($ticketId));
$this->close();
}
/**
* @param $ticket Ticket
* @param $heskSettings array
*/
function updateBasicTicketInfo($ticket, $heskSettings) {
$this->init();
// Escaped vars
$subject = hesk_dbEscape($ticket->subject);
$message = hesk_dbEscape($ticket->message);
$language = hesk_dbEscape($ticket->language);
$name = hesk_dbEscape($ticket->name);
$email = hesk_dbEscape($ticket->email);
// Prepare SQL for custom fields
$customSql = '';
for ($i=1; $i<=50; $i++)
{
$customSql .= ", `custom{$i}` = '" . (isset($ticket->customFields[$i]) ? hesk_dbEscape($ticket->customFields[$i]) : '') . "'";
}
hesk_dbQuery("UPDATE `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets`
SET `subject` = '{$subject}',
`message` = '{$message}',
`language` = '{$language}',
`name` = '{$name}',
`email` = '{$email}',
`html` = " . ($ticket->usesHtml ? 1 : 0) . ",
{$customSql}
WHERE `id` = " . intval($ticket->id));
$this->close();
}
}

@ -0,0 +1,16 @@
<?php
namespace DataAccess\Tickets;
use DataAccess\CommonDao;
class VerifiedEmailGateway extends CommonDao {
function isEmailVerified($emailAddress, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT 1 FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "verified_emails` WHERE `Email` = '" . hesk_dbEscape($emailAddress) . "'");
return hesk_dbNumRows($rs) > 0;
}
}

@ -0,0 +1,211 @@
<?php
/**
* @class Main class of the Link router that helps you create and deploy routes
*/
class Link
{
/**
* @var array A collection of the routes originally passed into all function. Used by static function route
*/
private static $routes = array();
/**
* @var array A collection of functions that are executed before a route completion ( valid for all routes ), aka universal before functions
*/
private static $beforeFuncs = array();
/**
* @var array A collection of function that are executed after a route completion ( valid for all routes ), aka universal after functions
*/
private static $afterFuncs = array();
/**
* Static function of the class Link that deploys the route according to the passed handler and path
*
* @param array $routes An array of combination of the path and its handler, that are final deployed for a particular url
*/
public static function all( $routes )
{
/* Call all functions that are to be executed before routing */
foreach( self::$beforeFuncs as $beforeFunc ) {
if( $beforeFunc[1] ) {
call_user_func_array( $beforeFunc[0] , $beforeFunc[1] );
} else {
call_user_func( $beforeFunc[0] );
}
}
self::$routes = $routes;
$method = strtolower($_SERVER['REQUEST_METHOD']);
$path = '/';
$handler = null;
$matched = array();
if ( !empty ( $_SERVER['PATH_INFO'] ) ) {
$path = $_SERVER['PATH_INFO'];
} else if ( !empty ( $_SERVER['REQUEST_URI'] ) ) {
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
}
if ( isset($routes[$path] ) ) {
if( is_array( $routes[$path] ) ) {
$handler = $routes[$path][0];
} else {
$handler = $routes[$path];
}
} else if ( $routes ) {
$regex = array(
'/{i}/',
'/{s}/',
'/{a}/'
);
$replacements = array(
'([\d]+)' ,
'([a-zA-Z]+)',
'([\w-]+)'
);
foreach ( $routes as $routePath => $routeDesc ){
$routePath = preg_replace( $regex, $replacements, $routePath );
if( preg_match( '#^/?' . $routePath . '/?$#', $path, $matches ) ){
if( is_array( $routeDesc ) ) {
$handler = $routeDesc[0];
if( isset( $routeDesc[2] )) {
$middleware = $routeDesc[2];
}
}
else
$handler = $routeDesc;
$matched = $matches;
break;
}
}
}
unset( $matched[0] );
if( isset($middleware) ){
$newMatched = self::callFunction( $middleware, $matched, $method );
/* If new wildcard param are there pass them to main handler */
if( $newMatched ) {
self::callFunction( $handler, $newMatched, $method );
} else {
self::callFunction( $handler, $matched, $method );
}
} else {
self::callFunction( $handler, $matched, $method );
}
/* Call all the function that are to be executed after routing */
foreach( self::$afterFuncs as $afterFunc )
if( $afterFunc[1] ) {
call_user_func_array( $afterFunc[0] , $afterFunc[1] );
} else {
call_user_func( $afterFunc[0] );
}
}
/**
* Static function that helps you generate links effortlessly and pass parameters to them, thus enabling to generate dynamic links
*
* @param string $name name of the route for which the link has to be generated
* @param array $params An array of parameters that are replaced in the route if it contains wildcards
* For e.g. if route is /name/{i}/{a} and parameters passed are 1, aps then link generated will be /name/1/aps
*/
public static function route( $name, $params = array() )
{
$href = null;
foreach ( self::$routes as $routePath => $routeDesc ) {
if( is_array( $routeDesc ) ){
if( $name == $routeDesc[1] ){
$href = $routePath;
for( $i = 0; $i < count($params); $i++){
$href = preg_replace('#{(.*?)}#', $params[$i], $href, 1);
}
}
}
}
return $href;
}
/**
* Static function to handle cases when route is not found, call handler of 404 if defined else
* sends a 404 header
*/
public static function handle404()
{
/* Call '404' route if it exists */
if( isset ( self::$routes['404'] ) ) {
call_user_func( self::$routes['404'] );
} else {
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
}
}
/**
* Static function to handle both middlewares' call and main handler's call.
*
* @param array|string $handler Handler that will handle the routes call or middleware
* @param array $matched The parameters that we get from the route wildcard
* @return array $newParams The parameters return in the case of middleware if you intend to
* the wildcards that were originally passed, this newParams will
* be next passed to main handler
*/
public static function callFunction( $handler , $matched, $method )
{
if ( $handler ) {
if ( is_callable( $handler ) ) {
$newParams = call_user_func_array( $handler, $matched ) ;
} else {
/* Check if class exists in the case user is using RESTful pattern */
if( class_exists( $handler ) ) {
$instanceOfHandler = new $handler(); // Won't work in case of middleware since we aren't using RESTful in that
} else {
print_r('Class or function ' . $handler . ' not found');
header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);
die();
}
}
} else {
self::handle404();
}
if( isset( $instanceOfHandler ) ) {
if( method_exists( $instanceOfHandler, $method ) ) {
$newParams = call_user_func_array( array( $instanceOfHandler, $method ), $matched );
}
}
if( isset( $newParams ) && $newParams ) {
return $newParams;
}
}
/**
* Static function to add functions that are to be excuted before each routing, must be called before Link::all
*
* @param string $funcName Name of the funtion to be called upon before
* @param array $params Array of parameters that are to be passed to before function, can be null but if not
* it must be an array
*/
public static function before( $funcName, $params = null )
{
array_push( self::$beforeFuncs, [ $funcName, $params ]);
}
/**
* Static function to add functions that are to be excuted after each routing, must be called before Link::all
*
* @param string $funcName Name of the funtion to be called upon after
* @param array $params Array of parameters that are to be passed to after function, can be null but if not
* it must be an array
*/
public static function after( $funcName, $params = null )
{
array_push( self::$afterFuncs, [ $funcName, $params ]);
}
}

@ -0,0 +1,38 @@
<?php
class AutoLoader {
static private $classNames = array();
/**
* Store the filename (sans extension) & full path of all ".php" files found
*/
public static function registerDirectory($dirName) {
$di = new DirectoryIterator($dirName);
foreach ($di as $file) {
if ($file->isDir() && !$file->isLink() && !$file->isDot()) {
// recurse into directories other than a few special ones
self::registerDirectory($file->getPathname());
} elseif (substr($file->getFilename(), -4) === '.php') {
// save the class name / path of a .php file found
$className = substr($file->getFilename(), 0, -4);
AutoLoader::registerClass($className, $file->getPathname());
}
}
}
public static function registerClass($className, $fileName) {
AutoLoader::$classNames[$className] = $fileName;
}
public static function loadClass($className) {
if (isset(AutoLoader::$classNames[$className])) {
require_once(AutoLoader::$classNames[$className]);
}
}
}
spl_autoload_register(array('AutoLoader', 'loadClass'));

@ -0,0 +1,330 @@
<?php
namespace BusinessLogic\Attachments;
use BusinessLogic\Exceptions\ValidationException;
use BusinessLogic\Security\UserContext;
use BusinessLogic\Security\UserPrivilege;
use BusinessLogic\Security\UserToTicketChecker;
use BusinessLogic\Tickets\Reply;
use BusinessLogic\Tickets\Ticket;
use DataAccess\Attachments\AttachmentGateway;
use DataAccess\Files\FileDeleter;
use DataAccess\Files\FileWriter;
use DataAccess\Tickets\TicketGateway;
use PHPUnit\Framework\TestCase;
class AttachmentHandlerTest extends TestCase {
/* @var $attachmentHandler AttachmentHandler */
private $attachmentHandler;
/* @var $createAttachmentModel CreateAttachmentForTicketModel */
private $createAttachmentForTicketModel;
/* @var $ticketGateway \PHPUnit_Framework_MockObject_MockObject */
private $ticketGateway;
/* @var $attachmentGateway \PHPUnit_Framework_MockObject_MockObject */
private $attachmentGateway;
/* @var $userToTicketChecker \PHPUnit_Framework_MockObject_MockObject */
private $userToTicketChecker;
/* @var $fileWriter \PHPUnit_Framework_MockObject_MockObject */
private $fileWriter;
/* @var $fileDeleter \PHPUnit_Framework_MockObject_MockObject */
private $fileDeleter;
/* @var $userContext UserContext */
private $userContext;
/* @var $heskSettings array */
private $heskSettings;
protected function setUp() {
$this->ticketGateway = $this->createMock(TicketGateway::class);
$this->attachmentGateway = $this->createMock(AttachmentGateway::class);
$this->fileWriter = $this->createMock(FileWriter::class);
$this->fileDeleter = $this->createMock(FileDeleter::class);
$this->userToTicketChecker = $this->createMock(UserToTicketChecker::class);
$this->heskSettings = array(
'attach_dir' => 'attachments',
'attachments' => array(
'allowed_types' => array('.txt'),
'max_size' => 999
)
);
$this->attachmentHandler = new AttachmentHandler($this->ticketGateway,
$this->attachmentGateway,
$this->fileWriter,
$this->userToTicketChecker,
$this->fileDeleter);
$this->createAttachmentForTicketModel = new CreateAttachmentForTicketModel();
$this->createAttachmentForTicketModel->attachmentContents = base64_encode('string');
$this->createAttachmentForTicketModel->displayName = 'DisplayName.txt';
$this->createAttachmentForTicketModel->ticketId = 1;
$this->createAttachmentForTicketModel->type = AttachmentType::MESSAGE;
$this->userContext = new UserContext();
}
function testThatValidateThrowsAnExceptionWhenTheAttachmentBodyIsNull() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->attachmentContents = null;
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/CONTENTS_EMPTY/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testThatValidateThrowsAnExceptionWhenTheAttachmentBodyIsEmpty() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->attachmentContents = '';
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/CONTENTS_EMPTY/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testThatValidateThrowsAnExceptionWhenTheAttachmentBodyIsInvalidBase64() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->attachmentContents = 'invalid base 64';
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/CONTENTS_NOT_BASE_64/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testThatValidateThrowsAnExceptionWhenTheDisplayNameIsNull() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->displayName = null;
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/DISPLAY_NAME_EMPTY/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testThatValidateThrowsAnExceptionWhenTheDisplayNameIsEmpty() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->displayName = '';
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/DISPLAY_NAME_EMPTY/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testThatValidateThrowsAnExceptionWhenTheTicketIdIsNull() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->ticketId = null;
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/TICKET_ID_MISSING/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testThatValidateThrowsAnExceptionWhenTheTicketIdIsANonPositiveInteger() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->ticketId = 0;
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/TICKET_ID_MISSING/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testThatValidateThrowsAnExceptionWhenTheFileExtensionIsNotPermitted() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->heskSettings['attachments']['allowed_types'] = array('.gif');
$this->createAttachmentForTicketModel->ticketId = 0;
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/EXTENSION_NOT_PERMITTED/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testThatValidateThrowsAnExceptionWhenTheFileSizeIsLargerThanMaxPermitted() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->attachmentContents = base64_encode("msg");
$this->heskSettings['attachments']['max_size'] = 1;
//-- Assert
$this->expectException(ValidationException::class);
$this->expectExceptionMessageRegExp('/FILE_SIZE_TOO_LARGE/');
//-- Act
$this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
}
function testItSavesATicketWithTheProperProperties() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->ticketId = 1;
$ticket = new Ticket();
$ticket->trackingId = 'ABC-DEF-1234';
$this->ticketGateway->method('getTicketById')->with(1, $this->anything())->willReturn($ticket);
$ticketAttachment = new TicketAttachment();
$ticketAttachment->displayName = $this->createAttachmentForTicketModel->displayName;
$ticketAttachment->ticketTrackingId = $ticket->trackingId;
$ticketAttachment->type = 0;
$ticketAttachment->downloadCount = 0;
$ticketAttachment->id = 50;
$this->attachmentGateway->method('createAttachmentForTicket')->willReturn(50);
//-- Act
$actual = $this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
//-- Assert
self::assertThat($actual->id, self::equalTo(50));
self::assertThat($actual->downloadCount, self::equalTo(0));
self::assertThat($actual->type, self::equalTo(AttachmentType::MESSAGE));
self::assertThat($actual->ticketTrackingId, self::equalTo($ticket->trackingId));
self::assertThat($actual->displayName, self::equalTo($this->createAttachmentForTicketModel->displayName));
}
function testItSavesTheFileToTheFileSystem() {
//-- Arrange
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
$this->createAttachmentForTicketModel->ticketId = 1;
$ticket = new Ticket();
$ticket->trackingId = 'ABC-DEF-1234';
$this->ticketGateway->method('getTicketById')->with(1, $this->anything())->willReturn($ticket);
$ticketAttachment = new TicketAttachment();
$ticketAttachment->displayName = $this->createAttachmentForTicketModel->displayName;
$ticketAttachment->ticketTrackingId = $ticket->trackingId;
$ticketAttachment->type = AttachmentType::MESSAGE;
$ticketAttachment->downloadCount = 0;
$ticketAttachment->id = 50;
$this->fileWriter->method('writeToFile')->willReturn(1024);
$this->attachmentGateway->method('createAttachmentForTicket')->willReturn(50);
//-- Act
$actual = $this->attachmentHandler->createAttachmentForTicket($this->createAttachmentForTicketModel, $this->userContext, $this->heskSettings);
//-- Assert
self::assertThat($actual->fileSize, self::equalTo(1024));
}
//-- TODO Test UserToTicketChecker
function testDeleteThrowsAnExceptionWhenTheUserDoesNotHaveAccessToTheTicket() {
//-- Arrange
$ticketId = 1;
$ticket = new Ticket();
$this->ticketGateway->method('getTicketById')
->with($ticketId, $this->heskSettings)->willReturn($ticket);
$this->userToTicketChecker->method('isTicketAccessibleToUser')
->with($this->userContext, $ticket, $this->heskSettings, array(UserPrivilege::CAN_EDIT_TICKETS))
->willReturn(false);
//-- Assert
$this->expectException(\Exception::class);
$this->expectExceptionMessage("User does not have access to ticket {$ticketId} being created / edited!");
//-- Act
$this->attachmentHandler->deleteAttachmentFromTicket($ticketId, 1, $this->userContext, $this->heskSettings);
}
function testDeleteActuallyDeletesTheFile() {
//-- Arrange
$ticketId = 1;
$ticket = new Ticket();
$attachment = new Attachment();
$attachment->id = 5;
$attachment->savedName = 'foobar.txt';
$this->heskSettings['attach_dir'] = 'attach-dir';
$ticket->attachments = array($attachment);
$ticket->replies = array();
$this->ticketGateway->method('getTicketById')->willReturn($ticket);
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
//-- Assert
$this->fileDeleter->expects($this->once())->method('deleteFile')->with('foobar.txt', 'attach-dir');
//-- Act
$this->attachmentHandler->deleteAttachmentFromTicket($ticketId, 5, $this->userContext, $this->heskSettings);
}
function testDeleteUpdatesTheTicketItselfAndSavesIt() {
//-- Arrange
$ticketId = 1;
$ticket = new Ticket();
$ticket->replies = array();
$attachment = new Attachment();
$attachment->id = 5;
$attachment->savedName = 'foobar.txt';
$this->heskSettings['attach_dir'] = 'attach-dir';
$ticket->attachments = array($attachment);
$this->ticketGateway->method('getTicketById')->willReturn($ticket);
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
//-- Assert
$this->ticketGateway->expects($this->once())->method('updateAttachmentsForTicket');
//-- Act
$this->attachmentHandler->deleteAttachmentFromTicket($ticketId, 5, $this->userContext, $this->heskSettings);
}
function testDeleteHandlesReplies() {
//-- Arrange
$ticketId = 1;
$ticket = new Ticket();
$reply = new Reply();
$reply->id = 10;
$attachment = new Attachment();
$attachment->id = 5;
$attachment->savedName = 'foobar.txt';
$this->heskSettings['attach_dir'] = 'attach-dir';
$reply->attachments = array($attachment);
$ticket->replies = array(10 => $reply);
$this->ticketGateway->method('getTicketById')->willReturn($ticket);
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
//-- Assert
$this->ticketGateway->expects($this->once())->method('updateAttachmentsForReply');
//-- Act
$this->attachmentHandler->deleteAttachmentFromTicket($ticketId, 5, $this->userContext, $this->heskSettings);
}
}

@ -0,0 +1,65 @@
<?php
namespace BusinessLogic\Attachments;
use BusinessLogic\Security\UserContext;
use BusinessLogic\Security\UserToTicketChecker;
use DataAccess\Attachments\AttachmentGateway;
use DataAccess\Files\FileReader;
use DataAccess\Tickets\TicketGateway;
use PHPUnit\Framework\TestCase;
class AttachmentRetrieverTest extends TestCase {
/* @var $attachmentGateway \PHPUnit_Framework_MockObject_MockObject */
private $attachmentGateway;
/* @var $fileReader \PHPUnit_Framework_MockObject_MockObject */
private $fileReader;
/* @var $ticketGateway \PHPUnit_Framework_MockObject_MockObject */
private $ticketGateway;
/* @var $userToTicketChecker \PHPUnit_Framework_MockObject_MockObject */
private $userToTicketChecker;
/* @var $attachmentRetriever AttachmentRetriever */
private $attachmentRetriever;
/* @var $heskSettings array */
private $heskSettings;
protected function setUp() {
$this->attachmentGateway = $this->createMock(AttachmentGateway::class);
$this->fileReader = $this->createMock(FileReader::class);
$this->ticketGateway = $this->createMock(TicketGateway::class);
$this->userToTicketChecker = $this->createMock(UserToTicketChecker::class);
$this->heskSettings = array('attach_dir' => 'attachments');
$this->attachmentRetriever = new AttachmentRetriever($this->attachmentGateway, $this->fileReader,
$this->ticketGateway, $this->userToTicketChecker);
$this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
}
function testItGetsTheMetadataFromTheGateway() {
//-- Arrange
$attachmentMeta = new Attachment();
$attachmentMeta->savedName = '5';
$attachmentContents = 'string';
$expectedContents = base64_encode($attachmentContents);
$this->attachmentGateway->method('getAttachmentById')
->with(4, $this->heskSettings)
->willReturn($attachmentMeta);
$this->fileReader->method('readFromFile')
->with('5', $this->heskSettings['attach_dir'])
->willReturn($attachmentContents);
//-- Act
$actualContents = $this->attachmentRetriever->getAttachmentContentsForTicket(0, 4, new UserContext(), $this->heskSettings);
//-- Assert
self::assertThat($actualContents, self::equalTo($expectedContents));
}
}

@ -0,0 +1,109 @@
<?php
namespace BusinessLogic\Emails;
use BusinessLogic\IntegrationTestCaseBase;
use BusinessLogic\Tickets\Attachment;
class BasicEmailSenderIntegrationTest extends IntegrationTestCaseBase {
/**
* @var $emailSender BasicEmailSender;
*/
private $emailSender;
/**
* @var $heskSettings array
*/
private $heskSettings;
/**
* @var $modsForHeskSettings array
*/
private $modsForHeskSettings;
/**
* @var $attachmentsToPurge string[]
*/
private $attachmentsToPurge;
protected function setUp() {
global $hesk_settings, $modsForHesk_settings;
$this->skip();
if (!defined('IN_SCRIPT')) {
define('IN_SCRIPT', 1);
}
require(__DIR__ . '/../../../../hesk_settings.inc.php');
require(__DIR__ . '/../../integration_test_mfh_settings.php');
$this->emailSender = new BasicEmailSender();
$this->heskSettings = $hesk_settings;
$this->modsForHeskSettings = $modsForHesk_settings;
$this->attachmentsToPurge = array();
}
protected function tearDown() {
foreach ($this->attachmentsToPurge as $file) {
unlink($file);
}
}
function testItCanSendHtmlMail() {
//-- Arrange
//$hesk_settings['smtp'] = 0 //Uncomment this to use PHPMail
$emailBuilder = new EmailBuilder();
$emailBuilder->to = array('mfh1@mailinator.com');
$emailBuilder->cc = array('mfh2@mailinator.com');
$emailBuilder->bcc = array('mfh3@mailinator.com');
$emailBuilder->message = "Test PLAIN TEXT message";
$emailBuilder->htmlMessage = "Test <b>HTML</b> <i>message</i>";
$emailBuilder->subject = "BasicEmailSenderIntegrationTest";
$attachment = new Attachment();
$attachment->id = 1;
$attachment->fileName = "file.txt";
$attachment->savedName = "test1.txt";
$filename1 = __DIR__ . '/../../../../' . $this->heskSettings['attach_dir'] . '/' . $attachment->savedName;
file_put_contents($filename1, 'TEST DATA');
$otherAttachment = new Attachment();
$otherAttachment->id = 2;
$otherAttachment->fileName = "file2.txt";
$otherAttachment->savedName = "test2.txt";
$filename2 = __DIR__ . '/../../../../' . $this->heskSettings['attach_dir'] . '/' . $otherAttachment->savedName;
file_put_contents($filename2, 'TEST DATA 2');
$emailBuilder->attachments = array($attachment, $otherAttachment);
$this->attachmentsToPurge = array($filename1, $filename2);
//-- Act
$result = $this->emailSender->sendEmail($emailBuilder, $this->heskSettings, $this->modsForHeskSettings, true);
//-- Assert
if ($result !== true) {
$this->fail($result);
}
}
function testItCanSendPlaintextMail() {
//-- Arrange
//$hesk_settings['smtp'] = 0 //Uncomment this to use PHPMail
$emailBuilder = new EmailBuilder();
$emailBuilder->to = array('mfh1@mailinator.com');
$emailBuilder->cc = array('mfh2@mailinator.com');
$emailBuilder->bcc = array('mfh3@mailinator.com');
$emailBuilder->message = "Test PLAIN TEXT message";
$emailBuilder->subject = "BasicEmailSenderIntegrationTest";
//-- Act
$result = $this->emailSender->sendEmail($emailBuilder, $this->heskSettings, $this->modsForHeskSettings, false);
//-- Assert
if ($result !== true) {
$this->fail($result);
}
}
}

@ -0,0 +1,131 @@
<?php
namespace BusinessLogic\Emails;
use BusinessLogic\Tickets\Ticket;
use PHPUnit\Framework\TestCase;
class EmailSenderHelperTest extends TestCase {
/**
* @var $emailTemplateParser \PHPUnit_Framework_MockObject_MockObject
*/
private $emailTemplateParser;
/**
* @var $basicEmailSender \PHPUnit_Framework_MockObject_MockObject
*/
private $basicEmailSender;
/**
* @var $mailgunEmailSender \PHPUnit_Framework_MockObject_MockObject
*/
private $mailgunEmailSender;
/**
* @var $emailSenderHelper EmailSenderHelper
*/
private $emailSenderHelper;
/**
* @var $heskSettings array
*/
private $heskSettings;
/**
* @var $modsForHeskSettings array
*/
private $modsForHeskSettings;
protected function setUp() {
$this->emailTemplateParser = $this->createMock(EmailTemplateParser::class);
$this->basicEmailSender = $this->createMock(BasicEmailSender::class);
$this->mailgunEmailSender = $this->createMock(MailgunEmailSender::class);
$this->heskSettings = array(
'languages' => array(
'English' => array('folder' => 'en')
),
'custom_fields' => array()
);
$this->modsForHeskSettings = array(
'attachments' => 0,
'use_mailgun' => 0,
'html_emails' => 0
);
$this->emailSenderHelper = new EmailSenderHelper($this->emailTemplateParser, $this->basicEmailSender,
$this->mailgunEmailSender);
}
function testItParsesTheTemplateForTheTicket() {
//-- Arrange
$templateId = EmailTemplateRetriever::NEW_NOTE;
$languageCode = 'en';
$ticket = new Ticket();
$this->emailTemplateParser->method('getFormattedEmailForLanguage')->willReturn(new ParsedEmailProperties('Subject', 'Message', 'HTML Message'));
//-- Assert
$this->emailTemplateParser->expects($this->once())
->method('getFormattedEmailForLanguage')
->with($templateId, $languageCode, $ticket, $this->heskSettings, $this->modsForHeskSettings);
//-- Act
$this->emailSenderHelper->sendEmailForTicket($templateId, 'English', new Addressees(), $ticket, $this->heskSettings, $this->modsForHeskSettings);
}
function testItSendsTheEmailThroughTheMailgunEmailSender() {
//-- Arrange
$addressees = new Addressees();
$addressees->to = ['to@email'];
$addressees->cc = ['cc1', 'cc2'];
$addressees->bcc = ['bcc1', 'bcc2'];
$this->modsForHeskSettings['use_mailgun'] = 1;
$this->modsForHeskSettings['html_emails'] = true;
$expectedEmailBuilder = new EmailBuilder();
$expectedEmailBuilder->to = $addressees->to;
$expectedEmailBuilder->cc = $addressees->cc;
$expectedEmailBuilder->bcc = $addressees->bcc;
$expectedEmailBuilder->subject = 'Subject';
$expectedEmailBuilder->message = 'Message';
$expectedEmailBuilder->htmlMessage = 'HTML Message';
$this->emailTemplateParser->method('getFormattedEmailForLanguage')->willReturn(new ParsedEmailProperties('Subject', 'Message', 'HTML Message'));
//-- Assert
$this->mailgunEmailSender->expects($this->once())
->method('sendEmail')
->with($expectedEmailBuilder, $this->heskSettings, $this->modsForHeskSettings, true);
//-- Act
$this->emailSenderHelper->sendEmailForTicket(EmailTemplateRetriever::NEW_NOTE, 'English', $addressees, new Ticket(), $this->heskSettings, $this->modsForHeskSettings);
}
function testItSendsTheEmailThroughTheBasicEmailSender() {
//-- Arrange
$addressees = new Addressees();
$addressees->to = ['to@email'];
$addressees->cc = ['cc1', 'cc2'];
$addressees->bcc = ['bcc1', 'bcc2'];
$this->modsForHeskSettings['use_mailgun'] = 0;
$this->modsForHeskSettings['html_emails'] = true;
$expectedEmailBuilder = new EmailBuilder();
$expectedEmailBuilder->to = $addressees->to;
$expectedEmailBuilder->cc = $addressees->cc;
$expectedEmailBuilder->bcc = $addressees->bcc;
$expectedEmailBuilder->subject = 'Subject';
$expectedEmailBuilder->message = 'Message';
$expectedEmailBuilder->htmlMessage = 'HTML Message';
$this->emailTemplateParser->method('getFormattedEmailForLanguage')->willReturn(new ParsedEmailProperties('Subject', 'Message', 'HTML Message'));
//-- Assert
$this->basicEmailSender->expects($this->once())
->method('sendEmail')
->with($expectedEmailBuilder, $this->heskSettings, $this->modsForHeskSettings, true);
//-- Act
$this->emailSenderHelper->sendEmailForTicket(EmailTemplateRetriever::NEW_NOTE, 'English', $addressees, new Ticket(), $this->heskSettings, $this->modsForHeskSettings);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save