escape($plugin->description); ?>
+| Extension | Current | Available |
|---|---|---|
| escape($upd->name); ?> | +escape($upd->current_version); ?> | +escape($upd->version); ?> | +
| Article | User | Since |
|---|---|---|
| escape(mb_substr($item->title, 0, 30)); ?> | +escape($item->username ?? ''); ?> | +checked_out_time, 'M d H:i'); ?> | +
| IP | Rule | Time |
|---|---|---|
escape($block->ip); ?> |
+ escape($block->rule); ?> | +created, 'M d H:i'); ?> | +
| User | IP | Time |
|---|---|---|
| escape($login->username ?? ''); ?> | +escape($login->ip_address ?? ''); ?> |
+ log_date, 'M d H:i'); ?> | +
description); ?>
+ +
+
+
+ Jjoomla_version ?? ''); ?> / PHP php_version ?? ''); ?>
+
+
+
+
+ element] ?? $p->element;
+ $badge = $p->enabled ? 'bg-success' : 'bg-secondary';
+ $icon = $p->enabled ? 'icon-check' : 'icon-times';
+ $configUrl = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . (int) $p->extension_id);
+ ?>
+
+
+
+
+
+
+
+
+
+ Check Updates
+
+ updates > 0): ?>
+
+ updates; ?> updateupdates > 1 ? 's' : ''; ?>
+
+
+
+ Your request has been blocked by the security firewall.
'; + exit; + } + + // ================================================================== + // Input Scanning + // ================================================================== + + private function scanInput(array $input, string $pattern): ?string + { + foreach ($input as $key => $value) + { + if (\is_array($value)) + { + $match = $this->scanInput($value, $pattern); + + if ($match !== null) + { + return $match; + } + + continue; + } + + $value = (string) $value; + $decoded = urldecode($value); + + if (preg_match($pattern, $value) || preg_match($pattern, $decoded)) + { + return substr($value, 0, 200); + } + + if (preg_match($pattern, (string) $key)) + { + return substr((string) $key, 0, 200); + } + } + + return null; + } + + private function ipMatchesList(string $ip, array $entries): bool + { + $ipLong = ip2long($ip); + + if ($ipLong === false) + { + return false; + } + + foreach ($entries as $entry) + { + if (empty($entry['enabled']) || empty($entry['ip'])) + { + continue; + } + + $range = trim($entry['ip']); + + if (str_contains($range, '*')) + { + $pattern = '/^' . str_replace(['.', '*'], ['\\.', '\\d+'], $range) . '$/'; + + if (preg_match($pattern, $ip)) + { + return true; + } + + continue; + } + + if (str_contains($range, '/')) + { + [$subnet, $bits] = explode('/', $range, 2); + $subnetLong = ip2long($subnet); + $mask = -1 << (32 - (int) $bits); + + if ($subnetLong !== false && ($ipLong & $mask) === ($subnetLong & $mask)) + { + return true; + } + + continue; + } + + if ($ip === $range) + { + return true; + } + } + + return false; + } + + // ================================================================== + // Existing Features + // ================================================================== + + public function onUserBeforeSave($event): void + { + $newUser = $event[2] ?? $event->getArgument(2, []); + + if (empty($newUser['password_clear'])) + { + return; + } + + $password = $newUser['password_clear']; + $errors = []; + $minLen = (int) $this->params->get('password_min_length', 12); + + if (\strlen($password) < $minLen) + { + $errors[] = \sprintf('Password must be at least %d characters.', $minLen); + } + + if ($this->params->get('password_require_uppercase', 1) && !preg_match('/[A-Z]/', $password)) + { + $errors[] = 'Password must contain an uppercase letter.'; + } + + if ($this->params->get('password_require_number', 1) && !preg_match('/\d/', $password)) + { + $errors[] = 'Password must contain a number.'; + } + + if ($this->params->get('password_require_special', 1) && !preg_match('/[^A-Za-z0-9]/', $password)) + { + $errors[] = 'Password must contain a special character.'; + } + + if (!empty($errors)) + { + throw new \RuntimeException(implode(' ', $errors)); + } + } + + private function enforceHttps(): void + { + if (!$this->params->get('force_https', 0)) + { + return; + } + + $app = $this->getApplication(); + + if ($app->isClient('cli')) + { + return; + } + + $isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') + || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; + + if (!$isHttps) + { + $app->redirect('https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], 301); + } + } + + private function enforceAdminSessionTimeout(): void + { + $timeout = (int) $this->params->get('admin_session_timeout', 0); + + if ($timeout <= 0) + { + return; + } + + if (MokoWaaSHelper::isMasterUser() || $this->ipIsTrusted()) + { + return; + } + + $session = Factory::getSession(); + $lastHit = $session->get('mokowaas.last_activity', 0); + $now = time(); + + if ($lastHit > 0 && ($now - $lastHit) > ($timeout * 60)) + { + $this->getApplication()->logout(); + $this->getApplication()->redirect(Route::_('index.php', false)); + + return; + } + + $session->set('mokowaas.last_activity', $now); + } + + private function ipIsTrusted(): bool + { + $entries = $this->params->get('trusted_ips', ''); + + if (empty($entries)) + { + return false; + } + + if (\is_string($entries)) + { + $entries = json_decode($entries, true); + } + + if (!\is_array($entries)) + { + return false; + } + + return $this->ipMatchesList($_SERVER['REMOTE_ADDR'] ?? '', $entries); + } + + private function enforceUploadRestrictions(): void + { + $types = $this->params->get('upload_allowed_types', ''); + $maxMb = (int) $this->params->get('upload_max_size_mb', 0); + + if (empty($types) && $maxMb <= 0) + { + return; + } + + $config = $this->getApplication()->getConfig(); + + if (!empty($types)) + { + $config->set('upload_extensions', $types); + } + + if ($maxMb > 0) + { + $config->set('upload_maxsize', $maxMb); + } + } +} diff --git a/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.ini b/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.ini new file mode 100644 index 0000000..6211522 --- /dev/null +++ b/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.ini @@ -0,0 +1,11 @@ +; MokoWaaS Health Monitor Plugin +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_SYSTEM_MOKOWAAS_MONITOR="System - MokoWaaS Monitor" +PLG_SYSTEM_MOKOWAAS_MONITOR_DESC="Site health monitoring, Grafana heartbeat integration, and diagnostics." + +PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC="Monitoring" +PLG_SYSTEM_MOKOWAAS_MONITOR_FIELDSET_BASIC_DESC="Configure health monitoring and heartbeat settings." +PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_LABEL="Grafana Heartbeat" +PLG_SYSTEM_MOKOWAAS_MONITOR_HEARTBEAT_DESC="Send heartbeat registration to the Grafana monitoring receiver when plugin settings are saved." diff --git a/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.sys.ini b/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.sys.ini new file mode 100644 index 0000000..fca62b0 --- /dev/null +++ b/src/packages/plg_system_mokowaas_monitor/language/en-GB/plg_system_mokowaas_monitor.sys.ini @@ -0,0 +1,3 @@ +; MokoWaaS Health Monitor Plugin - System strings +PLG_SYSTEM_MOKOWAAS_MONITOR="System - MokoWaaS Monitor" +PLG_SYSTEM_MOKOWAAS_MONITOR_DESC="Site health monitoring, Grafana heartbeat integration, and diagnostics." diff --git a/src/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml b/src/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml new file mode 100644 index 0000000..8288308 --- /dev/null +++ b/src/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml @@ -0,0 +1,42 @@ + +