4b9a675d0f
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 36s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 40s
Generic: Project CI / Tests (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
All Joomla element names, PHP classes, language files, folder structure, and manifest references renamed from mokosuite to mokosuiteclient. This repo is now the client-facing tracker for the MokoSuite platform.
417 lines
9.6 KiB
PHP
417 lines
9.6 KiB
PHP
<?php
|
|
/**
|
|
* @package MokoSuiteClient
|
|
* @subpackage com_mokosuiteclient
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Moko\Component\MokoSuiteClient\Administrator\Service;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Log\Log;
|
|
use Joomla\CMS\Uri\Uri;
|
|
|
|
/**
|
|
* Helpdesk email notification service.
|
|
*
|
|
* Sends emails for ticket events to Joomla users (by ID) and/or
|
|
* raw email addresses. Uses Joomla's configured mailer.
|
|
*
|
|
* @since 02.32.00
|
|
*/
|
|
class NotificationService
|
|
{
|
|
/**
|
|
* Send a ticket notification email.
|
|
*
|
|
* @param string $event Event name (ticket_created, ticket_replied, status_changed, ticket_assigned)
|
|
* @param object $ticket Ticket object with id, subject, status, priority, created_by, assigned_to
|
|
* @param array $extra Extra context (reply body, old status, etc.)
|
|
*/
|
|
public static function notify(string $event, object $ticket, array $extra = []): void
|
|
{
|
|
try
|
|
{
|
|
$recipients = self::getRecipients($event, $ticket);
|
|
|
|
if (empty($recipients))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$subject = self::buildSubject($event, $ticket);
|
|
$body = self::buildBody($event, $ticket, $extra);
|
|
|
|
$mailer = Factory::getMailer();
|
|
$mailer->isHtml(false);
|
|
$mailer->setSubject($subject);
|
|
$mailer->setBody($body);
|
|
|
|
foreach ($recipients as $email)
|
|
{
|
|
$email = trim($email);
|
|
|
|
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
$mailer->clearAddresses();
|
|
$mailer->addRecipient($email);
|
|
$mailer->Send();
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
|
}
|
|
}
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine recipients based on event type and ticket data.
|
|
*/
|
|
private static function getRecipients(string $event, object $ticket): array
|
|
{
|
|
$emails = [];
|
|
|
|
// Get notification config from component params
|
|
$config = self::getNotificationConfig();
|
|
|
|
// Always notify configured admin emails
|
|
$adminEmails = array_filter(array_map('trim', explode(',', $config['admin_emails'] ?? '')));
|
|
$emails = array_merge($emails, $adminEmails);
|
|
|
|
// Always notify configured admin user IDs
|
|
$adminUserIds = array_filter(array_map('intval', explode(',', $config['admin_user_ids'] ?? '')));
|
|
|
|
foreach ($adminUserIds as $uid)
|
|
{
|
|
$email = self::getUserEmail($uid);
|
|
|
|
if ($email)
|
|
{
|
|
$emails[] = $email;
|
|
}
|
|
}
|
|
|
|
switch ($event)
|
|
{
|
|
case 'ticket_created':
|
|
// Notify assigned user if any
|
|
if (!empty($ticket->assigned_to))
|
|
{
|
|
$email = self::getUserEmail((int) $ticket->assigned_to);
|
|
|
|
if ($email)
|
|
{
|
|
$emails[] = $email;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'ticket_replied':
|
|
// Notify ticket creator (customer gets notified of staff reply)
|
|
if (!empty($ticket->created_by))
|
|
{
|
|
$email = self::getUserEmail((int) $ticket->created_by);
|
|
|
|
if ($email)
|
|
{
|
|
$emails[] = $email;
|
|
}
|
|
}
|
|
|
|
// Notify assigned user
|
|
if (!empty($ticket->assigned_to))
|
|
{
|
|
$email = self::getUserEmail((int) $ticket->assigned_to);
|
|
|
|
if ($email)
|
|
{
|
|
$emails[] = $email;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'status_changed':
|
|
// Notify ticket creator
|
|
if (!empty($ticket->created_by))
|
|
{
|
|
$email = self::getUserEmail((int) $ticket->created_by);
|
|
|
|
if ($email)
|
|
{
|
|
$emails[] = $email;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'ticket_assigned':
|
|
// Notify newly assigned user
|
|
if (!empty($ticket->assigned_to))
|
|
{
|
|
$email = self::getUserEmail((int) $ticket->assigned_to);
|
|
|
|
if ($email)
|
|
{
|
|
$emails[] = $email;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return array_unique($emails);
|
|
}
|
|
|
|
/**
|
|
* Build email subject line.
|
|
*/
|
|
private static function buildSubject(string $event, object $ticket): string
|
|
{
|
|
$siteName = Factory::getConfig()->get('sitename', 'Support');
|
|
$prefix = '[' . $siteName . ' #' . $ticket->id . '] ';
|
|
|
|
switch ($event)
|
|
{
|
|
case 'ticket_created':
|
|
return $prefix . 'New Ticket: ' . ($ticket->subject ?? '');
|
|
|
|
case 'ticket_replied':
|
|
return $prefix . 'Reply: ' . ($ticket->subject ?? '');
|
|
|
|
case 'status_changed':
|
|
return $prefix . 'Status Changed: ' . ($ticket->subject ?? '');
|
|
|
|
case 'ticket_assigned':
|
|
return $prefix . 'Assigned: ' . ($ticket->subject ?? '');
|
|
|
|
default:
|
|
return $prefix . ($ticket->subject ?? '');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build email body.
|
|
*/
|
|
private static function buildBody(string $event, object $ticket, array $extra): string
|
|
{
|
|
$siteName = Factory::getConfig()->get('sitename', 'Support');
|
|
$siteUrl = rtrim(Uri::root(), '/');
|
|
$ticketUrl = $siteUrl . '/index.php?option=com_mokosuiteclient&view=ticket&id=' . $ticket->id;
|
|
|
|
$lines = [];
|
|
$lines[] = $siteName . ' Support';
|
|
$lines[] = str_repeat('-', 40);
|
|
$lines[] = '';
|
|
|
|
switch ($event)
|
|
{
|
|
case 'ticket_created':
|
|
$lines[] = 'A new support ticket has been created.';
|
|
$lines[] = '';
|
|
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
|
|
$lines[] = 'Priority: ' . ucfirst($ticket->priority ?? 'normal');
|
|
$lines[] = 'Category: ' . ($ticket->category_title ?? 'General');
|
|
$lines[] = '';
|
|
|
|
if (!empty($ticket->body))
|
|
{
|
|
$lines[] = 'Description:';
|
|
$lines[] = strip_tags($ticket->body);
|
|
$lines[] = '';
|
|
}
|
|
break;
|
|
|
|
case 'ticket_replied':
|
|
$lines[] = 'A new reply has been added to your ticket.';
|
|
$lines[] = '';
|
|
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
|
|
$lines[] = 'Status: ' . ucwords(str_replace('_', ' ', $ticket->status ?? ''));
|
|
$lines[] = '';
|
|
|
|
if (!empty($extra['reply_body']))
|
|
{
|
|
$lines[] = 'Reply:';
|
|
$lines[] = strip_tags($extra['reply_body']);
|
|
$lines[] = '';
|
|
}
|
|
break;
|
|
|
|
case 'status_changed':
|
|
$lines[] = 'Your ticket status has been updated.';
|
|
$lines[] = '';
|
|
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
|
|
$lines[] = 'New Status: ' . ucwords(str_replace('_', ' ', $ticket->status ?? ''));
|
|
|
|
if (!empty($extra['old_status']))
|
|
{
|
|
$lines[] = 'Old Status: ' . ucwords(str_replace('_', ' ', $extra['old_status']));
|
|
}
|
|
|
|
$lines[] = '';
|
|
break;
|
|
|
|
case 'ticket_assigned':
|
|
$lines[] = 'A ticket has been assigned to you.';
|
|
$lines[] = '';
|
|
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
|
|
$lines[] = 'Priority: ' . ucfirst($ticket->priority ?? 'normal');
|
|
$lines[] = '';
|
|
break;
|
|
}
|
|
|
|
$lines[] = 'View ticket: ' . $ticketUrl;
|
|
$lines[] = '';
|
|
$lines[] = '-- ';
|
|
$lines[] = $siteName . ' | Powered by MokoSuiteClient';
|
|
|
|
return implode("\n", $lines);
|
|
}
|
|
|
|
/**
|
|
* Get email address for a Joomla user ID.
|
|
*/
|
|
private static function getUserEmail(int $userId): ?string
|
|
{
|
|
if ($userId <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
$db = Factory::getDbo();
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select($db->quoteName('email'))
|
|
->from($db->quoteName('#__users'))
|
|
->where($db->quoteName('id') . ' = ' . $userId)
|
|
);
|
|
|
|
return $db->loadResult() ?: null;
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get notification configuration from component params.
|
|
*/
|
|
private static function getNotificationConfig(): array
|
|
{
|
|
try
|
|
{
|
|
$db = Factory::getDbo();
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select($db->quoteName('params'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuiteclient'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
|
);
|
|
|
|
$params = json_decode($db->loadResult() ?? '{}', true);
|
|
|
|
return $params['notifications'] ?? [];
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ==================================================================
|
|
// Security Event Notifications (#131)
|
|
// ==================================================================
|
|
|
|
/**
|
|
* Send a security alert to admin emails.
|
|
*/
|
|
public static function securityAlert(string $event, string $subject, string $body): void
|
|
{
|
|
try
|
|
{
|
|
$config = self::getNotificationConfig();
|
|
$enabled = $config['security_alerts'] ?? '1';
|
|
|
|
if (!$enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
$adminEmails = array_filter(array_map('trim', explode(',', $config['admin_emails'] ?? '')));
|
|
$adminUserIds = array_filter(array_map('intval', explode(',', $config['admin_user_ids'] ?? '')));
|
|
|
|
$recipients = $adminEmails;
|
|
|
|
foreach ($adminUserIds as $uid)
|
|
{
|
|
$email = self::getUserEmail($uid);
|
|
|
|
if ($email)
|
|
{
|
|
$recipients[] = $email;
|
|
}
|
|
}
|
|
|
|
$recipients = array_unique($recipients);
|
|
|
|
if (empty($recipients))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$siteName = Factory::getConfig()->get('sitename', 'Site');
|
|
$fullSubject = '[' . $siteName . ' Security] ' . $subject;
|
|
|
|
$lines = [
|
|
$siteName . ' Security Alert',
|
|
str_repeat('-', 40),
|
|
'',
|
|
'Event: ' . $event,
|
|
'Time: ' . gmdate('Y-m-d H:i:s') . ' UTC',
|
|
'',
|
|
$body,
|
|
'',
|
|
'-- ',
|
|
$siteName . ' | MokoSuiteClient Security',
|
|
];
|
|
|
|
$mailer = Factory::getMailer();
|
|
$mailer->isHtml(false);
|
|
$mailer->setSubject($fullSubject);
|
|
$mailer->setBody(implode("\n", $lines));
|
|
|
|
foreach ($recipients as $email)
|
|
{
|
|
try
|
|
{
|
|
$mailer->clearAddresses();
|
|
$mailer->addRecipient(trim($email));
|
|
$mailer->Send();
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
|
}
|
|
}
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
|
}
|
|
}
|
|
}
|