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