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';