945fe0de93
- Pass contact_id, assign_users, assign_groups, custom_fields from controller to model on ticket creation - Fix updateTicketStatus: getString→getInt (was passing string to int param) - Add getBackendUsers() and getUserGroups() to TicketsModel - Add assign users/groups multi-select fields to creation modal
983 lines
30 KiB
PHP
983 lines
30 KiB
PHP
<?php
|
|
/**
|
|
* @package MokoSuite
|
|
* @subpackage com_mokosuite
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Moko\Component\MokoSuite\Administrator\Controller;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\MVC\Controller\BaseController;
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Session\Session;
|
|
|
|
class DisplayController extends BaseController
|
|
{
|
|
protected $default_view = 'dashboard';
|
|
|
|
/**
|
|
* ACL map: view name => required permission.
|
|
*/
|
|
private const VIEW_ACL = [
|
|
'dashboard' => 'mokosuite.dashboard',
|
|
'extensions' => 'mokosuite.extensions',
|
|
'htaccess' => 'mokosuite.htaccess',
|
|
'tickets' => 'mokosuite.tickets',
|
|
'ticket' => 'mokosuite.tickets',
|
|
'privacy' => 'core.admin',
|
|
'waflog' => 'core.admin',
|
|
'categories' => 'mokosuite.tickets',
|
|
'canned' => 'mokosuite.tickets',
|
|
'automation' => 'core.admin',
|
|
'database' => 'core.admin',
|
|
'cleanup' => 'mokosuite.cache',
|
|
];
|
|
|
|
public function display($cachable = false, $urlparams = [])
|
|
{
|
|
$view = $this->input->get('view', $this->default_view);
|
|
$acl = self::VIEW_ACL[$view] ?? 'core.manage';
|
|
|
|
if (!$this->checkAcl($acl))
|
|
{
|
|
Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
|
Factory::getApplication()->redirect(Route::_('index.php', false));
|
|
|
|
return;
|
|
}
|
|
|
|
return parent::display($cachable, $urlparams);
|
|
}
|
|
|
|
// ==================================================================
|
|
// Plugin toggle
|
|
// ==================================================================
|
|
|
|
public function togglePlugin()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.plugins.toggle'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$app = Factory::getApplication();
|
|
$model = $this->getModel('Dashboard');
|
|
|
|
$result = $model->togglePlugin(
|
|
$app->getInput()->getInt('extension_id', 0),
|
|
$app->getInput()->getInt('enabled', 0)
|
|
);
|
|
|
|
$this->jsonResponse($result);
|
|
}
|
|
|
|
// ==================================================================
|
|
// Heartbeat
|
|
// ==================================================================
|
|
|
|
public function sendHeartbeat()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
try
|
|
{
|
|
$monitorPlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuite_monitor');
|
|
|
|
if (!$monitorPlugin)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Monitor plugin not enabled.']);
|
|
|
|
return;
|
|
}
|
|
|
|
$params = new \Joomla\Registry\Registry($monitorPlugin->params);
|
|
$baseUrl = rtrim($params->get('base_url', ''), '/');
|
|
|
|
// Fall back to manifest XML default if not yet saved in params
|
|
if (empty($baseUrl))
|
|
{
|
|
$manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml';
|
|
|
|
if (is_file($manifestFile))
|
|
{
|
|
$xml = simplexml_load_file($manifestFile);
|
|
|
|
if ($xml)
|
|
{
|
|
foreach ($xml->xpath('//field[@name="base_url"]') as $field)
|
|
{
|
|
$baseUrl = rtrim((string) $field['default'], '/');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($baseUrl))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'MokoSuiteHQ URL not configured in monitor plugin.']);
|
|
|
|
return;
|
|
}
|
|
|
|
$corePlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuite');
|
|
$coreParams = new \Joomla\Registry\Registry($corePlugin ? $corePlugin->params : '{}');
|
|
$healthToken = $coreParams->get('health_api_token', '');
|
|
|
|
if (empty($healthToken))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Health token not configured.']);
|
|
|
|
return;
|
|
}
|
|
|
|
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
|
|
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
|
|
$timestamp = time();
|
|
|
|
$payload = json_encode([
|
|
'token' => $healthToken,
|
|
'domain' => $domain,
|
|
'site_name' => Factory::getConfig()->get('sitename', 'Joomla'),
|
|
'site_url' => $siteUrl,
|
|
'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(),
|
|
'php_version' => PHP_VERSION,
|
|
'timestamp' => $timestamp,
|
|
], JSON_UNESCAPED_SLASHES);
|
|
|
|
// RSA sign the request
|
|
$headers = ['Content-Type: application/json'];
|
|
$signingKeyB64 = $params->get('signing_key', '');
|
|
|
|
// Fall back to manifest XML default if not yet saved in params
|
|
if (empty($signingKeyB64))
|
|
{
|
|
$manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml';
|
|
|
|
if (is_file($manifestFile))
|
|
{
|
|
$xml = simplexml_load_file($manifestFile);
|
|
|
|
if ($xml)
|
|
{
|
|
foreach ($xml->xpath('//field[@name="signing_key"]') as $field)
|
|
{
|
|
$signingKeyB64 = (string) $field['default'];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($signingKeyB64))
|
|
{
|
|
$privateKeyPem = base64_decode($signingKeyB64);
|
|
$privateKey = openssl_pkey_get_private($privateKeyPem);
|
|
|
|
if ($privateKey !== false)
|
|
{
|
|
$message = $domain . '|' . $timestamp . '|' . $healthToken;
|
|
$signature = '';
|
|
|
|
if (openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256))
|
|
{
|
|
$headers[] = 'X-MokoSuite-Signature: ' . base64_encode($signature);
|
|
$headers[] = 'X-MokoSuite-Timestamp: ' . $timestamp;
|
|
}
|
|
}
|
|
}
|
|
|
|
$endpoint = $baseUrl . '/api/index.php/v1/mokosuitehq/heartbeat';
|
|
|
|
$ch = curl_init($endpoint);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 15,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($error)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Connection failed: ' . $error]);
|
|
}
|
|
elseif ($code >= 200 && $code < 300)
|
|
{
|
|
$body = json_decode($response, true);
|
|
$this->jsonResponse(['success' => true, 'message' => 'Heartbeat sent: ' . ($body['status'] ?? 'ok')]);
|
|
}
|
|
else
|
|
{
|
|
$body = json_decode($response, true);
|
|
$this->jsonResponse(['success' => false, 'message' => 'HTTP ' . $code . ': ' . ($body['error'] ?? $body['message'] ?? 'Unknown')]);
|
|
}
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
// Cache
|
|
// ==================================================================
|
|
|
|
public function clearCache()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.cache'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Dashboard')->clearCache());
|
|
}
|
|
|
|
public function clearTemp()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.cache'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Dashboard')->clearTemp());
|
|
}
|
|
|
|
// ==================================================================
|
|
// Extensions
|
|
// ==================================================================
|
|
|
|
public function installExtension()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.extensions'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$downloadUrl = Factory::getApplication()->getInput()->getString('download_url', '');
|
|
|
|
if (empty($downloadUrl))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Missing download URL.']);
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Extensions')->installFromUrl($downloadUrl));
|
|
}
|
|
|
|
// ==================================================================
|
|
// .htaccess
|
|
// ==================================================================
|
|
|
|
public function saveHtaccess()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.htaccess'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$app = Factory::getApplication();
|
|
$input = $app->getInput();
|
|
$model = $this->getModel('Htaccess');
|
|
|
|
$options = [];
|
|
|
|
foreach ($input->getArray() as $key => $value)
|
|
{
|
|
if (str_starts_with($key, 'opt_'))
|
|
{
|
|
$options[substr($key, 4)] = $value;
|
|
}
|
|
}
|
|
|
|
if (!empty($options))
|
|
{
|
|
$model->saveOptions($options);
|
|
}
|
|
|
|
$this->jsonResponse($model->saveHtaccess($input->getRaw('content', '')));
|
|
}
|
|
|
|
public function generateHtaccess()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.htaccess'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$model = $this->getModel('Htaccess');
|
|
$options = Factory::getApplication()->getInput()->getArray();
|
|
|
|
$model->saveOptions($options);
|
|
|
|
$app = Factory::getApplication();
|
|
$app->setHeader('Content-Type', 'application/json');
|
|
echo json_encode([
|
|
'htaccess' => $model->generateHtaccess($options),
|
|
'nginx' => $model->generateNginx($options),
|
|
]);
|
|
$app->close();
|
|
}
|
|
|
|
// ==================================================================
|
|
// Tickets
|
|
// ==================================================================
|
|
|
|
public function createTicket()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.tickets.create'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$input = Factory::getApplication()->getInput();
|
|
|
|
$this->jsonResponse($this->getModel('Tickets')->createTicket([
|
|
'subject' => $input->getString('subject', ''),
|
|
'body' => $input->getRaw('body', ''),
|
|
'priority' => $input->getString('priority', 'normal'),
|
|
'category_id' => $input->getInt('category_id', 0),
|
|
'contact_id' => $input->getInt('contact_id', 0),
|
|
'assign_users' => $input->get('assign_users', [], 'ARRAY'),
|
|
'assign_groups' => $input->get('assign_groups', [], 'ARRAY'),
|
|
'custom_fields' => $input->get('custom_fields', [], 'ARRAY'),
|
|
]));
|
|
}
|
|
|
|
public function addTicketReply()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.tickets'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$input = Factory::getApplication()->getInput();
|
|
|
|
$this->jsonResponse($this->getModel('Tickets')->addReply(
|
|
$input->getInt('ticket_id', 0),
|
|
$input->getRaw('body', ''),
|
|
(bool) $input->getInt('is_internal', 0)
|
|
));
|
|
}
|
|
|
|
public function updateTicketStatus()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.tickets'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$input = Factory::getApplication()->getInput();
|
|
|
|
$this->jsonResponse($this->getModel('Tickets')->updateStatus(
|
|
$input->getInt('ticket_id', 0),
|
|
$input->getInt('status', 0)
|
|
));
|
|
}
|
|
|
|
// ==================================================================
|
|
// KB Search
|
|
// ==================================================================
|
|
|
|
public function searchKb()
|
|
{
|
|
$query = Factory::getApplication()->getInput()->getString('q', '');
|
|
|
|
if (strlen($query) < 3)
|
|
{
|
|
$this->jsonResponse(['results' => []]);
|
|
}
|
|
|
|
try
|
|
{
|
|
$db = Factory::getDbo();
|
|
$escaped = $db->quote('%' . $db->escape($query, true) . '%');
|
|
|
|
$results = $db->setQuery(
|
|
$db->getQuery(true)
|
|
->select([$db->quoteName('l.title'), $db->quoteName('l.url'), $db->quoteName('l.description')])
|
|
->from($db->quoteName('#__finder_links', 'l'))
|
|
->where($db->quoteName('l.published') . ' = 1')
|
|
->where('(' . $db->quoteName('l.title') . ' LIKE ' . $escaped
|
|
. ' OR ' . $db->quoteName('l.description') . ' LIKE ' . $escaped . ')')
|
|
->order($db->quoteName('l.title') . ' ASC')
|
|
->setLimit(8)
|
|
)->loadObjectList() ?: [];
|
|
|
|
foreach ($results as $r)
|
|
{
|
|
$r->description = mb_substr(strip_tags($r->description ?? ''), 0, 150);
|
|
}
|
|
|
|
$this->jsonResponse(['results' => $results]);
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$this->jsonResponse(['results' => []]);
|
|
}
|
|
}
|
|
|
|
// ==================================================================
|
|
// Maintenance (#127, #128)
|
|
// ==================================================================
|
|
|
|
public function optimizeDb()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
|
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
|
$this->jsonResponse($model->optimizeTables());
|
|
}
|
|
|
|
public function repairDb()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
|
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
|
$this->jsonResponse($model->repairTables());
|
|
}
|
|
|
|
public function purgeSessions()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
|
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
|
$this->jsonResponse($model->purgeSessions());
|
|
}
|
|
|
|
public function cleanDirectory()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.cache')) { $this->jsonForbidden(); return; }
|
|
$dirKey = Factory::getApplication()->getInput()->getString('dir_key', '');
|
|
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
|
$this->jsonResponse($model->cleanDirectory($dirKey));
|
|
}
|
|
|
|
// ==================================================================
|
|
// Helpdesk CRUD (#137, #138, #139)
|
|
// ==================================================================
|
|
|
|
public function saveCategory()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$input = Factory::getApplication()->getInput();
|
|
$db = Factory::getDbo();
|
|
$id = $input->getInt('id', 0);
|
|
$data = (object) [
|
|
'title' => $input->getString('title', ''),
|
|
'alias' => \Joomla\CMS\Filter\OutputFilter::stringURLSafe($input->getString('title', '')),
|
|
'sla_response_minutes' => $input->getInt('sla_response_minutes', 480),
|
|
'sla_resolution_minutes' => $input->getInt('sla_resolution_minutes', 2880),
|
|
'auto_assign_user' => $input->getInt('auto_assign_user', 0) ?: null,
|
|
'published' => $input->getInt('published', 1),
|
|
];
|
|
if ($id) {
|
|
$data->id = $id;
|
|
$db->updateObject('#__mokosuite_ticket_categories', $data, 'id');
|
|
} else {
|
|
$data->ordering = 0;
|
|
$db->insertObject('#__mokosuite_ticket_categories', $data, 'id');
|
|
}
|
|
$this->jsonResponse(['success' => true, 'message' => 'Category saved.', 'id' => (int) $data->id]);
|
|
}
|
|
|
|
public function deleteCategory()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$db = Factory::getDbo();
|
|
$db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
|
$this->jsonResponse(['success' => true, 'message' => 'Category deleted.']);
|
|
}
|
|
|
|
public function reorderCategory()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
|
|
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
|
|
$db = Factory::getDbo();
|
|
foreach ($order as $i => $id) {
|
|
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuite_ticket_categories') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
|
|
}
|
|
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
|
|
}
|
|
|
|
public function saveCanned()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$input = Factory::getApplication()->getInput();
|
|
$db = Factory::getDbo();
|
|
$data = (object) [
|
|
'title' => $input->getString('title', ''),
|
|
'body' => $input->getRaw('body', ''),
|
|
'category_id' => $input->getInt('category_id', 0) ?: null,
|
|
'ordering' => 0,
|
|
];
|
|
$id = $input->getInt('id', 0);
|
|
if ($id) { $data->id = $id; $db->updateObject('#__mokosuite_ticket_canned', $data, 'id'); }
|
|
else { $db->insertObject('#__mokosuite_ticket_canned', $data, 'id'); }
|
|
$this->jsonResponse(['success' => true, 'message' => 'Canned response saved.', 'id' => (int) $data->id]);
|
|
}
|
|
|
|
public function deleteCanned()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$db = Factory::getDbo();
|
|
$db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
|
$this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']);
|
|
}
|
|
|
|
public function reorderCanned()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
|
|
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
|
|
$db = Factory::getDbo();
|
|
foreach ($order as $i => $id) {
|
|
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuite_ticket_canned') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
|
|
}
|
|
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
|
|
}
|
|
|
|
public function uploadAttachment()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$input = Factory::getApplication()->getInput();
|
|
$ticketId = $input->getInt('ticket_id', 0);
|
|
$replyId = $input->getInt('reply_id', 0) ?: null;
|
|
if (!$ticketId) { $this->jsonResponse(['success' => false, 'message' => 'Missing ticket_id']); return; }
|
|
$files = $input->files->get('attachments', [], 'raw');
|
|
if (empty($files) || empty($files['name'])) { $this->jsonResponse(['success' => false, 'message' => 'No files uploaded']); return; }
|
|
$saved = \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::upload($ticketId, $replyId, $files);
|
|
$this->jsonResponse(['success' => true, 'message' => count($saved) . ' file(s) uploaded', 'count' => count($saved)]);
|
|
}
|
|
|
|
public function downloadAttachment()
|
|
{
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
|
$db = Factory::getDbo();
|
|
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuite_ticket_attachments')->where('id = ' . $id));
|
|
$att = $db->loadObject();
|
|
if (!$att) { throw new \RuntimeException('Attachment not found', 404); }
|
|
$path = \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::getAbsolutePath($att);
|
|
if (!file_exists($path)) { throw new \RuntimeException('File not found', 404); }
|
|
$app = Factory::getApplication();
|
|
$app->setHeader('Content-Type', $att->mimetype ?: 'application/octet-stream');
|
|
$app->setHeader('Content-Disposition', 'attachment; filename="' . $att->filename . '"');
|
|
$app->setHeader('Content-Length', (string) filesize($path));
|
|
$app->sendHeaders();
|
|
readfile($path);
|
|
$app->close();
|
|
}
|
|
|
|
public function deleteAttachment()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
|
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
|
$ok = \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::delete($id);
|
|
$this->jsonResponse(['success' => $ok, 'message' => $ok ? 'Attachment deleted' : 'Not found']);
|
|
}
|
|
|
|
public function rateTicket()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
$input = Factory::getApplication()->getInput();
|
|
$ticketId = $input->getInt('ticket_id', 0);
|
|
$rating = $input->getInt('rating', 0);
|
|
$feedback = $input->getString('feedback', '');
|
|
if (!$ticketId || $rating < 1 || $rating > 5) {
|
|
$this->jsonResponse(['success' => false, 'message' => 'Invalid rating (1-5)']);
|
|
return;
|
|
}
|
|
$db = Factory::getDbo();
|
|
$db->setQuery(
|
|
'UPDATE ' . $db->quoteName('#__mokosuite_tickets')
|
|
. ' SET satisfaction_rating = ' . $rating
|
|
. ', satisfaction_feedback = ' . $db->quote($feedback)
|
|
. ', satisfaction_rated_at = ' . $db->quote(Factory::getDate()->toSql())
|
|
. ' WHERE id = ' . $ticketId
|
|
)->execute();
|
|
$this->jsonResponse(['success' => true, 'message' => 'Thank you for your feedback!']);
|
|
}
|
|
|
|
public function saveAutomation()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
|
$input = Factory::getApplication()->getInput();
|
|
$db = Factory::getDbo();
|
|
$data = (object) [
|
|
'title' => $input->getString('title', ''),
|
|
'trigger_event' => $input->getString('trigger_event', 'ticket_created'),
|
|
'conditions' => $input->getRaw('conditions', '[]'),
|
|
'actions' => $input->getRaw('actions', '[]'),
|
|
'behavior' => $input->getString('behavior', 'append'),
|
|
'enabled' => 1,
|
|
'ordering' => 0,
|
|
];
|
|
$id = $input->getInt('id', 0);
|
|
if ($id) { $data->id = $id; $db->updateObject('#__mokosuite_ticket_automation', $data, 'id'); }
|
|
else { $db->insertObject('#__mokosuite_ticket_automation', $data, 'id'); }
|
|
$this->jsonResponse(['success' => true, 'message' => 'Rule saved.', 'id' => (int) $data->id]);
|
|
}
|
|
|
|
public function deleteAutomation()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
|
$db = Factory::getDbo();
|
|
$db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
|
$this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']);
|
|
}
|
|
|
|
public function toggleAutomation()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
|
$input = Factory::getApplication()->getInput();
|
|
$db = Factory::getDbo();
|
|
$db->setQuery($db->getQuery(true)->update('#__mokosuite_ticket_automation')
|
|
->set('enabled = ' . $input->getInt('enabled', 0))
|
|
->where('id = ' . $input->getInt('id', 0)))->execute();
|
|
$this->jsonResponse(['success' => true, 'message' => 'Rule updated.']);
|
|
}
|
|
|
|
public function reorderAutomation()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
|
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
|
|
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
|
|
$db = Factory::getDbo();
|
|
foreach ($order as $i => $id) {
|
|
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuite_ticket_automation') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
|
|
}
|
|
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
|
|
}
|
|
|
|
// ==================================================================
|
|
// Settings Import/Export (#132)
|
|
// ==================================================================
|
|
|
|
public function exportSettings()
|
|
{
|
|
Session::checkToken('get') or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$db = Factory::getDbo();
|
|
$settings = [];
|
|
|
|
// Export all MokoSuite plugin params
|
|
$plugins = ['mokosuite', 'mokosuite_firewall', 'mokosuite_tenant', 'mokosuite_devtools', 'mokosuite_offline'];
|
|
|
|
foreach ($plugins as $element)
|
|
{
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select($db->quoteName('params'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote($element))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
|
);
|
|
$settings['plugins'][$element] = json_decode($db->loadResult() ?? '{}', true);
|
|
}
|
|
|
|
// Export component params
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select($db->quoteName('params'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
|
);
|
|
$settings['component'] = json_decode($db->loadResult() ?? '{}', true);
|
|
$settings['exported'] = gmdate('Y-m-d\TH:i:s\Z');
|
|
$settings['site'] = Factory::getConfig()->get('sitename', '');
|
|
|
|
$this->jsonResponse(['success' => true, 'settings' => $settings]);
|
|
}
|
|
|
|
public function importSettings()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$json = Factory::getApplication()->getInput()->getRaw('settings_json', '');
|
|
$data = json_decode($json, true);
|
|
|
|
if (empty($data) || empty($data['plugins']))
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => 'Invalid settings JSON.']);
|
|
return;
|
|
}
|
|
|
|
$db = Factory::getDbo();
|
|
$count = 0;
|
|
|
|
foreach ($data['plugins'] ?? [] as $element => $params)
|
|
{
|
|
if (!is_array($params))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote($element))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
|
)->execute();
|
|
$count++;
|
|
}
|
|
|
|
if (!empty($data['component']) && is_array($data['component']))
|
|
{
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($data['component'])))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
|
)->execute();
|
|
$count++;
|
|
}
|
|
|
|
$this->jsonResponse(['success' => true, 'message' => "Imported settings for {$count} extensions."]);
|
|
}
|
|
|
|
// ==================================================================
|
|
// WAF Log
|
|
// ==================================================================
|
|
|
|
public function purgeWafLog()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$days = Factory::getApplication()->getInput()->getInt('days', 30);
|
|
$model = new \Moko\Component\MokoSuite\Administrator\Model\WaflogModel();
|
|
|
|
$this->jsonResponse($model->purgeLogs($days));
|
|
}
|
|
|
|
public function banIpFromLog()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$ip = Factory::getApplication()->getInput()->getString('ip', '');
|
|
$model = new \Moko\Component\MokoSuite\Administrator\Model\WaflogModel();
|
|
|
|
$this->jsonResponse($model->banIp($ip));
|
|
}
|
|
|
|
// ==================================================================
|
|
// Privacy Guard
|
|
// ==================================================================
|
|
|
|
public function processDataRequest()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$input = Factory::getApplication()->getInput();
|
|
$model = new \Moko\Component\MokoSuite\Administrator\Model\PrivacyModel();
|
|
$action = $input->getString('action', 'deny');
|
|
|
|
if ($action === 'create')
|
|
{
|
|
$result = $model->createRequest(
|
|
$input->getInt('user_id', 0),
|
|
$input->getString('type', 'export')
|
|
);
|
|
$this->jsonResponse($result);
|
|
return;
|
|
}
|
|
|
|
if ($action === 'approve' && !$input->getInt('request_id', 0) && $input->getInt('user_id', 0))
|
|
{
|
|
// Auto-process: create then immediately approve
|
|
$result = $model->createRequest(
|
|
$input->getInt('user_id', 0),
|
|
$input->getString('type', 'export')
|
|
);
|
|
|
|
if ($result['success'] && !empty($result['id']))
|
|
{
|
|
$result = $model->processRequest((int) $result['id'], 'approve');
|
|
}
|
|
|
|
$this->jsonResponse($result);
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($model->processRequest(
|
|
$input->getInt('request_id', 0),
|
|
$action
|
|
));
|
|
}
|
|
|
|
public function exportUserData()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$model = new \Moko\Component\MokoSuite\Administrator\Model\PrivacyModel();
|
|
|
|
$this->jsonResponse($model->exportUserData(
|
|
Factory::getApplication()->getInput()->getInt('user_id', 0)
|
|
));
|
|
}
|
|
|
|
// ==================================================================
|
|
// Importers
|
|
// ==================================================================
|
|
|
|
public function importAts()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('mokosuite.tickets'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Import')->importAts());
|
|
}
|
|
|
|
public function importAdminTools()
|
|
{
|
|
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
|
|
|
if (!$this->checkAcl('core.admin'))
|
|
{
|
|
$this->jsonForbidden();
|
|
return;
|
|
}
|
|
|
|
$this->jsonResponse($this->getModel('Import')->importAdminTools());
|
|
}
|
|
|
|
// ==================================================================
|
|
// Helpers
|
|
// ==================================================================
|
|
|
|
/**
|
|
* Check a MokoSuite ACL permission for the current user.
|
|
*/
|
|
private function checkAcl(string $action): bool
|
|
{
|
|
$user = Factory::getApplication()->getIdentity();
|
|
|
|
// Super admins always pass
|
|
if ($user->authorise('core.admin', 'com_mokosuite'))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return $user->authorise($action, 'com_mokosuite');
|
|
}
|
|
|
|
/**
|
|
* Send a JSON response and close.
|
|
*/
|
|
private function jsonResponse(array $data): void
|
|
{
|
|
$app = Factory::getApplication();
|
|
$app->setHeader('Content-Type', 'application/json');
|
|
echo json_encode($data);
|
|
$app->close();
|
|
}
|
|
|
|
/**
|
|
* Send a 403 JSON response and close.
|
|
*/
|
|
private function jsonForbidden(): void
|
|
{
|
|
$this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]);
|
|
return;
|
|
}
|
|
|
|
}
|