From 66358e0998de8ca9f2d8bca041ba92c2e5d9a2d6 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 1 Oct 2026 02:39:53 -0400 Subject: [PATCH] Adopt plugin table API and relocate schema to includes/database.php (#254) * Relocate schema to includes/database.php via plugin table API + db_update_table upgrade * CI: allowlist unmeasured schema bootstrap and web entry points * CI: keep include_once for the settings-page-only lib/reports.php load (uncoverable) * Add manifest.json + upgrade-time file pruning; consolidate themes/ into css/ * database: keep user-owned dashboards table across uninstall api_plugin_db_table_create() records plugin_monitor_dashboards in plugin_db_changes, so api_plugin_db_changes_remove() (run by api_plugin_uninstall() after plugin_monitor_uninstall()) would drop it even though monitor_drop_tables() intentionally leaves it in place. Remove the 'create' ownership record after provisioning (install and upgrade paths) so user dashboards survive an uninstall. Adds a regression test. * Prune phpunit.xml on upgrade and ignore markdown-lint configs phpunit.xml ships in the repo for CI but is a dev-only test artifact, so it is removed from manifest.json 'expected', pruned from installs on upgrade (like tests/), and excluded from the manifest drift check. The retired markdown-lint configs (.mdlrc, .md_style.rb) are deleted, and .md* is now ignored like .git*: protected from pruning and excluded from drift if it reappears. * Move libraries into includes/, flatten css, drop sonar config db_functions.php, monitor_render.php and monitor_controller.php move into includes/ (the latter two lose the monitor_ prefix, becoming render.php and controller.php); monitor.php and the tests load them from there. Each per-theme stylesheet css//monitor.css becomes css/.css so users can drop their own css/.css into the css base, and plugin_monitor_page_head() looks there, falling back to css/monitor.css. The unreferenced sonar-project.properties is removed. Old paths are tombstoned so existing installs drop the stale copies on upgrade. * Rename includes/db_functions.php to includes/functions.php Follows the fleet convention for the plugin's shared library filename; monitor.php, the measured-source list and the unit test load it from the new name. * Regenerate cacti.pot for the includes/ file relocations Moving the plugin's library files into includes/ changed the source-reference lines in the translation template; regenerate it so the 'Verify translation template' CI check passes. * Harden prune against tampered-manifest path traversal The upgrade-time prune now rejects any tombstone containing '.'/'..' segments (which could escape the plugin directory or resolve to its root) and treats ancestors of whitelist entries as protected, so a tombstone on a parent directory can no longer delete a whitelisted file beneath it. * docs: update and align the Project Structure block Point the structure tree at the relocated files (libraries now under includes/, data files under docs/, stylesheets under css/) and align the trailing '# ...' comments to a single column so they no longer drift right. * docs/tests: standardize test header and correct manifest-contract notes - Use the full license header (from the plugin's own setup.php) in tests/Unit/PruneFilesTest.php instead of the abbreviated copyright banner. - Document that the manifest drift check and the upgrade-time prune also handle phpunit.xml and .md* files, matching the implemented behavior. * refactor: rename prune helpers to the plugin-slug function prefix The repository naming contract reserves the plugin__ prefix for plugin lifecycle / hook-registration functions; all other functions use the plain _ prefix. Rename the internal upgrade helpers accordingly: plugin__prune_files() -> _prune_files() plugin__rmtree() -> _rmtree() The call site, unit tests, and the copilot-instructions.md references are updated to match. No behavioral change. * chore: set the Cacti compatibility floor to 1.2.29 Align the compat metadata (and the contributor-guide references) with the team decision to standardize the minimum supported Cacti version at 1.2.29; the previously proposed 1.2.32 floor was not adopted. * Bump plugin version to 3.0 so the upgrade-time cleanup migration runs Add a 3.0 CHANGELOG section for this cycle's schema/manifest work and set INFO version = 3.0 so existing 2.9 installs detect the version change and run the schema refresh and manifest-based file prune. --------- Co-authored-by: Copilot --- .github/copilot-instructions.md | 38 +- .github/workflows/plugin-ci-workflow.yml | 3 + CHANGELOG.md | 8 + INFO | 4 +- themes/classic/monitor.css => css/classic.css | 0 themes/dark/monitor.css => css/dark.css | 0 {themes/classic => css}/index.php | 0 .../monitor.css => css/midwinter.css | 0 themes/modern/monitor.css => css/modern.css | 0 .../monitor.css => css/paper-plane.css | 0 themes/paw/monitor.css => css/paw.css | 0 themes/sunrise/monitor.css => css/sunrise.css | 0 .../controller.php | 4 +- includes/database.php | 204 ++++ db_functions.php => includes/functions.php | 0 monitor_render.php => includes/render.php | 0 locales/po/cacti.pot | 924 +++++++++--------- manifest.json | 29 + monitor.php | 8 +- phpunit.xml | 2 +- setup.php | 285 ++++-- sonar-project.properties | 10 - .../MonitorRequestOutputWiringTest.php | 16 +- tests/Integration/OutputEscapingTest.php | 30 +- tests/Unit/DbFunctionsTest.php | 4 +- tests/Unit/MonitorLifecycleTest.php | 30 + tests/Unit/MonitorPageHeadTest.php | 58 ++ tests/Unit/MonitorSetupTableAndPollerTest.php | 13 + tests/Unit/PruneFilesTest.php | 255 +++++ tests/bin/patch-coverage.php | 3 + tests/bin/validate-manifest.php | 73 ++ tests/bootstrap-unit.php | 44 +- themes/dark/index.php | 26 - themes/index.php | 26 - themes/midwinter/index.php | 26 - themes/modern/index.php | 26 - themes/paper-plane/index.php | 26 - themes/paw/index.php | 26 - themes/sunrise/index.php | 26 - 39 files changed, 1414 insertions(+), 813 deletions(-) rename themes/classic/monitor.css => css/classic.css (100%) rename themes/dark/monitor.css => css/dark.css (100%) rename {themes/classic => css}/index.php (100%) rename themes/midwinter/monitor.css => css/midwinter.css (100%) rename themes/modern/monitor.css => css/modern.css (100%) rename themes/paper-plane/monitor.css => css/paper-plane.css (100%) rename themes/paw/monitor.css => css/paw.css (100%) rename themes/sunrise/monitor.css => css/sunrise.css (100%) rename monitor_controller.php => includes/controller.php (99%) create mode 100644 includes/database.php rename db_functions.php => includes/functions.php (100%) rename monitor_render.php => includes/render.php (100%) create mode 100644 manifest.json delete mode 100644 sonar-project.properties create mode 100644 tests/Unit/MonitorPageHeadTest.php create mode 100644 tests/Unit/PruneFilesTest.php create mode 100644 tests/bin/validate-manifest.php delete mode 100644 themes/dark/index.php delete mode 100644 themes/index.php delete mode 100644 themes/midwinter/index.php delete mode 100644 themes/modern/index.php delete mode 100644 themes/paper-plane/index.php delete mode 100644 themes/paw/index.php delete mode 100644 themes/sunrise/index.php diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6644c5b..9b84745 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -20,32 +20,30 @@ When generating code for this repository: ### Key Dependencies - Cacti core framework (`api_plugin_*`, `db_*`, `read_config_option()`, `get_filter_request_var()`) -- `themes/` CSS overlays, `sounds/` audible alert assets, `js/` client-side dashboard code +- `css/` CSS overlays, `sounds/` audible alert assets, `js/` client-side dashboard code ## Project Structure ``` -monitor/ # Repository root (install to plugins/monitor/ in Cacti) -├── js/ # Dashboard client-side logic -├── sounds/ # Alert sound assets -├── themes/ # CSS theme overlays (monitor.css) -├── tests/ # Test suite -├── db_functions.php # SQL filter/join helpers and status/device query utilities -├── monitor.php # Web entrypoint/bootstrap (session/request setup) -├── monitor_controller.php # Action flow, filter handling, page orchestration -├── monitor_render.php # Dashboard/group rendering and view-specific output -├── poller_functions.php # Poller helper logic (uptime checks, notifications, email payloads) -├── poller_monitor.php # Background poller entry point (CLI: --help, --version, --debug) -├── INFO # Plugin metadata (name, version, compat) +monitor/ # Repository root (install to plugins/monitor/ in Cacti) +├── js/ # Dashboard client-side logic +├── sounds/ # Alert sound assets +├── css/ # CSS theme overlays (monitor.css) +├── tests/ # Test suite +├── includes/ # functions.php (DB/query helpers), controller.php (action flow), render.php (rendering), database.php (schema) +├── monitor.php # Web entrypoint/bootstrap (session/request setup) +├── poller_functions.php # Poller helper logic (uptime checks, notifications, email payloads) +├── poller_monitor.php # Background poller entry point (CLI: --help, --version, --debug) +├── INFO # Plugin metadata (name, version, compat) ├── README.md -└── setup.php # Plugin install/uninstall/upgrade hooks, hook registration, config +└── setup.php # Plugin install/uninstall/upgrade hooks, hook registration, config ``` ## Naming Conventions ### Function Names - Procedural functions use **lowerCamelCase**, the `monitor_` prefix, or the required `plugin_monitor_` lifecycle prefix, matching current file conventions — keep plugin hook callback names exactly synchronized between their definitions and their `api_plugin_register_hook()` registration strings. -- Keep top-level entrypoints lightweight; place reusable logic in the appropriate helper file (`db_functions.php`, `poller_functions.php`). +- Keep top-level entrypoints lightweight; place reusable logic in the appropriate helper file (`includes/functions.php`, `poller_functions.php`). ### Database Tables Plugin tables are prefixed `plugin_monitor_`. Reuse existing table names and avoid introducing parallel schema variants; keep schema evolution inside the existing setup/upgrade lifecycle functions in `setup.php`. @@ -100,9 +98,9 @@ Use gettext calls with the `monitor` domain for user-facing strings: `__('Text', ### File Responsibilities - `setup.php`: plugin lifecycle, hook registration, config arrays/settings, install/upgrade table management. - `monitor.php`: web entrypoint/bootstrap, includes, session/request setup. -- `monitor_controller.php`: action flow, filter handling, page orchestration. -- `monitor_render.php`: dashboard/group rendering and view-specific output. -- `db_functions.php`: SQL filter/join helpers and status/device query utilities. +- `includes/controller.php`: action flow, filter handling, page orchestration. +- `includes/render.php`: dashboard/group rendering and view-specific output. +- `includes/functions.php`: SQL filter/join helpers and status/device query utilities. - `poller_monitor.php`: CLI poller entrypoint. - `poller_functions.php`: poller helper logic (uptime checks, notifications, email payload building). @@ -175,3 +173,7 @@ existing code or adding new code, not just in dedicated cleanup passes: line, `@param` lines, a blank comment line, then `@return`. Infer parameter/return types from actual usage; don't change the function's real type-hints in the same pass (let static analysis flag mismatches separately). Skip vendored third-party library files. + +## File manifest & upgrade pruning + +The plugin ships a root `manifest.json` with three arrays: `tombstones` (files/directories older versions shipped that have since moved or been removed), `expected` (the top-level files and directories that ship today, directories written with a trailing `/`), and `whitelist` (paths holding user data that must never be touched). Keep `expected` current: CI runs `tests/bin/validate-manifest.php`, which fails on any drift between `expected` and the real top-level tree (it ignores `tests/`, `phpunit.xml`, `.git*`, `.md*`, and whitelisted paths). Custom customer CSS/theme files belong in `expected`, and stylesheets live in `css/` (not `themes/`). On upgrade, `monitor_prune_files()` deletes the tombstoned paths, the dev-only `tests/` tree, and the `phpunit.xml` test config, leaves `whitelist`, `.git*`, and `.md*` alone, and logs (without removing) any top-level entry the manifest does not account for. As a safety measure it refuses any tombstone that resolves outside the plugin directory (a tampered manifest.json) and logs a warning for any file or directory it cannot remove. When you move or delete a shipped file, add its old path to `tombstones` and update `expected` in the same change. diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index ea03045..7ce0fb8 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -88,6 +88,9 @@ jobs: - name: Check PHP version run: php -v + - name: Validate plugin manifest (expected-file drift) + run: php cacti/plugins/monitor/tests/bin/validate-manifest.php + - name: Run apt-get update run: sudo apt-get update diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e1e51e..bd799cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Changelog +--- 3.0 --- + +* feature: Adopt the Cacti plugin table API and relocate schema provisioning into includes/database.php +* feature: Add a fleet-wide manifest.json with upgrade-time file pruning +* feature: Consolidate per-theme override stylesheets from themes/ into css/ +* dev: Rename includes/db_functions.php to includes/functions.php and move bundled libraries under includes/ +* dev: Set the Cacti compatibility floor to 1.2.29 + --- 2.9 --- * dev: Measure CI coverage with xdebug instead of pcov so the plugin's own sources are instrumented (pcov auto-scopes to the Composer root and skipped cacti/plugins/, leaving the patch-coverage gate with nothing to measure) diff --git a/INFO b/INFO index 8642661..c5bc2f0 100644 --- a/INFO +++ b/INFO @@ -21,11 +21,11 @@ [info] name = monitor -version = 2.9 +version = 3.0 longname = Device Monitoring author = The Cacti Group email = homepage = http://www.cacti.net -compat = 1.2.32 +compat = 1.2.29 requires = thold:1.2.1 capabilities = online_view:1, online_mgmt:1, offline_view:0, offline_mgmt:0, remote_collect:0 diff --git a/themes/classic/monitor.css b/css/classic.css similarity index 100% rename from themes/classic/monitor.css rename to css/classic.css diff --git a/themes/dark/monitor.css b/css/dark.css similarity index 100% rename from themes/dark/monitor.css rename to css/dark.css diff --git a/themes/classic/index.php b/css/index.php similarity index 100% rename from themes/classic/index.php rename to css/index.php diff --git a/themes/midwinter/monitor.css b/css/midwinter.css similarity index 100% rename from themes/midwinter/monitor.css rename to css/midwinter.css diff --git a/themes/modern/monitor.css b/css/modern.css similarity index 100% rename from themes/modern/monitor.css rename to css/modern.css diff --git a/themes/paper-plane/monitor.css b/css/paper-plane.css similarity index 100% rename from themes/paper-plane/monitor.css rename to css/paper-plane.css diff --git a/themes/paw/monitor.css b/css/paw.css similarity index 100% rename from themes/paw/monitor.css rename to css/paw.css diff --git a/themes/sunrise/monitor.css b/css/sunrise.css similarity index 100% rename from themes/sunrise/monitor.css rename to css/sunrise.css diff --git a/monitor_controller.php b/includes/controller.php similarity index 99% rename from monitor_controller.php rename to includes/controller.php index 23cfaa8..7441fcc 100644 --- a/monitor_controller.php +++ b/includes/controller.php @@ -1047,8 +1047,8 @@ function monitorGetAjaxStatusLinks(array $host, array $config): string { } if (api_plugin_is_enabled('syslog') && api_plugin_user_realm_auth('syslog.php')) { - include($config['base_path'] . '/plugins/syslog/config.php'); - include_once($config['base_path'] . '/plugins/syslog/functions.php'); + require($config['base_path'] . '/plugins/syslog/config.php'); + require_once($config['base_path'] . '/plugins/syslog/functions.php'); $syslog_logs = syslog_db_fetch_cell_prepared( 'SELECT count(*) diff --git a/includes/database.php b/includes/database.php new file mode 100644 index 0000000..146d480 --- /dev/null +++ b/includes/database.php @@ -0,0 +1,204 @@ + Table definition consumed by + * api_plugin_db_table_create()/db_update_table(). + */ +function monitor_notify_history_table_data(): array { + $data = []; + $data['columns'][] = ['name' => 'id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => false, 'auto_increment' => true]; + $data['columns'][] = ['name' => 'host_id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => true, 'default' => null]; + $data['columns'][] = ['name' => 'notify_type', 'type' => 'tinyint(3)', 'unsigned' => true, 'NULL' => true, 'default' => null]; + $data['columns'][] = ['name' => 'ping_time', 'type' => 'double', 'NULL' => true, 'default' => null]; + $data['columns'][] = ['name' => 'ping_threshold', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => true, 'default' => null]; + $data['columns'][] = ['name' => 'notification_time', 'type' => 'timestamp', 'NULL' => false, 'default' => '0000-00-00 00:00:00']; + $data['columns'][] = ['name' => 'notes', 'type' => 'varchar(255)', 'NULL' => true, 'default' => null]; + $data['primary'] = ['id']; + $data['unique_keys'][] = ['name' => 'unique_key', 'columns' => ['host_id', 'notify_type', 'notification_time']]; + $data['type'] = 'InnoDB'; + $data['comment'] = 'Stores Notification Event History'; + + return $data; +} + +/** + * The plugin_monitor_reboot_history table definition (one row per detected + * device reboot). + * + * @return array Table definition. + */ +function monitor_reboot_history_table_data(): array { + $data = []; + $data['columns'][] = ['name' => 'id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => false, 'auto_increment' => true]; + $data['columns'][] = ['name' => 'host_id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => true, 'default' => null]; + $data['columns'][] = ['name' => 'reboot_time', 'type' => 'timestamp', 'NULL' => false, 'default' => '0000-00-00 00:00:00']; + $data['columns'][] = ['name' => 'log_time', 'type' => 'timestamp', 'NULL' => false, 'default' => 'CURRENT_TIMESTAMP']; + $data['primary'] = ['id']; + $data['keys'][] = ['name' => 'host_id', 'columns' => ['host_id']]; + $data['keys'][] = ['name' => 'log_time', 'columns' => ['log_time']]; + $data['keys'][] = ['name' => 'reboot_time', 'columns' => ['reboot_time']]; + $data['type'] = 'InnoDB'; + $data['comment'] = 'Keeps Track of Device Reboot Times'; + + return $data; +} + +/** + * The plugin_monitor_uptime table definition (last known uptime per + * device). Uses the current column layout (uptime NOT NULL). + * + * @return array Table definition. + */ +function monitor_uptime_table_data(): array { + $data = []; + $data['columns'][] = ['name' => 'host_id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => false, 'default' => 0]; + $data['columns'][] = ['name' => 'uptime', 'type' => 'bigint(20)', 'unsigned' => true, 'NULL' => false, 'default' => 0]; + $data['primary'] = ['host_id']; + $data['keys'][] = ['name' => 'uptime', 'columns' => ['uptime']]; + $data['type'] = 'InnoDB'; + $data['comment'] = 'Keeps Track of the Devices last uptime to track agent restarts and reboots'; + + return $data; +} + +/** + * The plugin_monitor_dashboards table definition (per-user predefined + * dashboards). + * + * @return array Table definition. + */ +function monitor_dashboards_table_data(): array { + $data = []; + $data['columns'][] = ['name' => 'id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => false, 'auto_increment' => true]; + $data['columns'][] = ['name' => 'user_id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => true, 'default' => 0]; + $data['columns'][] = ['name' => 'name', 'type' => 'varchar(128)', 'NULL' => true, 'default' => '']; + $data['columns'][] = ['name' => 'url', 'type' => 'varchar(1024)', 'NULL' => true, 'default' => '']; + $data['primary'] = ['id']; + $data['keys'][] = ['name' => 'user_id', 'columns' => ['user_id']]; + $data['type'] = 'InnoDB'; + $data['comment'] = 'Stores predefined dashboard information for a user or users'; + + return $data; +} + +/** + * Removes the plugin_db_changes 'create' ownership record for the user-owned + * dashboards table so api_plugin_db_changes_remove() (run by + * api_plugin_uninstall()) does not drop it, preserving user dashboards across + * an uninstall. + * + * @return void + */ +function monitor_disown_dashboards_table() { + db_execute("DELETE FROM plugin_db_changes + WHERE plugin = 'monitor' + AND `table` = 'plugin_monitor_dashboards' + AND method = 'create'"); +} + +/** + * Creates this plugin's own tables through Cacti's tracked plugin table + * API and ensures the core host table carries the monitoring columns this + * plugin relies on. Called from plugin_monitor_install() and (idempotently) + * from monitor_check_upgrade(). + * + * @return void + */ +function monitor_setup_table() { + api_plugin_db_table_create('monitor', 'plugin_monitor_notify_history', monitor_notify_history_table_data()); + api_plugin_db_table_create('monitor', 'plugin_monitor_reboot_history', monitor_reboot_history_table_data()); + api_plugin_db_table_create('monitor', 'plugin_monitor_uptime', monitor_uptime_table_data()); + api_plugin_db_table_create('monitor', 'plugin_monitor_dashboards', monitor_dashboards_table_data()); + + /* dashboards holds user data: create it but do not record plugin ownership, so uninstall preserves it. */ + monitor_disown_dashboards_table(); + + if (db_table_exists('host')) { + $row_format = db_fetch_cell("SELECT ROW_FORMAT + FROM information_schema.tables + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'host'"); + + if (strtoupper((string) $row_format) !== 'DYNAMIC') { + db_execute('ALTER TABLE host ROW_FORMAT=DYNAMIC'); + } + } + + db_execute('SET SESSION innodb_strict_mode=0'); + + api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor', 'type' => 'char(3)', 'NULL' => true, 'default' => 'on']); + api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_text', 'type' => 'text', 'NULL' => false]); + api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_criticality', 'type' => 'tinyint', 'unsigned' => true, 'NULL' => false, 'default' => '0']); + api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_warn', 'type' => 'double', 'NULL' => false, 'default' => '0']); + api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_alert', 'type' => 'double', 'NULL' => false, 'default' => '0']); + api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_icon', 'type' => 'varchar(30)', 'NULL' => false, 'default' => '']); + + db_execute('SET SESSION innodb_strict_mode=1'); +} + +/** + * Refreshes this plugin's own tables to their current definition on + * upgrade: db_update_table() diffs the live schema against the definition + * and issues the exact ALTER when the table already exists, otherwise the + * table is created outright. Called from monitor_check_upgrade(). + * + * @return void + */ +function monitor_upgrade_tables() { + $tables = [ + 'plugin_monitor_notify_history' => monitor_notify_history_table_data(), + 'plugin_monitor_reboot_history' => monitor_reboot_history_table_data(), + 'plugin_monitor_uptime' => monitor_uptime_table_data(), + 'plugin_monitor_dashboards' => monitor_dashboards_table_data(), + ]; + + foreach ($tables as $table => $table_data) { + if (db_table_exists($table)) { + db_update_table($table, $table_data); + } else { + api_plugin_db_table_create('monitor', $table, $table_data); + } + } + + monitor_disown_dashboards_table(); +} + +/** + * Drops the tables this plugin owns that hold transient monitoring state + * (notification/reboot history and uptime). The user-defined dashboards + * table is intentionally left in place. Called from + * plugin_monitor_uninstall(). + * + * @return void + */ +function monitor_drop_tables() { + db_execute('DROP TABLE IF EXISTS plugin_monitor_notify_history'); + db_execute('DROP TABLE IF EXISTS plugin_monitor_reboot_history'); + db_execute('DROP TABLE IF EXISTS plugin_monitor_uptime'); +} diff --git a/db_functions.php b/includes/functions.php similarity index 100% rename from db_functions.php rename to includes/functions.php diff --git a/monitor_render.php b/includes/render.php similarity index 100% rename from monitor_render.php rename to includes/render.php diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index d66b5ee..ee1c27c 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cacti \n" "Report-Msgid-Bugs-To: developers@cacti.net\n" -"POT-Creation-Date: 2026-09-28 16:06-0400\n" +"POT-Creation-Date: 2026-09-30 21:27-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Cacti Developers >\n" "Language-Team: Cacti Developers \n" @@ -17,6 +17,467 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: includes/controller.php +msgid "New Dashboard" +msgstr "" + +#: includes/controller.php +msgid "Dashboard" +msgstr "" + +#: includes/controller.php +msgid "Enter the Dashboard Name and then press 'Save' to continue, else press 'Cancel'" +msgstr "" + +#: includes/controller.php +msgid "Create New Dashboard" +msgstr "" + +#: includes/controller.php includes/render.php +msgid "Monitored Devices" +msgstr "" + +#: includes/controller.php monitor.php +msgid "Devices without Thresholds" +msgstr "" + +#: includes/controller.php +msgid "Not Monitored Devices" +msgstr "" + +#: includes/controller.php monitor.php +msgid "All Devices" +msgstr "" + +#: includes/controller.php monitor.php +msgid "All Monitored Devices" +msgstr "" + +#: includes/controller.php +msgid "Monitored Devices either Down or Recovering" +msgstr "" + +#: includes/controller.php +msgid "Monitored Devices either Down, Recovering, or with Triggered Thresholds" +msgstr "" + +#: includes/controller.php +msgid "Monitored Devices either Down, Recovering, or with Breached or Triggered Thresholds" +msgstr "" + +#: includes/controller.php +#, php-format +msgid "Unknown monitoring status (%s)" +msgstr "" + +#: includes/controller.php +msgid ", and All Criticalities" +msgstr "" + +#: includes/controller.php +msgid ", and of Low Criticality or Higher" +msgstr "" + +#: includes/controller.php +msgid ", and of Medium Criticality or Higher" +msgstr "" + +#: includes/controller.php +msgid ", and of High Criticality or Higher" +msgstr "" + +#: includes/controller.php +msgid ", and of Mission Critical Status" +msgstr "" + +#: includes/controller.php +msgid "
Remember to first select eligible Devices to be Monitored from the Devices page!
" +msgstr "" + +#: includes/controller.php +msgid "Unsaved" +msgstr "" + +#: includes/controller.php +msgid "Layout" +msgstr "" + +#: includes/controller.php includes/render.php +msgid "Status" +msgstr "" + +#: includes/controller.php setup.php +msgid "View" +msgstr "" + +#: includes/controller.php setup.php +msgid "Grouping" +msgstr "" + +#: includes/controller.php includes/render.php +msgid "Devices" +msgstr "" + +#: includes/controller.php +msgid "Refresh" +msgstr "" + +#: includes/controller.php +msgid "Refresh the Device List" +msgstr "" + +#: includes/controller.php +msgid "Clear" +msgstr "" + +#: includes/controller.php +msgid "Clear the set filter" +msgstr "" + +#: includes/controller.php +msgid "Save" +msgstr "" + +#: includes/controller.php +msgid "Save Filter Settings" +msgstr "" + +#: includes/controller.php +msgid "New" +msgstr "" + +#: includes/controller.php +msgid "Save New Dashboard" +msgstr "" + +#: includes/controller.php +msgid "Rename" +msgstr "" + +#: includes/controller.php +msgid "Rename Dashboard" +msgstr "" + +#: includes/controller.php +msgid "Delete" +msgstr "" + +#: includes/controller.php +msgid "Delete Dashboard" +msgstr "" + +#: includes/controller.php +#, php-format +msgid "%s Alert for downed Devices" +msgstr "" + +#: includes/controller.php +#, php-format +msgid "%s Alerts for downed Devices" +msgstr "" + +#: includes/controller.php includes/render.php poller_functions.php setup.php +msgid "Criticality" +msgstr "" + +#: includes/controller.php +msgid "Size" +msgstr "" + +#: includes/controller.php +msgid "Trim" +msgstr "" + +#: includes/controller.php +msgid "All Trees" +msgstr "" + +#: includes/controller.php includes/render.php +msgid "Non-Tree Devices" +msgstr "" + +#: includes/controller.php monitor.php setup.php +msgid "Tree" +msgstr "" + +#: includes/controller.php +msgid "All Sites" +msgstr "" + +#: includes/controller.php includes/render.php +msgid "Non-Site Devices" +msgstr "" + +#: includes/controller.php +msgid "Sites" +msgstr "" + +#: includes/controller.php +msgid "All Templates" +msgstr "" + +#: includes/controller.php +msgid "Non-Templated Devices" +msgstr "" + +#: includes/controller.php +msgid "Template" +msgstr "" + +#: includes/controller.php +msgid " [ Filter Settings Saved ]" +msgstr "" + +#: includes/controller.php +msgid "Cancel" +msgstr "" + +#: includes/controller.php +#, php-format +msgid " [ Refresh Again in %d Seconds ]" +msgstr "" + +#: includes/controller.php +#, php-format +msgid "Monitor Filter [ Last Refresh: %s ]" +msgstr "" + +#: includes/controller.php +msgid "[ Showing only first 30 Devices ]" +msgstr "" + +#: includes/controller.php +msgid "Search" +msgstr "" + +#: includes/controller.php +msgid "Mute" +msgstr "" + +#: includes/controller.php +msgid "Acknowledge" +msgstr "" + +#: includes/controller.php +msgid "Un-Mute" +msgstr "" + +#: includes/controller.php +msgid "Reset" +msgstr "" + +#: includes/controller.php +#, php-format +msgid "Dashboard '%s' Removed." +msgstr "" + +#: includes/controller.php +#, php-format +msgid "Dashboard '%s' is not owned by you." +msgstr "" + +#: includes/controller.php +#, php-format +msgid "Dashboard '%s' has been Saved!" +msgstr "" + +#: includes/controller.php +#, php-format +msgid "Dashboard '%s' could not be Saved!" +msgstr "" + +#: includes/controller.php +msgid "Edit Device" +msgstr "" + +#: includes/controller.php +msgid "View Graphs" +msgstr "" + +#: includes/controller.php +msgid "View Thresholds/Alerts" +msgstr "" + +#: includes/controller.php +msgid "View Device Syslog Alerts" +msgstr "" + +#: includes/controller.php +msgid "View Device Syslog Entries" +msgstr "" + +#: includes/controller.php +msgid "Device Status Information" +msgstr "" + +#: includes/controller.php +msgid "Device:" +msgstr "" + +#: includes/controller.php +msgid "Site:" +msgstr "" + +#: includes/controller.php +msgid "Location:" +msgstr "" + +#: includes/controller.php +msgid "Criticality:" +msgstr "" + +#: includes/controller.php +msgid "Status:" +msgstr "" + +#: includes/controller.php +msgid "Admin Note:" +msgstr "" + +#: includes/controller.php +msgid "IP/Hostname:" +msgstr "" + +#: includes/controller.php +msgid "Curr/Avg:" +msgstr "" + +#: includes/controller.php includes/render.php +#, php-format +msgid "%d ms" +msgstr "" + +#: includes/controller.php +msgid "Warn/Alert:" +msgstr "" + +#: includes/controller.php includes/render.php +#, php-format +msgid "%0.2d ms" +msgstr "" + +#: includes/controller.php +msgid "Last Fail:" +msgstr "" + +#: includes/controller.php +msgid "Time In State:" +msgstr "" + +#: includes/controller.php +msgid "Availability:" +msgstr "" + +#: includes/controller.php +msgid "Agent Uptime:" +msgstr "" + +#: includes/controller.php includes/render.php +msgid "N/A" +msgstr "" + +#: includes/controller.php +msgid "Sys Description:" +msgstr "" + +#: includes/controller.php +msgid "Contact:" +msgstr "" + +#: includes/controller.php +msgid "Notes:" +msgstr "" + +#: includes/controller.php includes/render.php +msgid "Never" +msgstr "" + +#: includes/controller.php +msgid "Unspecified" +msgstr "" + +#: includes/controller.php setup.php +msgid "None" +msgstr "" + +#: includes/render.php +msgid "Non-Site Device" +msgstr "" + +#: includes/render.php +msgid "Root Branch" +msgstr "" + +#: includes/render.php +#, php-format +msgid "Tree: %s" +msgstr "" + +#: includes/render.php +#, php-format +msgid "Branch: %s" +msgstr "" + +#: includes/render.php monitor.php +msgid "Unknown" +msgstr "" + +#: includes/render.php +msgid "Just Up" +msgstr "" + +#: includes/render.php poller_functions.php +msgid "Hostname" +msgstr "" + +#: includes/render.php +msgid "Hostname of device" +msgstr "" + +#: includes/render.php +msgid "ID" +msgstr "" + +#: includes/render.php poller_functions.php +msgid "Description" +msgstr "" + +#: includes/render.php monitor.php +msgid "Site" +msgstr "" + +#: includes/render.php +msgid "Length in Status" +msgstr "" + +#: includes/render.php +msgid "Averages" +msgstr "" + +#: includes/render.php +msgid "Warning" +msgstr "" + +#: includes/render.php +msgid "Admin" +msgstr "" + +#: includes/render.php +msgid "Monitor Text Column represents 'Admin'" +msgstr "" + +#: includes/render.php +msgid "Notes" +msgstr "" + +#: includes/render.php +msgid "Availability" +msgstr "" + +#: includes/render.php +msgid "Last Fail" +msgstr "" + #: monitor.php poller_functions.php setup.php msgid "Disabled" msgstr "" @@ -37,10 +498,6 @@ msgstr "" msgid "Mission Critical" msgstr "" -#: monitor.php monitor_render.php -msgid "Unknown" -msgstr "" - #: monitor.php msgid "Down" msgstr "" @@ -97,14 +554,6 @@ msgstr "" msgid "Zoom" msgstr "" -#: monitor.php monitor_controller.php -msgid "All Devices" -msgstr "" - -#: monitor.php monitor_controller.php -msgid "All Monitored Devices" -msgstr "" - #: monitor.php msgid "Not Up" msgstr "" @@ -117,10 +566,6 @@ msgstr "" msgid "Not Up, Triggered or Breached" msgstr "" -#: monitor.php monitor_controller.php -msgid "Devices without Thresholds" -msgstr "" - #: monitor.php msgid "Devices not Monitored" msgstr "" @@ -145,14 +590,6 @@ msgstr "" msgid "Tiles & Time" msgstr "" -#: monitor.php monitor_controller.php setup.php -msgid "Tree" -msgstr "" - -#: monitor.php monitor_render.php -msgid "Site" -msgstr "" - #: monitor.php setup.php msgid "Device Template" msgstr "" @@ -189,443 +626,6 @@ msgstr "" msgid "100 Chars" msgstr "" -#: monitor_controller.php -msgid "New Dashboard" -msgstr "" - -#: monitor_controller.php -msgid "Dashboard" -msgstr "" - -#: monitor_controller.php -msgid "Enter the Dashboard Name and then press 'Save' to continue, else press 'Cancel'" -msgstr "" - -#: monitor_controller.php -msgid "Create New Dashboard" -msgstr "" - -#: monitor_controller.php monitor_render.php -msgid "Monitored Devices" -msgstr "" - -#: monitor_controller.php -msgid "Not Monitored Devices" -msgstr "" - -#: monitor_controller.php -msgid "Monitored Devices either Down or Recovering" -msgstr "" - -#: monitor_controller.php -msgid "Monitored Devices either Down, Recovering, or with Triggered Thresholds" -msgstr "" - -#: monitor_controller.php -msgid "Monitored Devices either Down, Recovering, or with Breached or Triggered Thresholds" -msgstr "" - -#: monitor_controller.php -#, php-format -msgid "Unknown monitoring status (%s)" -msgstr "" - -#: monitor_controller.php -msgid ", and All Criticalities" -msgstr "" - -#: monitor_controller.php -msgid ", and of Low Criticality or Higher" -msgstr "" - -#: monitor_controller.php -msgid ", and of Medium Criticality or Higher" -msgstr "" - -#: monitor_controller.php -msgid ", and of High Criticality or Higher" -msgstr "" - -#: monitor_controller.php -msgid ", and of Mission Critical Status" -msgstr "" - -#: monitor_controller.php -msgid "
Remember to first select eligible Devices to be Monitored from the Devices page!
" -msgstr "" - -#: monitor_controller.php -msgid "Unsaved" -msgstr "" - -#: monitor_controller.php -msgid "Layout" -msgstr "" - -#: monitor_controller.php monitor_render.php -msgid "Status" -msgstr "" - -#: monitor_controller.php setup.php -msgid "View" -msgstr "" - -#: monitor_controller.php setup.php -msgid "Grouping" -msgstr "" - -#: monitor_controller.php monitor_render.php -msgid "Devices" -msgstr "" - -#: monitor_controller.php -msgid "Refresh" -msgstr "" - -#: monitor_controller.php -msgid "Refresh the Device List" -msgstr "" - -#: monitor_controller.php -msgid "Clear" -msgstr "" - -#: monitor_controller.php -msgid "Clear the set filter" -msgstr "" - -#: monitor_controller.php -msgid "Save" -msgstr "" - -#: monitor_controller.php -msgid "Save Filter Settings" -msgstr "" - -#: monitor_controller.php -msgid "New" -msgstr "" - -#: monitor_controller.php -msgid "Save New Dashboard" -msgstr "" - -#: monitor_controller.php -msgid "Rename" -msgstr "" - -#: monitor_controller.php -msgid "Rename Dashboard" -msgstr "" - -#: monitor_controller.php -msgid "Delete" -msgstr "" - -#: monitor_controller.php -msgid "Delete Dashboard" -msgstr "" - -#: monitor_controller.php -#, php-format -msgid "%s Alert for downed Devices" -msgstr "" - -#: monitor_controller.php -#, php-format -msgid "%s Alerts for downed Devices" -msgstr "" - -#: monitor_controller.php monitor_render.php poller_functions.php setup.php -msgid "Criticality" -msgstr "" - -#: monitor_controller.php -msgid "Size" -msgstr "" - -#: monitor_controller.php -msgid "Trim" -msgstr "" - -#: monitor_controller.php -msgid "All Trees" -msgstr "" - -#: monitor_controller.php monitor_render.php -msgid "Non-Tree Devices" -msgstr "" - -#: monitor_controller.php -msgid "All Sites" -msgstr "" - -#: monitor_controller.php monitor_render.php -msgid "Non-Site Devices" -msgstr "" - -#: monitor_controller.php -msgid "Sites" -msgstr "" - -#: monitor_controller.php -msgid "All Templates" -msgstr "" - -#: monitor_controller.php -msgid "Non-Templated Devices" -msgstr "" - -#: monitor_controller.php -msgid "Template" -msgstr "" - -#: monitor_controller.php -msgid " [ Filter Settings Saved ]" -msgstr "" - -#: monitor_controller.php -msgid "Cancel" -msgstr "" - -#: monitor_controller.php -#, php-format -msgid " [ Refresh Again in %d Seconds ]" -msgstr "" - -#: monitor_controller.php -#, php-format -msgid "Monitor Filter [ Last Refresh: %s ]" -msgstr "" - -#: monitor_controller.php -msgid "[ Showing only first 30 Devices ]" -msgstr "" - -#: monitor_controller.php -msgid "Search" -msgstr "" - -#: monitor_controller.php -msgid "Mute" -msgstr "" - -#: monitor_controller.php -msgid "Acknowledge" -msgstr "" - -#: monitor_controller.php -msgid "Un-Mute" -msgstr "" - -#: monitor_controller.php -msgid "Reset" -msgstr "" - -#: monitor_controller.php -#, php-format -msgid "Dashboard '%s' Removed." -msgstr "" - -#: monitor_controller.php -#, php-format -msgid "Dashboard '%s' is not owned by you." -msgstr "" - -#: monitor_controller.php -#, php-format -msgid "Dashboard '%s' has been Saved!" -msgstr "" - -#: monitor_controller.php -#, php-format -msgid "Dashboard '%s' could not be Saved!" -msgstr "" - -#: monitor_controller.php -msgid "Edit Device" -msgstr "" - -#: monitor_controller.php -msgid "View Graphs" -msgstr "" - -#: monitor_controller.php -msgid "View Thresholds/Alerts" -msgstr "" - -#: monitor_controller.php -msgid "View Device Syslog Alerts" -msgstr "" - -#: monitor_controller.php -msgid "View Device Syslog Entries" -msgstr "" - -#: monitor_controller.php -msgid "Device Status Information" -msgstr "" - -#: monitor_controller.php -msgid "Device:" -msgstr "" - -#: monitor_controller.php -msgid "Site:" -msgstr "" - -#: monitor_controller.php -msgid "Location:" -msgstr "" - -#: monitor_controller.php -msgid "Criticality:" -msgstr "" - -#: monitor_controller.php -msgid "Status:" -msgstr "" - -#: monitor_controller.php -msgid "Admin Note:" -msgstr "" - -#: monitor_controller.php -msgid "IP/Hostname:" -msgstr "" - -#: monitor_controller.php -msgid "Curr/Avg:" -msgstr "" - -#: monitor_controller.php monitor_render.php -#, php-format -msgid "%d ms" -msgstr "" - -#: monitor_controller.php -msgid "Warn/Alert:" -msgstr "" - -#: monitor_controller.php monitor_render.php -#, php-format -msgid "%0.2d ms" -msgstr "" - -#: monitor_controller.php -msgid "Last Fail:" -msgstr "" - -#: monitor_controller.php -msgid "Time In State:" -msgstr "" - -#: monitor_controller.php -msgid "Availability:" -msgstr "" - -#: monitor_controller.php -msgid "Agent Uptime:" -msgstr "" - -#: monitor_controller.php monitor_render.php -msgid "N/A" -msgstr "" - -#: monitor_controller.php -msgid "Sys Description:" -msgstr "" - -#: monitor_controller.php -msgid "Contact:" -msgstr "" - -#: monitor_controller.php -msgid "Notes:" -msgstr "" - -#: monitor_controller.php monitor_render.php -msgid "Never" -msgstr "" - -#: monitor_controller.php -msgid "Unspecified" -msgstr "" - -#: monitor_controller.php setup.php -msgid "None" -msgstr "" - -#: monitor_render.php -msgid "Non-Site Device" -msgstr "" - -#: monitor_render.php -msgid "Root Branch" -msgstr "" - -#: monitor_render.php -#, php-format -msgid "Tree: %s" -msgstr "" - -#: monitor_render.php -#, php-format -msgid "Branch: %s" -msgstr "" - -#: monitor_render.php -msgid "Just Up" -msgstr "" - -#: monitor_render.php poller_functions.php -msgid "Hostname" -msgstr "" - -#: monitor_render.php -msgid "Hostname of device" -msgstr "" - -#: monitor_render.php -msgid "ID" -msgstr "" - -#: monitor_render.php poller_functions.php -msgid "Description" -msgstr "" - -#: monitor_render.php -msgid "Length in Status" -msgstr "" - -#: monitor_render.php -msgid "Averages" -msgstr "" - -#: monitor_render.php -msgid "Warning" -msgstr "" - -#: monitor_render.php -msgid "Admin" -msgstr "" - -#: monitor_render.php -msgid "Monitor Text Column represents 'Admin'" -msgstr "" - -#: monitor_render.php -msgid "Notes" -msgstr "" - -#: monitor_render.php -msgid "Availability" -msgstr "" - -#: monitor_render.php -msgid "Last Fail" -msgstr "" - #: poller_functions.php msgid "Description: " msgstr "" diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..0957674 --- /dev/null +++ b/manifest.json @@ -0,0 +1,29 @@ +{ + "tombstones": [ + "db_functions.php", + "monitor_controller.php", + "monitor_render.php", + "sonar-project.properties", + "themes/" + ], + "expected": [ + "CHANGELOG.md", + "INFO", + "LICENSE", + "README.md", + "css/", + "docs/", + "images/", + "includes/", + "index.php", + "js/", + "locales/", + "manifest.json", + "monitor.php", + "poller_functions.php", + "poller_monitor.php", + "setup.php", + "sounds/" + ], + "whitelist": [] +} diff --git a/monitor.php b/monitor.php index e0bd462..956e96a 100644 --- a/monitor.php +++ b/monitor.php @@ -26,7 +26,7 @@ $guest_account = true; chdir('../../'); -include_once('./include/auth.php'); +require_once('./include/auth.php'); set_default_action(); @@ -126,9 +126,9 @@ if (!isset($_SESSION['monitor_muted_hosts'])) { $_SESSION['monitor_muted_hosts'] = []; } -include_once __DIR__ . '/db_functions.php'; -include_once __DIR__ . '/monitor_render.php'; -include_once __DIR__ . '/monitor_controller.php'; +include_once __DIR__ . '/includes/functions.php'; +include_once __DIR__ . '/includes/render.php'; +include_once __DIR__ . '/includes/controller.php'; validateRequestVars(); diff --git a/phpunit.xml b/phpunit.xml index 6cf36a5..e07992d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -28,7 +28,7 @@ setup.php - db_functions.php + includes/functions.php poller_functions.php diff --git a/setup.php b/setup.php index 0037827..a9611db 100644 --- a/setup.php +++ b/setup.php @@ -50,6 +50,10 @@ function plugin_monitor_csp_nonce(): string { * @return void */ function plugin_monitor_install() { + global $config; + + require_once($config['base_path'] . '/plugins/monitor/includes/database.php'); + // core plugin functionality api_plugin_register_hook('monitor', 'top_header_tabs', 'monitor_show_tab', 'setup.php'); api_plugin_register_hook('monitor', 'top_graph_header_tabs', 'monitor_show_tab', 'setup.php'); @@ -232,9 +236,11 @@ function monitor_device_table_bottom() { * @return void */ function plugin_monitor_uninstall() { - db_execute('DROP TABLE IF EXISTS plugin_monitor_notify_history'); - db_execute('DROP TABLE IF EXISTS plugin_monitor_reboot_history'); - db_execute('DROP TABLE IF EXISTS plugin_monitor_uptime'); + global $config; + + require_once($config['base_path'] . '/plugins/monitor/includes/database.php'); + + monitor_drop_tables(); } /** @@ -253,8 +259,8 @@ function plugin_monitor_page_head() { print get_md5_include_css('plugins/monitor/css/monitor.css') . PHP_EOL; - if (file_exists($config['base_path'] . '/plugins/monitor/themes/' . get_selected_theme() . '/monitor.css')) { - print get_md5_include_css('plugins/monitor/themes/' . get_selected_theme() . '/monitor.css') . PHP_EOL; + if (file_exists($config['base_path'] . '/plugins/monitor/css/' . get_selected_theme() . '.css')) { + print get_md5_include_css('plugins/monitor/css/' . get_selected_theme() . '.css') . PHP_EOL; } } @@ -274,7 +280,7 @@ function plugin_monitor_check_config() { // Here we will check to ensure everything is configured monitor_check_upgrade(); - include_once($config['library_path'] . '/database.php'); + require_once($config['library_path'] . '/database.php'); $r = read_config_option('monitor_refresh'); if ($r == '' || $r < 1 || $r > 300) { @@ -315,20 +321,22 @@ function monitor_check_upgrade() { return; } + global $config; + + require_once($config['base_path'] . '/plugins/monitor/includes/database.php'); + $info = plugin_monitor_version(); $current = $info['version']; $old = db_fetch_cell('SELECT version FROM plugin_config WHERE directory = "monitor"'); if ($current != $old) { monitor_setup_table(); + monitor_upgrade_tables(); api_plugin_register_hook('monitor', 'page_head', 'plugin_monitor_page_head', 'setup.php', 1); db_execute('ALTER TABLE host MODIFY COLUMN monitor char(3) DEFAULT "on"'); - db_execute('ALTER TABLE plugin_monitor_uptime - MODIFY COLUMN uptime BIGINT unsigned NOT NULL default "0"'); - api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_icon', 'type' => 'varchar(30)', 'NULL' => false, 'default' => '']); if (function_exists('api_plugin_upgrade_register')) { @@ -346,6 +354,9 @@ function monitor_check_upgrade() { ] ); } + + // Remove files tombstoned in manifest.json plus the dev-only tests/ tree. + monitor_prune_files(); } } @@ -1373,90 +1384,6 @@ function monitor_draw_navigation_text($nav) { return $nav; } -/** - * Creates (if not already present) all of this plugin's database - * tables (notify/reboot history, uptime, dashboards), ensures the host - * table uses DYNAMIC row format (needed for its added TEXT column), - * and adds this plugin's monitoring columns to the host table. Called - * from plugin_monitor_install() and monitor_check_upgrade(). - * - * @return void - */ -function monitor_setup_table() { - if (!db_table_exists('plugin_monitor_notify_history')) { - db_execute("CREATE TABLE IF NOT EXISTS plugin_monitor_notify_history ( - id int(10) unsigned NOT NULL AUTO_INCREMENT, - host_id int(10) unsigned DEFAULT NULL, - notify_type tinyint(3) unsigned DEFAULT NULL, - ping_time double DEFAULT NULL, - ping_threshold int(10) unsigned DEFAULT NULL, - notification_time timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', - notes varchar(255) DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY unique_key (host_id,notify_type,notification_time)) - ENGINE=InnoDB - COMMENT='Stores Notification Event History'"); - } - - if (!db_table_exists('plugin_monitor_reboot_history')) { - db_execute("CREATE TABLE IF NOT EXISTS plugin_monitor_reboot_history ( - id int(10) unsigned NOT NULL AUTO_INCREMENT, - host_id int(10) unsigned DEFAULT NULL, - reboot_time timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', - log_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY host_id (host_id), - KEY log_time (log_time), - KEY reboot_time (reboot_time)) - ENGINE=InnoDB - COMMENT='Keeps Track of Device Reboot Times'"); - } - - if (!db_table_exists('plugin_monitor_uptime')) { - db_execute("CREATE TABLE IF NOT EXISTS plugin_monitor_uptime ( - host_id int(10) unsigned DEFAULT '0', - uptime bigint(20) unsigned DEFAULT '0', - PRIMARY KEY (host_id), - KEY uptime (uptime)) - ENGINE=InnoDB - COMMENT='Keeps Track of the Devices last uptime to track agent restarts and reboots'"); - } - - if (!db_table_exists('plugin_monitor_dashboards')) { - db_execute("CREATE TABLE IF NOT EXISTS plugin_monitor_dashboards ( - id int(10) unsigned auto_increment, - user_id int(10) unsigned DEFAULT '0', - name varchar(128) DEFAULT '', - url varchar(1024) DEFAULT '', - PRIMARY KEY (id), - KEY user_id (user_id)) - ENGINE=InnoDB - COMMENT='Stores predefined dashboard information for a user or users'"); - } - - if (db_table_exists('host')) { - $row_format = db_fetch_cell("SELECT ROW_FORMAT - FROM information_schema.tables - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'host'"); - - if (strtoupper((string) $row_format) !== 'DYNAMIC') { - db_execute('ALTER TABLE host ROW_FORMAT=DYNAMIC'); - } - } - - db_execute('SET SESSION innodb_strict_mode=0'); - - api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor', 'type' => 'char(3)', 'NULL' => true, 'default' => 'on']); - api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_text', 'type' => 'text', 'NULL' => false]); - api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_criticality', 'type' => 'tinyint', 'unsigned' => true, 'NULL' => false, 'default' => '0']); - api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_warn', 'type' => 'double', 'NULL' => false, 'default' => '0']); - api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_alert', 'type' => 'double', 'NULL' => false, 'default' => '0']); - api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_icon', 'type' => 'varchar(30)', 'NULL' => false, 'default' => '']); - - db_execute('SET SESSION innodb_strict_mode=1'); -} - /** * Poller_bottom hook: on the main poller (poller_id 1), launches this * plugin's poller_monitor.php script as a background process at the @@ -1473,7 +1400,7 @@ function monitor_poller_bottom() { global $config; if ($config['poller_id'] == 1) { - include_once($config['library_path'] . '/poller.php'); + require_once($config['library_path'] . '/poller.php'); $command_string = trim(read_config_option('path_php_binary')); @@ -1486,3 +1413,173 @@ function monitor_poller_bottom() { exec_background($command_string, $extra_args); } } + +/** + * Removes files and directories that a previous version of this plugin + * shipped but that have since moved or been deleted, using the tombstone + * and whitelist lists in manifest.json. Whitelisted (user-data) paths and + * any VCS metadata (.git*) are never touched; the dev-only tests/ tree is + * removed. Any path that resolves outside the plugin directory (a tampered + * manifest.json) is refused, and any file/directory that cannot be removed + * (e.g. read-only) is reported to the Cacti log. Any top-level entry that is + * neither expected nor a tombstone nor whitelisted is logged to the Cacti + * log and left in place. Called on a plugin version change. + * + * @return void + * + * @global array $config Cacti global configuration array; used to resolve + * the plugin directory. + */ +function monitor_prune_files(): void { + global $config; + + $plugin_dir = $config['base_path'] . '/plugins/monitor'; + $manifest_path = $plugin_dir . '/manifest.json'; + + if (!is_readable($manifest_path)) { + return; + } + + $manifest = json_decode((string) file_get_contents($manifest_path), true); + + if (!is_array($manifest)) { + cacti_log('WARNING: monitor manifest.json could not be parsed; skipping file prune', false, 'MONITOR'); + + return; + } + + $tombstones = isset($manifest['tombstones']) && is_array($manifest['tombstones']) ? $manifest['tombstones'] : []; + $expected = isset($manifest['expected']) && is_array($manifest['expected']) ? $manifest['expected'] : []; + $whitelist = isset($manifest['whitelist']) && is_array($manifest['whitelist']) ? $manifest['whitelist'] : []; + + $protected = function (string $rel) use ($whitelist): bool { + if (strncmp($rel, '.git', 4) === 0 || strncmp($rel, '.md', 3) === 0) { + return true; + } + + foreach ($whitelist as $entry) { + $entry = trim((string) $entry, '/'); + + if ($entry !== '' && ($rel === $entry + || strncmp($rel, $entry . '/', strlen($entry) + 1) === 0 + || strncmp($entry, $rel . '/', strlen($rel) + 1) === 0)) { + return true; + } + } + + return false; + }; + + // Security: resolve the plugin directory so a tampered manifest.json + // cannot steer the prune outside of it. + $plugin_real = realpath($plugin_dir); + + // Remove tombstoned (moved/deleted) paths plus the dev-only tests/ + // tree and the phpunit.xml test configuration. + $remove = $tombstones; + $remove[] = 'tests/'; + $remove[] = 'phpunit.xml'; + + foreach ($remove as $rel) { + $rel = trim((string) $rel, '/'); + + if ($rel === '' || $protected($rel)) { + continue; + } + + // A tombstone must never contain '.'/'..' segments; a tampered manifest + // could use them to escape the plugin directory or target its root. + $segments = explode('/', $rel); + + if (in_array('.', $segments, true) || in_array('..', $segments, true)) { + cacti_log(sprintf('WARNING: monitor prune refused to remove %s: path contains a traversal segment (tampered manifest.json?)', $rel), false, 'MONITOR'); + + continue; + } + + $path = $plugin_dir . '/' . $rel; + + if (!is_link($path) && !file_exists($path)) { + continue; + } + + // Refuse any path that, after resolving symlinks and ../ segments, + // escapes the plugin directory (protects user data from a tampered + // manifest.json). + $anchor = is_link($path) ? dirname($path) : $path; + $real = realpath($anchor); + + if ($real === false || ($real !== $plugin_real && strncmp($real, $plugin_real . DIRECTORY_SEPARATOR, strlen((string) $plugin_real) + 1) !== 0)) { + cacti_log(sprintf('WARNING: monitor prune refused to remove %s: path resolves outside the plugin directory (tampered manifest.json?)', $rel), false, 'MONITOR'); + + continue; + } + + if (is_dir($path) && !is_link($path)) { + $removed = monitor_rmtree($path); + } else { + $removed = @unlink($path); + } + + if (!$removed) { + cacti_log(sprintf('WARNING: monitor upgrade could not remove %s (check file/directory permissions)', $rel), false, 'MONITOR'); + } + } + + // Surface any top-level entry the manifest does not account for. + $known = []; + + foreach (array_merge($expected, $tombstones) as $entry) { + $top = explode('/', trim((string) $entry, '/'))[0]; + + if ($top !== '') { + $known[$top] = true; + } + } + + $entries = scandir($plugin_dir); + + foreach (($entries !== false ? $entries : []) as $entry) { + if ($entry === '.' || $entry === '..' || $entry === 'tests' || $entry === 'phpunit.xml' || $protected($entry) || isset($known[$entry])) { + continue; + } + + cacti_log(sprintf('WARNING: monitor upgrade found a file/directory not described in manifest.json: %s (left in place)', $entry), false, 'MONITOR'); + } +} + +/** + * Recursively deletes a directory and its contents. Symlinks are removed + * without being followed. Helper for monitor_prune_files(). + * + * @param string $dir Absolute path to the directory to remove. + * + * @return bool True if the directory and everything under it was removed; + * false if any entry could not be deleted. + */ +function monitor_rmtree(string $dir): bool { + $entries = scandir($dir); + $ok = true; + + foreach (($entries !== false ? $entries : []) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + + $path = $dir . '/' . $entry; + + if (is_dir($path) && !is_link($path)) { + if (!monitor_rmtree($path)) { + $ok = false; + } + } elseif (!@unlink($path)) { + $ok = false; + } + } + + if (!@rmdir($dir)) { + $ok = false; + } + + return $ok; +} diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 5ed8db9..0000000 --- a/sonar-project.properties +++ /dev/null @@ -1,10 +0,0 @@ -sonar.projectKey=plugin_monitor -sonar.sources=. -sonar.exclusions=vendor/** - -# php:S4833 - "Use namespaces instead of include/require" -# This is a Cacti plugin with a required procedural architecture. -# Namespaces cannot be used; include_once is the correct and only mechanism. -sonar.issue.ignore.multicriteria=e1 -sonar.issue.ignore.multicriteria.e1.ruleKey=php:S4833 -sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.php diff --git a/tests/Integration/MonitorRequestOutputWiringTest.php b/tests/Integration/MonitorRequestOutputWiringTest.php index eacc2e8..bf19036 100644 --- a/tests/Integration/MonitorRequestOutputWiringTest.php +++ b/tests/Integration/MonitorRequestOutputWiringTest.php @@ -8,14 +8,14 @@ */ dataset('output_hardening', [ - 'controller downhosts' => ['monitor_controller.php', "html_escape(get_request_var('downhosts'))"], - 'controller mute' => ['monitor_controller.php', "html_escape(get_request_var('mute'))"], - 'controller tree' => ['monitor_controller.php', "html_escape(get_request_var('tree'))"], - 'controller site' => ['monitor_controller.php', "html_escape(get_request_var('site'))"], - 'controller template' => ['monitor_controller.php', "html_escape(get_request_var('template'))"], - 'controller size' => ['monitor_controller.php', "html_escape(get_request_var('size'))"], - 'controller trim' => ['monitor_controller.php', "html_escape(get_request_var('trim'))"], - 'render rfilter' => ['monitor_render.php', "rawurlencode(get_request_var('rfilter'))"], + 'controller downhosts' => ['includes/controller.php', "html_escape(get_request_var('downhosts'))"], + 'controller mute' => ['includes/controller.php', "html_escape(get_request_var('mute'))"], + 'controller tree' => ['includes/controller.php', "html_escape(get_request_var('tree'))"], + 'controller site' => ['includes/controller.php', "html_escape(get_request_var('site'))"], + 'controller template' => ['includes/controller.php', "html_escape(get_request_var('template'))"], + 'controller size' => ['includes/controller.php', "html_escape(get_request_var('size'))"], + 'controller trim' => ['includes/controller.php', "html_escape(get_request_var('trim'))"], + 'render rfilter' => ['includes/render.php', "rawurlencode(get_request_var('rfilter'))"], ]); it('keeps expected output hardening in place', function (string $file, string $pattern) { diff --git a/tests/Integration/OutputEscapingTest.php b/tests/Integration/OutputEscapingTest.php index 81932e8..2cb6818 100644 --- a/tests/Integration/OutputEscapingTest.php +++ b/tests/Integration/OutputEscapingTest.php @@ -16,14 +16,14 @@ $root = dirname(__DIR__, 2); dataset('escaped_outputs', [ - ['monitor_controller.php', "html_escape(get_request_var('downhosts'))"], - ['monitor_controller.php', "html_escape(get_request_var('mute'))"], - ['monitor_controller.php', "html_escape(get_request_var('tree'))"], - ['monitor_controller.php', "html_escape(get_request_var('site'))"], - ['monitor_controller.php', "html_escape(get_request_var('template'))"], - ['monitor_controller.php', "html_escape(get_request_var('size'))"], - ['monitor_controller.php', "html_escape(get_request_var('trim'))"], - ['monitor_render.php', "rawurlencode(get_request_var('rfilter'))"], + ['includes/controller.php', "html_escape(get_request_var('downhosts'))"], + ['includes/controller.php', "html_escape(get_request_var('mute'))"], + ['includes/controller.php', "html_escape(get_request_var('tree'))"], + ['includes/controller.php', "html_escape(get_request_var('site'))"], + ['includes/controller.php', "html_escape(get_request_var('template'))"], + ['includes/controller.php', "html_escape(get_request_var('size'))"], + ['includes/controller.php', "html_escape(get_request_var('trim'))"], + ['includes/render.php', "rawurlencode(get_request_var('rfilter'))"], ]); it('escapes request values before printing them', function (string $file, string $pattern) use ($root) { @@ -31,13 +31,13 @@ it('escapes request values before printing them', function (string $file, string })->with('escaped_outputs'); dataset('raw_reuse', [ - ['monitor_controller.php', "get_request_var('downhosts') . '\">'"], - ['monitor_controller.php', "get_request_var('site') . '\">'"], - ['monitor_controller.php', "get_request_var('template') . '\">'"], - ['monitor_controller.php', "get_request_var('size') . '\">'"], - ['monitor_controller.php', "get_request_var('trim') . '\">'"], - ['monitor_render.php', "monitor.php?rfilter=' . get_request_var('rfilter')"], + ['includes/controller.php', "get_request_var('downhosts') . '\">'"], + ['includes/controller.php', "get_request_var('site') . '\">'"], + ['includes/controller.php', "get_request_var('template') . '\">'"], + ['includes/controller.php', "get_request_var('size') . '\">'"], + ['includes/controller.php', "get_request_var('trim') . '\">'"], + ['includes/render.php', "monitor.php?rfilter=' . get_request_var('rfilter')"], ]); it('never concatenates a raw request value into markup', function (string $file, string $pattern) use ($root) { diff --git a/tests/Unit/DbFunctionsTest.php b/tests/Unit/DbFunctionsTest.php index 3f69cd0..f60b799 100644 --- a/tests/Unit/DbFunctionsTest.php +++ b/tests/Unit/DbFunctionsTest.php @@ -3,13 +3,13 @@ +-------------------------------------------------------------------------+ | Copyright (C) 2004-2026 The Cacti Group | +-------------------------------------------------------------------------+ - | Exercises db_functions.php through the Cacti stubs in bootstrap-unit.php.| + | Exercises includes/functions.php through the Cacti stubs in bootstrap-unit.php.| | These load and run plugin code, unlike the source-matching rules in | | tests/Integration. | +-------------------------------------------------------------------------+ */ -require_once dirname(__DIR__, 2) . '/db_functions.php'; +require_once dirname(__DIR__, 2) . '/includes/functions.php'; it('selects the breached threshold clause for status 2', function () { test_set_request(['status' => '2']); diff --git a/tests/Unit/MonitorLifecycleTest.php b/tests/Unit/MonitorLifecycleTest.php index dcf368c..73f1f53 100644 --- a/tests/Unit/MonitorLifecycleTest.php +++ b/tests/Unit/MonitorLifecycleTest.php @@ -18,6 +18,10 @@ beforeAll(function () { require_once __DIR__ . '/../../setup.php'; + // Define the schema functions (monitor_setup_table, monitor_upgrade_tables) + // from the real checkout so monitor_check_upgrade() can run while base_path + // is sandboxed for the upgrade-time prune. + require_once dirname(__DIR__, 2) . '/includes/database.php'; $stubLibraryPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'monitor-test-lib-stub'; @@ -36,6 +40,22 @@ beforeEach(function () { $GLOBALS['__test_db_calls'] = array(); $GLOBALS['__test_config_options'] = array(); $_SERVER['PHP_SELF'] = '/monitor.php'; + + // Sandbox base_path (with a minimal INFO + empty includes/database.php stub) + // so any upgrade-path test runs monitor_prune_files() against a + // throwaway tree, never the real checkout. + $GLOBALS['__monitor_base_restore'] = $GLOBALS['config']['base_path']; + $base = sys_get_temp_dir() . '/monitor-test-' . uniqid(); + mkdir($base . '/plugins/monitor/includes', 0777, true); + file_put_contents($base . '/plugins/monitor/INFO', "[info]\nversion = 9.9.9\nname = monitor\nlongname = Monitor\nauthor = x\nhomepage = x\n"); + file_put_contents($base . '/plugins/monitor/includes/database.php', "toContain('ALTER TABLE host'); }); + +it('launches the poller with a php fallback when no binary is configured', function () { + $GLOBALS['config']['poller_id'] = 1; + $GLOBALS['__test_exec_calls'] = array(); + + monitor_poller_bottom(); + + expect($GLOBALS['__test_exec_calls'])->not->toBeEmpty(); + expect($GLOBALS['__test_exec_calls'][0]['command'])->toBe('php'); +}); diff --git a/tests/Unit/MonitorPageHeadTest.php b/tests/Unit/MonitorPageHeadTest.php new file mode 100644 index 0000000..f35aa3c --- /dev/null +++ b/tests/Unit/MonitorPageHeadTest.php @@ -0,0 +1,58 @@ +.css. + */ + +beforeAll(function () { + require_once __DIR__ . '/../../setup.php'; +}); + +afterEach(function () { + unset($GLOBALS['__test_selected_theme']); +}); + +it('links the per-theme stylesheet from css/ when it exists', function () { + $restore = $GLOBALS['config']['base_path']; + $base = sys_get_temp_dir() . '/monitor-ph-' . uniqid(); + mkdir($base . '/plugins/monitor/css', 0777, true); + file_put_contents($base . '/plugins/monitor/css/modern.css', ''); + $GLOBALS['__test_selected_theme'] = 'modern'; + $GLOBALS['config']['base_path'] = $base; + + ob_start(); + + try { + plugin_monitor_page_head(); + } finally { + $output = ob_get_clean(); + $GLOBALS['config']['base_path'] = $restore; + } + + expect($output)->toContain('plugins/monitor/css/modern.css'); +}); + +it('emits only the base stylesheet when no per-theme override exists', function () { + $restore = $GLOBALS['config']['base_path']; + $base = sys_get_temp_dir() . '/monitor-ph-' . uniqid(); + mkdir($base . '/plugins/monitor/css', 0777, true); + $GLOBALS['__test_selected_theme'] = 'no-such-theme'; + $GLOBALS['config']['base_path'] = $base; + + ob_start(); + + try { + plugin_monitor_page_head(); + } finally { + $output = ob_get_clean(); + $GLOBALS['config']['base_path'] = $restore; + } + + expect($output)->toContain('plugins/monitor/css/monitor.css'); +}); diff --git a/tests/Unit/MonitorSetupTableAndPollerTest.php b/tests/Unit/MonitorSetupTableAndPollerTest.php index d4569d4..9852b3a 100644 --- a/tests/Unit/MonitorSetupTableAndPollerTest.php +++ b/tests/Unit/MonitorSetupTableAndPollerTest.php @@ -17,6 +17,7 @@ beforeAll(function () { require_once __DIR__ . '/../../setup.php'; + require_once __DIR__ . '/../../includes/database.php'; $stubLibraryPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'monitor-test-lib-stub'; @@ -47,6 +48,18 @@ it('creates every table the plugin owns', function () { } }); +it('does not keep plugin ownership of the user-owned dashboards table', function () { + monitor_setup_table(); + + $disowned = array_filter($GLOBALS['__test_db_calls'], function ($call) { + return $call['fn'] === 'db_execute' + && stripos($call['sql'], 'DELETE FROM plugin_db_changes') !== false + && stripos($call['sql'], 'plugin_monitor_dashboards') !== false; + }); + + expect($disowned)->not->toBeEmpty(); +}); + it('launches poller_monitor.php in the background on the primary poller', function () { $GLOBALS['__test_config_options']['path_php_binary'] = '/usr/bin/php'; diff --git a/tests/Unit/PruneFilesTest.php b/tests/Unit/PruneFilesTest.php new file mode 100644 index 0000000..ca30cdd --- /dev/null +++ b/tests/Unit/PruneFilesTest.php @@ -0,0 +1,255 @@ + ['include/', 'oldfile.php', 'userdata/', 'gone.png'], + 'expected' => ['INFO', 'setup.php', 'includes/', 'manifest.json'], + 'whitelist' => ['userdata/'], + ]; + + $base = monitor_prune_fixture($manifest); + $plugin = $base . '/plugins/monitor'; + $restore = $GLOBALS['config']['base_path']; + + $GLOBALS['config']['base_path'] = $base; + + try { + monitor_prune_files(); + } finally { + $GLOBALS['config']['base_path'] = $restore; + } + + // Tombstone and the dev-only tests/ tree are gone. + expect(is_dir($plugin . '/include'))->toBeFalse(); + expect(is_dir($plugin . '/tests'))->toBeFalse(); + expect(is_file($plugin . '/oldfile.php'))->toBeFalse(); + expect(is_file($plugin . '/phpunit.xml'))->toBeFalse(); + + // Whitelisted user data, VCS metadata, and expected files are untouched. + // (userdata/ is even listed as a tombstone, but the whitelist wins.) + expect(is_file($plugin . '/userdata/keep.dat'))->toBeTrue(); + expect(is_dir($plugin . '/.git'))->toBeTrue(); + expect(is_file($plugin . '/INFO'))->toBeTrue(); + expect(is_dir($plugin . '/includes'))->toBeTrue(); + expect(is_file($plugin . '/.mdlrc'))->toBeTrue(); + expect(is_file($plugin . '/.md_style.rb'))->toBeTrue(); + + // An unexpected, non-whitelisted stray is left in place but logged. + expect(is_file($plugin . '/stray.php'))->toBeTrue(); + + $logged = implode("\n", $GLOBALS['__test_cacti_log']); + expect($logged)->toContain('stray.php'); + expect($logged)->not->toContain('userdata'); + expect($logged)->not->toContain('.git'); + expect($logged)->not->toContain('.mdlrc'); + expect($logged)->not->toContain('.md_style.rb'); +}); + +it('is a safe no-op when the manifest is missing', function () { + $base = sys_get_temp_dir() . '/monitor-prune-missing-' . uniqid(); + mkdir($base . '/plugins/monitor', 0777, true); + $restore = $GLOBALS['config']['base_path']; + + $GLOBALS['config']['base_path'] = $base; + + try { + monitor_prune_files(); + } finally { + $GLOBALS['config']['base_path'] = $restore; + } + + expect($GLOBALS['__test_cacti_log'])->toBe([]); +}); + +it('logs and skips pruning when the manifest is malformed', function () { + $base = sys_get_temp_dir() . '/monitor-prune-bad-' . uniqid(); + $plugin = $base . '/plugins/monitor'; + mkdir($plugin, 0777, true); + file_put_contents($plugin . '/manifest.json', 'not json'); + mkdir($plugin . '/tests', 0777, true); + $restore = $GLOBALS['config']['base_path']; + + $GLOBALS['config']['base_path'] = $base; + + try { + monitor_prune_files(); + } finally { + $GLOBALS['config']['base_path'] = $restore; + } + + // A malformed manifest must not delete anything. + expect(is_dir($plugin . '/tests'))->toBeTrue(); + expect(implode("\n", $GLOBALS['__test_cacti_log']))->toContain('could not be parsed'); +}); + +it('refuses to remove a tombstone that resolves outside the plugin directory', function () { + $manifest = [ + 'tombstones' => ['../escapee.txt'], + 'expected' => ['manifest.json'], + 'whitelist' => [], + ]; + + $base = monitor_prune_fixture($manifest); + $plugin = $base . '/plugins/monitor'; + $outside = $base . '/plugins/escapee.txt'; + file_put_contents($outside, 'precious user data'); + $restore = $GLOBALS['config']['base_path']; + + $GLOBALS['config']['base_path'] = $base; + + try { + monitor_prune_files(); + } finally { + $GLOBALS['config']['base_path'] = $restore; + } + + // The out-of-tree file is untouched and the refusal is logged. + expect(is_file($outside))->toBeTrue(); + expect(implode("\n", $GLOBALS['__test_cacti_log']))->toContain('a traversal segment'); +}); + +it('warns when a tombstoned path cannot be removed', function () { + $manifest = [ + 'tombstones' => ['locked/'], + 'expected' => ['manifest.json'], + 'whitelist' => [], + ]; + + $base = monitor_prune_fixture($manifest); + $plugin = $base . '/plugins/monitor'; + mkdir($plugin . '/locked/sub', 0777, true); + file_put_contents($plugin . '/locked/sub/data', 'x'); + chmod($plugin . '/locked/sub', 0500); // read-only dir: its child cannot be unlinked + $restore = $GLOBALS['config']['base_path']; + + $GLOBALS['config']['base_path'] = $base; + + set_error_handler(static fn () => true); // swallow the expected unlink warning + + try { + monitor_prune_files(); + } finally { + restore_error_handler(); + $GLOBALS['config']['base_path'] = $restore; + @chmod($plugin . '/locked/sub', 0700); + } + + expect(implode("\n", $GLOBALS['__test_cacti_log']))->toContain('could not remove'); +})->skip(function () { + return function_exists('posix_getuid') && posix_getuid() === 0; +}, 'permission checks are bypassed for the root user'); + +it('refuses a tombstone that escapes through a symlinked directory', function () { + $manifest = [ + 'tombstones' => ['escdir/secret.txt'], + 'expected' => ['manifest.json'], + 'whitelist' => [], + ]; + + $base = monitor_prune_fixture($manifest); + $plugin = $base . '/plugins/monitor'; + $outside = $base . '/outside'; + mkdir($outside, 0777, true); + file_put_contents($outside . '/secret.txt', 'precious user data'); + @symlink($outside, $plugin . '/escdir'); + $restore = $GLOBALS['config']['base_path']; + + $GLOBALS['config']['base_path'] = $base; + + try { + monitor_prune_files(); + } finally { + $GLOBALS['config']['base_path'] = $restore; + } + + // The out-of-tree file reached through the symlink is untouched and logged. + expect(is_file($outside . '/secret.txt'))->toBeTrue(); + expect(implode("\n", $GLOBALS['__test_cacti_log']))->toContain('outside the plugin directory'); +})->skip(function () { + $probe = sys_get_temp_dir() . '/.prune-symlink-probe-' . uniqid(); + $ok = @symlink(__FILE__, $probe); + @unlink($probe); + + return $ok === false; +}, 'symlinks are not supported on this filesystem'); + +it('protects a whitelisted file from a tombstone on its parent directory', function () { + $manifest = [ + 'tombstones' => ['userdata/'], + 'expected' => ['manifest.json'], + 'whitelist' => ['userdata/keep.dat'], + ]; + + $base = monitor_prune_fixture($manifest); + $plugin = $base . '/plugins/monitor'; + $restore = $GLOBALS['config']['base_path']; + + $GLOBALS['config']['base_path'] = $base; + + try { + monitor_prune_files(); + } finally { + $GLOBALS['config']['base_path'] = $restore; + } + + // A whitelisted file shields its parent directory from a tombstone. + expect(is_file($plugin . '/userdata/keep.dat'))->toBeTrue(); +}); diff --git a/tests/bin/patch-coverage.php b/tests/bin/patch-coverage.php index e65b53d..059d044 100644 --- a/tests/bin/patch-coverage.php +++ b/tests/bin/patch-coverage.php @@ -160,6 +160,9 @@ foreach ($clover->xpath('//file') as $file) { * Empty by default; add entries per repository as the need arises. */ $unmeasured_allowlist = [ + 'includes/database.php', // install/upgrade/drop schema bootstrap; require_once's the live Cacti library, only runs during a real install/upgrade + 'monitor.php', // web UI entry point (chdir + require auth.php); not loadable in the isolated unit process + 'includes/controller.php', // request-controller library included by monitor.php after auth.php; not loadable in the isolated unit process ]; $unmeasured = array_values(array_diff(array_keys($changed), array_keys($measured))); $unexpected_unmeasured = array_values(array_diff($unmeasured, $unmeasured_allowlist)); diff --git a/tests/bin/validate-manifest.php b/tests/bin/validate-manifest.php new file mode 100644 index 0000000..c54fab5 --- /dev/null +++ b/tests/bin/validate-manifest.php @@ -0,0 +1,73 @@ +"; - } -} - -if (!function_exists('get_md5_include_css')) { - function get_md5_include_css($path) { - return ""; - } -} - +if (!function_exists('get_md5_include_js')) { + function get_md5_include_js($path, $async = false) { + return ""; + } +} + +if (!function_exists('get_md5_include_css')) { + function get_md5_include_css($path) { + return ""; + } +} + +if (!function_exists('get_selected_theme')) { + function get_selected_theme() { + return $GLOBALS['__test_selected_theme'] ?? 'modern'; + } +} + if (!function_exists('monitor_test_mock_db')) { function monitor_test_mock_db($fn, $match, $result) { $GLOBALS['__test_db_fixtures'][] = array('fn' => $fn, 'match' => $match, 'result' => $result); @@ -209,6 +215,16 @@ if (!function_exists('api_plugin_db_add_column')) { if (!function_exists('api_plugin_db_table_create')) { function api_plugin_db_table_create($plugin, $table, $data) { + $GLOBALS['__test_db_calls'][] = array('fn' => 'api_plugin_db_table_create', 'plugin' => $plugin, 'table' => $table, 'data' => $data, 'sql' => $table); + + return true; + } +} + +if (!function_exists('db_update_table')) { + function db_update_table($table, $data) { + $GLOBALS['__test_db_calls'][] = array('fn' => 'db_update_table', 'table' => $table, 'data' => $data, 'sql' => $table); + return true; } } @@ -294,6 +310,8 @@ if (!function_exists('__esc')) { if (!function_exists('cacti_log')) { function cacti_log($message, $also_print = false, $log_type = '', $level = 0) { + $args = func_get_args(); + $GLOBALS["__test_cacti_log"][] = isset($args[0]) ? (string) $args[0] : ""; } } @@ -349,7 +367,7 @@ function test_set_request(array $vars): void { $GLOBALS['__test_request'] = $vars; } -// validateRequestVars() is not stubbed here: monitor_controller.php declares +// validateRequestVars() is not stubbed here: includes/controller.php declares // it unconditionally, so a global stub would fatal with a redeclare error // once a test loads that file via loadPluginSource()/monitor_test_load(). diff --git a/themes/dark/index.php b/themes/dark/index.php deleted file mode 100644 index 362957c..0000000 --- a/themes/dark/index.php +++ /dev/null @@ -1,26 +0,0 @@ -