diff --git a/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php index a0554548..237c4611 100644 --- a/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php +++ b/source/packages/com_mokosuiteclient/admin/src/Controller/DisplayController.php @@ -500,6 +500,7 @@ class DisplayController extends BaseController if (strlen($query) < 3) { $this->jsonResponse(['results' => []]); + return; } try @@ -527,7 +528,8 @@ class DisplayController extends BaseController } catch (\Throwable $e) { - $this->jsonResponse(['results' => []]); + Log::add('KB search failed: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient'); + $this->jsonResponse(['results' => [], 'error' => 'Search unavailable']); } } @@ -575,7 +577,7 @@ class DisplayController extends BaseController public function saveCategory() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $input = Factory::getApplication()->getInput(); $db = Factory::getDbo(); $id = $input->getInt('id', 0); @@ -600,7 +602,7 @@ class DisplayController extends BaseController public function deleteCategory() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $db = Factory::getDbo(); $db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute(); $this->jsonResponse(['success' => true, 'message' => 'Category deleted.']); @@ -609,7 +611,7 @@ class DisplayController extends BaseController public function reorderCategory() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true); if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; } $db = Factory::getDbo(); @@ -622,7 +624,7 @@ class DisplayController extends BaseController public function saveCanned() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $input = Factory::getApplication()->getInput(); $db = Factory::getDbo(); $data = (object) [ @@ -640,7 +642,7 @@ class DisplayController extends BaseController public function deleteCanned() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $db = Factory::getDbo(); $db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute(); $this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']); @@ -649,7 +651,7 @@ class DisplayController extends BaseController public function reorderCanned() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true); if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; } $db = Factory::getDbo(); @@ -662,7 +664,7 @@ class DisplayController extends BaseController public function uploadAttachment() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $input = Factory::getApplication()->getInput(); $ticketId = $input->getInt('ticket_id', 0); $replyId = $input->getInt('reply_id', 0) ?: null; @@ -675,7 +677,7 @@ class DisplayController extends BaseController public function downloadAttachment() { - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $id = Factory::getApplication()->getInput()->getInt('id', 0); $db = Factory::getDbo(); $db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuiteclient_ticket_attachments')->where('id = ' . $id)); @@ -696,7 +698,7 @@ class DisplayController extends BaseController public function deleteAttachment() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $id = Factory::getApplication()->getInput()->getInt('id', 0); $ok = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::delete($id); $this->jsonResponse(['success' => $ok, 'message' => $ok ? 'Attachment deleted' : 'Not found']); @@ -705,7 +707,7 @@ class DisplayController extends BaseController public function rateTicket() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); } + if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; } $input = Factory::getApplication()->getInput(); $ticketId = $input->getInt('ticket_id', 0); $rating = $input->getInt('rating', 0); @@ -728,7 +730,7 @@ class DisplayController extends BaseController public function saveAutomation() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); } + if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; } $input = Factory::getApplication()->getInput(); $db = Factory::getDbo(); $data = (object) [ @@ -749,7 +751,7 @@ class DisplayController extends BaseController public function deleteAutomation() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); } + if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; } $db = Factory::getDbo(); $db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute(); $this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']); @@ -758,7 +760,7 @@ class DisplayController extends BaseController public function toggleAutomation() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); } + if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; } $input = Factory::getApplication()->getInput(); $db = Factory::getDbo(); $db->setQuery($db->getQuery(true)->update('#__mokosuiteclient_ticket_automation') @@ -770,7 +772,7 @@ class DisplayController extends BaseController public function reorderAutomation() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); } + if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; } $order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true); if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; } $db = Factory::getDbo(); diff --git a/source/packages/com_mokosuiteclient/admin/src/Service/AttachmentService.php b/source/packages/com_mokosuiteclient/admin/src/Service/AttachmentService.php index 1ade4802..e150db3c 100644 --- a/source/packages/com_mokosuiteclient/admin/src/Service/AttachmentService.php +++ b/source/packages/com_mokosuiteclient/admin/src/Service/AttachmentService.php @@ -52,8 +52,9 @@ class AttachmentService $ticketDir = self::STORAGE_DIR . '/' . $ticketId; - if (!is_dir($ticketDir)) { - Folder::create($ticketDir); + if (!is_dir($ticketDir) && !Folder::create($ticketDir)) { + Log::add("Failed to create attachment directory: {$ticketDir}", Log::ERROR, 'mokosuiteclient'); + return []; } $userId = (int) Factory::getUser()->id; @@ -62,6 +63,7 @@ class AttachmentService for ($i = 0, $count = count($files['name']); $i < $count; $i++) { if ($files['error'][$i] !== UPLOAD_ERR_OK) { + Log::add("Attachment upload error for '{$files['name'][$i]}': PHP error code {$files['error'][$i]}", Log::WARNING, 'mokosuiteclient'); continue; } @@ -127,9 +129,13 @@ class AttachmentService /** * Get the absolute filesystem path for an attachment. */ - public static function getAbsolutePath(object $attachment): string + public static function getAbsolutePath(object $attachment): ?string { - return self::STORAGE_DIR . '/' . $attachment->filepath; + $path = realpath(self::STORAGE_DIR . '/' . $attachment->filepath); + if ($path === false || !str_starts_with($path, realpath(self::STORAGE_DIR))) { + return null; + } + return $path; } /** diff --git a/source/packages/com_mokosuiteclient/admin/src/Service/AutomationEngine.php b/source/packages/com_mokosuiteclient/admin/src/Service/AutomationEngine.php index 52b30a9f..69458a04 100644 --- a/source/packages/com_mokosuiteclient/admin/src/Service/AutomationEngine.php +++ b/source/packages/com_mokosuiteclient/admin/src/Service/AutomationEngine.php @@ -187,7 +187,7 @@ class AutomationEngine } catch (\Throwable $e) { - Log::add("Automation action {$type} failed: " . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); + Log::add("Automation action '{$type}' failed for rule #{$rule->id}: " . $e->getMessage(), Log::ERROR, 'mokosuiteclient'); } } } diff --git a/source/packages/com_mokosuiteclient/admin/src/Service/NotificationService.php b/source/packages/com_mokosuiteclient/admin/src/Service/NotificationService.php index 8cf71f3c..caba2318 100644 --- a/source/packages/com_mokosuiteclient/admin/src/Service/NotificationService.php +++ b/source/packages/com_mokosuiteclient/admin/src/Service/NotificationService.php @@ -305,6 +305,7 @@ class NotificationService } catch (\Throwable $e) { + Log::add('Failed to look up email for user ID ' . $userId . ': ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); return null; } } @@ -331,6 +332,7 @@ class NotificationService } catch (\Throwable $e) { + Log::add('Failed to load notification config: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient'); return []; } } @@ -397,12 +399,16 @@ class NotificationService curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 5); - curl_exec($ch); - + $response = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); curl_close($ch); - if ($httpCode < 200 || $httpCode >= 300) + if ($response === false) + { + Log::add("Ntfy push connection failed for event {$event}: " . $curlError, Log::WARNING, 'mokosuiteclient'); + } + elseif ($httpCode < 200 || $httpCode >= 300) { Log::add("Ntfy push failed (HTTP {$httpCode}) for event {$event}", Log::WARNING, 'mokosuiteclient'); } diff --git a/source/packages/com_mokosuiteclient/api/src/Controller/TicketsController.php b/source/packages/com_mokosuiteclient/api/src/Controller/TicketsController.php index fdbfb1ad..e66297c7 100644 --- a/source/packages/com_mokosuiteclient/api/src/Controller/TicketsController.php +++ b/source/packages/com_mokosuiteclient/api/src/Controller/TicketsController.php @@ -264,6 +264,7 @@ class TicketsController extends BaseController $user = Factory::getUser(); if (!$user->authorise($action, $asset)) { $this->sendJson(403, ['error' => 'Not authorized']); + throw new \RuntimeException('Not authorized', 403); } } diff --git a/source/packages/plg_system_mokosuiteclient/script.php b/source/packages/plg_system_mokosuiteclient/script.php index 217df622..0eb2c326 100644 --- a/source/packages/plg_system_mokosuiteclient/script.php +++ b/source/packages/plg_system_mokosuiteclient/script.php @@ -527,7 +527,7 @@ class plgSystemMokoSuiteClientInstallerScript implements InstallerScriptInterfac } catch (\Exception $e) { - // Don't break install if email fails + Log::add('Install notification email failed: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); } } diff --git a/source/packages/plg_task_mokosuiteclient_tickets/src/Extension/TicketAutomation.php b/source/packages/plg_task_mokosuiteclient_tickets/src/Extension/TicketAutomation.php index 55013bab..66ea7c08 100644 --- a/source/packages/plg_task_mokosuiteclient_tickets/src/Extension/TicketAutomation.php +++ b/source/packages/plg_task_mokosuiteclient_tickets/src/Extension/TicketAutomation.php @@ -251,6 +251,7 @@ class TicketAutomation extends CMSPlugin implements SubscriberInterface } catch (\Throwable $e) { + Log::add('Failed to load component config: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient'); return []; } }