diff --git a/plist b/plist index 9c3735674..78452bdbb 100644 --- a/plist +++ b/plist @@ -575,6 +575,7 @@ /usr/local/opnsense/mvc/app/library/OPNsense/System/Status/DiskSpaceStatus.php /usr/local/opnsense/mvc/app/library/OPNsense/System/Status/FirewallStatus.php /usr/local/opnsense/mvc/app/library/OPNsense/System/Status/LiveMediaStatus.php +/usr/local/opnsense/mvc/app/library/OPNsense/System/Status/OpensshOverrideStatus.php /usr/local/opnsense/mvc/app/library/OPNsense/System/Status/SystemBootingStatus.php /usr/local/opnsense/mvc/app/library/OPNsense/System/SystemStatus.php /usr/local/opnsense/mvc/app/library/OPNsense/System/SystemStatusCode.php diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php index a9dabb3c2..97efbf0c3 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php @@ -75,8 +75,8 @@ class SystemController extends ApiControllerBase foreach ($statuses as $subsystem => $status) { $statuses[$subsystem]['status'] = $order[$status['statusCode']]; - if (!empty($status['logLocation'])) { - if (!$acl->isPageAccessible($this->getUserName(), $status['logLocation'])) { + if (!empty($status['location'])) { + if (!$acl->isPageAccessible($this->getUserName(), $status['location'])) { unset($statuses[$subsystem]); continue; } @@ -181,8 +181,8 @@ class SystemController extends ApiControllerBase $subsystem = $this->request->getPost("subject"); $system = json_decode(trim($backend->configdRun('system status')), true); if (array_key_exists($subsystem, $system)) { - if (!empty($system[$subsystem]['logLocation'])) { - $aclCheck = $system[$subsystem]['logLocation']; + if (!empty($system[$subsystem]['location'])) { + $aclCheck = $system[$subsystem]['location']; if ( $acl->isPageAccessible($this->getUserName(), $aclCheck) || !$acl->hasPrivilege($this->getUserName(), 'user-config-readonly') diff --git a/src/opnsense/mvc/app/library/OPNsense/System/AbstractStatus.php b/src/opnsense/mvc/app/library/OPNsense/System/AbstractStatus.php index d91a38e45..7fc742a03 100644 --- a/src/opnsense/mvc/app/library/OPNsense/System/AbstractStatus.php +++ b/src/opnsense/mvc/app/library/OPNsense/System/AbstractStatus.php @@ -32,9 +32,10 @@ abstract class AbstractStatus { protected $internalPriority = 100; protected $internalPersistent = false; + protected $internalIsBanner = false; protected $internalTitle = null; protected $internalMessage = null; - protected $internalLogLocation = null; + protected $internalLocation = null; protected $internalStatus = SystemStatusCode::OK; protected $internalTimestamp = null; @@ -48,6 +49,11 @@ abstract class AbstractStatus return $this->internalPersistent; } + public function isBanner() + { + return $this->internalIsBanner; + } + public function getTitle() { return $this->internalTitle; @@ -63,9 +69,9 @@ abstract class AbstractStatus return $this->internalMessage ?? gettext('No problems were detected.'); } - public function getLogLocation() + public function getLocation() { - return $this->internalLogLocation; + return $this->internalLocation; } public function getTimestamp() diff --git a/src/opnsense/mvc/app/library/OPNsense/System/Status/CrashReporterStatus.php b/src/opnsense/mvc/app/library/OPNsense/System/Status/CrashReporterStatus.php index 47d554d0d..e75cc8275 100644 --- a/src/opnsense/mvc/app/library/OPNsense/System/Status/CrashReporterStatus.php +++ b/src/opnsense/mvc/app/library/OPNsense/System/Status/CrashReporterStatus.php @@ -41,7 +41,7 @@ class CrashReporterStatus extends AbstractStatus $this->internalPriority = 10; $this->internalTitle = gettext('Crash Reporter'); - $this->internalLogLocation = '/crash_reporter.php'; + $this->internalLocation = '/crash_reporter.php'; $src_errors = count($src_logs) > 0; if ($src_errors) { diff --git a/src/opnsense/mvc/app/library/OPNsense/System/Status/FirewallStatus.php b/src/opnsense/mvc/app/library/OPNsense/System/Status/FirewallStatus.php index 0fd4f6007..8743f9d93 100644 --- a/src/opnsense/mvc/app/library/OPNsense/System/Status/FirewallStatus.php +++ b/src/opnsense/mvc/app/library/OPNsense/System/Status/FirewallStatus.php @@ -39,7 +39,7 @@ class FirewallStatus extends AbstractStatus { $this->internalPriority = 20; $this->internalTitle = gettext('Firewall'); - $this->internalLogLocation = '/ui/diagnostics/log/core/firewall'; + $this->internalLocation = '/ui/diagnostics/log/core/firewall'; if (file_exists($this->rules_error)) { $this->internalMessage = file_get_contents($this->rules_error); /* XXX */ diff --git a/src/opnsense/mvc/app/library/OPNsense/System/Status/LiveMediaStatus.php b/src/opnsense/mvc/app/library/OPNsense/System/Status/LiveMediaStatus.php index cdab2c0e8..37075c7b0 100644 --- a/src/opnsense/mvc/app/library/OPNsense/System/Status/LiveMediaStatus.php +++ b/src/opnsense/mvc/app/library/OPNsense/System/Status/LiveMediaStatus.php @@ -38,6 +38,7 @@ class LiveMediaStatus extends AbstractStatus { $this->internalPriority = 2; $this->internalPersistent = true; + $this->internalIsBanner = true; $this->internalTitle = gettext('Live Media'); /* diff --git a/src/opnsense/mvc/app/library/OPNsense/System/Status/OpensshOverrideStatus.php b/src/opnsense/mvc/app/library/OPNsense/System/Status/OpensshOverrideStatus.php new file mode 100644 index 000000000..484656619 --- /dev/null +++ b/src/opnsense/mvc/app/library/OPNsense/System/Status/OpensshOverrideStatus.php @@ -0,0 +1,48 @@ +internalPriority = 2; + $this->internalPersistent = true; + $this->internalTitle = gettext('OpenSSH config override'); + $this->internalLocation = '/system_advanced_admin.php'; + + if (count(glob('/usr/local/etc/ssh/sshd_config.d/*.conf'))) { + $this->internalMessage = gettext('The OpenSSH GUI configuration may be overridden by currently provided files on the disk.'); + $this->internalStatus = SystemStatusCode::NOTICE; + } + } +} diff --git a/src/opnsense/mvc/app/library/OPNsense/System/Status/SystemBootingStatus.php b/src/opnsense/mvc/app/library/OPNsense/System/Status/SystemBootingStatus.php index 25de9ea10..ee6c069be 100644 --- a/src/opnsense/mvc/app/library/OPNsense/System/Status/SystemBootingStatus.php +++ b/src/opnsense/mvc/app/library/OPNsense/System/Status/SystemBootingStatus.php @@ -37,6 +37,7 @@ class SystemBootingStatus extends AbstractStatus { $this->internalPriority = 1; $this->internalPersistent = true; + $this->internalIsBanner = true; $this->internalTitle = gettext('System Booting'); /* XXX boot detection from final class product in config.inc */ diff --git a/src/opnsense/mvc/app/library/OPNsense/System/SystemStatus.php b/src/opnsense/mvc/app/library/OPNsense/System/SystemStatus.php index 9457b5b77..453c822ad 100644 --- a/src/opnsense/mvc/app/library/OPNsense/System/SystemStatus.php +++ b/src/opnsense/mvc/app/library/OPNsense/System/SystemStatus.php @@ -75,9 +75,10 @@ class SystemStatus 'title' => $obj->getTitle(), 'statusCode' => $obj->getStatus(), 'message' => $obj->getMessage(), - 'logLocation' => $obj->getLogLocation(), + 'location' => $obj->getLocation(), 'timestamp' => $obj->getTimestamp(), 'persistent' => $obj->getPersistent(), + 'isBanner' => $obj->isBanner(), 'priority' => $obj->getPriority(), ]; } diff --git a/src/opnsense/www/js/opnsense_status.js b/src/opnsense/www/js/opnsense_status.js index 2362f0859..cb01516b6 100644 --- a/src/opnsense/www/js/opnsense_status.js +++ b/src/opnsense/www/js/opnsense_status.js @@ -1,5 +1,5 @@ /** - * Copyright (C) 2022-2024 Deciso B.V. + * Copyright (C) 2022-2025 Deciso B.V. * * All rights reserved. * @@ -25,181 +25,224 @@ * POSSIBILITY OF SUCH DAMAGE. */ -function updateStatusDialog(dialog, status) { - let $ret = $(` -
- -

- - -   - System -

-

No pending messages.

-
-
- `); +class Status { + constructor() { + this.observers = []; + this.data = null; + } - let $message = $( - '
' + - '
' + - '
' - ); - - for (let [shortname, subject] of Object.entries(status)) { - $message.find('a').last().addClass('__mb'); - - let formattedSubject = subject.title; - if (subject.age != undefined) { - formattedSubject += ' (' + subject.age + ')'; - } - - let ref = subject.logLocation != null ? `href="${subject.logLocation}"` : ''; - let $closeBtn = ` - - `; - let $listItem = $(` - -

- -   - ${formattedSubject} - ${subject.persistent ? '' : $closeBtn} -

-

${subject.message}

-
- `) - - $message.find('#opn-status-list').append($listItem); - - $message.find('#dismiss-' + shortname).on('click', function (e) { - e.preventDefault(); - $.ajax('/api/core/system/dismissStatus', { - type: 'post', - data: {'subject': shortname}, - dialogRef: dialog, - success: function() { - updateSystemStatus(this.dialogRef); - } + updateStatus() { + const fetch = new Promise((resolve, reject) => { + ajaxGet('/api/core/system/status', {}, function (data) { + resolve(data); }); }); - $ret = $message; - } - return $ret; -} - -function parseStatusIcon(subject) { - switch (subject.status) { - case "ERROR": - subject.icon = 'fa fa-circle text-danger'; - subject.banner = 'alert-danger'; - subject.severity = BootstrapDialog.TYPE_DANGER; - break; - case "WARNING": - subject.icon = 'fa fa-circle text-warning'; - subject.banner ='alert-warning'; - subject.severity = BootstrapDialog.TYPE_WARNING; - break; - case "NOTICE": - subject.icon = 'fa fa-circle text-info'; - subject.banner = 'alert-info'; - subject.severity = BootstrapDialog.TYPE_INFO; - break; - default: - subject.icon = 'fa fa-circle text-muted'; - subject.banner = 'alert-info'; - subject.severity = BootstrapDialog.TYPE_PRIMARY; - break; - } -} - -function fetchSystemStatus() { - return new Promise((resolve, reject) => { - ajaxGet('/api/core/system/status', {}, function (data) { - resolve(data); + fetch.then((data) => { + this.notify(data); }); - }); + } + + attach(observer) { + this.observers.push(observer); + } + + notify(data) { + this.observers.forEach(observer => observer.update(data)); + } } -function parseStatus(data) { - let system = data.metadata.system; +class StatusIcon { + update(status) { + const icon = this._parseStatusIcon(status.metadata.system.status); + $('#system_status').removeClass().addClass(icon); + } - // handle initial page load status icon - parseStatusIcon(system); - $('#system_status').removeClass().addClass(system.icon); + _parseStatusIcon(statusCode) { + switch (statusCode) { + case "ERROR": + return 'fa fa-circle text-danger'; + case "WARNING": + return 'fa fa-circle text-warning'; + case "NOTICE": + return 'fa fa-circle text-info'; + default: + return 'fa fa-circle text-muted'; + } + } - let notifications = {}; - let bannerMessages = {}; - for (let [shortname, subject] of Object.entries(data.subsystems)) { - parseStatusIcon(subject); + asClass(statusCode) { + return this._parseStatusIcon(statusCode); + } +} - if (subject.status == "OK") - continue; +class StatusDialog { + constructor() { + this.clickHandlerRegistered = false; + this.dialogOpen = false; + this.currentStatus = null; + this.dialog = null; + } - if (subject.persistent) { - bannerMessages[shortname] = subject; + update(status) { + this.currentStatus = status; + if (!this.clickHandlerRegistered) { + this.clickHandlerRegistered = true; + const translations = status.metadata.translations; + $('#system_status').click(() => { + this.dialog = new BootstrapDialog({ + title: translations.dialogTitle, + draggable: true, + buttons: [{ + id: 'close', + label: translations.dialogCloseButton, + action: (dialogRef) => { + dialogRef.close(); + this.dialogOpen = false; + } + }], + }); + + this._setDialogContent(this.currentStatus); + + this.dialog.open(); + this.dialogOpen = true; + }); } else { - notifications[shortname] = subject; + this._setDialogContent(this.currentStatus); + + if (!this.dialogOpen) { + this.dialog.open(); + this.dialogOpen = true; + } } } - return { - 'banners': bannerMessages, - 'notifications': notifications - }; -} - -function updateSystemStatus(dialog = null) { - fetchSystemStatus().then((data) => { - let status = parseStatus(data); // will also update status icon - - if (dialog != null) { - dialog.setMessage(function(dialogRef) { - return updateStatusDialog(dialogRef, status.notifications); - }) - } - - if (!$.isEmptyObject(status.banners)) { - let banner = Object.values(status.banners)[0]; - $('.page-content-main > .container-fluid > .row').prepend($(` -
-
- ${banner.message} -
+ _setDialogContent(status) { + this.dialog.setMessage((dialog) => { + let $ret = $(` +
+ +

+ + +   + System +

+

No pending messages.

+
- `)); + `); - } - }); + let $message = $( + '
' + + '
' + + '
' + ); - $("#system_status").click(function() { - fetchSystemStatus().then((data) => { - let translations = data.metadata.translations; - let status = parseStatus(data); + for (let [shortname, subject] of Object.entries(status.subsystems)) { + if (subject.status == "OK") + continue; - dialog = new BootstrapDialog({ - title: translations.dialogTitle, - buttons: [{ - id: 'close', - label: translations.dialogCloseButton, - action: function(dialogRef) { - dialogRef.close(); - } - }], - }); + $message.find('a').last().addClass('__mb'); - dialog.setMessage(function(dialogRef) { - // intentionally do banners first, as these should always show on top - // in both cases normal backend sorting applies - return updateStatusDialog(dialogRef, {...status.banners, ...status.notifications}); - }) + let formattedSubject = subject.title; + if (subject.age != undefined) { + formattedSubject += ' (' + subject.age + ')'; + } - dialog.open(); + let ref = subject.location != null ? `href="${subject.location}"` : ''; + let hoverStyle = subject.location == null ? 'cursor: default; pointer-events: none;' : ''; + + let $closeBtn = ` + + `; + + let $listItem = $(` + +

+ +   + ${formattedSubject} + ${subject.persistent ? '' : $closeBtn} +

+

${subject.message}

+
+ `) + + $message.find('#opn-status-list').append($listItem); + + $message.find('#dismiss-' + shortname).on('click', function (e) { + e.preventDefault(); + $.ajax('/api/core/system/dismissStatus', { + type: 'post', + data: {'subject': shortname}, + dialogRef: dialog, + itemRef: $listItem, + success: function() { + statusObj.updateStatus(); + } + }); + }); + + $ret = $message; + } + + return $ret; }); - }); - + } +} + +class StatusBanner { + constructor() { + this.bannerActive = false; + } + + update(status) { + for (let [name, subject] of Object.entries(status.subsystems)) { + if (subject.status == "OK") + continue; + + if (subject.isBanner && !this.bannerActive) { + if (!this.bannerActive) { + $('.page-content-main > .container-fluid > .row').prepend($(` +
+
+ ${subject.message} +
+
+ `)); + this.bannerActive = true; + break; + } else { + $('#notification-banner').text(subject.message); + $('#notification-banner').removeClass().addClass(`alert alert-info ${this.parseStatusBanner(subject.status)}`); + } + } + } + } + + parseStatusBanner(statusCode) { + switch (statusCode) { + case "ERROR": + return 'alert-danger'; + case "WARNING": + return 'alert-warning'; + default: + return 'alert-info'; + } + } +} + +const statusObj = new Status(); + +function updateSystemStatus() { + statusObj.attach(new StatusIcon()); + statusObj.attach(new StatusDialog()); + statusObj.attach(new StatusBanner()); + + statusObj.updateStatus(); }