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/<theme>/monitor.css becomes css/<theme>.css so users can drop
their own css/<theme>.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_<name>_ prefix for plugin
lifecycle / hook-registration functions; all other functions use the plain
<name>_ prefix. Rename the internal upgrade helpers accordingly:

  plugin_<name>_prune_files() -> <name>_prune_files()
  plugin_<name>_rmtree()      -> <name>_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 <copilot@users.noreply.github.com>
This commit is contained in:
TheWitnessandCopilot authored and GitHub committed 2026-10-01 01:39:53 -05:00
1 parent 314169ae76
commit 66358e0998
39 files changed
+1393 -792

No files matched your search

+11 -9
View File
@@ -20,7 +20,7 @@ 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
@@ -28,12 +28,10 @@ When generating code for this repository:
monitor/ # Repository root (install to plugins/monitor/ in Cacti)
├── js/ # Dashboard client-side logic
├── sounds/ # Alert sound assets
├── themes/ # CSS theme overlays (monitor.css)
├── css/ # CSS theme overlays (monitor.css)
├── tests/ # Test suite
├── db_functions.php # SQL filter/join helpers and status/device query utilities
├── includes/ # functions.php (DB/query helpers), controller.php (action flow), render.php (rendering), database.php (schema)
├── 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)
@@ -45,7 +43,7 @@ monitor/ # Repository root (install to plugins/monitor/ in Ca
### 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.
+3
View File
@@ -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
+8
View File
@@ -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)
+2 -2
View File
@@ -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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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(*)
+204
View File
@@ -0,0 +1,204 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/**
* The plugin_monitor_notify_history table definition (one row per
* notification event). Shared by the create and upgrade paths so both
* stay in sync from a single source.
*
* @return array<string,mixed> 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<string,mixed> 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<string,mixed> 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<string,mixed> 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');
}
File renamed without changes.
File renamed without changes.
+462 -462
View File
@@ -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 <developers@cacti.net>>\n"
"Language-Team: Cacti Developers <developers@cacti.net>\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 "</div><div class=\"center monitorFooterTextBold\">Remember to first select eligible Devices to be Monitored from the Devices page!</div></div></div>"
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 <i style=\"padding:0px !important;margin:0px;\" id=\"timer\">%d</i> 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 "</div><div class=\"center monitorFooterTextBold\">Remember to first select eligible Devices to be Monitored from the Devices page!</div></div></div>"
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 <i style=\"padding:0px !important;margin:0px;\" id=\"timer\">%d</i> 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 ""
+29
View File
@@ -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": []
}
+4 -4
View File
@@ -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();
+1 -1
View File
@@ -28,7 +28,7 @@
<source>
<include>
<file>setup.php</file>
<file>db_functions.php</file>
<file>includes/functions.php</file>
<file>poller_functions.php</file>
</include>
</source>
+191 -94
View File
@@ -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;
}
-10
View File
@@ -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
@@ -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) {
+15 -15
View File
@@ -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') . '\"><input id=\"mute\" type=\"hidden\" value=\"' . get_request_var('mute')"],
['monitor_controller.php', "get_request_var('tree') . '\"></td>'"],
['monitor_controller.php', "get_request_var('site') . '\"></td>'"],
['monitor_controller.php', "get_request_var('template') . '\"></td>'"],
['monitor_controller.php', "get_request_var('size') . '\"></td>'"],
['monitor_controller.php', "get_request_var('trim') . '\"></td>'"],
['monitor_render.php', "monitor.php?rfilter=' . get_request_var('rfilter')"],
['includes/controller.php', "get_request_var('downhosts') . '\"><input id=\"mute\" type=\"hidden\" value=\"' . get_request_var('mute')"],
['includes/controller.php', "get_request_var('tree') . '\"></td>'"],
['includes/controller.php', "get_request_var('site') . '\"></td>'"],
['includes/controller.php', "get_request_var('template') . '\"></td>'"],
['includes/controller.php', "get_request_var('size') . '\"></td>'"],
['includes/controller.php', "get_request_var('trim') . '\"></td>'"],
['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) {
+2 -2
View File
@@ -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']);
+30
View File
@@ -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', "<?php\n");
$GLOBALS['config']['base_path'] = $base;
});
afterEach(function () {
if (isset($GLOBALS['__monitor_base_restore'])) {
$GLOBALS['config']['base_path'] = $GLOBALS['__monitor_base_restore'];
}
});
it('drops every table it owns on uninstall', function () {
@@ -85,3 +105,13 @@ it('updates the stored plugin_config version when it drifts', function () {
expect($sql)->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');
});
+58
View File
@@ -0,0 +1,58 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* Unit coverage for plugin_monitor_page_head()'s stylesheet selection in
* setup.php, including the per-theme override loaded from css/<theme>.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');
});
@@ -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';
+255
View File
@@ -0,0 +1,255 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDTool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/*
* Unit coverage for monitor_prune_files(): tombstone/tests removal,
* whitelist and .git protection, and logging of unaccounted-for entries.
*/
require_once __DIR__ . '/../../setup.php';
function monitor_prune_fixture(array $manifest): string {
$base = sys_get_temp_dir() . '/monitor-prune-' . uniqid();
$plugin = $base . '/plugins/monitor';
mkdir($plugin . '/include', 0777, true);
file_put_contents($plugin . '/include/old.php', "<?php\n");
mkdir($plugin . '/tests/Unit', 0777, true);
file_put_contents($plugin . '/tests/Unit/SomeTest.php', "<?php\n");
mkdir($plugin . '/userdata', 0777, true);
file_put_contents($plugin . '/userdata/keep.dat', 'keep');
mkdir($plugin . '/includes', 0777, true);
file_put_contents($plugin . '/INFO', "[info]\n");
file_put_contents($plugin . '/setup.php', "<?php\n");
file_put_contents($plugin . '/oldfile.php', "<?php\n");
file_put_contents($plugin . '/stray.php', "<?php\n");
file_put_contents($plugin . '/phpunit.xml', '');
file_put_contents($plugin . '/.mdlrc', '');
file_put_contents($plugin . '/.md_style.rb', '');
mkdir($plugin . '/.git', 0777, true);
file_put_contents($plugin . '/.git/config', '');
file_put_contents($plugin . '/manifest.json', json_encode($manifest));
return $base;
}
beforeEach(function () {
$GLOBALS['__test_cacti_log'] = [];
});
it('removes tombstoned paths and the tests/ tree, keeps whitelist/.git/expected, logs strays', function () {
$manifest = [
'tombstones' => ['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();
});
+3
View File
@@ -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));
+73
View File
@@ -0,0 +1,73 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Validates that manifest.json's "expected" array matches the plugin's |
| actual top-level tree, so the manifest that drives upgrade-time pruning |
| cannot silently drift. tests/, phpunit.xml, .git* and .md* are |
| excluded, as are whitelisted (user-data) paths. Non-zero exit on drift. |
+-------------------------------------------------------------------------+
*/
$root = dirname(__DIR__, 2);
$manifestPath = $root . '/manifest.json';
if (!is_readable($manifestPath)) {
fwrite(STDERR, "manifest.json not found at plugin root\n");
exit(1);
}
$manifest = json_decode((string) file_get_contents($manifestPath), true);
if (!is_array($manifest) || !isset($manifest['expected']) || !is_array($manifest['expected'])) {
fwrite(STDERR, "manifest.json is missing a valid 'expected' array\n");
exit(1);
}
$whitelist = isset($manifest['whitelist']) && is_array($manifest['whitelist']) ? $manifest['whitelist'] : [];
$whitelistTop = [];
foreach ($whitelist as $entry) {
$entry = trim((string) $entry, '/');
if ($entry !== '') {
$whitelistTop[explode('/', $entry)[0]] = true;
}
}
$actual = [];
foreach (scandir($root) as $entry) {
if ($entry === '.' || $entry === '..' || $entry === 'tests' || $entry === 'phpunit.xml') {
continue;
}
if (strncmp($entry, '.git', 4) === 0 || strncmp($entry, '.md', 3) === 0 || isset($whitelistTop[$entry])) {
continue;
}
$actual[] = is_dir($root . '/' . $entry) ? $entry . '/' : $entry;
}
$expected = $manifest['expected'];
sort($actual);
sort($expected);
$missing = array_values(array_diff($expected, $actual));
$extra = array_values(array_diff($actual, $expected));
if ($missing === [] && $extra === []) {
echo "manifest.json 'expected' matches the plugin tree.\n";
exit(0);
}
if ($missing !== []) {
fwrite(STDERR, "manifest.json lists 'expected' entries that are missing on disk:\n " . implode("\n ", $missing) . "\n");
}
if ($extra !== []) {
fwrite(STDERR, "Plugin tree has top-level entries not in manifest.json 'expected'\n(add them to 'expected', or record them under 'tombstones'/'whitelist'):\n " . implode("\n ", $extra) . "\n");
}
exit(1);
+19 -1
View File
@@ -95,6 +95,12 @@ if (!function_exists('get_md5_include_css')) {
}
}
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().
-26
View File
@@ -1,26 +0,0 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
header('Location:../index.php');
-26
View File
@@ -1,26 +0,0 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
header('Location:../index.php');
-26
View File
@@ -1,26 +0,0 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
header('Location:../index.php');
-26
View File
@@ -1,26 +0,0 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
header('Location:../index.php');
-26
View File
@@ -1,26 +0,0 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
header('Location:../index.php');
-26
View File
@@ -1,26 +0,0 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
header('Location:../index.php');
-26
View File
@@ -1,26 +0,0 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
header('Location:../index.php');