diff --git a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php
index e7696638..e910b66c 100644
--- a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php
+++ b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php
@@ -270,6 +270,104 @@ class DisplayController extends BaseController
}
}
+ // ==================================================================
+ // Settings Import/Export (#132)
+ // ==================================================================
+
+ public function exportSettings()
+ {
+ if (!$this->checkAcl('core.admin'))
+ {
+ $this->jsonForbidden();
+ }
+
+ $db = Factory::getDbo();
+ $settings = [];
+
+ // Export all MokoWaaS plugin params
+ $plugins = ['mokowaas', 'mokowaas_firewall', 'mokowaas_tenant', 'mokowaas_devtools', 'mokowaas_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_mokowaas'))
+ ->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();
+ }
+
+ $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.']);
+ }
+
+ $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_mokowaas'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
+ )->execute();
+ $count++;
+ }
+
+ $this->jsonResponse(['success' => true, 'message' => "Imported settings for {$count} extensions."]);
+ }
+
// ==================================================================
// WAF Log
// ==================================================================
diff --git a/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php b/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php
index 4da50048..186ad4f0 100644
--- a/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php
+++ b/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php
@@ -65,6 +65,10 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
offline): ?>
+
+
+ escape($_SERVER['REMOTE_ADDR'] ?? ''); ?>
+
diff --git a/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php b/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php
index d3b1c191..d5b14142 100644
--- a/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php
+++ b/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php
@@ -33,6 +33,7 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
$data['counts'] = $helper->getCounts($db);
$data['disk'] = $helper->getDiskInfo();
$data['currentIp'] = $helper->getCurrentIp();
+ $data['ssl'] = $helper->getSslStatus();
return $data;
}
diff --git a/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php b/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php
index 7160329e..a50d895e 100644
--- a/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php
+++ b/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php
@@ -136,4 +136,54 @@ class CpanelHelper
{
return $_SERVER['REMOTE_ADDR'] ?? '';
}
+
+ /**
+ * Check SSL certificate expiry (#148).
+ *
+ * @return object|null {expires, days_remaining, warning} or null if check fails
+ */
+ public function getSslStatus(): ?object
+ {
+ try
+ {
+ $host = parse_url(\Joomla\CMS\Uri\Uri::root(), PHP_URL_HOST);
+
+ if (empty($host))
+ {
+ return null;
+ }
+
+ $context = stream_context_create(['ssl' => ['capture_peer_cert' => true, 'verify_peer' => false]]);
+ $client = @stream_socket_client('ssl://' . $host . ':443', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $context);
+
+ if (!$client)
+ {
+ return null;
+ }
+
+ $params = stream_context_get_params($client);
+ fclose($client);
+
+ $cert = openssl_x509_parse($params['options']['ssl']['peer_certificate'] ?? '');
+
+ if (empty($cert['validTo_time_t']))
+ {
+ return null;
+ }
+
+ $expires = $cert['validTo_time_t'];
+ $days = (int) floor(($expires - time()) / 86400);
+
+ return (object) [
+ 'expires' => date('Y-m-d', $expires),
+ 'days_remaining' => $days,
+ 'warning' => $days <= 30,
+ 'critical' => $days <= 7,
+ ];
+ }
+ catch (\Throwable $e)
+ {
+ return null;
+ }
+ }
}
diff --git a/src/packages/mod_mokowaas_cpanel/tmpl/default.php b/src/packages/mod_mokowaas_cpanel/tmpl/default.php
index b3ed62f2..a65553f5 100644
--- a/src/packages/mod_mokowaas_cpanel/tmpl/default.php
+++ b/src/packages/mod_mokowaas_cpanel/tmpl/default.php
@@ -130,6 +130,12 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
+
+
+
+ SSL days_remaining; ?>d
+
+
Jjoomla_version ?? ''); ?> / PHP php_version ?? ''); ?>
diff --git a/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml b/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml
index f2b25dfb..b67360d2 100644
--- a/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml
+++ b/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml
@@ -127,6 +127,53 @@
+
+
+
+
+
+
+
checkDirectPhpAccess();
}
- // Existing features
+ // Security headers + existing features
+ $this->injectSecurityHeaders();
$this->enforceHttps();
$this->enforceUploadRestrictions();
@@ -400,6 +401,28 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi
'created' => gmdate('Y-m-d H:i:s'),
];
$db->insertObject('#__mokowaas_waf_log', $row);
+
+ // Auto-ban: if IP has N+ blocks in last M minutes, add to blocklist (#143)
+ $threshold = (int) $this->params->get('autoban_threshold', 10);
+ $window = (int) $this->params->get('autoban_window', 5);
+
+ if ($threshold > 0 && $window > 0)
+ {
+ $cutoff = gmdate('Y-m-d H:i:s', time() - ($window * 60));
+ $db->setQuery(
+ $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__mokowaas_waf_log'))
+ ->where($db->quoteName('ip') . ' = ' . $db->quote($ip))
+ ->where($db->quoteName('created') . ' >= ' . $db->quote($cutoff))
+ );
+ $recentBlocks = (int) $db->loadResult();
+
+ if ($recentBlocks >= $threshold)
+ {
+ $this->autoBanIp($ip, $db);
+ }
+ }
}
catch (\Throwable $e)
{
@@ -418,6 +441,51 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi
// Input Scanning
// ==================================================================
+ /**
+ * Auto-ban an IP by adding it to the blocklist params (#143).
+ */
+ private function autoBanIp(string $ip, $db): void
+ {
+ try
+ {
+ $query = $db->getQuery(true)
+ ->select($db->quoteName('params'))
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
+ $db->setQuery($query);
+ $params = new \Joomla\Registry\Registry($db->loadResult() ?? '{}');
+ $blocklist = json_decode($params->get('ip_blocklist', '[]'), true) ?: [];
+
+ foreach ($blocklist as $entry)
+ {
+ if (($entry['ip'] ?? '') === $ip)
+ {
+ return;
+ }
+ }
+
+ $blocklist[] = ['ip' => $ip, 'enabled' => '1', 'label' => 'Auto-banned by WAF (' . gmdate('Y-m-d H:i') . ')'];
+ $params->set('ip_blocklist', json_encode($blocklist));
+
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__extensions'))
+ ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
+ )->execute();
+
+ Log::add('WAF auto-banned IP: ' . $ip, Log::WARNING, 'mokowaas');
+ }
+ catch (\Throwable $e)
+ {
+ // Silent
+ }
+ }
+
private function scanInput(array $input, string $pattern): ?string
{
foreach ($input as $key => $value)
@@ -547,6 +615,68 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi
}
}
+ /**
+ * Inject HTTP security headers at runtime (#124).
+ */
+ private function injectSecurityHeaders(): void
+ {
+ $app = $this->getApplication();
+
+ if ($app->isClient('cli'))
+ {
+ return;
+ }
+
+ if ($this->params->get('header_xframe', 1))
+ {
+ $app->setHeader('X-Frame-Options', 'SAMEORIGIN', true);
+ }
+
+ if ($this->params->get('header_xcontent', 1))
+ {
+ $app->setHeader('X-Content-Type-Options', 'nosniff', true);
+ }
+
+ if ($this->params->get('header_xxss', 1))
+ {
+ $app->setHeader('X-XSS-Protection', '1; mode=block', true);
+ }
+
+ $referrer = $this->params->get('header_referrer', '');
+
+ if (!empty($referrer) && $referrer !== 'off')
+ {
+ $app->setHeader('Referrer-Policy', $referrer, true);
+ }
+
+ if ($this->params->get('header_hsts', 0))
+ {
+ $maxAge = (int) $this->params->get('header_hsts_maxage', 31536000);
+ $hsts = 'max-age=' . $maxAge;
+
+ if ($this->params->get('header_hsts_subdomains', 0))
+ {
+ $hsts .= '; includeSubDomains';
+ }
+
+ $app->setHeader('Strict-Transport-Security', $hsts, true);
+ }
+
+ $csp = $this->params->get('header_csp', '');
+
+ if (!empty($csp))
+ {
+ $app->setHeader('Content-Security-Policy', $csp, true);
+ }
+
+ $perms = $this->params->get('header_permissions', '');
+
+ if (!empty($perms))
+ {
+ $app->setHeader('Permissions-Policy', $perms, true);
+ }
+ }
+
private function enforceHttps(): void
{
if (!$this->params->get('force_https', 0))