INFO: set compat to 1.2.29 (#838)

* INFO: set compat to 1.2.32

* docs: update Copilot instructions Cacti baseline to 1.2.32 (matches INFO compat)

* INFO: back off compat to 1.2.29

* Add manifest.json + upgrade-time file pruning

* 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 thold_functions.php and thold_webapi.php into includes/; flatten css

The main threshold library becomes includes/functions.php and the web API
helper becomes includes/webapi.php; every caller (setup.php, includes/*, the
CLI/poller/notify entry points and the test suite) loads them from includes/.
Each per-theme stylesheet css/<theme>/main.css becomes css/<theme>.css so users
can drop their own css/<theme>.css into the css base, and the page_head hook
looks there. Old paths are tombstoned so existing installs drop the stale
copies on upgrade.

* 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 the hook-include test and make RPN integer ops explicit

The HookFunctionsIncludeTest invokes real hooks with placeholder input purely
to execute their relocated include_once lines; it now swallows engine
notices/warnings/deprecations during those calls (and restores the error
handler and config afterward) so it cannot trip the suite's failOnDeprecation
or leak state into other tests. The RPN % and ^ operators now cast their
operands to int explicitly, matching their long-standing integer-truncation
behaviour while silencing the implicit float-to-int conversion deprecation.

* 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.

---------

Co-authored-by: Copilot <copilot@users.noreply.github.com>
This commit is contained in:
TheWitnessandCopilot authored and GitHub committed 2026-10-01 01:40:00 -05:00
1 parent 5d08f0d2d3
commit 9b217e0e3a
81 files changed
+1356 -894

No files matched your search

+183 -181
View File
@@ -1,182 +1,184 @@
# GitHub Copilot Instructions # GitHub Copilot Instructions
## Priority Guidelines ## Priority Guidelines
When generating code for this repository: When generating code for this repository:
1. **Version Compatibility**: This is a Cacti plugin (`thold`, "Thresholds", version 1.8.2) targeting Cacti 1.2.25+ 1. **Version Compatibility**: This is a Cacti plugin (`thold`, "Thresholds", version 1.8.2) targeting Cacti 1.2.29+
2. **Context Files**: Prioritize patterns and standards defined in this file (`.github/copilot-instructions.md`) 2. **Context Files**: Prioritize patterns and standards defined in this file (`.github/copilot-instructions.md`)
3. **Codebase Patterns**: When context files don't provide specific guidance, scan the codebase for established patterns 3. **Codebase Patterns**: When context files don't provide specific guidance, scan the codebase for established patterns
4. **Architectural Consistency**: Maintain plugin-based architecture extending Cacti core 4. **Architectural Consistency**: Maintain plugin-based architecture extending Cacti core
5. **Code Quality**: Prioritize security, maintainability, and compatibility in all generated code 5. **Code Quality**: Prioritize security, maintainability, and compatibility in all generated code
## Technology Stack ## Technology Stack
### Core Technologies ### Core Technologies
- **PHP**: 8.1+ (CI matrix tests 8.1-8.4) - **PHP**: 8.1+ (CI matrix tests 8.1-8.4)
- **Platform**: Cacti Plugin Architecture (Cacti 1.2.25+) - **Platform**: Cacti Plugin Architecture (Cacti 1.2.29+)
- **Database**: MySQL/MariaDB with InnoDB engine - **Database**: MySQL/MariaDB with InnoDB engine
- **Alerting**: Email, Syslog, and SNMP Traps/Informs notification channels - **Alerting**: Email, Syslog, and SNMP Traps/Informs notification channels
### Key Dependencies ### Key Dependencies
- Cacti core framework (`api_plugin_*`, `db_*`) - Cacti core framework (`api_plugin_*`, `db_*`)
- `CACTI-THOLD-MIB` for SNMP trap/inform definitions - `CACTI-THOLD-MIB` for SNMP trap/inform definitions
- Optional: `gettext` for internationalization - Optional: `gettext` for internationalization
## Project Structure ## Project Structure
``` ```
thold/ # Repository root (install to plugins/thold/ in Cacti) thold/ # Repository root (install to plugins/thold/ in Cacti)
├── extras/ # Supplementary assets ├── extras/ # Supplementary assets
├── includes/ # polling.php (poller hooks), settings.php (config UI), tab.php ├── includes/ # functions.php (core logic), webapi.php (web API), polling.php (poller hooks), settings.php (config UI), tab.php
├── service/ # systemd unit for thold_daemon ├── service/ # systemd unit for thold_daemon
├── tests/ # Test suite (phpunit.xml) ├── tests/ # Test suite (phpunit.xml)
├── themes/ # CSS theme overlays ├── css/ # CSS theme overlays
├── cli_import.php / cli_thresholds.php # CLI threshold management utilities ├── cli_import.php / cli_thresholds.php # CLI threshold management utilities
├── notify_lists.php / notify_queue.php # Notification list/queue administration ├── notify_lists.php / notify_queue.php # Notification list/queue administration
├── thold.php # Main threshold administration UI ├── thold.php # Main threshold administration UI
├── thold_daemon.php # Standalone high-scale daemon (bypasses poller hook) ├── thold_daemon.php # Standalone high-scale daemon (bypasses poller hook)
├── thold_functions.php # Core utility/business logic ├── thold_graph.php / thold_notify.php # Graph-threshold view / notification dispatch
├── thold_graph.php / thold_notify.php # Graph-threshold view / notification dispatch ├── thold_process.php / thold_templates.php # Background processing / threshold templates
├── thold_process.php / thold_templates.php # Background processing / threshold templates ├── poller_thold.php # Background poller entry point (CLI)
├── thold_webapi.php # Web API endpoints ├── INFO # Plugin metadata (name, version, compat)
├── poller_thold.php # Background poller entry point (CLI) ├── README.md
├── INFO # Plugin metadata (name, version, compat) └── setup.php # Plugin install/uninstall/upgrade hooks
├── README.md ```
└── setup.php # Plugin install/uninstall/upgrade hooks
``` ## Naming Conventions
## Naming Conventions ### Function Names
- **All functions and global variables** MUST be prefixed `thold_`: `thold_functions.php`, `thold_poller_output()`, `thold_config_settings()`.
### Function Names - Match the existing prefix used by the function you are editing; do not introduce a new naming scheme.
- **All functions and global variables** MUST be prefixed `thold_`: `thold_functions.php`, `thold_poller_output()`, `thold_config_settings()`.
- Match the existing prefix used by the function you are editing; do not introduce a new naming scheme. ### Database Tables
All plugin tables are prefixed `plugin_thold_`.
### Database Tables
All plugin tables are prefixed `plugin_thold_`. ### Variables and Constants
- Access Cacti configuration via the global `$config` array; use `$config['base_path']` for absolute file paths.
### Variables and Constants
- Access Cacti configuration via the global `$config` array; use `$config['base_path']` for absolute file paths. ## Code Style
## Code Style ### Indentation and Formatting
- **Tabs**: Use tabs (not spaces) for indentation throughout all PHP files.
### Indentation and Formatting - **Braces**: Opening brace on the same line for functions and control structures.
- **Tabs**: Use tabs (not spaces) for indentation throughout all PHP files. - **Spacing**: Space after control structure keywords (`if`, `foreach`, `while`).
- **Braces**: Opening brace on the same line for functions and control structures.
- **Spacing**: Space after control structure keywords (`if`, `foreach`, `while`). ### File Headers
ALL PHP files MUST include the standard GPL v2 license header used throughout this repository (see `setup.php`), crediting "The Cacti Group".
### File Headers
ALL PHP files MUST include the standard GPL v2 license header used throughout this repository (see `setup.php`), crediting "The Cacti Group". ## Security Standards
## Security Standards ### SQL Query Security
**ALWAYS use prepared statements** for database operations:
### SQL Query Security
**ALWAYS use prepared statements** for database operations: ```php
// CORRECT
```php db_execute_prepared($sql, $params);
// CORRECT db_fetch_assoc($sql); // only for queries with no variable input
db_execute_prepared($sql, $params); db_fetch_cell($sql); // only for queries with no variable input
db_fetch_assoc($sql); // only for queries with no variable input
db_fetch_cell($sql); // only for queries with no variable input // WRONG - never concatenate request input into SQL
db_fetch_row("SELECT * FROM plugin_thold_thresholds WHERE id = $id");
// WRONG - never concatenate request input into SQL ```
db_fetch_row("SELECT * FROM plugin_thold_thresholds WHERE id = $id");
``` ### Input Validation and Sanitization
Sanitize inputs using `sanitize_thold_sort_string()` or Cacti's built-in input validation functions (`get_filter_request_var()`, `get_nfilter_request_var()`); never read `$_GET`/`$_POST` directly.
### Input Validation and Sanitization
Sanitize inputs using `sanitize_thold_sort_string()` or Cacti's built-in input validation functions (`get_filter_request_var()`, `get_nfilter_request_var()`); never read `$_GET`/`$_POST` directly. `get_filter_request_var()` (and its `gfrv()` shorthand, where available) called with only the
`$name` argument (no regex/filter as the 2nd/3rd argument) already validates the value as numeric
`get_filter_request_var()` (and its `gfrv()` shorthand, where available) called with only the and returns it as a **string** -- it does not return an int, and it halts execution if the request
`$name` argument (no regex/filter as the 2nd/3rd argument) already validates the value as numeric value is not numeric. Because of this, do NOT cast its output to `(int)` when the result is only
and returns it as a **string** -- it does not return an int, and it halts execution if the request used for string output (e.g. `print`/`echo`, string concatenation, embedding in HTML/JS); the cast
value is not numeric. Because of this, do NOT cast its output to `(int)` when the result is only is redundant. Only cast when the value is genuinely used in an integer/numeric context (e.g.
used for string output (e.g. `print`/`echo`, string concatenation, embedding in HTML/JS); the cast arithmetic, strict `===` comparisons).
is redundant. Only cast when the value is genuinely used in an integer/numeric context (e.g.
arithmetic, strict `===` comparisons). ## Database Operations
## Database Operations Use Cacti's global database functions: `db_execute_prepared($sql, $params)` for writes, `db_fetch_assoc($sql)` / `db_fetch_cell($sql)` for reads.
Use Cacti's global database functions: `db_execute_prepared($sql, $params)` for writes, `db_fetch_assoc($sql)` / `db_fetch_cell($sql)` for reads. ## Internationalization
## Internationalization Use `__('String', 'thold')` for all user-facing strings to support internationalization.
Use `__('String', 'thold')` for all user-facing strings to support internationalization. ## Plugin Architecture
## Plugin Architecture ### Data Flow
1. **Data Collection**: Cacti poller collects data.
### Data Flow 2. **Interception**: `thold_poller_output()` (in `includes/polling.php`) receives the data.
1. **Data Collection**: Cacti poller collects data. 3. **Processing**: Standard mode processes immediately within the poller hook; Daemon mode queues data for `thold_daemon.php` to process asynchronously.
2. **Interception**: `thold_poller_output()` (in `includes/polling.php`) receives the data. 4. **Alerting**: If a threshold is breached, `includes/functions.php` handles notification dispatch.
3. **Processing**: Standard mode processes immediately within the poller hook; Daemon mode queues data for `thold_daemon.php` to process asynchronously.
4. **Alerting**: If a threshold is breached, `thold_functions.php` handles notification dispatch. ### Plugin Hooks
Register hooks in `setup.php` (see the full list of ~30 hooks covering device/graph/data-source actions, poller integration, and template change events); keep new hooks registered the same way via `api_plugin_register_hook($plugin, 'hook_name', 'callback', 'file.php')`.
### Plugin Hooks
Register hooks in `setup.php` (see the full list of ~30 hooks covering device/graph/data-source actions, poller integration, and template change events); keep new hooks registered the same way via `api_plugin_register_hook($plugin, 'hook_name', 'callback', 'file.php')`. ### Daemon Mode
`thold_daemon.php` is a standalone daemon for high-scalability environments, bypassing the standard poller hook — requires systemd service installation (`service/systemd/thold_daemon.service`). Keep daemon-mode processing logic in sync with the standard poller-hook processing path in `includes/polling.php`.
### Daemon Mode
`thold_daemon.php` is a standalone daemon for high-scalability environments, bypassing the standard poller hook — requires systemd service installation (`service/systemd/thold_daemon.service`). Keep daemon-mode processing logic in sync with the standard poller-hook processing path in `includes/polling.php`. ## Best Practices
## Best Practices 1. Keep all new functions and globals under the single `thold_` prefix.
2. Prefer `db_*_prepared()` over string-concatenated SQL.
1. Keep all new functions and globals under the single `thold_` prefix. 3. Keep daemon-mode and poller-hook-mode threshold evaluation logic consistent.
2. Prefer `db_*_prepared()` over string-concatenated SQL. 4. Wrap all user-facing strings with `__('text', 'thold')`.
3. Keep daemon-mode and poller-hook-mode threshold evaluation logic consistent.
4. Wrap all user-facing strings with `__('text', 'thold')`. ## Common Pitfalls to Avoid
## Common Pitfalls to Avoid ```php
// WRONG - concatenated SQL
```php $sql = "SELECT * FROM plugin_thold_thresholds WHERE id = $id";
// WRONG - concatenated SQL
$sql = "SELECT * FROM plugin_thold_thresholds WHERE id = $id"; // CORRECT
$row = db_fetch_row_prepared('SELECT * FROM plugin_thold_thresholds WHERE id = ?', array($id));
// CORRECT ```
$row = db_fetch_row_prepared('SELECT * FROM plugin_thold_thresholds WHERE id = ?', array($id));
``` ## Version Control
## Version Control Testing changes in a safe environment is crucial, especially when dealing with database interactions and alerting mechanisms. Document all changes in `CHANGELOG.md`.
Testing changes in a safe environment is crucial, especially when dealing with database interactions and alerting mechanisms. Document all changes in `CHANGELOG.md`. ## CI & Dependency Baselines
## CI & Dependency Baselines - Do not commit a `composer.json` or `composer.lock` in this plugin's own repo root — the shared CI workflow installs Pest/dev dependencies into Cacti's own Composer-managed vendor tree (checked out alongside the plugin). Use Cacti's `composer.json`, not a plugin-local one.
- Do not add a plugin-local `.phpstan.neon`/`phpstan.neon` or `.php-cs-fixer.php`/`.php-cs-fixer.dist.php` — lint/static-analysis steps run against Cacti's own config from the Cacti core checkout, targeting this plugin's directory. Use the Cacti version, not a plugin-local config.
- Do not commit a `composer.json` or `composer.lock` in this plugin's own repo root — the shared CI workflow installs Pest/dev dependencies into Cacti's own Composer-managed vendor tree (checked out alongside the plugin). Use Cacti's `composer.json`, not a plugin-local one. - Prefer Cacti's `cacti_count()`/`cacti_sizeof()` wrappers over the raw `count()`/`sizeof()` builtins in new or edited code.
- Do not add a plugin-local `.phpstan.neon`/`phpstan.neon` or `.php-cs-fixer.php`/`.php-cs-fixer.dist.php` — lint/static-analysis steps run against Cacti's own config from the Cacti core checkout, targeting this plugin's directory. Use the Cacti version, not a plugin-local config.
- Prefer Cacti's `cacti_count()`/`cacti_sizeof()` wrappers over the raw `count()`/`sizeof()` builtins in new or edited code. ## Internationalization (i18n)
## Internationalization (i18n) - Translatable strings are managed with GNU gettext via `locales/build_gettext.sh`. `locales/po/cacti.pot` is the source template; Weblate owns syncing the per-language `.po`/`.mo` files from it.
- **Never commit the per-language `.po` or compiled `.mo` files** (`locales/po/*.po`, `locales/LC_MESSAGES/*.mo`) in a plugin PR. Weblate is the sole owner of those catalogs, and regenerating them here produces spurious diffs and merge conflicts. `locales/po/cacti.pot` is the ONLY translation artifact a PR may add or modify.
- Translatable strings are managed with GNU gettext via `locales/build_gettext.sh`. `locales/po/cacti.pot` is the source template; Weblate owns syncing the per-language `.po`/`.mo` files from it.
- **Never commit the per-language `.po` or compiled `.mo` files** (`locales/po/*.po`, `locales/LC_MESSAGES/*.mo`) in a plugin PR. Weblate is the sole owner of those catalogs, and regenerating them here produces spurious diffs and merge conflicts. `locales/po/cacti.pot` is the ONLY translation artifact a PR may add or modify.
- When a pull request adds or changes a string wrapped in `__()`/`__n()`/`__esc()`/`__x()`/`__xn()`/`__gettext()`, run `locales/build_gettext.sh` before pushing and stage `locales/po/cacti.pot` only. `build_gettext.sh` also rewrites the `.po`/`.mo` files as a side effect; revert those before committing (`git checkout -- locales/po/*.po locales/LC_MESSAGES`), or run only the `xgettext` step that targets `cacti.pot`. - When a pull request adds or changes a string wrapped in `__()`/`__n()`/`__esc()`/`__x()`/`__xn()`/`__gettext()`, run `locales/build_gettext.sh` before pushing and stage `locales/po/cacti.pot` only. `build_gettext.sh` also rewrites the `.po`/`.mo` files as a side effect; revert those before committing (`git checkout -- locales/po/*.po locales/LC_MESSAGES`), or run only the `xgettext` step that targets `cacti.pot`.
## References ## References
- [Cacti DB Functions](https://github.com/Cacti/cacti/blob/1.2.x/lib/database.php) - [Cacti DB Functions](https://github.com/Cacti/cacti/blob/1.2.x/lib/database.php)
- [Cacti Documentation](https://www.github.com/Cacti/documentation) - [Cacti Documentation](https://www.github.com/Cacti/documentation)
- `README.md` for feature descriptions - `README.md` for feature descriptions
- `CHANGELOG.md` for version history - `CHANGELOG.md` for version history
## Security & Quality Conventions ## Security & Quality Conventions
These conventions apply across the Cacti plugin fleet and should be followed whenever touching These conventions apply across the Cacti plugin fleet and should be followed whenever touching
existing code or adding new code, not just in dedicated cleanup passes: existing code or adding new code, not just in dedicated cleanup passes:
- **No hardcoded third-party hosts.** Never hardcode a third-party IP address, hostname, or URL - **No hardcoded third-party hosts.** Never hardcode a third-party IP address, hostname, or URL
in plugin code (even for tooling/download helpers). Expose it as a plugin setting instead, with in plugin code (even for tooling/download helpers). Expose it as a plugin setting instead, with
secure-by-default values (e.g. an SSL-verification setting that defaults to verify-on). secure-by-default values (e.g. an SSL-verification setting that defaults to verify-on).
- **Prepared statements over `db_qstr()`.** Build dynamic `WHERE` clauses using the - **Prepared statements over `db_qstr()`.** Build dynamic `WHERE` clauses using the
`$sql_where`/`$sql_params` prepared-statement pattern, not string concatenation via `db_qstr()`. `$sql_where`/`$sql_params` prepared-statement pattern, not string concatenation via `db_qstr()`.
- **Use `html_escape_request_var()`.** Prefer it over the `html_escape(get_request_var(...))` call - **Use `html_escape_request_var()`.** Prefer it over the `html_escape(get_request_var(...))` call
chain. chain.
- **Harden `unserialize()`.** Always pass `['allow_classes' => false]` as the second argument. - **Harden `unserialize()`.** Always pass `['allow_classes' => false]` as the second argument.
- **i18n text domain.** Every `__()`/`__esc()` call must include this plugin's text domain as the - **i18n text domain.** Every `__()`/`__esc()` call must include this plugin's text domain as the
final argument, except when deliberately comparing against a literal, untranslated Cacti-core final argument, except when deliberately comparing against a literal, untranslated Cacti-core
label. label.
- **Plugin table-creation API.** Use `api_plugin_db_table_create()`/`api_plugin_db_add_column()` - **Plugin table-creation API.** Use `api_plugin_db_table_create()`/`api_plugin_db_add_column()`
(from Cacti core's `lib/plugins.php`) instead of raw `CREATE TABLE`/`ALTER TABLE ... ADD COLUMN`. (from Cacti core's `lib/plugins.php`) instead of raw `CREATE TABLE`/`ALTER TABLE ... ADD COLUMN`.
Both are idempotent (safe no-ops when already applied), so the same call can run unconditionally Both are idempotent (safe no-ops when already applied), so the same call can run unconditionally
from both the install AND upgrade paths. from both the install AND upgrade paths.
- **PHPDoc shape.** Every function gets a PHPDoc block: a one-line description, a blank comment - **PHPDoc shape.** Every function gets a PHPDoc block: a one-line description, a blank comment
line, `@param` lines, a blank comment line, then `@return`. Infer parameter/return types from 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 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. 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, `thold_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
@@ -89,6 +89,9 @@ jobs:
- name: Check PHP version - name: Check PHP version
run: php -v run: php -v
- name: Validate plugin manifest (expected-file drift)
run: php cacti/plugins/thold/tests/bin/validate-manifest.php
- name: Run apt-get update - name: Run apt-get update
run: sudo apt-get update run: sudo apt-get update
-24
View File
@@ -1,24 +0,0 @@
# +-------------------------------------------------------------------------+
# | Copyright (C) 2004-2025 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/ |
# +-------------------------------------------------------------------------+
# mdl cli configuration
style ".mdl_style.rb"
verbose false
+1 -1
View File
@@ -26,5 +26,5 @@ longname = Thresholds
author = The Cacti Group author = The Cacti Group
email = email =
homepage = http://www.cacti.net homepage = http://www.cacti.net
compat = 1.2.25 compat = 1.2.29
capabilities = online_view:1, online_mgmt:1, offline_view:0, offline_mgmt:0, remote_collect:1 capabilities = online_view:1, online_mgmt:1, offline_view:0, offline_mgmt:0, remote_collect:1
+6
View File
@@ -129,6 +129,12 @@ thresholds, note that you must modify and install the thold_daemon.service file
into your systemd configuration, and then start and test the service. If you into your systemd configuration, and then start and test the service. If you
fail to perform these steps, thold will appear to not work as expected. fail to perform these steps, thold will appear to not work as expected.
When upgrading the plugin while the Threshold Daemon is running, restart the
`thold_daemon` service (for example `systemctl restart thold_daemon`) after
the upgrade so the long-lived process picks up the new code. On upgrade the
plugin also prunes its own bundled development-only files (for example the
`tests/` directory) from the installed tree.
Lastly, please note that several forks of the thold plugin are available from Lastly, please note that several forks of the thold plugin are available from
different sources. These forks of thold are not necessarily compatible with the different sources. These forks of thold are not necessarily compatible with the
current version of Cacti's thold plugin. Please be aware of this when current version of Cacti's thold plugin. Please be aware of this when
+1 -1
View File
@@ -29,7 +29,7 @@ error_reporting(E_ALL);
include(__DIR__ . '/../../include/cli_check.php'); include(__DIR__ . '/../../include/cli_check.php');
include_once($config['base_path'] . '/lib/xml.php'); include_once($config['base_path'] . '/lib/xml.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
// set the defaults // set the defaults
$force = false; $force = false;
+1 -1
View File
@@ -35,7 +35,7 @@ if (strpos($dir, 'plugins') !== false) {
include('./include/cli_check.php'); include('./include/cli_check.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
// set the defaults // set the defaults
$force = false; $force = false;
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
View File
File renamed without changes.
File renamed without changes.
+1 -1
View File
@@ -50,7 +50,7 @@ function thold_upgrade_database($force = false) {
thold_setup_database(); thold_setup_database();
include_once($config['base_path'] . '/plugins/thold/setup.php'); include_once($config['base_path'] . '/plugins/thold/setup.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
$v = plugin_thold_version(); $v = plugin_thold_version();
@@ -482,12 +482,12 @@ function thold_rpn_math_binary($operator, $v2, $v1) {
case '/': case '/':
return $v2 / $v1; return $v2 / $v1;
case '%': case '%':
return $v2 % $v1; return (int) $v2 % (int) $v1;
case '^': case '^':
// Bitwise XOR, not exponentiation: eval('$v3 = ' . $v2 . ' ^ ' . $v1 . ';') // Bitwise XOR, not exponentiation: eval('$v3 = ' . $v2 . ' ^ ' . $v1 . ';')
// always computed XOR (PHP's ^ operator), and existing user thresholds // always computed XOR (PHP's ^ operator), and existing user thresholds
// rely on that. See TholdExpressionMathRpnTest::testCaretOperatorIsIntegerXorNotExponentiation. // rely on that. See TholdExpressionMathRpnTest::testCaretOperatorIsIntegerXorNotExponentiation.
return $v2 ^ $v1; return (int) $v2 ^ (int) $v1;
default: default:
cacti_log("ERROR: RPN unknown binary operator '$operator'", false, 'THOLD'); cacti_log("ERROR: RPN unknown binary operator '$operator'", false, 'THOLD');
$rpn_error = true; $rpn_error = true;
+3 -3
View File
@@ -116,7 +116,7 @@ function thold_cleanup_log() {
function thold_poller_output(&$rrd_update_array) { function thold_poller_output(&$rrd_update_array) {
global $config, $debug; global $config, $debug;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['library_path'] . '/snmp.php'); include_once($config['library_path'] . '/snmp.php');
$rrd_reindexed = []; $rrd_reindexed = [];
@@ -338,7 +338,7 @@ function thold_check_all_thresholds() {
global $config; global $config;
include($config['base_path'] . '/plugins/thold/includes/arrays.php'); include($config['base_path'] . '/plugins/thold/includes/arrays.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['base_path'] . '/lib/time.php'); include_once($config['base_path'] . '/lib/time.php');
if (read_config_option('remote_storage_method') == 1) { if (read_config_option('remote_storage_method') == 1) {
@@ -461,7 +461,7 @@ function thold_update_host_status() {
return 0; return 0;
} }
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['library_path'] . '/snmp.php'); include_once($config['library_path'] . '/snmp.php');
if (api_plugin_is_enabled('maint')) { if (api_plugin_is_enabled('maint')) {
+1 -1
View File
@@ -466,7 +466,7 @@ function thold_config_settings() {
include($config['base_path'] . '/plugins/thold/includes/arrays.php'); include($config['base_path'] . '/plugins/thold/includes/arrays.php');
include_once($config['base_path'] . '/plugins/thold/setup.php'); include_once($config['base_path'] . '/plugins/thold/setup.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['base_path'] . '/plugins/thold/includes/database.php'); include_once($config['base_path'] . '/plugins/thold/includes/database.php');
// check to see if there is an upgrade required // check to see if there is an upgrade required
+1 -1
View File
@@ -39,7 +39,7 @@
function thold_add_graphs_action_execute() { function thold_add_graphs_action_execute() {
global $config; global $config;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
$host_id = get_filter_request_var('host_id'); $host_id = get_filter_request_var('host_id');
$local_graph_id = get_filter_request_var('local_graph_id'); $local_graph_id = get_filter_request_var('local_graph_id');
+453 -453
View File
File diff suppressed because it is too large. Load diff
+35
View File
@@ -0,0 +1,35 @@
{
"tombstones": [
"themes/",
"thold_functions.php",
"thold_webapi.php"
],
"expected": [
"CACTI-THOLD-MIB",
"CHANGELOG.md",
"INFO",
"LICENSE",
"README.md",
"cli_import.php",
"cli_thresholds.php",
"css/",
"extras/",
"images/",
"includes/",
"index.php",
"locales/",
"manifest.json",
"notify_lists.php",
"notify_queue.php",
"poller_thold.php",
"service/",
"setup.php",
"thold.php",
"thold_daemon.php",
"thold_graph.php",
"thold_notify.php",
"thold_process.php",
"thold_templates.php"
],
"whitelist": []
}
+1 -1
View File
@@ -25,7 +25,7 @@
chdir('../..'); chdir('../..');
include('./include/auth.php'); include('./include/auth.php');
include_once($config['base_path'] . '/lib/reports.php'); include_once($config['base_path'] . '/lib/reports.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include($config['base_path'] . '/plugins/thold/includes/arrays.php'); include($config['base_path'] . '/plugins/thold/includes/arrays.php');
$actions = [ $actions = [
+1 -1
View File
@@ -26,7 +26,7 @@ chdir('../../');
include_once('./include/auth.php'); include_once('./include/auth.php');
require_once($config['base_path'] . '/lib/rrd.php'); require_once($config['base_path'] . '/lib/rrd.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['base_path'] . '/plugins/thold/setup.php'); include_once($config['base_path'] . '/plugins/thold/setup.php');
include_once($config['base_path'] . '/plugins/thold/includes/database.php'); include_once($config['base_path'] . '/plugins/thold/includes/database.php');
include($config['base_path'] . '/plugins/thold/includes/arrays.php'); include($config['base_path'] . '/plugins/thold/includes/arrays.php');
+2 -2
View File
@@ -24,7 +24,7 @@
Pest reads this PHPUnit-compatible configuration through Cacti's Pest reads this PHPUnit-compatible configuration through Cacti's
Composer-managed vendor tree. Composer-managed vendor tree.
thold_functions.php, includes/polling.php, and setup.php are the only includes/functions.php, includes/polling.php, and setup.php are the only
plugin sources that load without a running Cacti (no top-level side plugin sources that load without a running Cacti (no top-level side
effects); every other file includes ./include/auth.php and dispatches effects); every other file includes ./include/auth.php and dispatches
on the request at top level. tests/Integration/PluginLifecycleTest.php on the request at top level. tests/Integration/PluginLifecycleTest.php
@@ -32,7 +32,7 @@
--> -->
<source> <source>
<include> <include>
<file>thold_functions.php</file> <file>includes/functions.php</file>
<file>includes/polling.php</file> <file>includes/polling.php</file>
<file>setup.php</file> <file>setup.php</file>
+1 -1
View File
@@ -35,7 +35,7 @@ if (strpos($dir, 'plugins') !== false) {
include('./include/cli_check.php'); include('./include/cli_check.php');
include_once($config['base_path'] . '/plugins/thold/setup.php'); include_once($config['base_path'] . '/plugins/thold/setup.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['base_path'] . '/plugins/thold/includes/polling.php'); include_once($config['base_path'] . '/plugins/thold/includes/polling.php');
include_once($config['base_path'] . '/lib/rrd.php'); include_once($config['base_path'] . '/lib/rrd.php');
include_once($config['base_path'] . '/lib/poller.php'); include_once($config['base_path'] . '/lib/poller.php');
+192 -21
View File
@@ -211,6 +211,7 @@ function plugin_thold_upgrade() {
if ($current != $old) { if ($current != $old) {
plugin_thold_install(true); plugin_thold_install(true);
thold_prune_files();
} }
return true; return true;
@@ -401,7 +402,7 @@ function thold_multiexplode($delimiters, $string) {
function thold_rrd_graph_graph_options($g) { function thold_rrd_graph_graph_options($g) {
global $config; global $config;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
// handle thold replacement variables // handle thold replacement variables
$needles = []; $needles = [];
@@ -830,7 +831,7 @@ function thold_device_action_execute($action) {
return $action; return $action;
} }
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
$selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items')); $selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items'));
@@ -859,7 +860,7 @@ function thold_device_action_execute($action) {
function thold_api_device_new($save) { function thold_api_device_new($save) {
global $config; global $config;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if (read_config_option('thold_autocreate') == 'on') { if (read_config_option('thold_autocreate') == 'on') {
if (!empty($save['id'])) { if (!empty($save['id'])) {
@@ -943,7 +944,7 @@ function thold_api_device_save($save) {
WHERE id = ?', WHERE id = ?',
[$save['id']]); [$save['id']]);
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if ($save['id'] > 0) { if ($save['id'] > 0) {
if ($save['disabled'] != $result['disabled']) { if ($save['disabled'] != $result['disabled']) {
@@ -1120,7 +1121,7 @@ function thold_user_admin_setup_sql_save($save) {
function thold_data_source_action_execute($action) { function thold_data_source_action_execute($action) {
global $config, $form_array; global $config, $form_array;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if ($action == 'plugin_thold_create') { if ($action == 'plugin_thold_create') {
$selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items')); $selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items'));
@@ -1327,7 +1328,7 @@ function thold_data_source_action_array($action) {
function thold_graphs_action_execute($action) { function thold_graphs_action_execute($action) {
global $config, $form_array; global $config, $form_array;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if ($action == 'plugin_thold_create') { if ($action == 'plugin_thold_create') {
$selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items')); $selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items'));
@@ -1526,8 +1527,8 @@ function thold_graphs_action_array($action) {
* @return void * @return void
*/ */
function thold_host_edit_bottom() { function thold_host_edit_bottom() {
// This hook can fire on core pages that have not loaded thold_functions.php, where plugin_thold_csp_nonce() lives. // This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/thold_functions.php'); require_once(__DIR__ . '/includes/functions.php');
?> ?>
<script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>> <script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>>
@@ -1600,11 +1601,11 @@ function thold_snmpagent_cache_uninstall() {
function thold_page_head() { function thold_page_head() {
global $config; global $config;
// This hook can fire on core pages that have not loaded thold_functions.php, where plugin_thold_csp_nonce() lives. // This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/thold_functions.php'); require_once(__DIR__ . '/includes/functions.php');
if (file_exists($config['base_path'] . '/plugins/thold/themes/' . get_selected_theme() . '/main.css')) { if (file_exists($config['base_path'] . '/plugins/thold/css/' . get_selected_theme() . '.css')) {
print get_md5_include_css('plugins/thold/themes/' . get_selected_theme() . '/main.css'); print get_md5_include_css('plugins/thold/css/' . get_selected_theme() . '.css');
} }
?> ?>
@@ -1640,8 +1641,8 @@ function thold_page_head() {
* @return void * @return void
*/ */
function thold_device_edit_pre_bottom() { function thold_device_edit_pre_bottom() {
// This hook can fire on core pages that have not loaded thold_functions.php, where plugin_thold_csp_nonce() lives. // This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/thold_functions.php'); require_once(__DIR__ . '/includes/functions.php');
html_start_box(__('Associated Threshold Templates', 'thold'), '100%', false, '3', 'center', ''); html_start_box(__('Associated Threshold Templates', 'thold'), '100%', false, '3', 'center', '');
@@ -1853,8 +1854,8 @@ function thold_device_top() {
* @return void * @return void
*/ */
function thold_device_template_edit() { function thold_device_template_edit() {
// This hook can fire on core pages that have not loaded thold_functions.php, where plugin_thold_csp_nonce() lives. // This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/thold_functions.php'); require_once(__DIR__ . '/includes/functions.php');
html_start_box(__('Associated Threshold Templates', 'thold'), '100%', false, '3', 'center', ''); html_start_box(__('Associated Threshold Templates', 'thold'), '100%', false, '3', 'center', '');
@@ -2094,7 +2095,7 @@ function thold_device_autocreate($host_id) {
function thold_create_graph_thold($save) { function thold_create_graph_thold($save) {
global $config; global $config;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if (read_config_option('thold_autocreate') == 'on') { if (read_config_option('thold_autocreate') == 'on') {
$graph = db_fetch_row_prepared('SELECT * $graph = db_fetch_row_prepared('SELECT *
@@ -2126,7 +2127,7 @@ function thold_create_graph_thold($save) {
function thold_data_source_remove($data_ids) { function thold_data_source_remove($data_ids) {
global $config; global $config;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
$tholds = array_rekey( $tholds = array_rekey(
db_fetch_assoc('SELECT id db_fetch_assoc('SELECT id
@@ -2185,7 +2186,7 @@ function thold_clog_regex_array($regex_array) {
function thold_clog_regex_threshold($matches) { function thold_clog_regex_threshold($matches) {
global $config; global $config;
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
$result = $matches[0]; $result = $matches[0];
@@ -2228,8 +2229,8 @@ function thold_clog_regex_threshold($matches) {
* @return void * @return void
*/ */
function thold_settings_bottom() { function thold_settings_bottom() {
// This hook can fire on core pages that have not loaded thold_functions.php, where plugin_thold_csp_nonce() lives. // This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/thold_functions.php'); require_once(__DIR__ . '/includes/functions.php');
?> ?>
<script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>> <script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>>
@@ -2239,3 +2240,173 @@ function thold_settings_bottom() {
</script> </script>
<?php <?php
} }
/**
* 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 thold_prune_files(): void {
global $config;
$plugin_dir = $config['base_path'] . '/plugins/thold';
$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: thold manifest.json could not be parsed; skipping file prune', false, 'THOLD');
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: thold prune refused to remove %s: path contains a traversal segment (tampered manifest.json?)', $rel), false, 'THOLD');
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: thold prune refused to remove %s: path resolves outside the plugin directory (tampered manifest.json?)', $rel), false, 'THOLD');
continue;
}
if (is_dir($path) && !is_link($path)) {
$removed = thold_rmtree($path);
} else {
$removed = @unlink($path);
}
if (!$removed) {
cacti_log(sprintf('WARNING: thold upgrade could not remove %s (check file/directory permissions)', $rel), false, 'THOLD');
}
}
// 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: thold upgrade found a file/directory not described in manifest.json: %s (left in place)', $entry), false, 'THOLD');
}
}
/**
* Recursively deletes a directory and its contents. Symlinks are removed
* without being followed. Helper for thold_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 thold_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 (!thold_rmtree($path)) {
$ok = false;
}
} elseif (!@unlink($path)) {
$ok = false;
}
}
if (!@rmdir($dir)) {
$ok = false;
}
return $ok;
}
+27
View File
@@ -60,3 +60,30 @@ it('does not reinstall when the installed version already matches', function ()
expect(plugin_thold_upgrade())->toBeTrue(); expect(plugin_thold_upgrade())->toBeTrue();
expect(CactiStubs::callsTo('api_plugin_register_hook'))->toBeEmpty(); expect(CactiStubs::callsTo('api_plugin_register_hook'))->toBeEmpty();
}); });
it('prunes bundled dev-only files on a version-drift upgrade', function () {
CactiStubs::willReturn('get_current_page', 'thold.php');
CactiStubs::willReturnFor('db_fetch_cell', 'plugin_config', '0.0.0');
// Force plugin_thold_install(true) to short-circuit immediately (its
// version_compare guard) so this exercises the drift branch + prune call
// without the full reinstall, which needs a live Cacti include/database.php.
$restoreCacti = $GLOBALS['config']['cacti_version'];
$GLOBALS['config']['cacti_version'] = '1.1';
// Sandbox base_path so thold_prune_files() runs against a throwaway
// tree with a copy of the real INFO (so plugin_thold_version() still
// matches) and no manifest.json (prune no-ops), never the real checkout.
$restoreBase = $GLOBALS['config']['base_path'];
$base = sys_get_temp_dir() . '/thold-itest-' . uniqid();
mkdir($base . '/plugins/thold', 0777, true);
copy(dirname(__DIR__, 2) . '/INFO', $base . '/plugins/thold/INFO');
$GLOBALS['config']['base_path'] = $base;
try {
expect(plugin_thold_upgrade())->toBeTrue();
} finally {
$GLOBALS['config']['base_path'] = $restoreBase;
$GLOBALS['config']['cacti_version'] = $restoreCacti;
}
});
+1 -1
View File
@@ -63,7 +63,7 @@ abstract class TestCase extends PHPUnit\Framework\TestCase {
* Load a plugin source file that only assigns file-scope variables (e.g. * Load a plugin source file that only assigns file-scope variables (e.g.
* includes/arrays.php), bypassing the include-once registry. * includes/arrays.php), bypassing the include-once registry.
* *
* thold_functions.php includes includes/arrays.php with a plain include() * includes/functions.php includes includes/arrays.php with a plain include()
* (not include_once()) from several of its own functions. Once any test * (not include_once()) from several of its own functions. Once any test
* exercises one of those call sites, a later loadPluginSource() of the * exercises one of those call sites, a later loadPluginSource() of the
* same file silently no-ops (require_once sees it as already included) * same file silently no-ops (require_once sees it as already included)
+4 -4
View File
@@ -25,7 +25,7 @@
/* /*
* Unit coverage for plugin_thold_csp_nonce(). * Unit coverage for plugin_thold_csp_nonce().
* *
* The helper is defined in thold_functions.php (the library every plugin page * The helper is defined in includes/functions.php (the library every plugin page
* includes), so these tests load that file directly - without setup.php - to * includes), so these tests load that file directly - without setup.php - to
* guard against the helper regressing back into a file the pages do not load. * guard against the helper regressing back into a file the pages do not load.
* *
@@ -38,7 +38,7 @@
*/ */
beforeAll(function () { beforeAll(function () {
require_once __DIR__ . '/../../thold_functions.php'; require_once __DIR__ . '/../../includes/functions.php';
}); });
test('csp nonce returns an empty string when CactiSecureHeaders is unavailable', function () { test('csp nonce returns an empty string when CactiSecureHeaders is unavailable', function () {
@@ -56,10 +56,10 @@ test('csp nonce helper is declared to return a string', function () {
}); });
test('csp nonce delegates to CactiSecureHeaders when the class is available', function () { test('csp nonce delegates to CactiSecureHeaders when the class is available', function () {
$wrapper = realpath(__DIR__ . '/../../thold_functions.php'); $wrapper = realpath(__DIR__ . '/../../includes/functions.php');
expect($wrapper)->not->toBeFalse(); expect($wrapper)->not->toBeFalse();
// thold_functions.php loads without a running Cacti, but hand the child the // includes/functions.php loads without a running Cacti, but hand the child the
// unit bootstrap too so any Cacti stubs it might reference exist at load time. // unit bootstrap too so any Cacti stubs it might reference exist at load time.
$bootstrap = realpath(__DIR__ . '/../bootstrap-unit.php'); $bootstrap = realpath(__DIR__ . '/../bootstrap-unit.php');
+1 -1
View File
@@ -27,7 +27,7 @@ final class GetAllowedThresholdsTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -26,7 +26,7 @@ final class GetCurrentValueTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+63
View File
@@ -0,0 +1,63 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* Several hooks lazy-load the threshold library on demand. After its move to
* includes/functions.php, invoke each one so the relocated include_once path
* is exercised; a bad relocation would surface here. The hooks run against the
* bootstrap's Cacti stubs and any downstream error is irrelevant to the goal.
*/
beforeAll(function () {
require_once __DIR__ . '/../../setup.php';
require_once __DIR__ . '/../../includes/polling.php';
});
it('resolves includes/functions.php from every hook that lazy-loads it', function () {
$restoreBase = $GLOBALS['config']['base_path'] ?? null;
$restoreOpts = CactiStubs::$configOptions;
$GLOBALS['config']['base_path'] = dirname(__DIR__, 4);
CactiStubs::$configOptions['alert_deadnotify'] = 'on';
$hooks = [
static fn () => thold_rrd_graph_graph_options([]),
static fn () => thold_device_action_execute('thold'),
static fn () => thold_api_device_new([]),
static fn () => thold_api_device_save([]),
static fn () => thold_data_source_action_execute(''),
static fn () => thold_graphs_action_execute(''),
static fn () => thold_create_graph_thold([]),
static fn () => thold_data_source_remove([]),
static fn () => thold_clog_regex_threshold([]),
static fn () => thold_update_host_status(),
];
// This suite runs failOnDeprecation/Warning/Notice; feeding the hooks
// placeholder input legitimately trips those, so swallow engine messages
// while the include_once lines execute, then return control to PHPUnit.
set_error_handler(static fn (): bool => true);
try {
foreach ($hooks as $hook) {
ob_start();
try {
$hook();
} catch (\Throwable $e) {
// Reaching the include_once at the top of each hook is the point.
} finally {
ob_end_clean();
}
}
} finally {
restore_error_handler();
CactiStubs::$configOptions = $restoreOpts;
$GLOBALS['config']['base_path'] = $restoreBase;
}
expect(function_exists('plugin_thold_csp_nonce'))->toBeTrue();
});
@@ -15,7 +15,7 @@
*/ */
beforeAll(function() { beforeAll(function() {
thold_test_load(dirname(__DIR__, 2) . '/thold_functions.php'); thold_test_load(dirname(__DIR__, 2) . '/includes/functions.php');
}); });
beforeEach(function() { beforeEach(function() {
@@ -46,7 +46,7 @@ test('combined device notifications use the sha256 deduplication path', function
process_device_notifications(0, 'all', 0); process_device_notifications(0, 'all', 0);
$source = file_get_contents(dirname(__DIR__, 2) . '/thold_functions.php'); $source = file_get_contents(dirname(__DIR__, 2) . '/includes/functions.php');
expect(CactiStubs::$mail)->toHaveCount(1) expect(CactiStubs::$mail)->toHaveCount(1)
->and($source)->toContain("hash('sha256', json_encode(") ->and($source)->toContain("hash('sha256', json_encode(")
+1 -1
View File
@@ -26,7 +26,7 @@ final class NotificationQueueClaimTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -19,7 +19,7 @@ final class NotificationQueueRetryTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -23,7 +23,7 @@ final class PollerSchedulingTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
self::loadPluginSource('includes/polling.php'); self::loadPluginSource('includes/polling.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 thold_prune_files(): tombstone/tests removal,
* whitelist and .git protection, and logging of unaccounted-for entries.
*/
require_once __DIR__ . '/../../setup.php';
function thold_prune_fixture(array $manifest): string {
$base = sys_get_temp_dir() . '/thold-prune-' . uniqid();
$plugin = $base . '/plugins/thold';
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 = thold_prune_fixture($manifest);
$plugin = $base . '/plugins/thold';
$restore = $GLOBALS['config']['base_path'];
$GLOBALS['config']['base_path'] = $base;
try {
thold_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() . '/thold-prune-missing-' . uniqid();
mkdir($base . '/plugins/thold', 0777, true);
$restore = $GLOBALS['config']['base_path'];
$GLOBALS['config']['base_path'] = $base;
try {
thold_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() . '/thold-prune-bad-' . uniqid();
$plugin = $base . '/plugins/thold';
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 {
thold_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 = thold_prune_fixture($manifest);
$plugin = $base . '/plugins/thold';
$outside = $base . '/plugins/escapee.txt';
file_put_contents($outside, 'precious user data');
$restore = $GLOBALS['config']['base_path'];
$GLOBALS['config']['base_path'] = $base;
try {
thold_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 = thold_prune_fixture($manifest);
$plugin = $base . '/plugins/thold';
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 {
thold_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 = thold_prune_fixture($manifest);
$plugin = $base . '/plugins/thold';
$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 {
thold_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 = thold_prune_fixture($manifest);
$plugin = $base . '/plugins/thold';
$restore = $GLOBALS['config']['base_path'];
$GLOBALS['config']['base_path'] = $base;
try {
thold_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();
});
+1 -1
View File
@@ -51,7 +51,7 @@ final class SetupCspHooksTest extends TestCase {
thold_page_head(); thold_page_head();
$output = ob_get_clean(); $output = ob_get_clean();
$this->assertStringContainsString('themes/modern/main.css', $output); $this->assertStringContainsString('css/modern.css', $output);
$this->assertStringContainsString("<script type='text/javascript'", $output); $this->assertStringContainsString("<script type='text/javascript'", $output);
} }
+1 -1
View File
@@ -26,7 +26,7 @@ final class TholdBuildCdefTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -23,7 +23,7 @@ final class TholdCalculateExpressionTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -24,7 +24,7 @@ final class TholdCalculateLowerUpperTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -26,7 +26,7 @@ final class TholdCalculatePercentTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -26,7 +26,7 @@ final class TholdCommandExecutionTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -27,7 +27,7 @@ final class TholdEvaluationContextTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -26,7 +26,7 @@ final class TholdExpandStringTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -27,7 +27,7 @@ final class TholdExpressionMathRpnTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -24,7 +24,7 @@ final class TholdExpressionStackOpsTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -23,7 +23,7 @@ final class TholdGetCachedNameTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -28,7 +28,7 @@ final class TholdGetCurrentvalTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -29,7 +29,7 @@ final class TholdLegendTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
// Publishes $thold_states, $thold_host_states and $thold_log_states. // Publishes $thold_states, $thold_host_states and $thold_log_states.
self::loadPluginSourceAlways('includes/arrays.php'); self::loadPluginSourceAlways('includes/arrays.php');
+1 -1
View File
@@ -26,7 +26,7 @@ final class TholdMailNotificationTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -27,7 +27,7 @@ final class TholdReplaceThresholdTagsTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
// Defines $thold_types, which the <THOLDTYPE> substitution reads. // Defines $thold_types, which the <THOLDTYPE> substitution reads.
self::loadPluginSourceAlways('includes/arrays.php'); self::loadPluginSourceAlways('includes/arrays.php');
+1 -1
View File
@@ -26,7 +26,7 @@ final class TholdRlikeClauseTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -15,7 +15,7 @@
*/ */
beforeAll(function() { beforeAll(function() {
thold_test_load(dirname(__DIR__, 2) . '/thold_functions.php'); thold_test_load(dirname(__DIR__, 2) . '/includes/functions.php');
}); });
beforeEach(function() { beforeEach(function() {
+1 -1
View File
@@ -32,7 +32,7 @@ final class TholdRpnCdefTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -15,7 +15,7 @@
*/ */
beforeAll(function() { beforeAll(function() {
thold_test_load(dirname(__DIR__, 2) . '/thold_functions.php'); thold_test_load(dirname(__DIR__, 2) . '/includes/functions.php');
}); });
beforeEach(function() { beforeEach(function() {
+1 -1
View File
@@ -27,7 +27,7 @@ final class TholdSetEnvironTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -26,7 +26,7 @@ final class TholdStrReplaceTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -24,7 +24,7 @@ final class TholdSubstituteHostDataTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
+1 -1
View File
@@ -27,7 +27,7 @@ final class TholdUnitSuffixTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
} }
/** /**
@@ -27,7 +27,7 @@ final class ThresholdBaselineCharacterizationTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
self::loadPluginSourceAlways('includes/arrays.php'); self::loadPluginSourceAlways('includes/arrays.php');
self::loadPluginConstants(); self::loadPluginConstants();
} }
@@ -27,7 +27,7 @@ final class ThresholdHiLowCharacterizationTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
self::loadPluginSourceAlways('includes/arrays.php'); self::loadPluginSourceAlways('includes/arrays.php');
self::loadPluginConstants(); self::loadPluginConstants();
} }
@@ -26,7 +26,7 @@ final class ThresholdTimeBasedCharacterizationTest extends TestCase {
* @return void * @return void
*/ */
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
self::loadPluginSource('thold_functions.php'); self::loadPluginSource('includes/functions.php');
self::loadPluginSourceAlways('includes/arrays.php'); self::loadPluginSourceAlways('includes/arrays.php');
self::loadPluginConstants(); self::loadPluginConstants();
} }
+1 -1
View File
@@ -29,7 +29,7 @@ final class OptionalCoreFunctionTest extends TestCase {
parent::setUp(); parent::setUp();
require_once dirname(__DIR__) . '/Fixtures/optional-core-functions.php'; require_once dirname(__DIR__) . '/Fixtures/optional-core-functions.php';
require_once dirname(__DIR__, 2) . '/thold_functions.php'; require_once dirname(__DIR__, 2) . '/includes/functions.php';
} }
/** /**
+1 -1
View File
@@ -194,7 +194,7 @@ $unmeasured_allowlist = [
// Web-page action handlers (thold_wizard() et al.) that read $_REQUEST // Web-page action handlers (thold_wizard() et al.) that read $_REQUEST
// and read/write the Cacti database directly; only meaningful inside a // and read/write the Cacti database directly; only meaningful inside a
// live authenticated request, not the isolated unit process. // live authenticated request, not the isolated unit process.
'thold_webapi.php', 'includes/webapi.php',
]; ];
$unmeasured = array_values(array_diff(array_keys($changed), array_keys($measured))); $unmeasured = array_values(array_diff(array_keys($changed), array_keys($measured)));
$unexpected_unmeasured = array_values(array_diff($unmeasured, $unmeasured_allowlist)); $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);
+3 -2
View File
@@ -72,7 +72,7 @@ if (!defined('POLLER_VERBOSITY_MEDIUM')) {
/* /*
* base_path has to point at the Cacti root two levels above this plugin: * base_path has to point at the Cacti root two levels above this plugin:
* thold_functions.php builds include paths from it at runtime. * includes/functions.php builds include paths from it at runtime.
*/ */
$GLOBALS['config'] = [ $GLOBALS['config'] = [
'base_path' => $cacti_root, 'base_path' => $cacti_root,
@@ -343,6 +343,7 @@ if (!function_exists('__esc')) {
if (!function_exists('cacti_log')) { if (!function_exists('cacti_log')) {
function cacti_log($message, $output = false, $environ = 'CMDPHP', $level = 0) { function cacti_log($message, $output = false, $environ = 'CMDPHP', $level = 0) {
CactiStubs::$log[] = $message; CactiStubs::$log[] = $message;
$GLOBALS['__test_cacti_log'][] = (string) $message;
CactiStubs::record('cacti_log', '', [$message, $output, $environ, $level]); CactiStubs::record('cacti_log', '', [$message, $output, $environ, $level]);
} }
} }
@@ -742,7 +743,7 @@ function thold_test_load($path) {
* thold_test_load(). * thold_test_load().
* *
* includes/arrays.php is also included with a plain include() (not * includes/arrays.php is also included with a plain include() (not
* include_once()) from several places in thold_functions.php itself (e.g. * include_once()) from several places in includes/functions.php itself (e.g.
* thold_log()), keyed off $config['base_path']. Once any test exercises one * thold_log()), keyed off $config['base_path']. Once any test exercises one
* of those call sites, PHP's include-once registry considers the file * of those call sites, PHP's include-once registry considers the file
* already included: a later thold_test_load() (require_once) on the same * already included: a later thold_test_load() (require_once) on the same
-25
View File
@@ -1,25 +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');
-25
View File
@@ -1,25 +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');
-25
View File
@@ -1,25 +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');
-25
View File
@@ -1,25 +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');
-25
View File
@@ -1,25 +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');
-25
View File
@@ -1,25 +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');
+2 -2
View File
@@ -36,9 +36,9 @@ include_once($config['base_path'] . '/lib/template.php');
include_once($config['base_path'] . '/lib/utility.php'); include_once($config['base_path'] . '/lib/utility.php');
include_once($config['base_path'] . '/lib/reports.php'); include_once($config['base_path'] . '/lib/reports.php');
include_once($config['base_path'] . '/lib/time.php'); include_once($config['base_path'] . '/lib/time.php');
include_once($config['base_path'] . '/plugins/thold/thold_webapi.php'); include_once($config['base_path'] . '/plugins/thold/includes/webapi.php');
include_once($config['base_path'] . '/plugins/thold/includes/arrays.php'); include_once($config['base_path'] . '/plugins/thold/includes/arrays.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
set_default_action(); set_default_action();
+1 -1
View File
@@ -41,7 +41,7 @@ chdir('../../');
include_once('./include/cli_check.php'); include_once('./include/cli_check.php');
include_once($config['base_path'] . '/lib/poller.php'); include_once($config['base_path'] . '/lib/poller.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
// install signal handlers for Linux/UNIX only // install signal handlers for Linux/UNIX only
if (function_exists('pcntl_signal')) { if (function_exists('pcntl_signal')) {
+1 -1
View File
@@ -28,7 +28,7 @@ chdir('../../');
include_once('./include/auth.php'); include_once('./include/auth.php');
require_once($config['base_path'] . '/lib/rrd.php'); require_once($config['base_path'] . '/lib/rrd.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['base_path'] . '/plugins/thold/setup.php'); include_once($config['base_path'] . '/plugins/thold/setup.php');
include_once($config['base_path'] . '/plugins/thold/includes/database.php'); include_once($config['base_path'] . '/plugins/thold/includes/database.php');
include($config['base_path'] . '/plugins/thold/includes/arrays.php'); include($config['base_path'] . '/plugins/thold/includes/arrays.php');
+1 -1
View File
@@ -38,7 +38,7 @@ chdir('../../');
require_once('./include/cli_check.php'); require_once('./include/cli_check.php');
require_once($config['base_path'] . '/lib/rrd.php'); require_once($config['base_path'] . '/lib/rrd.php');
require($config['base_path'] . '/plugins/thold/includes/arrays.php'); require($config['base_path'] . '/plugins/thold/includes/arrays.php');
require_once($config['base_path'] . '/plugins/thold/thold_functions.php'); require_once($config['base_path'] . '/plugins/thold/includes/functions.php');
require_once($config['library_path'] . '/snmp.php'); require_once($config['library_path'] . '/snmp.php');
require_once($config['base_path'] . '/lib/time.php'); require_once($config['base_path'] . '/lib/time.php');
+1 -1
View File
@@ -34,7 +34,7 @@ chdir('../../');
require_once('./include/cli_check.php'); require_once('./include/cli_check.php');
require_once($config['base_path'] . '/lib/rrd.php'); require_once($config['base_path'] . '/lib/rrd.php');
require($config['base_path'] . '/plugins/thold/includes/arrays.php'); require($config['base_path'] . '/plugins/thold/includes/arrays.php');
require_once($config['base_path'] . '/plugins/thold/thold_functions.php'); require_once($config['base_path'] . '/plugins/thold/includes/functions.php');
require_once($config['library_path'] . '/snmp.php'); require_once($config['library_path'] . '/snmp.php');
// install signal handlers for Linux/UNIX only // install signal handlers for Linux/UNIX only
+1 -1
View File
@@ -26,7 +26,7 @@ chdir('../../');
include_once('./include/auth.php'); include_once('./include/auth.php');
include_once($config['base_path'] . '/lib/reports.php'); include_once($config['base_path'] . '/lib/reports.php');
include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['base_path'] . '/plugins/thold/includes/arrays.php'); include_once($config['base_path'] . '/plugins/thold/includes/arrays.php');
include_once($config['base_path'] . '/lib/xml.php'); include_once($config['base_path'] . '/lib/xml.php');