diff --git a/.gitignore b/.gitignore
index 1aafcd90..70d3d8c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/admin/view_message_log.php b/admin/view_message_log.php
index 351d0753..e92f1bc2 100644
--- a/admin/view_message_log.php
+++ b/admin/view_message_log.php
@@ -97,6 +97,7 @@ require_once(HESK_PATH . 'inc/show_admin_nav.inc.php');
|
|
|
+ |
diff --git a/api/ApplicationContext.php b/api/ApplicationContext.php
new file mode 100644
index 00000000..95b312b8
--- /dev/null
+++ b/api/ApplicationContext.php
@@ -0,0 +1,133 @@
+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]);
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Attachments/Attachment.php b/api/BusinessLogic/Attachments/Attachment.php
new file mode 100644
index 00000000..380d00b3
--- /dev/null
+++ b/api/BusinessLogic/Attachments/Attachment.php
@@ -0,0 +1,21 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Attachments/AttachmentRetriever.php b/api/BusinessLogic/Attachments/AttachmentRetriever.php
new file mode 100644
index 00000000..59fdcc54
--- /dev/null
+++ b/api/BusinessLogic/Attachments/AttachmentRetriever.php
@@ -0,0 +1,50 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Attachments/AttachmentType.php b/api/BusinessLogic/Attachments/AttachmentType.php
new file mode 100644
index 00000000..e353d84a
--- /dev/null
+++ b/api/BusinessLogic/Attachments/AttachmentType.php
@@ -0,0 +1,9 @@
+AttachmentType] */
+ public $type;
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Categories/Category.php b/api/BusinessLogic/Categories/Category.php
new file mode 100644
index 00000000..ab9bc1af
--- /dev/null
+++ b/api/BusinessLogic/Categories/Category.php
@@ -0,0 +1,53 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Emails/Addressees.php b/api/BusinessLogic/Emails/Addressees.php
new file mode 100644
index 00000000..4e71c8bf
--- /dev/null
+++ b/api/BusinessLogic/Emails/Addressees.php
@@ -0,0 +1,21 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Emails/EmailBuilder.php b/api/BusinessLogic/Emails/EmailBuilder.php
new file mode 100644
index 00000000..c5691df3
--- /dev/null
+++ b/api/BusinessLogic/Emails/EmailBuilder.php
@@ -0,0 +1,43 @@
+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']);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Emails/EmailTemplate.php b/api/BusinessLogic/Emails/EmailTemplate.php
new file mode 100644
index 00000000..02884679
--- /dev/null
+++ b/api/BusinessLogic/Emails/EmailTemplate.php
@@ -0,0 +1,27 @@
+languageKey = $languageKey === null ? $fileName : $languageKey;
+ $this->fileName = $fileName;
+ $this->forStaff = $forStaff;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Emails/EmailTemplateParser.php b/api/BusinessLogic/Emails/EmailTemplateParser.php
new file mode 100644
index 00000000..927a0d00
--- /dev/null
+++ b/api/BusinessLogic/Emails/EmailTemplateParser.php
@@ -0,0 +1,330 @@
+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("
","\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 .= "
" . $hesklang['fatt'];
+ } else {
+ $msg .= "\n\n\n" . $hesklang['fatt'];
+ }
+
+ foreach ($ticket->attachments as $attachment) {
+ if ($html) {
+ $msg .= "
{$attachment->fileName}
";
+ } 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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Emails/EmailTemplateRetriever.php b/api/BusinessLogic/Emails/EmailTemplateRetriever.php
new file mode 100644
index 00000000..6263d8a7
--- /dev/null
+++ b/api/BusinessLogic/Emails/EmailTemplateRetriever.php
@@ -0,0 +1,65 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Emails/MailgunEmailSender.php b/api/BusinessLogic/Emails/MailgunEmailSender.php
new file mode 100644
index 00000000..2d290094
--- /dev/null
+++ b/api/BusinessLogic/Emails/MailgunEmailSender.php
@@ -0,0 +1,71 @@
+"; // 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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Emails/ParsedEmailProperties.php b/api/BusinessLogic/Emails/ParsedEmailProperties.php
new file mode 100644
index 00000000..5cbe594f
--- /dev/null
+++ b/api/BusinessLogic/Emails/ParsedEmailProperties.php
@@ -0,0 +1,33 @@
+subject = $subject;
+ $this->message = $message;
+ $this->htmlMessage = $htmlMessage;
+ }
+
+ /**
+ * @var $subject string
+ */
+ public $subject;
+
+ /**
+ * @var $message string
+ */
+ public $message;
+
+ /**
+ * @var $htmlMessage string
+ */
+ public $htmlMessage;
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Exceptions/AccessViolationException.php b/api/BusinessLogic/Exceptions/AccessViolationException.php
new file mode 100644
index 00000000..1c252646
--- /dev/null
+++ b/api/BusinessLogic/Exceptions/AccessViolationException.php
@@ -0,0 +1,10 @@
+title = $title;
+ $this->httpResponseCode = $httpResponseCode;
+
+ parent::__construct($message);
+ }
+
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Exceptions/EmailTemplateNotFoundException.php b/api/BusinessLogic/Exceptions/EmailTemplateNotFoundException.php
new file mode 100644
index 00000000..9f1e63a3
--- /dev/null
+++ b/api/BusinessLogic/Exceptions/EmailTemplateNotFoundException.php
@@ -0,0 +1,17 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Helpers.php b/api/BusinessLogic/Helpers.php
new file mode 100644
index 00000000..61f6af2d
--- /dev/null
+++ b/api/BusinessLogic/Helpers.php
@@ -0,0 +1,29 @@
+ $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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Security/BanRetriever.php b/api/BusinessLogic/Security/BanRetriever.php
new file mode 100644
index 00000000..e44fa344
--- /dev/null
+++ b/api/BusinessLogic/Security/BanRetriever.php
@@ -0,0 +1,52 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Security/BannedEmail.php b/api/BusinessLogic/Security/BannedEmail.php
new file mode 100644
index 00000000..691a6604
--- /dev/null
+++ b/api/BusinessLogic/Security/BannedEmail.php
@@ -0,0 +1,26 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Security/UserContextBuilder.php b/api/BusinessLogic/Security/UserContextBuilder.php
new file mode 100644
index 00000000..65c4972e
--- /dev/null
+++ b/api/BusinessLogic/Security/UserContextBuilder.php
@@ -0,0 +1,87 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Security/UserContextNotifications.php b/api/BusinessLogic/Security/UserContextNotifications.php
new file mode 100644
index 00000000..9c32547c
--- /dev/null
+++ b/api/BusinessLogic/Security/UserContextNotifications.php
@@ -0,0 +1,16 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Settings/ApiChecker.php b/api/BusinessLogic/Settings/ApiChecker.php
new file mode 100644
index 00000000..863a6e06
--- /dev/null
+++ b/api/BusinessLogic/Settings/ApiChecker.php
@@ -0,0 +1,21 @@
+modsForHeskSettingsGateway = $modsForHeskSettingsGateway;
+ }
+
+ function isApiEnabled($heskSettings) {
+ $modsForHeskSettings = $this->modsForHeskSettingsGateway->getAllSettings($heskSettings);
+
+ return intval($modsForHeskSettings['public_api']) === 1;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Settings/SettingsRetriever.php b/api/BusinessLogic/Settings/SettingsRetriever.php
new file mode 100644
index 00000000..e8db0e48
--- /dev/null
+++ b/api/BusinessLogic/Settings/SettingsRetriever.php
@@ -0,0 +1,86 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Statuses/Closable.php b/api/BusinessLogic/Statuses/Closable.php
new file mode 100644
index 00000000..9d87efca
--- /dev/null
+++ b/api/BusinessLogic/Statuses/Closable.php
@@ -0,0 +1,11 @@
+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;
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Statuses/StatusRetriever.php b/api/BusinessLogic/Statuses/StatusRetriever.php
new file mode 100644
index 00000000..0bcfe844
--- /dev/null
+++ b/api/BusinessLogic/Statuses/StatusRetriever.php
@@ -0,0 +1,20 @@
+statusGateway = $statusGateway;
+ }
+
+ function getAllStatuses($heskSettings) {
+ return $this->statusGateway->getStatuses($heskSettings);
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/Attachment.php b/api/BusinessLogic/Tickets/Attachment.php
new file mode 100644
index 00000000..295864bc
--- /dev/null
+++ b/api/BusinessLogic/Tickets/Attachment.php
@@ -0,0 +1,21 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/CreateTicketByCustomerModel.php b/api/BusinessLogic/Tickets/CreateTicketByCustomerModel.php
new file mode 100644
index 00000000..7163e6b8
--- /dev/null
+++ b/api/BusinessLogic/Tickets/CreateTicketByCustomerModel.php
@@ -0,0 +1,80 @@
+ticket = $ticket;
+ $this->emailVerified = $emailVerified;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/CustomFields/CustomFieldValidator.php b/api/BusinessLogic/Tickets/CustomFields/CustomFieldValidator.php
new file mode 100644
index 00000000..12e1ccc7
--- /dev/null
+++ b/api/BusinessLogic/Tickets/CustomFields/CustomFieldValidator.php
@@ -0,0 +1,18 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/Reply.php b/api/BusinessLogic/Tickets/Reply.php
new file mode 100644
index 00000000..77c1d4b9
--- /dev/null
+++ b/api/BusinessLogic/Tickets/Reply.php
@@ -0,0 +1,62 @@
+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();
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/TicketCreator.php b/api/BusinessLogic/Tickets/TicketCreator.php
new file mode 100644
index 00000000..245da2a1
--- /dev/null
+++ b/api/BusinessLogic/Tickets/TicketCreator.php
@@ -0,0 +1,194 @@
+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 NOT 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);
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/TicketDeleter.php b/api/BusinessLogic/Tickets/TicketDeleter.php
new file mode 100644
index 00000000..fde51ee7
--- /dev/null
+++ b/api/BusinessLogic/Tickets/TicketDeleter.php
@@ -0,0 +1,59 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/TicketEditor.php b/api/BusinessLogic/Tickets/TicketEditor.php
new file mode 100644
index 00000000..a5fe9774
--- /dev/null
+++ b/api/BusinessLogic/Tickets/TicketEditor.php
@@ -0,0 +1,137 @@
+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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/TicketGatewayGeneratedFields.php b/api/BusinessLogic/Tickets/TicketGatewayGeneratedFields.php
new file mode 100644
index 00000000..56c2a8b3
--- /dev/null
+++ b/api/BusinessLogic/Tickets/TicketGatewayGeneratedFields.php
@@ -0,0 +1,10 @@
+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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/TicketValidators.php b/api/BusinessLogic/Tickets/TicketValidators.php
new file mode 100644
index 00000000..15a53bb3
--- /dev/null
+++ b/api/BusinessLogic/Tickets/TicketValidators.php
@@ -0,0 +1,30 @@
+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'];
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/TrackingIdGenerator.php b/api/BusinessLogic/Tickets/TrackingIdGenerator.php
new file mode 100644
index 00000000..623ab1fe
--- /dev/null
+++ b/api/BusinessLogic/Tickets/TrackingIdGenerator.php
@@ -0,0 +1,137 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Tickets/VerifiedEmailChecker.php b/api/BusinessLogic/Tickets/VerifiedEmailChecker.php
new file mode 100644
index 00000000..fccb32fc
--- /dev/null
+++ b/api/BusinessLogic/Tickets/VerifiedEmailChecker.php
@@ -0,0 +1,27 @@
+verifiedEmailGateway = $verifiedEmailGateway;
+ }
+
+ function isEmailVerified($emailAddress, $heskSettings) {
+ return $this->verifiedEmailGateway->isEmailVerified($emailAddress, $heskSettings);
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/ValidationModel.php b/api/BusinessLogic/ValidationModel.php
new file mode 100644
index 00000000..b3e085cd
--- /dev/null
+++ b/api/BusinessLogic/ValidationModel.php
@@ -0,0 +1,14 @@
+errorKeys = [];
+ }
+}
\ No newline at end of file
diff --git a/api/BusinessLogic/Validators.php b/api/BusinessLogic/Validators.php
new file mode 100644
index 00000000..a718dfd4
--- /dev/null
+++ b/api/BusinessLogic/Validators.php
@@ -0,0 +1,123 @@
+ $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()
+}
\ No newline at end of file
diff --git a/api/Controllers/Attachments/StaffTicketAttachmentsController.php b/api/Controllers/Attachments/StaffTicketAttachmentsController.php
new file mode 100644
index 00000000..1d32f812
--- /dev/null
+++ b/api/Controllers/Attachments/StaffTicketAttachmentsController.php
@@ -0,0 +1,70 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/api/Controllers/Categories/CategoryController.php b/api/Controllers/Categories/CategoryController.php
new file mode 100644
index 00000000..e376afef
--- /dev/null
+++ b/api/Controllers/Categories/CategoryController.php
@@ -0,0 +1,31 @@
+get[CategoryRetriever::class];
+
+ return $categoryRetriever->getAllCategories($hesk_settings, $userContext);
+ }
+}
\ No newline at end of file
diff --git a/api/Controllers/JsonRetriever.php b/api/Controllers/JsonRetriever.php
new file mode 100644
index 00000000..6b0810d3
--- /dev/null
+++ b/api/Controllers/JsonRetriever.php
@@ -0,0 +1,16 @@
+get[SettingsRetriever::class];
+
+ output($settingsRetriever->getAllSettings($hesk_settings, $modsForHesk_settings));
+ }
+}
\ No newline at end of file
diff --git a/api/Controllers/Statuses/StatusController.php b/api/Controllers/Statuses/StatusController.php
new file mode 100644
index 00000000..f9e2f087
--- /dev/null
+++ b/api/Controllers/Statuses/StatusController.php
@@ -0,0 +1,17 @@
+get[StatusRetriever::class];
+
+ output($statusRetriever->getAllStatuses($hesk_settings));
+ }
+}
\ No newline at end of file
diff --git a/api/Controllers/Tickets/CustomerTicketController.php b/api/Controllers/Tickets/CustomerTicketController.php
new file mode 100644
index 00000000..cd8e9481
--- /dev/null
+++ b/api/Controllers/Tickets/CustomerTicketController.php
@@ -0,0 +1,71 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/Controllers/Tickets/StaffTicketController.php b/api/Controllers/Tickets/StaffTicketController.php
new file mode 100644
index 00000000..001cd556
--- /dev/null
+++ b/api/Controllers/Tickets/StaffTicketController.php
@@ -0,0 +1,68 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/Core/Constants/CustomField.php b/api/Core/Constants/CustomField.php
new file mode 100644
index 00000000..6d92cfe8
--- /dev/null
+++ b/api/Core/Constants/CustomField.php
@@ -0,0 +1,16 @@
+failingQuery = $failingQuery;
+
+ parent::__construct('A SQL exception occurred. Check the logs for more information.');
+ }
+}
\ No newline at end of file
diff --git a/api/core/database.inc.php b/api/Core/database.inc.php
similarity index 82%
rename from api/core/database.inc.php
rename to api/Core/database.inc.php
index 4b1e902f..fce008d2 100755
--- a/api/core/database.inc.php
+++ b/api/Core/database.inc.php
@@ -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()
diff --git a/api/core/database_mysqli.inc.php b/api/Core/database_mysqli.inc.php
similarity index 88%
rename from api/core/database_mysqli.inc.php
rename to api/Core/database_mysqli.inc.php
index 325c9871..d7a8e7fb 100755
--- a/api/core/database_mysqli.inc.php
+++ b/api/Core/database_mysqli.inc.php
@@ -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()
diff --git a/api/Core/json_error.php b/api/Core/json_error.php
new file mode 100644
index 00000000..24154ae5
--- /dev/null
+++ b/api/Core/json_error.php
@@ -0,0 +1,13 @@
+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();
+ }
+}
\ No newline at end of file
diff --git a/api/DataAccess/Categories/CategoryGateway.php b/api/DataAccess/Categories/CategoryGateway.php
new file mode 100644
index 00000000..abda07b9
--- /dev/null
+++ b/api/DataAccess/Categories/CategoryGateway.php
@@ -0,0 +1,41 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/DataAccess/CommonDao.php b/api/DataAccess/CommonDao.php
new file mode 100644
index 00000000..f98056fb
--- /dev/null
+++ b/api/DataAccess/CommonDao.php
@@ -0,0 +1,22 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/DataAccess/Logging/Severity.php b/api/DataAccess/Logging/Severity.php
new file mode 100644
index 00000000..46124c35
--- /dev/null
+++ b/api/DataAccess/Logging/Severity.php
@@ -0,0 +1,11 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/DataAccess/Security/UserGateway.php b/api/DataAccess/Security/UserGateway.php
new file mode 100644
index 00000000..e836f18a
--- /dev/null
+++ b/api/DataAccess/Security/UserGateway.php
@@ -0,0 +1,123 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/DataAccess/Settings/ModsForHeskSettingsGateway.php b/api/DataAccess/Settings/ModsForHeskSettingsGateway.php
new file mode 100644
index 00000000..aa56b229
--- /dev/null
+++ b/api/DataAccess/Settings/ModsForHeskSettingsGateway.php
@@ -0,0 +1,23 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/DataAccess/Statuses/StatusGateway.php b/api/DataAccess/Statuses/StatusGateway.php
new file mode 100644
index 00000000..c18e9b19
--- /dev/null
+++ b/api/DataAccess/Statuses/StatusGateway.php
@@ -0,0 +1,56 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/api/DataAccess/Tickets/TicketGateway.php b/api/DataAccess/Tickets/TicketGateway.php
new file mode 100644
index 00000000..1645618b
--- /dev/null
+++ b/api/DataAccess/Tickets/TicketGateway.php
@@ -0,0 +1,361 @@
+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();
+ }
+}
\ No newline at end of file
diff --git a/api/DataAccess/Tickets/VerifiedEmailGateway.php b/api/DataAccess/Tickets/VerifiedEmailGateway.php
new file mode 100644
index 00000000..4fa0667b
--- /dev/null
+++ b/api/DataAccess/Tickets/VerifiedEmailGateway.php
@@ -0,0 +1,16 @@
+init();
+
+ $rs = hesk_dbQuery("SELECT 1 FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "verified_emails` WHERE `Email` = '" . hesk_dbEscape($emailAddress) . "'");
+
+ return hesk_dbNumRows($rs) > 0;
+ }
+}
\ No newline at end of file
diff --git a/api/Link.php b/api/Link.php
new file mode 100644
index 00000000..6ae9a884
--- /dev/null
+++ b/api/Link.php
@@ -0,0 +1,211 @@
+ $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 ]);
+ }
+}
\ No newline at end of file
diff --git a/api/Tests/AutoLoader.php b/api/Tests/AutoLoader.php
new file mode 100644
index 00000000..6a72f7ec
--- /dev/null
+++ b/api/Tests/AutoLoader.php
@@ -0,0 +1,38 @@
+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'));
\ No newline at end of file
diff --git a/api/Tests/BusinessLogic/Attachments/AttachmentHandlerTest.php b/api/Tests/BusinessLogic/Attachments/AttachmentHandlerTest.php
new file mode 100644
index 00000000..7ee2eb84
--- /dev/null
+++ b/api/Tests/BusinessLogic/Attachments/AttachmentHandlerTest.php
@@ -0,0 +1,330 @@
+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);
+ }
+}
diff --git a/api/Tests/BusinessLogic/Attachments/AttachmentRetrieverTest.php b/api/Tests/BusinessLogic/Attachments/AttachmentRetrieverTest.php
new file mode 100644
index 00000000..5017ea91
--- /dev/null
+++ b/api/Tests/BusinessLogic/Attachments/AttachmentRetrieverTest.php
@@ -0,0 +1,65 @@
+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));
+ }
+}
diff --git a/api/Tests/BusinessLogic/Emails/BasicEmailSenderIntegrationTest.php b/api/Tests/BusinessLogic/Emails/BasicEmailSenderIntegrationTest.php
new file mode 100644
index 00000000..a3feb1a8
--- /dev/null
+++ b/api/Tests/BusinessLogic/Emails/BasicEmailSenderIntegrationTest.php
@@ -0,0 +1,109 @@
+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 HTML message";
+ $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);
+ }
+ }
+}
diff --git a/api/Tests/BusinessLogic/Emails/EmailSenderHelperTest.php b/api/Tests/BusinessLogic/Emails/EmailSenderHelperTest.php
new file mode 100644
index 00000000..c9d5c0cb
--- /dev/null
+++ b/api/Tests/BusinessLogic/Emails/EmailSenderHelperTest.php
@@ -0,0 +1,131 @@
+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);
+ }
+}
diff --git a/api/Tests/BusinessLogic/Emails/MailgunEmailSenderIntegrationTest.php b/api/Tests/BusinessLogic/Emails/MailgunEmailSenderIntegrationTest.php
new file mode 100644
index 00000000..86948697
--- /dev/null
+++ b/api/Tests/BusinessLogic/Emails/MailgunEmailSenderIntegrationTest.php
@@ -0,0 +1,108 @@
+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 MailgunEmailSender();
+ $this->heskSettings = $hesk_settings;
+ $this->modsForHeskSettings = $modsForHesk_settings;
+ $this->attachmentsToPurge = array();
+ }
+
+ protected function tearDown() {
+ foreach ($this->attachmentsToPurge as $file) {
+ unlink($file);
+ }
+ }
+
+ function testItCanSendMail() {
+ //-- Arrange
+ $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 HTML message";
+ $emailBuilder->subject = "MailgunEmailSenderIntegrationTest";
+
+ $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 = "MailgunEmailSenderIntegrationTest";
+
+
+ //-- Act
+ $result = $this->emailSender->sendEmail($emailBuilder, $this->heskSettings, $this->modsForHeskSettings, false);
+
+ //-- Assert
+ if ($result !== true) {
+ $this->fail($result);
+ }
+ }
+}
diff --git a/api/Tests/BusinessLogic/IntegrationTestCaseBase.php b/api/Tests/BusinessLogic/IntegrationTestCaseBase.php
new file mode 100644
index 00000000..963f223c
--- /dev/null
+++ b/api/Tests/BusinessLogic/IntegrationTestCaseBase.php
@@ -0,0 +1,10 @@
+markTestSkipped(sprintf("Skipping Integration Test %s", get_class($this)));
+ }
+}
diff --git a/api/Tests/BusinessLogic/Security/BanRetrieverTest.php b/api/Tests/BusinessLogic/Security/BanRetrieverTest.php
new file mode 100644
index 00000000..304bffa0
--- /dev/null
+++ b/api/Tests/BusinessLogic/Security/BanRetrieverTest.php
@@ -0,0 +1,86 @@
+banGateway = $this->createMock(BanGateway::class);
+ $this->banRetriever = new BanRetriever($this->banGateway);
+ }
+
+ function testItReturnsTrueWhenTheEmailIsBanned() {
+ //-- Arrange
+ $bannedEmail = new BannedEmail();
+ $bannedEmail->email = 'my@email.address';
+ $this->banGateway->method('getEmailBans')
+ ->willReturn([$bannedEmail]);
+
+ //-- Act
+ $result = $this->banRetriever->isEmailBanned('my@email.address', null);
+
+ //-- Assert
+ $this->assertThat($result, $this->isTrue());
+ }
+
+ function testItReturnsFalseWhenTheEmailIsNotBanned() {
+ //-- Arrange
+ $bannedEmail = new BannedEmail();
+ $bannedEmail->email = 'my@other.address';
+ $this->banGateway->method('getEmailBans')
+ ->willReturn([$bannedEmail]);
+
+ //-- Act
+ $result = $this->banRetriever->isEmailBanned('my@email.address', null);
+
+ //-- Assert
+ $this->assertThat($result, $this->isFalse());
+ }
+
+ function testItReturnsTrueWhenTheIpIsBanned() {
+ //-- Arrange
+ $bannedIp = new BannedIp();
+ $bannedIp->ipFrom = ip2long('1.0.0.0');
+ $bannedIp->ipTo = ip2long('1.0.0.5');
+ $this->banGateway->method('getIpBans')
+ ->willReturn([$bannedIp]);
+
+ //-- Act
+ $result = $this->banRetriever->isIpAddressBanned(ip2long('1.0.0.3'), null);
+
+ //-- Assert
+ $this->assertThat($result, $this->isTrue());
+ }
+
+ function testItReturnsFalseWhenTheIpIsNotBanned() {
+ //-- Arrange
+ $bannedIp = new BannedIp();
+ $bannedIp->ipFrom = ip2long('1.0.0.0');
+ $bannedIp->ipTo = ip2long('1.0.0.5');
+ $this->banGateway->method('getIpBans')
+ ->willReturn([$bannedIp]);
+
+ //-- Act
+ $result = $this->banRetriever->isIpAddressBanned(ip2long('2.0.0.3'), null);
+
+ //-- Assert
+ $this->assertThat($result, $this->isFalse());
+ }
+}
diff --git a/api/Tests/BusinessLogic/Security/UserToTicketCheckerTest.php b/api/Tests/BusinessLogic/Security/UserToTicketCheckerTest.php
new file mode 100644
index 00000000..d3afb126
--- /dev/null
+++ b/api/Tests/BusinessLogic/Security/UserToTicketCheckerTest.php
@@ -0,0 +1,114 @@
+userGateway = $this->createMock(UserGateway::class);
+ $this->userToTicketChecker = new UserToTicketChecker($this->userGateway);
+ }
+
+ function testItReturnsTrueWhenTheUserIsAnAdmin() {
+ //-- Arrange
+ $user = new UserContext();
+ $user->admin = true;
+ $user->id = 99;
+
+ $ticket = new Ticket();
+
+ //-- Act
+ $result = $this->userToTicketChecker->isTicketAccessibleToUser($user, $ticket, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($result, self::isTrue());
+ }
+
+ function testItReturnsTrueWhenTheUserHasAccessToTheCategory() {
+ //-- Arrange
+ $user = new UserContext();
+ $user->admin = false;
+ $user->categories = array(1);
+ $user->permissions = array(UserPrivilege::CAN_VIEW_TICKETS);
+ $user->id = 99;
+
+ $ticket = new Ticket();
+ $ticket->categoryId = 1;
+
+ //-- Act
+ $result = $this->userToTicketChecker->isTicketAccessibleToUser($user, $ticket, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($result, self::isTrue());
+ }
+
+ function testItReturnsFalseWhenTheUserCannotViewTickets() {
+ //-- Arrange
+ $user = new UserContext();
+ $user->admin = false;
+ $user->categories = array(1);
+ $user->permissions = array();
+ $user->id = 99;
+
+ $ticket = new Ticket();
+ $ticket->categoryId = 1;
+
+ //-- Act
+ $result = $this->userToTicketChecker->isTicketAccessibleToUser($user, $ticket, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($result, self::isFalse());
+ }
+
+ function testItReturnsFalseWhenTheUserCannotViewAndEditTicketsWhenEditFlagIsTrue() {
+ //-- Arrange
+ $user = new UserContext();
+ $user->admin = false;
+ $user->categories = array(1);
+ $user->permissions = array(UserPrivilege::CAN_VIEW_TICKETS, 'something else');
+ $user->id = 99;
+
+ $ticket = new Ticket();
+ $ticket->categoryId = 1;
+
+ //-- Act
+ $result = $this->userToTicketChecker->isTicketAccessibleToUser($user, $ticket, $this->heskSettings, array(UserPrivilege::CAN_EDIT_TICKETS));
+
+ //-- Assert
+ self::assertThat($result, self::isFalse());
+ }
+
+ function testItReturnsTrueWhenTheUserDoesNotHaveEditPermissionsButIsTheCategoryManager() {
+ //-- Arrange
+ $user = new UserContext();
+ $user->admin = false;
+ $user->categories = array(1);
+ $user->permissions = array(UserPrivilege::CAN_VIEW_TICKETS, 'something else');
+ $user->id = 1;
+ $this->userGateway->method('getManagerForCategory')->willReturn(1);
+
+ $ticket = new Ticket();
+ $ticket->categoryId = 1;
+
+ //-- Act
+ $result = $this->userToTicketChecker->isTicketAccessibleToUser($user, $ticket, $this->heskSettings, array(UserPrivilege::CAN_EDIT_TICKETS));
+
+ //-- Assert
+ self::assertThat($result, self::isTrue());
+ }
+}
diff --git a/api/Tests/BusinessLogic/Tickets/AutoassignerTest.php b/api/Tests/BusinessLogic/Tickets/AutoassignerTest.php
new file mode 100644
index 00000000..8e9836ac
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/AutoassignerTest.php
@@ -0,0 +1,184 @@
+categoryGateway = $this->createMock(CategoryGateway::class);
+ $this->userGateway = $this->createMock(UserGateway::class);
+ $this->autoassigner = new Autoassigner($this->categoryGateway, $this->userGateway);
+ $this->heskSettings = array(
+ 'autoassign' => 1
+ );
+ }
+
+ function testItReturnsNullWhenAutoassignIsDisabled() {
+ //-- Arrange
+ $this->heskSettings['autoassign'] = 0;
+
+ //-- Act
+ $owner = $this->autoassigner->getNextUserForTicket(0, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($owner, self::isNull());
+ }
+
+ function getPermissionsForUser() {
+ return array('can_view_tickets', 'can_reply_tickets');
+ }
+
+ function testItReturnsTheUsersWithLeastOpenTickets() {
+ //-- Arrange
+ $userWithNoOpenTickets = new UserContext();
+ $userWithNoOpenTickets->id = 1;
+ $userWithNoOpenTickets->categories = array(1);
+ $userWithNoOpenTickets->permissions = $this->getPermissionsForUser();
+ $userWithOneOpenTicket = new UserContext();
+ $userWithOneOpenTicket->id = 2;
+ $userWithOneOpenTicket->categories = array(1);
+ $userWithOneOpenTicket->permissions = $this->getPermissionsForUser();
+ $usersToReturn = array(
+ $userWithNoOpenTickets,
+ $userWithOneOpenTicket
+ );
+
+ $this->userGateway->method('getUsersByNumberOfOpenTicketsForAutoassign')
+ ->with($this->heskSettings)
+ ->willReturn($usersToReturn);
+
+ //-- Act
+ $actual = $this->autoassigner->getNextUserForTicket(1, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::equalTo($userWithNoOpenTickets));
+ }
+
+ function testItOnlyReturnsUsersWhoCanAccessTheCategory() {
+ //-- Arrange
+ $userWithNoOpenTickets = new UserContext();
+ $userWithNoOpenTickets->id = 1;
+ $userWithNoOpenTickets->categories = array(1);
+ $userWithNoOpenTickets->permissions = $this->getPermissionsForUser();
+ $userWithOneOpenTicket = new UserContext();
+ $userWithOneOpenTicket->id = 2;
+ $userWithOneOpenTicket->categories = array(2);
+ $userWithOneOpenTicket->permissions = $this->getPermissionsForUser();
+ $usersToReturn = array(
+ $userWithNoOpenTickets,
+ $userWithOneOpenTicket
+ );
+
+ $this->userGateway->method('getUsersByNumberOfOpenTicketsForAutoassign')
+ ->with($this->heskSettings)
+ ->willReturn($usersToReturn);
+
+ //-- Act
+ $actual = $this->autoassigner->getNextUserForTicket(2, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::equalTo($userWithOneOpenTicket));
+ }
+
+ function testItReturnsAdminUsers() {
+ //-- Arrange
+ $userWithNoOpenTickets = new UserContext();
+ $userWithNoOpenTickets->id = 1;
+ $userWithNoOpenTickets->categories = array(1);
+ $userWithNoOpenTickets->permissions = $this->getPermissionsForUser();
+ $userWithNoOpenTickets->admin = true;
+ $userWithOneOpenTicket = new UserContext();
+ $userWithOneOpenTicket->id = 2;
+ $userWithOneOpenTicket->categories = array(2);
+ $userWithOneOpenTicket->permissions = $this->getPermissionsForUser();
+ $usersToReturn = array(
+ $userWithNoOpenTickets,
+ $userWithOneOpenTicket
+ );
+
+ $this->userGateway->method('getUsersByNumberOfOpenTicketsForAutoassign')
+ ->with($this->heskSettings)
+ ->willReturn($usersToReturn);
+
+ //-- Act
+ $actual = $this->autoassigner->getNextUserForTicket(2, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::equalTo($userWithNoOpenTickets));
+ }
+
+ function testItOnlyReturnsUsersWhoCanViewAndRespondToTickets() {
+ //-- Arrange
+ $userWithNoOpenTickets = new UserContext();
+ $userWithNoOpenTickets->id = 1;
+ $userWithNoOpenTickets->categories = array(1);
+ $userWithNoOpenTickets->permissions = array();
+ $userWithOneOpenTicket = new UserContext();
+ $userWithOneOpenTicket->id = 2;
+ $userWithOneOpenTicket->categories = array(1);
+ $userWithOneOpenTicket->permissions = $this->getPermissionsForUser();
+ $usersToReturn = array(
+ $userWithNoOpenTickets,
+ $userWithOneOpenTicket
+ );
+
+ $this->userGateway->method('getUsersByNumberOfOpenTicketsForAutoassign')
+ ->with($this->heskSettings)
+ ->willReturn($usersToReturn);
+
+ //-- Act
+ $actual = $this->autoassigner->getNextUserForTicket(1, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::equalTo($userWithOneOpenTicket));
+ }
+
+ function testItReturnsNullWhenNoOneCanHandleTheTicket() {
+ //-- Arrange
+ $userWithNoOpenTickets = new UserContext();
+ $userWithNoOpenTickets->id = 1;
+ $userWithNoOpenTickets->categories = array(1);
+ $userWithNoOpenTickets->permissions = $this->getPermissionsForUser();
+ $userWithOneOpenTicket = new UserContext();
+ $userWithOneOpenTicket->id = 2;
+ $userWithOneOpenTicket->categories = array(1);
+ $userWithOneOpenTicket->permissions = $this->getPermissionsForUser();
+ $usersToReturn = array(
+ $userWithNoOpenTickets,
+ $userWithOneOpenTicket
+ );
+
+ $this->userGateway->method('getUsersByNumberOfOpenTicketsForAutoassign')
+ ->with($this->heskSettings)
+ ->willReturn($usersToReturn);
+
+ //-- Act
+ $actual = $this->autoassigner->getNextUserForTicket(2, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::isNull());
+ }
+}
diff --git a/api/Tests/BusinessLogic/Tickets/CustomFields/CustomFieldValidatorTest.php b/api/Tests/BusinessLogic/Tickets/CustomFields/CustomFieldValidatorTest.php
new file mode 100644
index 00000000..d08fde5e
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/CustomFields/CustomFieldValidatorTest.php
@@ -0,0 +1,80 @@
+ array(
+ 'custom1' => array(
+ 'use' => 1,
+ 'category' => array(1, 2)
+ )
+ )
+ );
+
+ //-- Act
+ $result = CustomFieldValidator::isCustomFieldInCategory(1, 1, false, $heskSettings);
+
+ //-- Assert
+ $this->assertThat($result, $this->isTrue());
+ }
+
+ function testItReturnsTrueWhenTheCustomFieldIsForAllCategories() {
+ //-- Arrange
+ $heskSettings = array(
+ 'custom_fields' => array(
+ 'custom1' => array(
+ 'use' => 1,
+ 'category' => []
+ )
+ )
+ );
+
+ //-- Act
+ $result = CustomFieldValidator::isCustomFieldInCategory(1, 1, false, $heskSettings);
+
+ //-- Assert
+ $this->assertThat($result, $this->isTrue());
+ }
+
+ function testItReturnsFalseWhenTheCustomFieldIsNotInTheCategory() {
+ //-- Arrange
+ $heskSettings = array(
+ 'custom_fields' => array(
+ 'custom1' => array(
+ 'use' => 1,
+ 'category' => array(1, 2)
+ )
+ )
+ );
+
+ //-- Act
+ $result = CustomFieldValidator::isCustomFieldInCategory(1, 50, false, $heskSettings);
+
+ //-- Assert
+ $this->assertThat($result, $this->isFalse());
+ }
+
+ function testItReturnsFalseWhenTheCustomFieldIsForStaffOnly() {
+ //-- Arrange
+ $heskSettings = array(
+ 'custom_fields' => array(
+ 'custom1' => array(
+ 'use' => 2,
+ 'category' => array(1, 2)
+ )
+ )
+ );
+
+ //-- Act
+ $result = CustomFieldValidator::isCustomFieldInCategory(1, 1, false, $heskSettings);
+
+ //-- Assert
+ $this->assertThat($result, $this->isFalse());
+ }
+}
diff --git a/api/Tests/BusinessLogic/Tickets/NewTicketValidatorTest.php b/api/Tests/BusinessLogic/Tickets/NewTicketValidatorTest.php
new file mode 100644
index 00000000..2ace5299
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/NewTicketValidatorTest.php
@@ -0,0 +1,459 @@
+banRetriever = $this->createMock(BanRetriever::class);
+ $this->categoryRetriever = $this->createMock(CategoryRetriever::class);
+ $this->ticketValidators = $this->createMock(TicketValidators::class);
+ $this->newTicketValidator = new NewTicketValidator($this->categoryRetriever, $this->banRetriever, $this->ticketValidators);
+ $this->userContext = new UserContext();
+
+ $this->ticketRequest = new CreateTicketByCustomerModel();
+ $this->ticketRequest->name = 'Name';
+ $this->ticketRequest->email = 'some@e.mail';
+ $this->ticketRequest->category = 1;
+ $this->ticketRequest->priority = Priority::HIGH;
+ $this->ticketRequest->subject = 'Subject';
+ $this->ticketRequest->message = 'Message';
+ $this->ticketRequest->customFields = array();
+ $this->heskSettings = array(
+ 'multi_eml' => false,
+ 'cust_urgency' => false,
+ 'require_subject' => 1,
+ 'require_message' => 1,
+ 'custom_fields' => array(),
+ );
+
+ $category = new Category();
+ $category->accessible = true;
+ $category->id = 1;
+ $categories = array();
+ $categories[1] = $category;
+ $this->categoryRetriever->method('getAllCategories')
+ ->willReturn($categories);
+ }
+
+ function testItAddsTheProperValidationErrorWhenNameIsNull() {
+ //-- Arrange
+ $this->ticketRequest->name = null;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['NO_NAME'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenNameIsBlank() {
+ //-- Arrange
+ $this->ticketRequest->name = '';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['NO_NAME'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenEmailIsNull() {
+ //-- Arrange
+ $this->ticketRequest->email = null;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['INVALID_OR_MISSING_EMAIL'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenEmailIsBlank() {
+ //-- Arrange
+ $this->ticketRequest->email = '';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['INVALID_OR_MISSING_EMAIL'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenEmailIsInvalid() {
+ //-- Arrange
+ $this->ticketRequest->email = 'something@';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['INVALID_OR_MISSING_EMAIL'], $validationModel->errorKeys);
+ }
+
+ function testItSupportsMultipleEmails() {
+ //-- Arrange
+ $this->ticketRequest->email = 'something@email.com;another@valid.email';
+ $this->ticketRequest->language = 'English';
+ $this->heskSettings['multi_eml'] = true;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ self::assertThat(count($validationModel->errorKeys), self::equalTo(0));
+ }
+
+ function testItAddsTheProperValidationErrorWhenCategoryIsNotANumber() {
+ //-- Arrange
+ $this->ticketRequest->category = 'something';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['NO_CATEGORY'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenCategoryIsNegative() {
+ //-- Arrange
+ $this->ticketRequest->category = -5;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['NO_CATEGORY'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCategoryDoesNotExist() {
+ //-- Arrange
+ $this->ticketRequest->category = 10;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['CATEGORY_DOES_NOT_EXIST'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithPriorityCritical() {
+ //-- Arrange
+ $this->ticketRequest->priority = Priority::CRITICAL;
+ $this->heskSettings['cust_urgency'] = true;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['CRITICAL_PRIORITY_FORBIDDEN'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithNullSubjectAndItIsRequired() {
+ //-- Arrange
+ $this->ticketRequest->subject = null;
+ $this->heskSettings['require_subject'] = 1;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['SUBJECT_REQUIRED'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithBlankSubjectAndItIsRequired() {
+ //-- Arrange
+ $this->ticketRequest->subject = '';
+ $this->heskSettings['require_subject'] = 1;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['SUBJECT_REQUIRED'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithNullMessageAndItIsRequired() {
+ //-- Arrange
+ $this->ticketRequest->message = null;
+ $this->heskSettings['require_message'] = 1;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['MESSAGE_REQUIRED'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithBlankMessageAndItIsRequired() {
+ //-- Arrange
+ $this->ticketRequest->message = '';
+ $this->heskSettings['require_message'] = 1;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['MESSAGE_REQUIRED'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithNullRequiredCustomField() {
+ //-- Arrange
+ $customField = array();
+ $customField['req'] = 1;
+ $customField['type'] = CustomField::TEXT;
+ $customField['use'] = 1;
+ $customField['category'] = array();
+ $this->heskSettings['custom_fields']['custom1'] = $customField;
+ $this->ticketRequest->customFields[1] = null;
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['CUSTOM_FIELD_1_INVALID::NO_VALUE'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithBlankRequiredCustomField() {
+ //-- Arrange
+ $customField = array();
+ $customField['req'] = 1;
+ $customField['type'] = CustomField::TEXT;
+ $customField['use'] = 1;
+ $customField['category'] = array();
+ $this->heskSettings['custom_fields']['custom1'] = $customField;
+ $this->ticketRequest->customFields[1] = '';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['CUSTOM_FIELD_1_INVALID::NO_VALUE'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithDateCustomFieldThatIsInvalid() {
+ //-- Arrange
+ $customField = array();
+ $customField['req'] = 1;
+ $customField['type'] = CustomField::DATE;
+ $customField['use'] = 1;
+ $customField['category'] = array();
+ $this->heskSettings['custom_fields']['custom1'] = $customField;
+ $this->ticketRequest->customFields[1] = '2017-30-00';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['CUSTOM_FIELD_1_INVALID::INVALID_DATE'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithDateThatIsBeforeMinDate() {
+ //-- Arrange
+ $customField = array();
+ $customField['req'] = 1;
+ $customField['type'] = CustomField::DATE;
+ $customField['use'] = 1;
+ $customField['category'] = array();
+ $customField['value'] = array(
+ 'dmin' => '2017-01-01',
+ 'dmax' => ''
+ );
+ $this->heskSettings['custom_fields']['custom1'] = $customField;
+ $this->ticketRequest->customFields[1] = '2016-12-31';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['CUSTOM_FIELD_1_INVALID::DATE_BEFORE_MIN::MIN:2017-01-01::ENTERED:2016-12-31'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithDateThatIsAfterMaxDate() {
+ //-- Arrange
+ $customField = array();
+ $customField['req'] = 1;
+ $customField['type'] = CustomField::DATE;
+ $customField['use'] = 1;
+ $customField['category'] = array();
+ $customField['value'] = array(
+ 'dmin' => '',
+ 'dmax' => '2017-01-01'
+ );
+ $this->heskSettings['custom_fields']['custom1'] = $customField;
+ $this->ticketRequest->customFields[1] = '2017-01-02';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['CUSTOM_FIELD_1_INVALID::DATE_AFTER_MAX::MAX:2017-01-01::ENTERED:2017-01-02'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithEmailThatIsInvalid() {
+ //-- Arrange
+ $customField = array();
+ $customField['req'] = 1;
+ $customField['type'] = CustomField::EMAIL;
+ $customField['use'] = 1;
+ $customField['category'] = array();
+ $customField['value'] = array(
+ 'multiple' => 0
+ );
+ $this->heskSettings['custom_fields']['custom1'] = $customField;
+ $this->ticketRequest->customFields[1] = 'invalid@';
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['CUSTOM_FIELD_1_INVALID::INVALID_EMAIL'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithABannedEmail() {
+ //-- Arrange
+ $this->ticketRequest->email = 'some@banned.email';
+ $this->banRetriever->method('isEmailBanned')
+ ->with($this->ticketRequest->email, $this->heskSettings)
+ ->willReturn(true);
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['EMAIL_BANNED'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWhenTheyAreMaxedOut() {
+ //-- Arrange
+ $this->ticketRequest->email = 'some@maxedout.email';
+ $this->ticketValidators->method('isCustomerAtMaxTickets')
+ ->with($this->ticketRequest->email, $this->heskSettings)
+ ->willReturn(true);
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['EMAIL_AT_MAX_OPEN_TICKETS'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithLanguageNull() {
+ //-- Arrange
+ $this->ticketRequest->language = null;
+ $this->ticketValidators->method('isCustomerAtMaxTickets')
+ ->with($this->ticketRequest->email, $this->heskSettings)
+ ->willReturn(false);
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['MISSING_LANGUAGE'], $validationModel->errorKeys);
+ }
+
+ function testItAddsTheProperValidationErrorWhenTheCustomerSubmitsTicketWithLanguageBlank() {
+ //-- Arrange
+ $this->ticketRequest->language = '';
+ $this->ticketValidators->method('isCustomerAtMaxTickets')
+ ->with($this->ticketRequest->email, $this->heskSettings)
+ ->willReturn(false);
+
+ //-- Act
+ $validationModel = $this->newTicketValidator->validateNewTicketForCustomer($this->ticketRequest,
+ $this->heskSettings,
+ $this->userContext);
+
+ //-- Assert
+ $this->assertArraySubset(['MISSING_LANGUAGE'], $validationModel->errorKeys);
+ }
+}
diff --git a/api/Tests/BusinessLogic/Tickets/TicketCreatorTests/CreateTicketForCustomerTest.php b/api/Tests/BusinessLogic/Tickets/TicketCreatorTests/CreateTicketForCustomerTest.php
new file mode 100644
index 00000000..a0a3ea07
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/TicketCreatorTests/CreateTicketForCustomerTest.php
@@ -0,0 +1,342 @@
+ticketGateway = $this->createMock(TicketGateway::class);
+ $this->newTicketValidator = $this->createMock(NewTicketValidator::class);
+ $this->trackingIdGenerator = $this->createMock(TrackingIdGenerator::class);
+ $this->autoassigner = $this->createMock(Autoassigner::class);
+ $this->statusGateway = $this->createMock(StatusGateway::class);
+ $this->verifiedEmailChecker = $this->createMock(VerifiedEmailChecker::class);
+ $this->emailSenderHelper = $this->createMock(EmailSenderHelper::class);
+ $this->userGateway = $this->createMock(UserGateway::class);
+ $this->modsForHeskSettingsGateway = $this->createMock(ModsForHeskSettingsGateway::class);
+
+ $this->ticketCreator = new TicketCreator($this->newTicketValidator, $this->trackingIdGenerator,
+ $this->autoassigner, $this->statusGateway, $this->ticketGateway, $this->verifiedEmailChecker,
+ $this->emailSenderHelper, $this->userGateway, $this->modsForHeskSettingsGateway);
+
+ $this->ticketRequest = new CreateTicketByCustomerModel();
+ $this->ticketRequest->name = 'Name';
+ $this->ticketRequest->email = 'some@e.mail';
+ $this->ticketRequest->category = 1;
+ $this->ticketRequest->priority = Priority::HIGH;
+ $this->ticketRequest->subject = 'Subject';
+ $this->ticketRequest->message = 'Message';
+ $this->ticketRequest->customFields = array();
+ $this->heskSettings = array(
+ 'multi_eml' => false,
+ 'cust_urgency' => false,
+ 'require_subject' => 1,
+ 'require_message' => 1,
+ 'custom_fields' => array(),
+ 'autoassign' => 0
+ );
+ $this->modsForHeskSettings = array(
+ 'customer_email_verification_required' => false
+ );
+ $this->userContext = new UserContext();
+
+ $this->newTicketValidator->method('validateNewTicketForCustomer')->willReturn(new ValidationModel());
+ $this->trackingIdGenerator->method('generateTrackingId')->willReturn('123-456-7890');
+ $this->ticketGatewayGeneratedFields = new TicketGatewayGeneratedFields();
+ $this->ticketGateway->method('createTicket')->willReturn($this->ticketGatewayGeneratedFields);
+ $this->userGateway->method('getUsersForNewTicketNotification')->willReturn(array());
+
+ $status = new Status();
+ $status->id = 1;
+ $this->statusGateway->method('getStatusForDefaultAction')
+ ->willReturn($status);
+ }
+
+ function testItSavesTheTicketToTheDatabase() {
+ //-- Arrange
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Assert
+ $this->ticketGateway->expects($this->once())->method('createTicket');
+
+ //-- Act
+ $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+ }
+
+ function testItSetsTheTrackingIdOnTheTicket() {
+ //-- Arrange
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Act
+ $ticket = $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+
+ //-- Assert
+ self::assertThat($ticket->ticket->trackingId, self::equalTo('123-456-7890'));
+ }
+
+ function testItSetsTheNextUserForAutoassign() {
+ //-- Arrange
+ $this->heskSettings['autoassign'] = 1;
+ $autoassignUser = new UserContext();
+ $notificationSettings = new UserContextNotifications();
+ $notificationSettings->newAssignedToMe = true;
+ $autoassignUser->notificationSettings = $notificationSettings;
+ $autoassignUser->id = 1;
+ $this->autoassigner->method('getNextUserForTicket')->willReturn($autoassignUser);
+ $this->userGateway->method('getUserById')->willReturn($autoassignUser);
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Act
+ $ticket = $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+
+ //-- Assert
+ self::assertThat($ticket->ticket->ownerId, self::equalTo(1));
+ }
+
+ function testItDoesntCallTheAutoassignerWhenDisabledInHesk() {
+ //-- Arrange
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Act
+ $ticket = $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+
+ //-- Assert
+ self::assertThat($ticket->ticket->ownerId, self::equalTo(null));
+ }
+
+ function testItTransformsTheBasicProperties() {
+ //-- Arrange
+ $this->ticketRequest->name = 'Name';
+ $this->ticketRequest->email = 'some@email.test';
+ $this->ticketRequest->priority = Priority::MEDIUM;
+ $this->ticketRequest->category = 1;
+ $this->ticketRequest->subject = 'Subject';
+ $this->ticketRequest->message = 'Message';
+ $this->ticketRequest->html = false;
+ $this->ticketRequest->customFields = array(
+ 1 => 'something'
+ );
+ $this->ticketRequest->location = ['10.157', '-10.177'];
+ $this->ticketRequest->suggestedKnowledgebaseArticleIds = [1, 2, 3];
+ $this->ticketRequest->userAgent = 'UserAgent';
+ $this->ticketRequest->screenResolution = [1400, 900];
+ $this->ticketRequest->ipAddress = '127.0.0.1';
+ $this->ticketRequest->language = 'English';
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Act
+ $ticket = $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+
+ //-- Assert
+ self::assertThat($ticket->ticket->name, self::equalTo($this->ticketRequest->name));
+ self::assertThat($ticket->ticket->email, self::equalTo($this->ticketRequest->email));
+ self::assertThat($ticket->ticket->priorityId, self::equalTo($this->ticketRequest->priority));
+ self::assertThat($ticket->ticket->categoryId, self::equalTo($this->ticketRequest->category));
+ self::assertThat($ticket->ticket->subject, self::equalTo($this->ticketRequest->subject));
+ self::assertThat($ticket->ticket->message, self::equalTo($this->ticketRequest->message));
+ self::assertThat($ticket->ticket->usesHtml, self::equalTo($this->ticketRequest->html));
+ self::assertThat($ticket->ticket->customFields[1], self::equalTo($this->ticketRequest->customFields[1]));
+ self::assertThat($ticket->ticket->location, self::equalTo($this->ticketRequest->location));
+ self::assertThat($ticket->ticket->suggestedArticles, self::equalTo($this->ticketRequest->suggestedKnowledgebaseArticleIds));
+ self::assertThat($ticket->ticket->userAgent, self::equalTo($this->ticketRequest->userAgent));
+ self::assertThat($ticket->ticket->screenResolution, self::equalTo($this->ticketRequest->screenResolution));
+ self::assertThat($ticket->ticket->ipAddress, self::equalTo($this->ticketRequest->ipAddress));
+ self::assertThat($ticket->ticket->language, self::equalTo($this->ticketRequest->language));
+ }
+
+ function testItReturnsTheGeneratedPropertiesOnTheTicket() {
+ //-- Arrange
+ $this->ticketGatewayGeneratedFields->dateCreated = 'date created';
+ $this->ticketGatewayGeneratedFields->dateModified = 'date modified';
+ $this->ticketGatewayGeneratedFields->id = 50;
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Act
+ $ticket = $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+
+ //-- Assert
+ self::assertThat($ticket->ticket->dateCreated, self::equalTo($this->ticketGatewayGeneratedFields->dateCreated));
+ self::assertThat($ticket->ticket->lastChanged, self::equalTo($this->ticketGatewayGeneratedFields->dateModified));
+ self::assertThat($ticket->ticket->id, self::equalTo($this->ticketGatewayGeneratedFields->id));
+ }
+
+ function testItSetsTheDefaultStatus() {
+ //-- Arrange
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Act
+ $ticket = $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+
+ //-- Assert
+ self::assertThat($ticket->ticket->statusId, self::equalTo(1));
+ }
+
+ function testItSetsTheDefaultProperties() {
+ //-- Arrange
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Act
+ $ticket = $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+
+ //-- Assert
+ self::assertThat($ticket->ticket->archived, self::isFalse());
+ self::assertThat($ticket->ticket->locked, self::isFalse());
+ self::assertThat($ticket->ticket->openedBy, self::equalTo(0));
+ self::assertThat($ticket->ticket->numberOfReplies, self::equalTo(0));
+ self::assertThat($ticket->ticket->numberOfStaffReplies, self::equalTo(0));
+ self::assertThat($ticket->ticket->timeWorked, self::equalTo('00:00:00'));
+ self::assertThat($ticket->ticket->lastReplier, self::equalTo(0));
+ }
+
+ function testItChecksIfTheEmailIsVerified() {
+ //-- Arrange
+ $this->modsForHeskSettings['customer_email_verification_required'] = true;
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Assert
+ $this->verifiedEmailChecker->expects($this->once())->method('isEmailVerified');
+
+ //-- Act
+ $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+ }
+
+ function testItSendsAnEmailToTheCustomerWhenTheTicketIsCreated() {
+ //-- Arrange
+ $this->ticketRequest->sendEmailToCustomer = true;
+ $this->ticketRequest->language = 'English';
+ $expectedAddressees = new Addressees();
+ $expectedAddressees->to = array($this->ticketRequest->email);
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Assert
+ $this->emailSenderHelper->expects($this->once())->method('sendEmailForTicket')
+ ->with(EmailTemplateRetriever::NEW_TICKET, 'English', $expectedAddressees, $this->anything(), $this->heskSettings, $this->anything());
+
+ //-- Act
+ $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+ }
+
+ function testItDoesNotSendsAnEmailToTheCustomerWhenTheTicketIsCreatedAndSendToCustomerIsFalse() {
+ //-- Arrange
+ $this->ticketRequest->sendEmailToCustomer = false;
+ $this->ticketRequest->language = 'English';
+ $expectedAddressees = new Addressees();
+ $expectedAddressees->to = array($this->ticketRequest->email);
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Assert
+ $this->emailSenderHelper->expects($this->never())->method('sendEmailForTicket');
+
+ //-- Act
+ $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+ }
+
+ function testItSendsAnEmailToTheAssignedToOwnerWhenTheTicketIsCreated() {
+ //-- Arrange
+ $this->ticketRequest->sendEmailToCustomer = true;
+ $this->ticketRequest->language = 'English';
+ $expectedAddressees = new Addressees();
+ $expectedAddressees->to = array($this->ticketRequest->email);
+ $this->modsForHeskSettingsGateway->method('getAllSettings')->willReturn($this->modsForHeskSettings);
+
+ //-- Assert
+ $this->emailSenderHelper->expects($this->once())->method('sendEmailForTicket')
+ ->with(EmailTemplateRetriever::NEW_TICKET, 'English', $expectedAddressees, $this->anything(), $this->heskSettings, $this->anything());
+
+ //-- Act
+ $this->ticketCreator->createTicketByCustomer($this->ticketRequest, $this->heskSettings, $this->userContext);
+ }
+}
diff --git a/api/Tests/BusinessLogic/Tickets/TicketDeleterTest.php b/api/Tests/BusinessLogic/Tickets/TicketDeleterTest.php
new file mode 100644
index 00000000..4ac994a8
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/TicketDeleterTest.php
@@ -0,0 +1,135 @@
+userToTicketChecker = $this->createMock(UserToTicketChecker::class);
+ $this->ticketGateway = $this->createMock(TicketGateway::class);
+ $this->attachmentHandler = $this->createMock(AttachmentHandler::class);
+
+ $this->ticketDeleter = new TicketDeleter($this->ticketGateway, $this->userToTicketChecker, $this->attachmentHandler);
+ }
+
+ function testItThrowsAnExceptionWhenTheUserDoesNotHavePermissionToDeleteTheTicket() {
+ //-- Arrange
+ $this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(false);
+
+ //-- Assert
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage("User does not have access to ticket 1");
+
+ //-- Act
+ $this->ticketDeleter->deleteTicket(1, $this->userContext, $this->heskSettings);
+ }
+
+ function testItDeletesAllAttachmentsForTheTicket() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $attachmentOne = new Attachment();
+ $attachmentOne->id = 1;
+ $attachmentTwo = new Attachment();
+ $attachmentTwo->id = 2;
+ $attachments = array($attachmentOne, $attachmentTwo);
+ $ticket->attachments = $attachments;
+ $ticket->replies = array();
+ $this->ticketGateway->method('getTicketById')->willReturn($ticket);
+ $this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
+
+ //-- Assert
+ $this->attachmentHandler->expects($this->exactly(2))->method('deleteAttachmentFromTicket');
+
+ //-- Act
+ $this->ticketDeleter->deleteTicket(1, $this->userContext, $this->heskSettings);
+ }
+
+ function testItDeletesAllRepliesForTheTicket() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $ticket->attachments = array();
+ $ticket->replies = array();
+ $ticket->id = 1;
+ $this->ticketGateway->method('getTicketById')->willReturn($ticket);
+ $this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
+
+ //-- Assert
+ $this->ticketGateway->expects($this->once())->method('deleteRepliesForTicket')->with(1, $this->heskSettings);
+
+ //-- Act
+ $this->ticketDeleter->deleteTicket(1, $this->userContext, $this->heskSettings);
+ }
+
+ function testItDeleteAllReplyDrafts() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $ticket->attachments = array();
+ $ticket->replies = array();
+ $ticket->id = 1;
+ $this->ticketGateway->method('getTicketById')->willReturn($ticket);
+ $this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
+
+ //-- Assert
+ $this->ticketGateway->expects($this->once())->method('deleteReplyDraftsForTicket')->with(1, $this->heskSettings);
+
+ //-- Act
+ $this->ticketDeleter->deleteTicket(1, $this->userContext, $this->heskSettings);
+ }
+
+ function testItDeletesTheTicketNotes() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $ticket->attachments = array();
+ $ticket->replies = array();
+ $ticket->id = 1;
+ $this->ticketGateway->method('getTicketById')->willReturn($ticket);
+ $this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
+
+ //-- Assert
+ $this->ticketGateway->expects($this->once())->method('deleteNotesForTicket')->with(1, $this->heskSettings);
+
+ //-- Act
+ $this->ticketDeleter->deleteTicket(1, $this->userContext, $this->heskSettings);
+ }
+
+ function testItDeletesTheTicket() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $ticket->attachments = array();
+ $ticket->replies = array();
+ $ticket->id = 1;
+ $this->ticketGateway->method('getTicketById')->willReturn($ticket);
+ $this->userToTicketChecker->method('isTicketAccessibleToUser')->willReturn(true);
+
+ //-- Assert
+ $this->ticketGateway->expects($this->once())->method('deleteTicket')->with(1, $this->heskSettings);
+
+ //-- Act
+ $this->ticketDeleter->deleteTicket(1, $this->userContext, $this->heskSettings);
+ }
+}
diff --git a/api/Tests/BusinessLogic/Tickets/TicketRetrieverTest.php b/api/Tests/BusinessLogic/Tickets/TicketRetrieverTest.php
new file mode 100644
index 00000000..6fd6b168
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/TicketRetrieverTest.php
@@ -0,0 +1,87 @@
+ticketGateway = $this->createMock(TicketGateway::class);
+ $this->heskSettings = array('email_view_ticket' => 0);
+
+ $this->ticketRetriever = new TicketRetriever($this->ticketGateway);
+ }
+
+ function testItGetsTheTicketByTrackingId() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $trackingId = '12345';
+ $this->ticketGateway->method('getTicketByTrackingId')->with($trackingId, $this->heskSettings)->willReturn($ticket);
+
+ //-- Act
+ $actual = $this->ticketRetriever->getTicketByTrackingIdAndEmail($trackingId, null, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::equalTo($ticket));
+ }
+
+ function testItGetsTheParentTicketIfTheUserEntersInAMergedTicketId() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $trackingId = '12345';
+ $this->ticketGateway->method('getTicketByTrackingId')->willReturn(null);
+ $this->ticketGateway->method('getTicketByMergedTrackingId')->with($trackingId, $this->heskSettings)->willReturn($ticket);
+
+ //-- Act
+ $actual = $this->ticketRetriever->getTicketByTrackingIdAndEmail($trackingId, null, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::equalTo($ticket));
+ }
+
+ function testItChecksTheTicketsEmailIfThePageRequiresIt() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $email = 'email@example.com';
+ $ticket->email = array('email2@example.com;email3@example.com,email4@example.com');
+ $trackingId = '12345';
+ $this->heskSettings['email_view_ticket'] = 1;
+ $this->ticketGateway->method('getTicketByTrackingId')->with($trackingId, $this->heskSettings)->willReturn($ticket);
+
+ //-- Assert
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage("Email 'email@example.com' entered in for ticket '12345' does not match!");
+
+ //-- Act
+ $this->ticketRetriever->getTicketByTrackingIdAndEmail($trackingId, $email, $this->heskSettings);
+ }
+
+ function testItCanHandleTicketsWithMultipleEmails() {
+ //-- Arrange
+ $ticket = new Ticket();
+ $email = 'email2@example.com';
+ $ticket->email = array('email2@example.com','email3@example.com','email4@example.com');
+ $trackingId = '12345';
+ $this->heskSettings['email_view_ticket'] = 1;
+ $this->ticketGateway->method('getTicketByTrackingId')->with($trackingId, $this->heskSettings)->willReturn($ticket);
+
+ //-- Act
+ $actual = $this->ticketRetriever->getTicketByTrackingIdAndEmail($trackingId, $email, $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::equalTo($ticket));
+ }
+
+ //-- TODO Validation tests
+}
diff --git a/api/Tests/BusinessLogic/Tickets/TicketValidatorsTest.php b/api/Tests/BusinessLogic/Tickets/TicketValidatorsTest.php
new file mode 100644
index 00000000..6fceb76e
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/TicketValidatorsTest.php
@@ -0,0 +1,75 @@
+ticketGateway = $this->createMock(TicketGateway::class);
+ $this->ticketValidator = new TicketValidators($this->ticketGateway);
+ }
+
+ function testItReturnsTrueWhenTheUserIsMaxedOutOnOpenTickets() {
+ //-- Arrange
+ $tickets = [new Ticket(), new Ticket(), new Ticket()];
+ $this->ticketGateway->method('getTicketsByEmail')
+ ->with('my@email.com')
+ ->willReturn($tickets);
+ $heskSettings = array(
+ 'max_open' => 3
+ );
+
+ //-- Act
+ $result = $this->ticketValidator->isCustomerAtMaxTickets('my@email.com', $heskSettings);
+
+ //-- Assert
+ $this->assertThat($result, $this->isTrue(), str_replace('test','',__FUNCTION__));
+ }
+
+ function testItReturnsFalseWhenTheUserIsNotMaxedOutOnOpenTickets() {
+ //-- Arrange
+ $tickets = [new Ticket(), new Ticket(), new Ticket()];
+ $this->ticketGateway->method('getTicketsByEmail')
+ ->with('my@email.com')
+ ->willReturn($tickets);
+ $heskSettings = array(
+ 'max_open' => 10
+ );
+
+ //-- Act
+ $result = $this->ticketValidator->isCustomerAtMaxTickets('my@email.com', $heskSettings);
+
+ //-- Assert
+ $this->assertThat($result, $this->isFalse(), str_replace('test','',__FUNCTION__));
+ }
+
+ function testItReturnsFalseWhenMaxOpenIsZero() {
+ //-- Arrange
+ $tickets = [new Ticket(), new Ticket(), new Ticket()];
+ $this->ticketGateway->method('getTicketsByEmail')
+ ->with('my@email.com')
+ ->willReturn($tickets);
+ $heskSettings = array(
+ 'max_open' => 0
+ );
+
+ //-- Act
+ $result = $this->ticketValidator->isCustomerAtMaxTickets('my@email.com', $heskSettings);
+
+ //-- Assert
+ $this->assertThat($result, $this->isFalse(), str_replace('test','',__FUNCTION__));
+ }
+}
diff --git a/api/Tests/BusinessLogic/Tickets/TrackingIdGeneratorTest.php b/api/Tests/BusinessLogic/Tickets/TrackingIdGeneratorTest.php
new file mode 100644
index 00000000..0ef20b1f
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/TrackingIdGeneratorTest.php
@@ -0,0 +1,60 @@
+ticketGateway = $this->createMock(TicketGateway::class);
+
+ $this->trackingIdGenerator = new TrackingIdGenerator($this->ticketGateway);
+ }
+
+ function testItReturnsTrackingIdInTheProperFormat() {
+ //-- Arrange
+ $this->ticketGateway->method('getTicketByTrackingId')
+ ->willReturn(null);
+ $acceptableCharacters = '[AEUYBDGHJLMNPQRSTVWXZ123456789]';
+ $format = "/^{$acceptableCharacters}{3}-{$acceptableCharacters}{3}-{$acceptableCharacters}{4}$/";
+
+ //-- Act
+ $trackingId = $this->trackingIdGenerator->generateTrackingId(array());
+
+ //-- Assert
+ $this->assertThat($trackingId, $this->matchesRegularExpression($format));
+ }
+
+ function testItThrowsAnExceptionWhenItWasUnableToGenerateAValidTrackingId() {
+ //-- Arrange
+ $exceptionThrown = false;
+ $this->ticketGateway->method('getTicketByTrackingId')
+ ->willReturn(new Ticket());
+
+ //-- Act
+ try {
+ $this->trackingIdGenerator->generateTrackingId(array());
+ } catch (UnableToGenerateTrackingIdException $e) {
+ //-- Assert (1/2)
+ $exceptionThrown = true;
+ }
+
+ //-- Assert (2/2)
+ $this->assertThat($exceptionThrown, $this->isTrue());
+ }
+
+ //-- Trying to test the database logic is tricky, so no tests here.
+}
diff --git a/api/Tests/BusinessLogic/Tickets/VerifiedEmailCheckerTest.php b/api/Tests/BusinessLogic/Tickets/VerifiedEmailCheckerTest.php
new file mode 100644
index 00000000..2cf838c5
--- /dev/null
+++ b/api/Tests/BusinessLogic/Tickets/VerifiedEmailCheckerTest.php
@@ -0,0 +1,56 @@
+verifiedEmailGateway = $this->createMock(VerifiedEmailGateway::class);
+ $this->heskSettings = array();
+ $this->verifiedEmailChecker = new VerifiedEmailChecker($this->verifiedEmailGateway);
+ }
+
+ function testItGetsTheValidationStateFromTheGatewayWhenItItTrue() {
+ //-- Arrange
+ $this->verifiedEmailGateway->method('isEmailVerified')
+ ->with('some email', $this->heskSettings)
+ ->willReturn(true);
+
+ //-- Act
+ $actual = $this->verifiedEmailChecker->isEmailVerified('some email', $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::isTrue());
+ }
+
+ function testItGetsTheValidationStateFromTheGatewayWhenItItFalse() {
+ //-- Arrange
+ $this->verifiedEmailGateway->method('isEmailVerified')
+ ->with('some email', $this->heskSettings)
+ ->willReturn(false);
+
+ //-- Act
+ $actual = $this->verifiedEmailChecker->isEmailVerified('some email', $this->heskSettings);
+
+ //-- Assert
+ self::assertThat($actual, self::isFalse());
+ }
+}
diff --git a/api/Tests/bootstrap.php b/api/Tests/bootstrap.php
new file mode 100644
index 00000000..a728a8a5
--- /dev/null
+++ b/api/Tests/bootstrap.php
@@ -0,0 +1,3 @@
+
+
\ No newline at end of file
diff --git a/api/autoload.php b/api/autoload.php
new file mode 100644
index 00000000..c12b9647
--- /dev/null
+++ b/api/autoload.php
@@ -0,0 +1,23 @@
+`false` otherwise
- * @apiSuccess {Integer} type `0` - Public
`1` - Private
- * @apiSuccess {Integer} priority Default priority of tickets created in this category
- * @apiSuccess {Integer} manager User ID of the category manager, or `null` if there is no manager.
- *
- * @apiSuccessExample {json} Success-Response:
- * HTTP/1.1 200 OK
- * {
- * "id": 1,
- * "name": "General",
- * "displayOrder": 10,
- * "autoassign": true,
- * "type": 0,
- * "priority": 2,
- * "manager": null
- * }
- */
-if ($request_method == 'GET') {
- if (isset($_GET['id'])) {
- $results = get_category($hesk_settings, $_GET['id']);
- } else {
- $results = get_category($hesk_settings);
- }
-
- if ($results == NULL) {
- return http_response_code(404);
- }
- return output($results);
-}
-
-return http_response_code(405);
diff --git a/api/composer.json b/api/composer.json
new file mode 100644
index 00000000..8b3f0002
--- /dev/null
+++ b/api/composer.json
@@ -0,0 +1,21 @@
+{
+ "name": "mike-koch/Mods-for-HESK",
+ "description": "New UI and features for HESK, a free helpdesk solution",
+ "minimum-stability": "dev",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Mike Koch",
+ "email": "mkoch227@gmail.com"
+ }
+ ],
+ "require": {
+ "phpunit/phpunit": "5.7.9",
+ "phpmailer/phpmailer": "^5.2",
+ "mailgun/mailgun-php": "^2.1",
+ "php-http/guzzle6-adapter": "^1.1",
+ "php-http/message": "^1.5",
+ "php-http/curl-client": "^1.7",
+ "guzzlehttp/psr7": "^1.3"
+ }
+}
diff --git a/api/composer.lock b/api/composer.lock
new file mode 100644
index 00000000..7ac81d56
--- /dev/null
+++ b/api/composer.lock
@@ -0,0 +1,2945 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "266b9167ab52abc3c4a2514bf29491ed",
+ "packages": [
+ {
+ "name": "clue/stream-filter",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/clue/php-stream-filter.git",
+ "reference": "e3bf9415da163d9ad6701dccb407ed501ae69785"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/clue/php-stream-filter/zipball/e3bf9415da163d9ad6701dccb407ed501ae69785",
+ "reference": "e3bf9415da163d9ad6701dccb407ed501ae69785",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Clue\\StreamFilter\\": "src/"
+ },
+ "files": [
+ "src/functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@lueck.tv"
+ }
+ ],
+ "description": "A simple and modern approach to stream filtering in PHP",
+ "homepage": "https://github.com/clue/php-stream-filter",
+ "keywords": [
+ "bucket brigade",
+ "callback",
+ "filter",
+ "php_user_filter",
+ "stream",
+ "stream_filter_append",
+ "stream_filter_register"
+ ],
+ "time": "2015-11-08T23:41:30+00:00"
+ },
+ {
+ "name": "doctrine/annotations",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/annotations.git",
+ "reference": "54cacc9b81758b14e3ce750f205a393d52339e97"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97",
+ "reference": "54cacc9b81758b14e3ce750f205a393d52339e97",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/lexer": "1.*",
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "doctrine/cache": "1.*",
+ "phpunit/phpunit": "^5.7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "Docblock Annotations Parser",
+ "homepage": "http://www.doctrine-project.org",
+ "keywords": [
+ "annotations",
+ "docblock",
+ "parser"
+ ],
+ "time": "2017-02-24 16:22:25"
+ },
+ {
+ "name": "doctrine/cache",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/cache.git",
+ "reference": "0da649fce4838f7a6121c501c9a86d4b8921b648"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/cache/zipball/0da649fce4838f7a6121c501c9a86d4b8921b648",
+ "reference": "0da649fce4838f7a6121c501c9a86d4b8921b648",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~5.6|~7.0"
+ },
+ "conflict": {
+ "doctrine/common": ">2.2,<2.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7",
+ "predis/predis": "~1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "Caching library offering an object-oriented API for many cache backends",
+ "homepage": "http://www.doctrine-project.org",
+ "keywords": [
+ "cache",
+ "caching"
+ ],
+ "time": "2017-03-06 14:38:51"
+ },
+ {
+ "name": "doctrine/collections",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/collections.git",
+ "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba",
+ "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "~0.1@dev",
+ "phpunit/phpunit": "^5.7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Doctrine\\Common\\Collections\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "Collections Abstraction library",
+ "homepage": "http://www.doctrine-project.org",
+ "keywords": [
+ "array",
+ "collections",
+ "iterator"
+ ],
+ "time": "2017-01-03T10:49:41+00:00"
+ },
+ {
+ "name": "doctrine/common",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/common.git",
+ "reference": "4b434dbf8d204198dac708f2e938f7c805864dd6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/common/zipball/4b434dbf8d204198dac708f2e938f7c805864dd6",
+ "reference": "4b434dbf8d204198dac708f2e938f7c805864dd6",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/annotations": "1.*",
+ "doctrine/cache": "1.*",
+ "doctrine/collections": "1.*",
+ "doctrine/inflector": "1.*",
+ "doctrine/lexer": "1.*",
+ "php": "~5.6|~7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\": "lib/Doctrine/Common"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "Common Library for Doctrine projects",
+ "homepage": "http://www.doctrine-project.org",
+ "keywords": [
+ "annotations",
+ "collections",
+ "eventmanager",
+ "persistence",
+ "spl"
+ ],
+ "time": "2017-03-06 07:30:42"
+ },
+ {
+ "name": "doctrine/dbal",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/dbal.git",
+ "reference": "50bf623418be0feb3282bb50d07a4aea977fb33a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/50bf623418be0feb3282bb50d07a4aea977fb33a",
+ "reference": "50bf623418be0feb3282bb50d07a4aea977fb33a",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/common": "^2.7.1",
+ "php": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.4.6",
+ "phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5",
+ "symfony/console": "2.*||^3.0"
+ },
+ "suggest": {
+ "symfony/console": "For helpful console commands such as SQL execution and import of files."
+ },
+ "bin": [
+ "bin/doctrine-dbal"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.6.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Doctrine\\DBAL\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ }
+ ],
+ "description": "Database Abstraction Layer",
+ "homepage": "http://www.doctrine-project.org",
+ "keywords": [
+ "database",
+ "dbal",
+ "persistence",
+ "queryobject"
+ ],
+ "time": "2017-02-25 22:09:19"
+ },
+ {
+ "name": "doctrine/inflector",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/inflector.git",
+ "reference": "803a2ed9fea02f9ca47cd45395089fe78769a392"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/803a2ed9fea02f9ca47cd45395089fe78769a392",
+ "reference": "803a2ed9fea02f9ca47cd45395089fe78769a392",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Doctrine\\Common\\Inflector\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "Common String Manipulations with regard to casing and singular/plural rules.",
+ "homepage": "http://www.doctrine-project.org",
+ "keywords": [
+ "inflection",
+ "pluralize",
+ "singularize",
+ "string"
+ ],
+ "time": "2016-05-12 17:23:41"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "68099b02b60bbf3b088ff5cb67bf506770ef9cac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/68099b02b60bbf3b088ff5cb67bf506770ef9cac",
+ "reference": "68099b02b60bbf3b088ff5cb67bf506770ef9cac",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3,<8.0-DEV"
+ },
+ "require-dev": {
+ "athletic/athletic": "~0.1.8",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpunit/phpunit": "~4.0",
+ "squizlabs/php_codesniffer": "~2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "http://ocramius.github.com/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://github.com/doctrine/instantiator",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "time": "2017-01-23 09:23:06"
+ },
+ {
+ "name": "doctrine/lexer",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/lexer.git",
+ "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
+ "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Doctrine\\Common\\Lexer\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
+ "homepage": "http://www.doctrine-project.org",
+ "keywords": [
+ "lexer",
+ "parser"
+ ],
+ "time": "2014-09-09 13:34:57"
+ },
+ {
+ "name": "guzzle/guzzle",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle3.git",
+ "reference": "f7778ed85e3db90009d79725afd6c3a82dab32fe"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/f7778ed85e3db90009d79725afd6c3a82dab32fe",
+ "reference": "f7778ed85e3db90009d79725afd6c3a82dab32fe",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "php": ">=5.3.3",
+ "symfony/event-dispatcher": "~2.1"
+ },
+ "replace": {
+ "guzzle/batch": "self.version",
+ "guzzle/cache": "self.version",
+ "guzzle/common": "self.version",
+ "guzzle/http": "self.version",
+ "guzzle/inflection": "self.version",
+ "guzzle/iterator": "self.version",
+ "guzzle/log": "self.version",
+ "guzzle/parser": "self.version",
+ "guzzle/plugin": "self.version",
+ "guzzle/plugin-async": "self.version",
+ "guzzle/plugin-backoff": "self.version",
+ "guzzle/plugin-cache": "self.version",
+ "guzzle/plugin-cookie": "self.version",
+ "guzzle/plugin-curlauth": "self.version",
+ "guzzle/plugin-error-response": "self.version",
+ "guzzle/plugin-history": "self.version",
+ "guzzle/plugin-log": "self.version",
+ "guzzle/plugin-md5": "self.version",
+ "guzzle/plugin-mock": "self.version",
+ "guzzle/plugin-oauth": "self.version",
+ "guzzle/service": "self.version",
+ "guzzle/stream": "self.version"
+ },
+ "require-dev": {
+ "doctrine/cache": "~1.3",
+ "monolog/monolog": "~1.0",
+ "phpunit/phpunit": "3.7.*",
+ "psr/log": "~1.0",
+ "symfony/class-loader": "~2.1",
+ "zendframework/zend-cache": "2.*,<2.3",
+ "zendframework/zend-log": "2.*,<2.3"
+ },
+ "suggest": {
+ "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Guzzle": "src/",
+ "Guzzle\\Tests": "tests/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Guzzle Community",
+ "homepage": "https://github.com/guzzle/guzzle/contributors"
+ }
+ ],
+ "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle",
+ "homepage": "http://guzzlephp.org/",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "rest",
+ "web service"
+ ],
+ "abandoned": "guzzlehttp/guzzle",
+ "time": "2016-10-26 18:22:07"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "6a99df94a22f01b4b9c32ed8789cf30d05bdba92"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/6a99df94a22f01b4b9c32ed8789cf30d05bdba92",
+ "reference": "6a99df94a22f01b4b9c32ed8789cf30d05bdba92",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/promises": "^1.0",
+ "guzzlehttp/psr7": "^1.3.1",
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "phpunit/phpunit": "^4.0",
+ "psr/log": "^1.0"
+ },
+ "suggest": {
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "6.2-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "homepage": "http://guzzlephp.org/",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "rest",
+ "web service"
+ ],
+ "time": "2017-02-19 15:59:27"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+ "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "time": "2016-12-20 10:07:11"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "41972f428b31bc3ebff0707f63dd2165d3ac4cf6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/41972f428b31bc3ebff0707f63dd2165d3ac4cf6",
+ "reference": "41972f428b31bc3ebff0707f63dd2165d3ac4cf6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0",
+ "psr/http-message": "~1.0"
+ },
+ "provide": {
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Schultze",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "time": "2017-02-18 11:43:27"
+ },
+ {
+ "name": "mailgun/mailgun-php",
+ "version": "v2.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mailgun/mailgun-php.git",
+ "reference": "54b7f851b8e0241d593897dc2d50906bf4a43995"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mailgun/mailgun-php/zipball/54b7f851b8e0241d593897dc2d50906bf4a43995",
+ "reference": "54b7f851b8e0241d593897dc2d50906bf4a43995",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5|^7.0",
+ "php-http/discovery": "^1.0",
+ "php-http/httplug": "^1.0",
+ "php-http/message": "^1.0",
+ "php-http/multipart-stream-builder": "^0.1"
+ },
+ "require-dev": {
+ "php-http/guzzle6-adapter": "^1.0",
+ "phpunit/phpunit": "~4.6"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Mailgun": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Travis Swientek",
+ "email": "travis@mailgunhq.com"
+ }
+ ],
+ "description": "The Mailgun SDK provides methods for all API functions.",
+ "time": "2016-08-10T16:58:18+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe",
+ "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "doctrine/collections": "1.*",
+ "phpunit/phpunit": "~4.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "homepage": "https://github.com/myclabs/DeepCopy",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "time": "2017-01-26T22:05:40+00:00"
+ },
+ {
+ "name": "php-http/curl-client",
+ "version": "v1.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/curl-client.git",
+ "reference": "0972ad0d7d37032a52077a5cbe27cf370f2007d8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/curl-client/zipball/0972ad0d7d37032a52077a5cbe27cf370f2007d8",
+ "reference": "0972ad0d7d37032a52077a5cbe27cf370f2007d8",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "php": "^5.5 || ^7.0",
+ "php-http/discovery": "^1.0",
+ "php-http/httplug": "^1.0",
+ "php-http/message": "^1.2",
+ "php-http/message-factory": "^1.0.2"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "1.0",
+ "php-http/client-implementation": "1.0"
+ },
+ "require-dev": {
+ "guzzlehttp/psr7": "^1.0",
+ "php-http/client-integration-tests": "^0.5.1",
+ "phpunit/phpunit": "^4.8.27",
+ "zendframework/zend-diactoros": "^1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Http\\Client\\Curl\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Михаил Красильников",
+ "email": "m.krasilnikov@yandex.ru"
+ }
+ ],
+ "description": "cURL client for PHP-HTTP",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "curl",
+ "http"
+ ],
+ "time": "2017-02-09T15:18:33+00:00"
+ },
+ {
+ "name": "php-http/discovery",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/discovery.git",
+ "reference": "cc5669d9cb51170ad0278a3b984cd3c7894d6ff9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/discovery/zipball/cc5669d9cb51170ad0278a3b984cd3c7894d6ff9",
+ "reference": "cc5669d9cb51170ad0278a3b984cd3c7894d6ff9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5 || ^7.0"
+ },
+ "require-dev": {
+ "henrikbjorn/phpspec-code-coverage": "^2.0.2",
+ "php-http/httplug": "^1.0",
+ "php-http/message-factory": "^1.0",
+ "phpspec/phpspec": "^2.4",
+ "puli/composer-plugin": "1.0.0-beta10"
+ },
+ "suggest": {
+ "php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories",
+ "puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Discovery\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Finds installed HTTPlug implementations and PSR-7 message factories",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "adapter",
+ "client",
+ "discovery",
+ "factory",
+ "http",
+ "message",
+ "psr7"
+ ],
+ "time": "2017-02-12 08:49:24"
+ },
+ {
+ "name": "php-http/guzzle6-adapter",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/guzzle6-adapter.git",
+ "reference": "c0168c6e5fa286c3837310d591114d2683b9b9a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/guzzle6-adapter/zipball/c0168c6e5fa286c3837310d591114d2683b9b9a5",
+ "reference": "c0168c6e5fa286c3837310d591114d2683b9b9a5",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.0",
+ "php": "^5.5 || ^7.0",
+ "php-http/httplug": "^1.0"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "1.0",
+ "php-http/client-implementation": "1.0"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "php-http/client-integration-tests": "^0.5.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Adapter\\Guzzle6\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ },
+ {
+ "name": "David de Boer",
+ "email": "david@ddeboer.nl"
+ }
+ ],
+ "description": "Guzzle 6 HTTP Adapter",
+ "homepage": "http://httplug.io",
+ "keywords": [
+ "Guzzle",
+ "http"
+ ],
+ "time": "2016-08-02 09:03:17"
+ },
+ {
+ "name": "php-http/httplug",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/httplug.git",
+ "reference": "f32fefee51cb96e99edb0c4bb1d11b5026ad5069"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/httplug/zipball/f32fefee51cb96e99edb0c4bb1d11b5026ad5069",
+ "reference": "f32fefee51cb96e99edb0c4bb1d11b5026ad5069",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4",
+ "php-http/promise": "^1.0",
+ "psr/http-message": "^1.0"
+ },
+ "require-dev": {
+ "henrikbjorn/phpspec-code-coverage": "^1.0",
+ "phpspec/phpspec": "^2.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Eric GELOEN",
+ "email": "geloen.eric@gmail.com"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "HTTPlug, the HTTP client abstraction for PHP",
+ "homepage": "http://httplug.io",
+ "keywords": [
+ "client",
+ "http"
+ ],
+ "time": "2017-01-02 06:37:42"
+ },
+ {
+ "name": "php-http/message",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/message.git",
+ "reference": "13df8c48f40ca7925303aa336f19be4b80984f01"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/message/zipball/13df8c48f40ca7925303aa336f19be4b80984f01",
+ "reference": "13df8c48f40ca7925303aa336f19be4b80984f01",
+ "shasum": ""
+ },
+ "require": {
+ "clue/stream-filter": "^1.3",
+ "php": ">=5.4",
+ "php-http/message-factory": "^1.0.2",
+ "psr/http-message": "^1.0"
+ },
+ "require-dev": {
+ "akeneo/phpspec-skip-example-extension": "^1.0",
+ "coduo/phpspec-data-provider-extension": "^1.0",
+ "ext-zlib": "*",
+ "guzzlehttp/psr7": "^1.0",
+ "henrikbjorn/phpspec-code-coverage": "^1.0",
+ "phpspec/phpspec": "^2.4",
+ "slim/slim": "^3.0",
+ "zendframework/zend-diactoros": "^1.0"
+ },
+ "suggest": {
+ "ext-zlib": "Used with compressor/decompressor streams",
+ "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories",
+ "slim/slim": "Used with Slim Framework PSR-7 implementation",
+ "zendframework/zend-diactoros": "Used with Diactoros Factories"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Message\\": "src/"
+ },
+ "files": [
+ "src/filters.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "HTTP Message related tools",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7"
+ ],
+ "time": "2017-02-14 08:58:37"
+ },
+ {
+ "name": "php-http/message-factory",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/message-factory.git",
+ "reference": "a2809d4fe294ebe8879aec8d4d5bf21faa029344"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/message-factory/zipball/a2809d4fe294ebe8879aec8d4d5bf21faa029344",
+ "reference": "a2809d4fe294ebe8879aec8d4d5bf21faa029344",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Factory interfaces for PSR-7 HTTP Message",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "stream",
+ "uri"
+ ],
+ "time": "2016-02-03 08:16:31"
+ },
+ {
+ "name": "php-http/multipart-stream-builder",
+ "version": "0.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/multipart-stream-builder.git",
+ "reference": "74d5ac517778ae87a065c6f4076316c35b58a777"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/74d5ac517778ae87a065c6f4076316c35b58a777",
+ "reference": "74d5ac517778ae87a065c6f4076316c35b58a777",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5 || ^7.0",
+ "php-http/discovery": "^1.0",
+ "php-http/message-factory": "^1.0.2",
+ "psr/http-message": "^1.0"
+ },
+ "require-dev": {
+ "php-http/message": "^1.5",
+ "phpunit/phpunit": "^4.8 || ^5.4",
+ "zendframework/zend-diactoros": "^1.3.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Message\\MultipartStream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ }
+ ],
+ "description": "A builder class that help you create a multipart stream",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "multipart stream",
+ "stream"
+ ],
+ "time": "2017-02-16T08:52:59+00:00"
+ },
+ {
+ "name": "php-http/promise",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/promise.git",
+ "reference": "810b30da8bcf69e4b82c4b9bc6b31518234293ab"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/promise/zipball/810b30da8bcf69e4b82c4b9bc6b31518234293ab",
+ "reference": "810b30da8bcf69e4b82c4b9bc6b31518234293ab",
+ "shasum": ""
+ },
+ "require-dev": {
+ "henrikbjorn/phpspec-code-coverage": "^1.0",
+ "phpspec/phpspec": "^2.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ },
+ {
+ "name": "Joel Wurtz",
+ "email": "joel.wurtz@gmail.com"
+ }
+ ],
+ "description": "Promise used for asynchronous HTTP requests",
+ "homepage": "http://httplug.io",
+ "keywords": [
+ "promise"
+ ],
+ "time": "2016-01-28 07:54:12"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
+ "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": [
+ "src"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "time": "2015-12-27 11:43:31"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e",
+ "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5",
+ "phpdocumentor/reflection-common": "^1.0@dev",
+ "phpdocumentor/type-resolver": "^0.2.0",
+ "webmozart/assert": "^1.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^0.9.4",
+ "phpunit/phpunit": "^4.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "time": "2016-09-30T07:12:33+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "0.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb",
+ "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5",
+ "phpdocumentor/reflection-common": "^1.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^0.9.4",
+ "phpunit/phpunit": "^5.2||^4.8.24"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "time": "2016-11-25T06:54:22+00:00"
+ },
+ {
+ "name": "phpmailer/phpmailer",
+ "version": "v5.2.22",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPMailer/PHPMailer.git",
+ "reference": "b18cb98131bd83103ccb26a888fdfe3177b8a663"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/b18cb98131bd83103ccb26a888fdfe3177b8a663",
+ "reference": "b18cb98131bd83103ccb26a888fdfe3177b8a663",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.0.0"
+ },
+ "require-dev": {
+ "phpdocumentor/phpdocumentor": "*",
+ "phpunit/phpunit": "4.7.*"
+ },
+ "suggest": {
+ "league/oauth2-google": "Needed for Google XOAUTH2 authentication"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "class.phpmailer.php",
+ "class.phpmaileroauth.php",
+ "class.phpmaileroauthgoogle.php",
+ "class.smtp.php",
+ "class.pop3.php",
+ "extras/EasyPeasyICS.php",
+ "extras/ntlm_sasl_client.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1"
+ ],
+ "authors": [
+ {
+ "name": "Jim Jagielski",
+ "email": "jimjag@gmail.com"
+ },
+ {
+ "name": "Marcus Bointon",
+ "email": "phpmailer@synchromedia.co.uk"
+ },
+ {
+ "name": "Andy Prevost",
+ "email": "codeworxtech@users.sourceforge.net"
+ },
+ {
+ "name": "Brent R. Matzelle"
+ }
+ ],
+ "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+ "time": "2017-01-09T09:33:47+00:00"
+ },
+ {
+ "name": "phpspec/prophecy",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "6c52c2722f8460122f96f86346600e1077ce22cb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb",
+ "reference": "6c52c2722f8460122f96f86346600e1077ce22cb",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.0.2",
+ "php": "^5.3|^7.0",
+ "phpdocumentor/reflection-docblock": "^2.0|^3.0.2",
+ "sebastian/comparator": "^1.1",
+ "sebastian/recursion-context": "^1.0|^2.0"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^2.0",
+ "phpunit/phpunit": "^4.8 || ^5.6.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.6.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Prophecy\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ },
+ {
+ "name": "Marcello Duarte",
+ "email": "marcello.duarte@gmail.com"
+ }
+ ],
+ "description": "Highly opinionated mocking framework for PHP 5.3+",
+ "homepage": "https://github.com/phpspec/prophecy",
+ "keywords": [
+ "Double",
+ "Dummy",
+ "fake",
+ "mock",
+ "spy",
+ "stub"
+ ],
+ "time": "2016-11-21 14:58:47"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "4.0.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "7a2bfe73aa381a76cb6d13599ae37bf74a12a02f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7a2bfe73aa381a76cb6d13599ae37bf74a12a02f",
+ "reference": "7a2bfe73aa381a76cb6d13599ae37bf74a12a02f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "phpunit/php-file-iterator": "~1.3",
+ "phpunit/php-text-template": "~1.2",
+ "phpunit/php-token-stream": "^1.4.2",
+ "sebastian/code-unit-reverse-lookup": "~1.0",
+ "sebastian/environment": "^1.3.2 || ^2.0",
+ "sebastian/version": "~1.0|~2.0"
+ },
+ "require-dev": {
+ "ext-xdebug": ">=2.1.4",
+ "phpunit/phpunit": "^5.4"
+ },
+ "suggest": {
+ "ext-dom": "*",
+ "ext-xdebug": ">=2.4.0",
+ "ext-xmlwriter": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sb@sebastian-bergmann.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "time": "2017-01-24 16:35:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
+ "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sb@sebastian-bergmann.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "time": "2016-10-03 07:40:28"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+ "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "time": "2015-06-21T13:50:34+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260",
+ "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4|~5"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sb@sebastian-bergmann.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "time": "2016-05-12T18:03:57+00:00"
+ },
+ {
+ "name": "phpunit/php-token-stream",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-token-stream.git",
+ "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b",
+ "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Wrapper around PHP's tokenizer extension.",
+ "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
+ "keywords": [
+ "tokenizer"
+ ],
+ "time": "2016-11-15 14:06:22"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "5.7.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "69f832b87c731d5cacad7f91948778fe98335fdd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69f832b87c731d5cacad7f91948778fe98335fdd",
+ "reference": "69f832b87c731d5cacad7f91948778fe98335fdd",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "myclabs/deep-copy": "~1.3",
+ "php": "^5.6 || ^7.0",
+ "phpspec/prophecy": "^1.6.2",
+ "phpunit/php-code-coverage": "^4.0.4",
+ "phpunit/php-file-iterator": "~1.4",
+ "phpunit/php-text-template": "~1.2",
+ "phpunit/php-timer": "^1.0.6",
+ "phpunit/phpunit-mock-objects": "^3.2",
+ "sebastian/comparator": "~1.2.2",
+ "sebastian/diff": "~1.2",
+ "sebastian/environment": "^1.3.4 || ^2.0",
+ "sebastian/exporter": "~2.0",
+ "sebastian/global-state": "^1.0 || ^2.0",
+ "sebastian/object-enumerator": "~2.0",
+ "sebastian/resource-operations": "~1.0",
+ "sebastian/version": "~1.0|~2.0",
+ "symfony/yaml": "~2.1|~3.0"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "3.0.2"
+ },
+ "require-dev": {
+ "ext-pdo": "*"
+ },
+ "suggest": {
+ "ext-xdebug": "*",
+ "phpunit/php-invoker": "~1.1"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.7.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "time": "2017-01-28T06:14:33+00:00"
+ },
+ {
+ "name": "phpunit/phpunit-mock-objects",
+ "version": "3.4.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
+ "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24",
+ "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.0.2",
+ "php": "^5.6 || ^7.0",
+ "phpunit/php-text-template": "^1.2",
+ "sebastian/exporter": "^1.2 || ^2.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.4"
+ },
+ "suggest": {
+ "ext-soap": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sb@sebastian-bergmann.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Mock Object library for PHPUnit",
+ "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
+ "keywords": [
+ "mock",
+ "xunit"
+ ],
+ "time": "2016-12-08 20:27:08"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "time": "2016-08-06 14:39:51"
+ },
+ {
+ "name": "sabre/event",
+ "version": "2.0.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fruux/sabre-event.git",
+ "reference": "337b6f5e10ea6e0b21e22c7e5788dd3883ae73ff"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fruux/sabre-event/zipball/337b6f5e10ea6e0b21e22c7e5788dd3883ae73ff",
+ "reference": "337b6f5e10ea6e0b21e22c7e5788dd3883ae73ff",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*",
+ "sabre/cs": "~0.0.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Sabre\\Event\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "sabre/event is a library for lightweight event-based programming",
+ "homepage": "http://sabre.io/event/",
+ "keywords": [
+ "EventEmitter",
+ "events",
+ "hooks",
+ "plugin",
+ "promise",
+ "signal"
+ ],
+ "time": "2015-05-19 10:24:22"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "11606652af09e847cdbbbc3ca17df26b1173a454"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/11606652af09e847cdbbbc3ca17df26b1173a454",
+ "reference": "11606652af09e847cdbbbc3ca17df26b1173a454",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "time": "2016-12-06 20:05:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "1.2.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+ "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "sebastian/diff": "~1.2",
+ "sebastian/exporter": "~1.2 || ~2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "http://www.github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "time": "2017-01-29 09:50:25"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "d0814318784b7756fb932116acd19ee3b0cbe67a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/d0814318784b7756fb932116acd19ee3b0cbe67a",
+ "reference": "d0814318784b7756fb932116acd19ee3b0cbe67a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff"
+ ],
+ "time": "2016-10-03 07:45:03"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
+ "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "time": "2016-11-26 07:53:53"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4",
+ "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "sebastian/recursion-context": "~2.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "time": "2016-11-19 08:54:04"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "ab3e5ce501d9d45288b53f885a54c87e0cdc7164"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ab3e5ce501d9d45288b53f885a54c87e0cdc7164",
+ "reference": "ab3e5ce501d9d45288b53f885a54c87e0cdc7164",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 || ^6.0"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "time": "2016-12-12 08:07:45"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35",
+ "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6",
+ "sebastian/recursion-context": "~2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "time": "2016-11-19 07:35:10"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a",
+ "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "time": "2016-11-19 07:33:16"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "fadc83f7c41fb2924e542635fea47ae546816ece"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/fadc83f7c41fb2924e542635fea47ae546816ece",
+ "reference": "fadc83f7c41fb2924e542635fea47ae546816ece",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "time": "2016-10-03 07:43:09"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
+ "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "time": "2016-10-03 07:35:21"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "2.8.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "3178c0e247b81da8a0265b460ac23bec6d2e6627"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3178c0e247b81da8a0265b460ac23bec6d2e6627",
+ "reference": "3178c0e247b81da8a0265b460ac23bec6d2e6627",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/config": "^2.0.5|~3.0.0",
+ "symfony/dependency-injection": "~2.6|~3.0.0",
+ "symfony/expression-language": "~2.6|~3.0.0",
+ "symfony/stopwatch": "~2.3|~3.0.0"
+ },
+ "suggest": {
+ "symfony/dependency-injection": "",
+ "symfony/http-kernel": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony EventDispatcher Component",
+ "homepage": "https://symfony.com",
+ "time": "2017-02-18 19:13:35"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "7928849b226f065dae93ec0e8be3b829f73ba67b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/7928849b226f065dae93ec0e8be3b829f73ba67b",
+ "reference": "7928849b226f065dae93ec0e8be3b829f73ba67b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5.9"
+ },
+ "require-dev": {
+ "symfony/console": "~2.8|~3.0"
+ },
+ "suggest": {
+ "symfony/console": "For validating YAML files using the lint command"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Yaml Component",
+ "homepage": "https://symfony.com",
+ "time": "2017-01-21 17:10:26"
+ },
+ {
+ "name": "vlucas/spot2",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/vlucas/spot2.git",
+ "reference": "f30e5439c1c8d969490d773bc3f87937e675083a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/vlucas/spot2/zipball/f30e5439c1c8d969490d773bc3f87937e675083a",
+ "reference": "f30e5439c1c8d969490d773bc3f87937e675083a",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "~2.4",
+ "php": ">=5.4",
+ "sabre/event": "~2.0",
+ "vlucas/valitron": "1.x"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Spot": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD"
+ ],
+ "authors": [
+ {
+ "name": "Vance Lucas",
+ "email": "vance@vancelucas.com",
+ "homepage": "http://www.vancelucas.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "Simple DataMapper built on top of Doctrine DBAL",
+ "homepage": "https://github.com/vlucas/spot2",
+ "keywords": [
+ "database",
+ "datamapper",
+ "dbal",
+ "doctrine",
+ "entity",
+ "mapper",
+ "model",
+ "orm"
+ ],
+ "time": "2014-07-03T14:29:08+00:00"
+ },
+ {
+ "name": "vlucas/valitron",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/vlucas/valitron.git",
+ "reference": "b33c79116260637337187b7125f955ae26d306cc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/vlucas/valitron/zipball/b33c79116260637337187b7125f955ae26d306cc",
+ "reference": "b33c79116260637337187b7125f955ae26d306cc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Valitron": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD"
+ ],
+ "authors": [
+ {
+ "name": "Vance Lucas",
+ "email": "vance@vancelucas.com",
+ "homepage": "http://www.vancelucas.com"
+ }
+ ],
+ "description": "Simple, elegant, stand-alone validation library with NO dependencies",
+ "homepage": "http://github.com/vlucas/valitron",
+ "keywords": [
+ "valid",
+ "validation",
+ "validator"
+ ],
+ "time": "2017-02-23T08:31:59+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/assert.git",
+ "reference": "4a8bf11547e139e77b651365113fc12850c43d9a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/4a8bf11547e139e77b651365113fc12850c43d9a",
+ "reference": "4a8bf11547e139e77b651365113fc12850c43d9a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.6",
+ "sebastian/version": "^1.0.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "time": "2016-11-23 20:04:41"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "dev",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": []
+}
diff --git a/api/core/headers.php b/api/core/headers.php
deleted file mode 100644
index 892fafb0..00000000
--- a/api/core/headers.php
+++ /dev/null
@@ -1,9 +0,0 @@
- $value) {
- if ($key != 'id') {
- $lowercase_key = lcfirst($key);
- $row[$lowercase_key] = $row[$key];
- unset($row[$key]);
- }
- if ($key == 'id' || $lowercase_key == 'closable'
- || $lowercase_key == 'key' || $lowercase_key == 'sort'
- || $lowercase_key == 'textColor') {
- continue;
- }
- $row[$lowercase_key] = $row[$lowercase_key] == true;
- }
-
- $language_sql = "SELECT * FROM `" . hesk_dbEscape($hesk_settings['db_pfix']) . "text_to_status_xref` "
- . "WHERE `status_id` = ".intval($row['id']);
-
- $language_rs = hesk_dbQuery($language_sql);
- if (hesk_dbNumRows($language_rs) > 0) {
- $row['key'] = NULL;
- $row['keys'] = array();
- }
- while ($language_row = hesk_dbFetchAssoc($language_rs)) {
- unset($language_row['id']);
- unset($language_row['status_id']);
- $row['keys'][] = $language_row;
- }
-
- $results[] = $row;
- }
-
- return $id == NULL ? $results : $results[0];
-}
\ No newline at end of file
diff --git a/api/dao/ticket_dao.php b/api/dao/ticket_dao.php
deleted file mode 100644
index 88ae5047..00000000
--- a/api/dao/ticket_dao.php
+++ /dev/null
@@ -1,59 +0,0 @@
- " . intval($user['id']) . "))";
- }
-
- $response = hesk_dbQuery($sql);
-
- if (hesk_dbNumRows($response) == 0) {
- return NULL;
- }
-
- $results = build_results($response);
-
- return $id == NULL ? $results : $results[0];
-}
-
-function build_results($response) {
- $results = array();
- while ($row = hesk_dbFetchAssoc($response)) {
- $row['id'] = intval($row['id']);
- $row['category'] = intval($row['category']);
- $row['priority'] = intval($row['priority']);
- $row['status'] = intval($row['status']);
- $row['replierid'] = intval($row['replierid']);
- $row['archive'] = $row['archive'] == true;
- $row['locked'] = $row['locked'] == true;
- $row['html'] = $row['html'] == true;
- $row['screen_resolution_height'] = convert_to_int($row['screen_resolution_height']);
- $row['screen_resolution_width'] = convert_to_int($row['screen_resolution_width']);
- $row['owner'] = convert_to_int($row['owner']);
- $row['parent'] = convert_to_int($row['parent']);
- $row['overdue_email_sent'] = $row['overdue_email_sent'] == true;
-
-
- $results[] = $row;
- }
-
- return $results;
-}
-
-function convert_to_int($item) {
- return $item != NULL ? intval($item) : NULL;
-}
\ No newline at end of file
diff --git a/api/dao/ticket_template_dao.php b/api/dao/ticket_template_dao.php
deleted file mode 100644
index 118ba218..00000000
--- a/api/dao/ticket_template_dao.php
+++ /dev/null
@@ -1,26 +0,0 @@
- "The endpoint '{$_SERVER['REQUEST_URI']}' was not found. Double-check your request and submit again.",
+ 'uri' => $_SERVER['REQUEST_URI']
+ ), 404);
+}
+
+function before() {
+ assertApiIsEnabled();
+
+ $token = \BusinessLogic\Helpers::getHeader('X-AUTH-TOKEN');
+ buildUserContext($token);
+}
+
+function assertApiIsEnabled() {
+ global $applicationContext, $hesk_settings;
+
+ /* @var $apiChecker \BusinessLogic\Settings\ApiChecker */
+ $apiChecker = $applicationContext->get[\BusinessLogic\Settings\ApiChecker::class];
+
+ if (!$apiChecker->isApiEnabled($hesk_settings)) {
+ print output(array('message' => 'API Disabled'), 404);
+ die();
+ }
+
+ return;
+}
+
+function buildUserContext($xAuthToken) {
+ global $applicationContext, $userContext, $hesk_settings;
+
+ /* @var $userContextBuilder \BusinessLogic\Security\UserContextBuilder */
+ $userContextBuilder = $applicationContext->get[\BusinessLogic\Security\UserContextBuilder::class];
+
+ $userContext = $userContextBuilder->buildUserContext($xAuthToken, $hesk_settings);
+}
+
+function errorHandler($errorNumber, $errorMessage, $errorFile, $errorLine) {
+ exceptionHandler(new Exception(sprintf("%s:%d\n\n%s", $errorFile, $errorLine, $errorMessage)));
+}
+
+/**
+ * @param $exception Exception
+ */
+function exceptionHandler($exception) {
+ global $applicationContext, $userContext, $hesk_settings;
+
+ if (strpos($exception->getTraceAsString(), 'LoggingGateway') !== false) {
+ //-- Suppress these for now, as it would cause issues to output two JSONs at one time.
+ return;
+ }
+
+
+ /* @var $loggingGateway \DataAccess\Logging\LoggingGateway */
+ $loggingGateway = $applicationContext->get[\DataAccess\Logging\LoggingGateway::class];
+
+ // We don't cast API Friendly Exceptions as they're user-generated errors
+ if (exceptionIsOfType($exception, \BusinessLogic\Exceptions\ApiFriendlyException::class)) {
+ /* @var $castedException \BusinessLogic\Exceptions\ApiFriendlyException */
+ $castedException = $exception;
+
+ print_error($castedException->title, $castedException->getMessage(), $castedException->httpResponseCode);
+ } elseif (exceptionIsOfType($exception, \Core\Exceptions\SQLException::class)) {
+ /* @var $castedException \Core\Exceptions\SQLException */
+ $castedException = $exception;
+
+ $logId = tryToLog(getLoggingLocation($exception),
+ "Fought an uncaught SQL exception: " . $castedException->failingQuery, $castedException->getTraceAsString(),
+ $userContext, $hesk_settings);
+
+ $logIdText = $logId === null ? "Additionally, the error could not be logged! :'(" : "Log ID: {$logId}";
+ print_error("SQL Exception", "Fought an uncaught SQL exception. Check the logs for more information. {$logIdText}");
+ } else {
+ $logId = tryToLog(getLoggingLocation($exception),
+ $exception->getMessage(), $exception->getTraceAsString(),
+ $userContext, $hesk_settings);
+
+ $logIdText = $logId === null ? "Additionally, the error could not be logged! :'(" : "Log ID: {$logId}";
+ print_error("Exception Occurred", "Fought an uncaught exception. Check the logs for more information. {$logIdText}");
+ }
+
+ die();
+}
+
+/**
+ * @param $location string
+ * @param $message string
+ * @param $stackTrace string
+ * @param $userContext \BusinessLogic\Security\UserContext
+ * @param $heskSettings array
+ * @return int|null The inserted ID, or null if failed to log
+ * @internal param Exception $exception
+ */
+function tryToLog($location, $message, $stackTrace, $userContext, $heskSettings) {
+ global $applicationContext;
+
+ /* @var $loggingGateway \DataAccess\Logging\LoggingGateway */
+ $loggingGateway = $applicationContext->get[\DataAccess\Logging\LoggingGateway::class];
+
+ try {
+ return $loggingGateway->logError($location, $message, $stackTrace, $userContext, $heskSettings);
+ } catch (Exception $squished) {
+ return null;
+ }
+}
+
+/**
+ * @param $exception Exception
+ * @return string The location of the exception
+ */
+function getLoggingLocation($exception) {
+ // http://stackoverflow.com/a/9133897/1509431
+ $trace = $exception->getTrace();
+ $lastCall = $trace[0];
+ $location = basename($lastCall['file'], '.php');
+ return "REST API: {$location}";
+}
+
+/**
+ * @param $exception Exception thrown exception
+ * @param $class string The name of the expected exception type
+ * @return bool
+ */
+function exceptionIsOfType($exception, $class) {
+ return is_a($exception, $class);
+}
+
+function fatalErrorShutdownHandler() {
+ $last_error = error_get_last();
+ if ($last_error['type'] === E_ERROR) {
+ // fatal error
+ errorHandler(E_ERROR, $last_error['message'], $last_error['file'], $last_error['line']);
+ }
+}
+
+Link::before('before');
+
+Link::all(array(
+ // Categories
+ '/v1/categories' => \Controllers\Categories\CategoryController::class . '::printAllCategories',
+ '/v1/categories/{i}' => \Controllers\Categories\CategoryController::class,
+ // Tickets
+ '/v1/tickets' => \Controllers\Tickets\CustomerTicketController::class,
+ // Tickets - Staff
+ '/v1/staff/tickets/{i}' => \Controllers\Tickets\StaffTicketController::class,
+ // Attachments
+ '/v1/staff/tickets/{i}/attachments' => \Controllers\Attachments\StaffTicketAttachmentsController::class,
+ '/v1/staff/tickets/{i}/attachments/{i}' => \Controllers\Attachments\StaffTicketAttachmentsController::class,
+ // Statuses
+ '/v1/statuses' => \Controllers\Statuses\StatusController::class,
+ // Settings
+ '/v1/settings' => \Controllers\Settings\SettingsController::class,
+
+ // Any URL that doesn't match goes to the 404 handler
+ '404' => 'handle404'
+));
\ No newline at end of file
diff --git a/api/ticket/index.php b/api/ticket/index.php
index ad8cf76a..fb9b5b5c 100644
--- a/api/ticket/index.php
+++ b/api/ticket/index.php
@@ -3,7 +3,6 @@ define('IN_SCRIPT', 1);
define('HESK_PATH', '../../');
define('API_PATH', '../');
require_once(HESK_PATH . 'hesk_settings.inc.php');
-require_once(HESK_PATH . 'inc/common.inc.php');
require_once(API_PATH . 'core/headers.php');
require_once(API_PATH . 'core/output.php');
require_once(API_PATH . 'businesslogic/ticket_retriever.php');
diff --git a/inc/common.inc.php b/inc/common.inc.php
index f3b063eb..920e0787 100644
--- a/inc/common.inc.php
+++ b/inc/common.inc.php
@@ -262,13 +262,13 @@ function hesk_load_database_functions()
function hesk_load_api_database_functions()
{
- require(HESK_PATH . 'api/core/json_error.php');
+ require(__DIR__ . '/../api/core/json_error.php');
// Preferrably use the MySQLi functions
if (function_exists('mysqli_connect')) {
- require(HESK_PATH . 'api/core/database_mysqli.inc.php');
+ require(__DIR__ . '/../api/core/database_mysqli.inc.php');
} // Default to MySQL
else {
- require(HESK_PATH . 'api/core/database.inc.php');
+ require(__DIR__ . '/../api/core/database.inc.php');
}
} // END hesk_load_database_functions()
@@ -1068,7 +1068,6 @@ function hesk_ticketToPlain($ticket, $specialchars = 0, $strip = 1)
}
} // END hesk_ticketToPlain()
-
function hesk_msgToPlain($msg, $specialchars = 0, $strip = 1)
{
$msg = preg_replace('/\/i', "$2", $msg);
diff --git a/install/mods-for-hesk/sql/installSql.php b/install/mods-for-hesk/sql/installSql.php
index 806d4a7d..7a970cda 100644
--- a/install/mods-for-hesk/sql/installSql.php
+++ b/install/mods-for-hesk/sql/installSql.php
@@ -986,4 +986,11 @@ function execute307Scripts() {
hesk_dbConnect();
updateVersion('3.0.7');
+}
+
+function execute310Scripts() {
+ global $hesk_settings;
+ hesk_dbConnect();
+
+ executeQuery("ALTER TABLE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "logging` ADD COLUMN `stack_trace` TEXT");
}
\ No newline at end of file
diff --git a/internal-api/dao/message_log_dao.php b/internal-api/dao/message_log_dao.php
index cb8ddece..498acd68 100644
--- a/internal-api/dao/message_log_dao.php
+++ b/internal-api/dao/message_log_dao.php
@@ -23,12 +23,15 @@ function search_log($hesk_settings, $location, $from_date, $to_date, $severity_i
if ($severity_id != NULL) {
$sql .= "AND `severity` = " . intval($severity_id);
}
+ $sql .= " ORDER BY `id` DESC";
$rs = hesk_dbQuery($sql);
$results = array();
while ($row = hesk_dbFetchAssoc($rs)) {
$row['timestamp'] = hesk_date($row['timestamp'], true);
+ $row['stackTrace'] = nl2br($row['stack_trace']);
+ unset($row['stack_trace']);
$results[] = $row;
}
diff --git a/internal-api/js/view-message-log.js b/internal-api/js/view-message-log.js
index c95f484a..6acbc741 100644
--- a/internal-api/js/view-message-log.js
+++ b/internal-api/js/view-message-log.js
@@ -53,7 +53,8 @@ function displayResults(data) {
'' + result.timestamp + ' | ' +
'' + result.username + ' | ' +
'' + result.location + ' | ' +
- '' + result.message + ' | ');
+ '' + result.message + ' | ' +
+ '' + result.stackTrace + ' | ');
}
}
}
diff --git a/language/en/text.php b/language/en/text.php
index d32a2868..8bfcbb46 100644
--- a/language/en/text.php
+++ b/language/en/text.php
@@ -45,6 +45,9 @@ $hesklang['_COLLATE']='utf8_unicode_ci';
// This is the email break line that will be used in email piping
$hesklang['EMAIL_HR']='------ Reply above this line ------';
+// ADDED OR MODIFIED IN Mods for HESK 3.1.0
+$hesklang['stack_trace_header'] = 'Stack Trace';
+
// ADDED OR MODIFIED IN Mods for HESK 3.0.0
$hesklang['you_have_x_messages'] = 'You have %s new %s'; // %s: Number of new messages, "message" or "messages", depending on #
$hesklang['message_lower_case'] = 'message';