diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 86f47c9f..cb897dac 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,182 +1,184 @@ -# GitHub Copilot Instructions - -## Priority Guidelines - -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+ -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 -4. **Architectural Consistency**: Maintain plugin-based architecture extending Cacti core -5. **Code Quality**: Prioritize security, maintainability, and compatibility in all generated code - -## Technology Stack - -### Core Technologies -- **PHP**: 8.1+ (CI matrix tests 8.1-8.4) -- **Platform**: Cacti Plugin Architecture (Cacti 1.2.25+) -- **Database**: MySQL/MariaDB with InnoDB engine -- **Alerting**: Email, Syslog, and SNMP Traps/Informs notification channels - -### Key Dependencies -- Cacti core framework (`api_plugin_*`, `db_*`) -- `CACTI-THOLD-MIB` for SNMP trap/inform definitions -- Optional: `gettext` for internationalization - -## Project Structure - -``` -thold/ # Repository root (install to plugins/thold/ in Cacti) -├── extras/ # Supplementary assets -├── includes/ # polling.php (poller hooks), settings.php (config UI), tab.php -├── service/ # systemd unit for thold_daemon -├── tests/ # Test suite (phpunit.xml) -├── themes/ # CSS theme overlays -├── cli_import.php / cli_thresholds.php # CLI threshold management utilities -├── notify_lists.php / notify_queue.php # Notification list/queue administration -├── thold.php # Main threshold administration UI -├── 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_process.php / thold_templates.php # Background processing / threshold templates -├── thold_webapi.php # Web API endpoints -├── poller_thold.php # Background poller entry point (CLI) -├── INFO # Plugin metadata (name, version, compat) -├── README.md -└── setup.php # Plugin install/uninstall/upgrade hooks -``` - -## Naming Conventions - -### Function Names -- **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_`. - -### Variables and Constants -- Access Cacti configuration via the global `$config` array; use `$config['base_path']` for absolute file paths. - -## Code Style - -### Indentation and Formatting -- **Tabs**: Use tabs (not spaces) for indentation throughout all PHP files. -- **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". - -## Security Standards - -### SQL Query Security -**ALWAYS use prepared statements** for database operations: - -```php -// CORRECT -db_execute_prepared($sql, $params); -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"); -``` - -### 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 -and returns it as a **string** -- it does not return an int, and it halts execution if the request -value is not numeric. Because of this, do NOT cast its output to `(int)` when the result is only -used for string output (e.g. `print`/`echo`, string concatenation, embedding in HTML/JS); the cast -is redundant. Only cast when the value is genuinely used in an integer/numeric context (e.g. -arithmetic, strict `===` comparisons). - -## 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. - -## Internationalization - -Use `__('String', 'thold')` for all user-facing strings to support internationalization. - -## Plugin Architecture - -### Data Flow -1. **Data Collection**: Cacti poller collects data. -2. **Interception**: `thold_poller_output()` (in `includes/polling.php`) receives the data. -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')`. - -### 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 - -1. Keep all new functions and globals under the single `thold_` prefix. -2. Prefer `db_*_prepared()` over string-concatenated SQL. -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 - -```php -// 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)); -``` - -## 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`. - -## 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. -- Prefer Cacti's `cacti_count()`/`cacti_sizeof()` wrappers over the raw `count()`/`sizeof()` builtins in new or edited code. - -## 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. +# GitHub Copilot Instructions + +## Priority Guidelines + +When generating code for this repository: + +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`) +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 +5. **Code Quality**: Prioritize security, maintainability, and compatibility in all generated code + +## Technology Stack + +### Core Technologies +- **PHP**: 8.1+ (CI matrix tests 8.1-8.4) +- **Platform**: Cacti Plugin Architecture (Cacti 1.2.29+) +- **Database**: MySQL/MariaDB with InnoDB engine +- **Alerting**: Email, Syslog, and SNMP Traps/Informs notification channels + +### Key Dependencies +- Cacti core framework (`api_plugin_*`, `db_*`) +- `CACTI-THOLD-MIB` for SNMP trap/inform definitions +- Optional: `gettext` for internationalization + +## Project Structure + +``` +thold/ # Repository root (install to plugins/thold/ in Cacti) +├── extras/ # Supplementary assets +├── 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 +├── tests/ # Test suite (phpunit.xml) +├── css/ # CSS theme overlays +├── cli_import.php / cli_thresholds.php # CLI threshold management utilities +├── notify_lists.php / notify_queue.php # Notification list/queue administration +├── thold.php # Main threshold administration UI +├── thold_daemon.php # Standalone high-scale daemon (bypasses poller hook) +├── thold_graph.php / thold_notify.php # Graph-threshold view / notification dispatch +├── thold_process.php / thold_templates.php # Background processing / threshold templates +├── poller_thold.php # Background poller entry point (CLI) +├── INFO # Plugin metadata (name, version, compat) +├── README.md +└── setup.php # Plugin install/uninstall/upgrade hooks +``` + +## Naming Conventions + +### Function Names +- **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_`. + +### Variables and Constants +- Access Cacti configuration via the global `$config` array; use `$config['base_path']` for absolute file paths. + +## Code Style + +### Indentation and Formatting +- **Tabs**: Use tabs (not spaces) for indentation throughout all PHP files. +- **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". + +## Security Standards + +### SQL Query Security +**ALWAYS use prepared statements** for database operations: + +```php +// CORRECT +db_execute_prepared($sql, $params); +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"); +``` + +### 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 +and returns it as a **string** -- it does not return an int, and it halts execution if the request +value is not numeric. Because of this, do NOT cast its output to `(int)` when the result is only +used for string output (e.g. `print`/`echo`, string concatenation, embedding in HTML/JS); the cast +is redundant. Only cast when the value is genuinely used in an integer/numeric context (e.g. +arithmetic, strict `===` comparisons). + +## 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. + +## Internationalization + +Use `__('String', 'thold')` for all user-facing strings to support internationalization. + +## Plugin Architecture + +### Data Flow +1. **Data Collection**: Cacti poller collects data. +2. **Interception**: `thold_poller_output()` (in `includes/polling.php`) receives the data. +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, `includes/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')`. + +### 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 + +1. Keep all new functions and globals under the single `thold_` prefix. +2. Prefer `db_*_prepared()` over string-concatenated SQL. +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 + +```php +// 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)); +``` + +## 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`. + +## 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. +- Prefer Cacti's `cacti_count()`/`cacti_sizeof()` wrappers over the raw `count()`/`sizeof()` builtins in new or edited code. + +## 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. - 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 - -- [Cacti DB Functions](https://github.com/Cacti/cacti/blob/1.2.x/lib/database.php) -- [Cacti Documentation](https://www.github.com/Cacti/documentation) -- `README.md` for feature descriptions -- `CHANGELOG.md` for version history - -## Security & Quality Conventions - -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: - -- **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 - 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 - `$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 - chain. -- **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 - final argument, except when deliberately comparing against a literal, untranslated Cacti-core - label. -- **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`. - Both are idempotent (safe no-ops when already applied), so the same call can run unconditionally - from both the install AND upgrade paths. -- **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 - 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. + +## References + +- [Cacti DB Functions](https://github.com/Cacti/cacti/blob/1.2.x/lib/database.php) +- [Cacti Documentation](https://www.github.com/Cacti/documentation) +- `README.md` for feature descriptions +- `CHANGELOG.md` for version history + +## Security & Quality Conventions + +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: + +- **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 + 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 + `$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 + chain. +- **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 + final argument, except when deliberately comparing against a literal, untranslated Cacti-core + label. +- **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`. + Both are idempotent (safe no-ops when already applied), so the same call can run unconditionally + from both the install AND upgrade paths. +- **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 + actual usage; don't change the function's real type-hints in the same pass (let static analysis + flag mismatches separately). Skip vendored third-party library files. + +## File manifest & upgrade pruning + +The plugin ships a root `manifest.json` with three arrays: `tombstones` (files/directories older versions shipped that have since moved or been removed), `expected` (the top-level files and directories that ship today, directories written with a trailing `/`), and `whitelist` (paths holding user data that must never be touched). Keep `expected` current: CI runs `tests/bin/validate-manifest.php`, which fails on any drift between `expected` and the real top-level tree (it ignores `tests/`, `phpunit.xml`, `.git*`, `.md*`, and whitelisted paths). Custom customer CSS/theme files belong in `expected`, and stylesheets live in `css/` (not `themes/`). On upgrade, `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. diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 9834da12..0998da03 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -89,6 +89,9 @@ jobs: - name: Check PHP version 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 run: sudo apt-get update diff --git a/.mdlrc b/.mdlrc deleted file mode 100644 index 42c9b1d2..00000000 --- a/.mdlrc +++ /dev/null @@ -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 diff --git a/INFO b/INFO index 7099aeb2..51e407fe 100644 --- a/INFO +++ b/INFO @@ -26,5 +26,5 @@ longname = Thresholds author = The Cacti Group email = 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 diff --git a/README.md b/README.md index a2676a09..7642c599 100644 --- a/README.md +++ b/README.md @@ -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 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 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 diff --git a/cli_import.php b/cli_import.php index 933f20fa..29737369 100644 --- a/cli_import.php +++ b/cli_import.php @@ -29,7 +29,7 @@ include(__DIR__ . '/../../include/cli_check.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 $force = false; diff --git a/cli_thresholds.php b/cli_thresholds.php index 903fca29..77ce6b99 100644 --- a/cli_thresholds.php +++ b/cli_thresholds.php @@ -35,7 +35,7 @@ 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 $force = false; diff --git a/themes/classic/main.css b/css/classic.css similarity index 100% rename from themes/classic/main.css rename to css/classic.css diff --git a/themes/dark/main.css b/css/dark.css similarity index 100% rename from themes/dark/main.css rename to css/dark.css diff --git a/themes/classic/index.php b/css/index.php similarity index 100% rename from themes/classic/index.php rename to css/index.php diff --git a/themes/midwinter/main.css b/css/midwinter.css similarity index 100% rename from themes/midwinter/main.css rename to css/midwinter.css diff --git a/themes/modern/main.css b/css/modern.css similarity index 100% rename from themes/modern/main.css rename to css/modern.css diff --git a/themes/paper-plane/main.css b/css/paper-plane.css similarity index 100% rename from themes/paper-plane/main.css rename to css/paper-plane.css diff --git a/themes/paw/main.css b/css/paw.css similarity index 100% rename from themes/paw/main.css rename to css/paw.css diff --git a/themes/sunrise/main.css b/css/sunrise.css similarity index 100% rename from themes/sunrise/main.css rename to css/sunrise.css diff --git a/includes/database.php b/includes/database.php index 34b2e700..8c270b5d 100644 --- a/includes/database.php +++ b/includes/database.php @@ -50,7 +50,7 @@ function thold_upgrade_database($force = false) { thold_setup_database(); 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(); diff --git a/thold_functions.php b/includes/functions.php similarity index 99% rename from thold_functions.php rename to includes/functions.php index 15f7a4b1..a616a234 100644 --- a/thold_functions.php +++ b/includes/functions.php @@ -482,12 +482,12 @@ function thold_rpn_math_binary($operator, $v2, $v1) { case '/': return $v2 / $v1; case '%': - return $v2 % $v1; + return (int) $v2 % (int) $v1; case '^': // Bitwise XOR, not exponentiation: eval('$v3 = ' . $v2 . ' ^ ' . $v1 . ';') // always computed XOR (PHP's ^ operator), and existing user thresholds // rely on that. See TholdExpressionMathRpnTest::testCaretOperatorIsIntegerXorNotExponentiation. - return $v2 ^ $v1; + return (int) $v2 ^ (int) $v1; default: cacti_log("ERROR: RPN unknown binary operator '$operator'", false, 'THOLD'); $rpn_error = true; diff --git a/includes/polling.php b/includes/polling.php index eab9f1cf..f6d8c313 100644 --- a/includes/polling.php +++ b/includes/polling.php @@ -116,7 +116,7 @@ function thold_cleanup_log() { function thold_poller_output(&$rrd_update_array) { 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'); $rrd_reindexed = []; @@ -338,7 +338,7 @@ function thold_check_all_thresholds() { global $config; 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'); if (read_config_option('remote_storage_method') == 1) { @@ -461,7 +461,7 @@ function thold_update_host_status() { 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'); if (api_plugin_is_enabled('maint')) { diff --git a/includes/settings.php b/includes/settings.php index 7ff858b9..a82d89d2 100644 --- a/includes/settings.php +++ b/includes/settings.php @@ -466,7 +466,7 @@ function thold_config_settings() { 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/thold_functions.php'); + include_once($config['base_path'] . '/plugins/thold/includes/functions.php'); include_once($config['base_path'] . '/plugins/thold/includes/database.php'); // check to see if there is an upgrade required diff --git a/thold_webapi.php b/includes/webapi.php similarity index 99% rename from thold_webapi.php rename to includes/webapi.php index a4afab8a..78c4c23c 100644 --- a/thold_webapi.php +++ b/includes/webapi.php @@ -39,7 +39,7 @@ function thold_add_graphs_action_execute() { 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'); $local_graph_id = get_filter_request_var('local_graph_id'); diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 6245e968..913cc0c2 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cacti \n" "Report-Msgid-Bugs-To: developers@cacti.net\n" -"POT-Creation-Date: 2026-09-28 16:08-0400\n" +"POT-Creation-Date: 2026-09-30 21:27-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Cacti Developers >\n" "Language-Team: Cacti Developers \n" @@ -236,7 +236,7 @@ msgstr "" msgid "Debug" msgstr "" -#: includes/arrays.php thold_functions.php +#: includes/arrays.php includes/functions.php msgid "Never" msgstr "" @@ -509,14 +509,293 @@ msgstr "" msgid "> %d %%% of Devices in a Site" msgstr "" -#: includes/polling.php -msgid "SNMP not in use" +#: includes/functions.php +msgid "< 1 Minute" +msgstr "" + +#: includes/functions.php +msgid "Since Created" +msgstr "" + +#: includes/functions.php includes/settings.php notify_lists.php thold.php +#: thold_graph.php thold_templates.php +msgid "Thresholds" +msgstr "" + +#: includes/functions.php +msgid "Log" +msgstr "" + +#: includes/functions.php includes/settings.php thold_graph.php +msgid "Device Status" +msgstr "" + +#: includes/functions.php +msgid "Time:" +msgstr "" + +#: includes/functions.php +msgid "User:" +msgstr "" + +#: includes/functions.php +msgid "System:" +msgstr "" + +#: includes/functions.php +msgid "Swaps:" +msgstr "" + +#: includes/functions.php +msgid "Pages:" +msgstr "" + +#: includes/functions.php +msgid "Only logging, maint device" +msgstr "" + +#: includes/functions.php notify_lists.php notify_queue.php thold.php +#: thold_graph.php thold_templates.php +msgid "Default" +msgstr "" + +#: includes/functions.php +msgid "Link to Graph in Cacti" +msgstr "" + +#: includes/functions.php +msgid "An alert has been issued that requires your attention.

Device: ()
URL:
Message:

" +msgstr "" + +#: includes/functions.php +msgid "A warning has been issued that requires your attention.

Device: ()
URL:
Message:

" +msgstr "" + +#: includes/functions.php +msgid "A Threshold has returned to normal status.

Device: ()
URL:
Message:

" +msgstr "" + +#: includes/functions.php +#, php-format +msgid "%% Dev [TIP:%s]" +msgstr "" + +#: includes/functions.php +#, php-format +msgid "Abs Val [TIP:%s]" +msgstr "" + +#: includes/functions.php +msgid "%%% Dev [AOT:%s]" +msgstr "" + +#: includes/functions.php +#, php-format +msgid "Abs Val [AOT:%s]" +msgstr "" + +#: includes/functions.php +msgid "The Device ID was not set while trying to create Graph and Threshold" +msgstr "" + +#: includes/functions.php +msgid "The Threshold Template ID was not found while trying to create Graph and Threshold" +msgstr "" + +#: includes/functions.php +msgid "The Threshold Template ID was not set while trying to create Graph and Threshold" +msgstr "" + +#: includes/functions.php +msgid "The Graph Creation failed for Threshold Template" +msgstr "" + +#: includes/functions.php +msgid "Threshold was Autocreated due to Device Template mapping" +msgstr "" + +#: includes/functions.php +msgid "Permission Denied" +msgstr "" + +#: includes/functions.php +msgid "Record Updated" +msgstr "" + +#: includes/functions.php +#, php-format +msgid "Failed to find linked Graph Template Item '%d' on Threshold '%d'" +msgstr "" + +#: includes/functions.php +#, php-format +msgid "Created Threshold: %s" +msgstr "" + +#: includes/functions.php +msgid "You must specify either 'High Alert Threshold' or 'Low Alert Threshold' or both!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: 'High Alert Threshold' smaller than the 'Low Alert Threshold'!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: 'High Warning Threshold' smaller than the 'Low Warning Threshold'!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: 'High Warning Threshold' larger than the 'High Alert Threshold'!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: 'Low Alert Threshold' larger than the 'Low Warning Threshold'!" +msgstr "" + +#: includes/functions.php +msgid "Time reference in the past must be set to positive integer value!" +msgstr "" + +#: includes/functions.php +msgid "You must specify either 'Baseline Deviation UP' or 'Baseline Deviation DOWN' or both!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: 'High Alert Threshold' smaller than the 'Low Alert Threshold'" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: The Alert Trigger Count must be greater or eval to 1!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: The Alert Trigger Count must be less than or equal to the Trigger Count or eval to 1!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: The Warning Trigger Count must be greater or eval to 1!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: The Warning Trigger Count must be less than or equal to the Trigger Count or eval to 1!" +msgstr "" + +#: includes/functions.php +msgid "Impossible threshold: 'Low Warning Threshold' smaller than the 'Low Alert Threshold'!" +msgstr "" + +#: includes/functions.php +msgid "Unknown Threshold Type!" +msgstr "" + +#: includes/functions.php +msgid "No Thresholds Templates associated with the Device's Template." +msgstr "" + +#: includes/functions.php setup.php +msgid "No Threshold(s) Created. Either they already exist, or no suitable matches found." +msgstr "" + +#: includes/functions.php +msgid "Mailer Error: No TO address set!!
If using the Test Mail link, please set the Alert Email setting." +msgstr "" + +#: includes/functions.php +msgid "Done" +msgstr "" + +#: includes/functions.php +msgid "Pending" +msgstr "" + +#: includes/functions.php +msgid "Errored" msgstr "" -#: includes/polling.php thold.php thold_functions.php thold_graph.php +#: includes/functions.php includes/polling.php thold.php thold_graph.php msgid "N/A" msgstr "" +#: includes/functions.php +msgid "Success" +msgstr "" + +#: includes/functions.php thold_graph.php +msgid "Acknowledge Threshold" +msgstr "" + +#: includes/functions.php +msgid "Click 'Continue' to Acknowledge the following Threshold(s)." +msgstr "" + +#: includes/functions.php thold.php +msgid "Acknowledge Threshold(s)" +msgstr "" + +#: includes/functions.php notify_lists.php notify_queue.php setup.php thold.php +#: thold_templates.php +msgid "Cancel" +msgstr "" + +#: includes/functions.php notify_lists.php notify_queue.php setup.php thold.php +#: thold_templates.php +msgid "Continue" +msgstr "" + +#: includes/functions.php +msgid "Threshold Template Subordinate Data Template Not Found!" +msgstr "" + +#: includes/functions.php +msgid "Threshold Template Subordinate Data Source Not Found!" +msgstr "" + +#: includes/functions.php +msgid "Threshold Template import columns do not match the database schema" +msgstr "" + +#: includes/functions.php +msgid "Imported" +msgstr "" + +#: includes/functions.php +#, php-format +msgid "Threshold Template '%s' %s!" +msgstr "" + +#: includes/functions.php +msgid "Updated" +msgstr "" + +#: includes/functions.php thold_templates.php +msgid "Import" +msgstr "" + +#: includes/functions.php +#, php-format +msgid "Threshold Template '%s' %s Failed!" +msgstr "" + +#: includes/functions.php +msgid "Update" +msgstr "" + +#: includes/functions.php +msgid "Errors encountered while attempting to import Threshold Template data." +msgstr "" + +#: includes/functions.php +msgid "Threshold Template Import data was not found to be XML data." +msgstr "" + +#: includes/functions.php +msgid "Threshold Template Import data was not correct while importing Threshold Template." +msgstr "" + +#: includes/polling.php +msgid "SNMP not in use" +msgstr "" + #: includes/polling.php msgid "Devices Notice: () returned from DOWN state" msgstr "" @@ -537,11 +816,6 @@ msgstr "" msgid "System Error : () is
Site:
Location:
Reason:

Average system response : ms
System availability:
Total Checks Since Clear:
Total Failed Checks:
Last Date Checked DOWN :
Devices Previously UP for:
NOTE: " msgstr "" -#: includes/settings.php notify_lists.php thold.php thold_functions.php -#: thold_graph.php thold_templates.php -msgid "Thresholds" -msgstr "" - #: includes/settings.php msgid "(actions)" msgstr "" @@ -562,10 +836,6 @@ msgstr "" msgid "Threshold Logs" msgstr "" -#: includes/settings.php thold_functions.php thold_graph.php -msgid "Device Status" -msgstr "" - #: includes/settings.php msgid "Thresholds - Failures" msgstr "" @@ -1235,51 +1505,210 @@ msgstr "" msgid "Thold" msgstr "" -#: notify_lists.php -msgid "Duplicate" +#: includes/webapi.php +msgid "Threshold(s) Already Exists - No Thresholds Created" msgstr "" -#: notify_lists.php -msgid "Associate" +#: includes/webapi.php setup.php +msgid "Create Threshold from Template" msgstr "" -#: notify_lists.php -msgid "Disassociate" +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Select a Threshold Type ]" msgstr "" -#: notify_lists.php thold_graph.php -msgid "Devices" +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Enter Custom Data and press 'Create' to Create your Threshold and Graph ]" msgstr "" -#: notify_lists.php -#, php-format -msgid "ERROR: You can not have your Email in both the To and BCC lines. The Email below is invalid!

%s" +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Select Available Data Query Rows ]" msgstr "" -#: notify_lists.php -msgid "Click 'Continue' to Delete Notification Lists(s). Any Device(s) or Threshold(s) associated with the List(s) will be reverted to the default." +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Select a Device ]" msgstr "" -#: notify_lists.php notify_queue.php setup.php thold.php thold_functions.php -#: thold_templates.php -msgid "Cancel" +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Select a Graph Template ]" msgstr "" -#: notify_lists.php notify_queue.php setup.php thold.php thold_functions.php -#: thold_templates.php -msgid "Continue" +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Select a Threshold Template ]" msgstr "" -#: notify_lists.php -msgid "Delete Notification List(s)" +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Press 'Create' to Create your Threshold ]" msgstr "" -#: notify_lists.php -msgid "Click 'Continue' to Duplicate the following Notification List(s)." +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Select a Data Source ]" msgstr "" -#: notify_lists.php -msgid "New List Name" +#: includes/webapi.php +msgid "Threshold Creation Wizard [ Select a Graph ]" +msgstr "" + +#: includes/webapi.php +msgid "Threshold Creation Criteria" +msgstr "" + +#: includes/webapi.php +msgid "Create Type" +msgstr "" + +#: includes/webapi.php +msgid "Select a Threshold Type to use for creating this Threshold." +msgstr "" + +#: includes/webapi.php +msgid "Select a Threshold Type" +msgstr "" + +#: includes/webapi.php +msgid "Non Templated" +msgstr "" + +#: includes/webapi.php +msgid "Threshold Template" +msgstr "" + +#: includes/webapi.php +msgid "Select a Threshold Template that the Graph and Threshold will be based upon." +msgstr "" + +#: includes/webapi.php setup.php +msgid "Select a Threshold Template" +msgstr "" + +#: includes/webapi.php +msgid "Graph Template" +msgstr "" + +#: includes/webapi.php +msgid "Select a Graph Template to use for the Graph to be created." +msgstr "" + +#: includes/webapi.php +msgid "Select a Graph Template" +msgstr "" + +#: includes/webapi.php thold_graph.php +msgid "Device" +msgstr "" + +#: includes/webapi.php +msgid "Select a Device to use for the Threshold and Graph to be created." +msgstr "" + +#: includes/webapi.php +msgid "Select a Device" +msgstr "" + +#: includes/webapi.php +msgid "Select an Available Item" +msgstr "" + +#: includes/webapi.php +msgid "Data Query Item" +msgstr "" + +#: includes/webapi.php +msgid "Select the applicable row from the Data Query for the Graph and Threshold." +msgstr "" + +#: includes/webapi.php +msgid "Creation Notes" +msgstr "" + +#: includes/webapi.php +msgid "Important Note:" +msgstr "" + +#: includes/webapi.php +msgid "This Threshold will be Templated. When using the Threshold Template option, you will be prompted for a Threshold Template, Graph Template, Device and possibly Data Query Item information before receiving the 'Create' prompt at which time, if any overridable Graph or Data Source information is allowed at the Graph and Data Source Template level, you will be prompted for it. Then, by pressing the 'Create' button, both the Graph and Threshold will be created simultaneously." +msgstr "" + +#: includes/webapi.php +msgid "Graph" +msgstr "" + +#: includes/webapi.php +msgid "Select the Graph for the Threshold." +msgstr "" + +#: includes/webapi.php +msgid "Select a Graph" +msgstr "" + +#: includes/webapi.php thold_templates.php +msgid "Data Source" +msgstr "" + +#: includes/webapi.php +msgid "Select a Data Source for the Threshold." +msgstr "" + +#: includes/webapi.php +msgid "Select a Data Source" +msgstr "" + +#: includes/webapi.php thold_templates.php +msgid "Create" +msgstr "" + +#: includes/webapi.php +msgid "This Threshold will NOT be Templated and will only work on existing Graphs. If you wish to both Create the Graph and the Threshold simultaneously, select Threshold Template from the drop down and continue until the 'Create' button appears." +msgstr "" + +#: includes/webapi.php +msgid "This Threshold will NOT be Templated. You can select either By Graph where you will then select an existing Device, Graph and Data Source before creating your Threshold, or you can select Threshold Template which will allow you to create a Non Templated Threshold and corresponding Graph simultaneously" +msgstr "" + +#: includes/webapi.php +msgid "Selected Graph" +msgstr "" + +#: includes/webapi.php +#, php-format +msgid "Created graph: %s" +msgstr "" + +#: notify_lists.php +msgid "Duplicate" +msgstr "" + +#: notify_lists.php +msgid "Associate" +msgstr "" + +#: notify_lists.php +msgid "Disassociate" +msgstr "" + +#: notify_lists.php thold_graph.php +msgid "Devices" +msgstr "" + +#: notify_lists.php +#, php-format +msgid "ERROR: You can not have your Email in both the To and BCC lines. The Email below is invalid!

%s" +msgstr "" + +#: notify_lists.php +msgid "Click 'Continue' to Delete Notification Lists(s). Any Device(s) or Threshold(s) associated with the List(s) will be reverted to the default." +msgstr "" + +#: notify_lists.php +msgid "Delete Notification List(s)" +msgstr "" + +#: notify_lists.php +msgid "Click 'Continue' to Duplicate the following Notification List(s)." +msgstr "" + +#: notify_lists.php +msgid "New List Name" msgstr "" #: notify_lists.php @@ -1464,11 +1893,6 @@ msgstr "" msgid "Device Template" msgstr "" -#: notify_lists.php notify_queue.php thold.php thold_functions.php -#: thold_graph.php thold_templates.php -msgid "Default" -msgstr "" - #: notify_lists.php msgid "Associated" msgstr "" @@ -1865,10 +2289,6 @@ msgstr "" msgid "Created %s thresholds" msgstr "" -#: setup.php thold_functions.php -msgid "No Threshold(s) Created. Either they already exist, or no suitable matches found." -msgstr "" - #: setup.php msgid "No Threshold(s) Created. Threshold(s) Template not found." msgstr "" @@ -1885,18 +2305,10 @@ msgstr "" msgid "Available Threshold Templates" msgstr "" -#: setup.php thold_webapi.php -msgid "Select a Threshold Template" -msgstr "" - #: setup.php msgid "There are no Threshold Templates associated with the following Data Sources" msgstr "" -#: setup.php thold_webapi.php -msgid "Create Threshold from Template" -msgstr "" - #: setup.php msgid "The following Graphs have no Threshold Templates associated with them" msgstr "" @@ -2014,10 +2426,6 @@ msgstr "" msgid "Click 'Continue' to Acknowledge the following Threshold(s). Thresholds that do not allows this, or that are not triggered, will be ignored." msgstr "" -#: thold.php thold_functions.php -msgid "Acknowledge Threshold(s)" -msgstr "" - #: thold.php msgid "Click 'Continue' to Resume Notifications for the following Threshold(s). Thresholds that do not allows this, or that are not triggered will be ignored." msgstr "" @@ -2763,253 +3171,6 @@ msgstr "" msgid "All Users Selected" msgstr "" -#: thold_functions.php -msgid "< 1 Minute" -msgstr "" - -#: thold_functions.php -msgid "Since Created" -msgstr "" - -#: thold_functions.php -msgid "Log" -msgstr "" - -#: thold_functions.php -msgid "Time:" -msgstr "" - -#: thold_functions.php -msgid "User:" -msgstr "" - -#: thold_functions.php -msgid "System:" -msgstr "" - -#: thold_functions.php -msgid "Swaps:" -msgstr "" - -#: thold_functions.php -msgid "Pages:" -msgstr "" - -#: thold_functions.php -msgid "Only logging, maint device" -msgstr "" - -#: thold_functions.php -msgid "Link to Graph in Cacti" -msgstr "" - -#: thold_functions.php -msgid "An alert has been issued that requires your attention.

Device: ()
URL:
Message:

" -msgstr "" - -#: thold_functions.php -msgid "A warning has been issued that requires your attention.

Device: ()
URL:
Message:

" -msgstr "" - -#: thold_functions.php -msgid "A Threshold has returned to normal status.

Device: ()
URL:
Message:

" -msgstr "" - -#: thold_functions.php -#, php-format -msgid "%% Dev [TIP:%s]" -msgstr "" - -#: thold_functions.php -#, php-format -msgid "Abs Val [TIP:%s]" -msgstr "" - -#: thold_functions.php -msgid "%%% Dev [AOT:%s]" -msgstr "" - -#: thold_functions.php -#, php-format -msgid "Abs Val [AOT:%s]" -msgstr "" - -#: thold_functions.php -msgid "The Device ID was not set while trying to create Graph and Threshold" -msgstr "" - -#: thold_functions.php -msgid "The Threshold Template ID was not found while trying to create Graph and Threshold" -msgstr "" - -#: thold_functions.php -msgid "The Threshold Template ID was not set while trying to create Graph and Threshold" -msgstr "" - -#: thold_functions.php -msgid "The Graph Creation failed for Threshold Template" -msgstr "" - -#: thold_functions.php -msgid "Threshold was Autocreated due to Device Template mapping" -msgstr "" - -#: thold_functions.php -msgid "Permission Denied" -msgstr "" - -#: thold_functions.php -msgid "Record Updated" -msgstr "" - -#: thold_functions.php -#, php-format -msgid "Failed to find linked Graph Template Item '%d' on Threshold '%d'" -msgstr "" - -#: thold_functions.php -#, php-format -msgid "Created Threshold: %s" -msgstr "" - -#: thold_functions.php -msgid "You must specify either 'High Alert Threshold' or 'Low Alert Threshold' or both!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: 'High Alert Threshold' smaller than the 'Low Alert Threshold'!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: 'High Warning Threshold' smaller than the 'Low Warning Threshold'!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: 'High Warning Threshold' larger than the 'High Alert Threshold'!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: 'Low Alert Threshold' larger than the 'Low Warning Threshold'!" -msgstr "" - -#: thold_functions.php -msgid "Time reference in the past must be set to positive integer value!" -msgstr "" - -#: thold_functions.php -msgid "You must specify either 'Baseline Deviation UP' or 'Baseline Deviation DOWN' or both!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: 'High Alert Threshold' smaller than the 'Low Alert Threshold'" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: The Alert Trigger Count must be greater or eval to 1!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: The Alert Trigger Count must be less than or equal to the Trigger Count or eval to 1!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: The Warning Trigger Count must be greater or eval to 1!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: The Warning Trigger Count must be less than or equal to the Trigger Count or eval to 1!" -msgstr "" - -#: thold_functions.php -msgid "Impossible threshold: 'Low Warning Threshold' smaller than the 'Low Alert Threshold'!" -msgstr "" - -#: thold_functions.php -msgid "Unknown Threshold Type!" -msgstr "" - -#: thold_functions.php -msgid "No Thresholds Templates associated with the Device's Template." -msgstr "" - -#: thold_functions.php -msgid "Mailer Error: No TO address set!!
If using the Test Mail link, please set the Alert Email setting." -msgstr "" - -#: thold_functions.php -msgid "Done" -msgstr "" - -#: thold_functions.php -msgid "Pending" -msgstr "" - -#: thold_functions.php -msgid "Errored" -msgstr "" - -#: thold_functions.php -msgid "Success" -msgstr "" - -#: thold_functions.php thold_graph.php -msgid "Acknowledge Threshold" -msgstr "" - -#: thold_functions.php -msgid "Click 'Continue' to Acknowledge the following Threshold(s)." -msgstr "" - -#: thold_functions.php -msgid "Threshold Template Subordinate Data Template Not Found!" -msgstr "" - -#: thold_functions.php -msgid "Threshold Template Subordinate Data Source Not Found!" -msgstr "" - -#: thold_functions.php -msgid "Threshold Template import columns do not match the database schema" -msgstr "" - -#: thold_functions.php -msgid "Imported" -msgstr "" - -#: thold_functions.php -#, php-format -msgid "Threshold Template '%s' %s!" -msgstr "" - -#: thold_functions.php -msgid "Updated" -msgstr "" - -#: thold_functions.php thold_templates.php -msgid "Import" -msgstr "" - -#: thold_functions.php -#, php-format -msgid "Threshold Template '%s' %s Failed!" -msgstr "" - -#: thold_functions.php -msgid "Update" -msgstr "" - -#: thold_functions.php -msgid "Errors encountered while attempting to import Threshold Template data." -msgstr "" - -#: thold_functions.php -msgid "Threshold Template Import data was not found to be XML data." -msgstr "" - -#: thold_functions.php -msgid "Threshold Template Import data was not correct while importing Threshold Template." -msgstr "" - #: thold_graph.php msgid "The Threshold has been Ascknowledged." msgstr "" @@ -3179,10 +3340,6 @@ msgstr "" msgid "Log Entries" msgstr "" -#: thold_graph.php thold_webapi.php -msgid "Device" -msgstr "" - #: thold_graph.php msgid "Time" msgstr "" @@ -3256,10 +3413,6 @@ msgstr "" msgid "Threshold Template Creation Wizard" msgstr "" -#: thold_templates.php thold_webapi.php -msgid "Data Source" -msgstr "" - #: thold_templates.php msgid "Please select a Data Template" msgstr "" @@ -3272,10 +3425,6 @@ msgstr "" msgid "Please press 'Create' to create your Threshold Template" msgstr "" -#: thold_templates.php thold_webapi.php -msgid "Create" -msgstr "" - #: thold_templates.php msgid "The Threshold Template requested can not be found" msgstr "" @@ -3557,152 +3706,3 @@ msgstr "" #: thold_templates.php msgid "ERROR:" msgstr "" - -#: thold_webapi.php -msgid "Threshold(s) Already Exists - No Thresholds Created" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Select a Threshold Type ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Enter Custom Data and press 'Create' to Create your Threshold and Graph ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Select Available Data Query Rows ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Select a Device ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Select a Graph Template ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Select a Threshold Template ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Press 'Create' to Create your Threshold ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Select a Data Source ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Wizard [ Select a Graph ]" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Creation Criteria" -msgstr "" - -#: thold_webapi.php -msgid "Create Type" -msgstr "" - -#: thold_webapi.php -msgid "Select a Threshold Type to use for creating this Threshold." -msgstr "" - -#: thold_webapi.php -msgid "Select a Threshold Type" -msgstr "" - -#: thold_webapi.php -msgid "Non Templated" -msgstr "" - -#: thold_webapi.php -msgid "Threshold Template" -msgstr "" - -#: thold_webapi.php -msgid "Select a Threshold Template that the Graph and Threshold will be based upon." -msgstr "" - -#: thold_webapi.php -msgid "Graph Template" -msgstr "" - -#: thold_webapi.php -msgid "Select a Graph Template to use for the Graph to be created." -msgstr "" - -#: thold_webapi.php -msgid "Select a Graph Template" -msgstr "" - -#: thold_webapi.php -msgid "Select a Device to use for the Threshold and Graph to be created." -msgstr "" - -#: thold_webapi.php -msgid "Select a Device" -msgstr "" - -#: thold_webapi.php -msgid "Select an Available Item" -msgstr "" - -#: thold_webapi.php -msgid "Data Query Item" -msgstr "" - -#: thold_webapi.php -msgid "Select the applicable row from the Data Query for the Graph and Threshold." -msgstr "" - -#: thold_webapi.php -msgid "Creation Notes" -msgstr "" - -#: thold_webapi.php -msgid "Important Note:" -msgstr "" - -#: thold_webapi.php -msgid "This Threshold will be Templated. When using the Threshold Template option, you will be prompted for a Threshold Template, Graph Template, Device and possibly Data Query Item information before receiving the 'Create' prompt at which time, if any overridable Graph or Data Source information is allowed at the Graph and Data Source Template level, you will be prompted for it. Then, by pressing the 'Create' button, both the Graph and Threshold will be created simultaneously." -msgstr "" - -#: thold_webapi.php -msgid "Graph" -msgstr "" - -#: thold_webapi.php -msgid "Select the Graph for the Threshold." -msgstr "" - -#: thold_webapi.php -msgid "Select a Graph" -msgstr "" - -#: thold_webapi.php -msgid "Select a Data Source for the Threshold." -msgstr "" - -#: thold_webapi.php -msgid "Select a Data Source" -msgstr "" - -#: thold_webapi.php -msgid "This Threshold will NOT be Templated and will only work on existing Graphs. If you wish to both Create the Graph and the Threshold simultaneously, select Threshold Template from the drop down and continue until the 'Create' button appears." -msgstr "" - -#: thold_webapi.php -msgid "This Threshold will NOT be Templated. You can select either By Graph where you will then select an existing Device, Graph and Data Source before creating your Threshold, or you can select Threshold Template which will allow you to create a Non Templated Threshold and corresponding Graph simultaneously" -msgstr "" - -#: thold_webapi.php -msgid "Selected Graph" -msgstr "" - -#: thold_webapi.php -#, php-format -msgid "Created graph: %s" -msgstr "" diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..7728d6b2 --- /dev/null +++ b/manifest.json @@ -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": [] +} diff --git a/notify_lists.php b/notify_lists.php index ac0cb2da..7c3a4f08 100644 --- a/notify_lists.php +++ b/notify_lists.php @@ -25,7 +25,7 @@ chdir('../..'); include('./include/auth.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'); $actions = [ diff --git a/notify_queue.php b/notify_queue.php index 2a7e7860..7949e22e 100644 --- a/notify_queue.php +++ b/notify_queue.php @@ -26,7 +26,7 @@ include_once('./include/auth.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/includes/database.php'); include($config['base_path'] . '/plugins/thold/includes/arrays.php'); diff --git a/phpunit.xml b/phpunit.xml index bc33485b..6ed6b04b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -24,7 +24,7 @@ Pest reads this PHPUnit-compatible configuration through Cacti's 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 effects); every other file includes ./include/auth.php and dispatches on the request at top level. tests/Integration/PluginLifecycleTest.php @@ -32,7 +32,7 @@ --> - thold_functions.php + includes/functions.php includes/polling.php setup.php diff --git a/poller_thold.php b/poller_thold.php index 845f9d5c..c13b030c 100644 --- a/poller_thold.php +++ b/poller_thold.php @@ -35,7 +35,7 @@ include('./include/cli_check.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'] . '/lib/rrd.php'); include_once($config['base_path'] . '/lib/poller.php'); diff --git a/setup.php b/setup.php index e197d49c..074dd48f 100644 --- a/setup.php +++ b/setup.php @@ -211,6 +211,7 @@ function plugin_thold_upgrade() { if ($current != $old) { plugin_thold_install(true); + thold_prune_files(); } return true; @@ -401,7 +402,7 @@ function thold_multiexplode($delimiters, $string) { function thold_rrd_graph_graph_options($g) { 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 $needles = []; @@ -830,7 +831,7 @@ function thold_device_action_execute($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')); @@ -859,7 +860,7 @@ function thold_device_action_execute($action) { function thold_api_device_new($save) { 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 (!empty($save['id'])) { @@ -943,7 +944,7 @@ function thold_api_device_save($save) { WHERE 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['disabled'] != $result['disabled']) { @@ -1120,7 +1121,7 @@ function thold_user_admin_setup_sql_save($save) { function thold_data_source_action_execute($action) { 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') { $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) { 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') { $selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items')); @@ -1526,8 +1527,8 @@ function thold_graphs_action_array($action) { * @return void */ 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. - require_once(__DIR__ . '/thold_functions.php'); + // This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives. + require_once(__DIR__ . '/includes/functions.php'); ?> toBeTrue(); 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; + } +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index cb46b047..81f8dd1c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -63,7 +63,7 @@ protected static function loadPluginSource($file) { * Load a plugin source file that only assigns file-scope variables (e.g. * 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 * exercises one of those call sites, a later loadPluginSource() of the * same file silently no-ops (require_once sees it as already included) diff --git a/tests/Unit/CspNonceTest.php b/tests/Unit/CspNonceTest.php index 0b224ed8..c42e5bda 100644 --- a/tests/Unit/CspNonceTest.php +++ b/tests/Unit/CspNonceTest.php @@ -25,7 +25,7 @@ /* * 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 * guard against the helper regressing back into a file the pages do not load. * @@ -38,7 +38,7 @@ */ 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 () { @@ -56,10 +56,10 @@ }); 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(); - // 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. $bootstrap = realpath(__DIR__ . '/../bootstrap-unit.php'); diff --git a/tests/Unit/GetAllowedThresholdsTest.php b/tests/Unit/GetAllowedThresholdsTest.php index f62796ab..4e41e268 100644 --- a/tests/Unit/GetAllowedThresholdsTest.php +++ b/tests/Unit/GetAllowedThresholdsTest.php @@ -27,7 +27,7 @@ final class GetAllowedThresholdsTest extends TestCase { * @return void */ public static function setUpBeforeClass(): void { - self::loadPluginSource('thold_functions.php'); + self::loadPluginSource('includes/functions.php'); } /** diff --git a/tests/Unit/GetCurrentValueTest.php b/tests/Unit/GetCurrentValueTest.php index e677a7d5..12b586c9 100644 --- a/tests/Unit/GetCurrentValueTest.php +++ b/tests/Unit/GetCurrentValueTest.php @@ -26,7 +26,7 @@ final class GetCurrentValueTest extends TestCase { * @return void */ public static function setUpBeforeClass(): void { - self::loadPluginSource('thold_functions.php'); + self::loadPluginSource('includes/functions.php'); } /** diff --git a/tests/Unit/HookFunctionsIncludeTest.php b/tests/Unit/HookFunctionsIncludeTest.php new file mode 100644 index 00000000..a105b442 --- /dev/null +++ b/tests/Unit/HookFunctionsIncludeTest.php @@ -0,0 +1,63 @@ + 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(); +}); diff --git a/tests/Unit/NotificationEmailDeduplicationTest.php b/tests/Unit/NotificationEmailDeduplicationTest.php index 97612bb4..6e2606b3 100644 --- a/tests/Unit/NotificationEmailDeduplicationTest.php +++ b/tests/Unit/NotificationEmailDeduplicationTest.php @@ -15,7 +15,7 @@ */ beforeAll(function() { - thold_test_load(dirname(__DIR__, 2) . '/thold_functions.php'); + thold_test_load(dirname(__DIR__, 2) . '/includes/functions.php'); }); beforeEach(function() { @@ -46,7 +46,7 @@ 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) ->and($source)->toContain("hash('sha256', json_encode(") diff --git a/tests/Unit/NotificationQueueClaimTest.php b/tests/Unit/NotificationQueueClaimTest.php index 3b5a1621..e2a81892 100644 --- a/tests/Unit/NotificationQueueClaimTest.php +++ b/tests/Unit/NotificationQueueClaimTest.php @@ -26,7 +26,7 @@ final class NotificationQueueClaimTest extends TestCase { * @return void */ public static function setUpBeforeClass(): void { - self::loadPluginSource('thold_functions.php'); + self::loadPluginSource('includes/functions.php'); } /** diff --git a/tests/Unit/NotificationQueueRetryTest.php b/tests/Unit/NotificationQueueRetryTest.php index 6d2ee421..7a36bdce 100644 --- a/tests/Unit/NotificationQueueRetryTest.php +++ b/tests/Unit/NotificationQueueRetryTest.php @@ -19,7 +19,7 @@ final class NotificationQueueRetryTest extends TestCase { * @return void */ public static function setUpBeforeClass(): void { - self::loadPluginSource('thold_functions.php'); + self::loadPluginSource('includes/functions.php'); } /** diff --git a/tests/Unit/PollerSchedulingTest.php b/tests/Unit/PollerSchedulingTest.php index 3d14c923..4cdfaaf0 100644 --- a/tests/Unit/PollerSchedulingTest.php +++ b/tests/Unit/PollerSchedulingTest.php @@ -23,7 +23,7 @@ final class PollerSchedulingTest extends TestCase { * @return void */ public static function setUpBeforeClass(): void { - self::loadPluginSource('thold_functions.php'); + self::loadPluginSource('includes/functions.php'); self::loadPluginSource('includes/polling.php'); } diff --git a/tests/Unit/PruneFilesTest.php b/tests/Unit/PruneFilesTest.php new file mode 100644 index 00000000..9f0accc6 --- /dev/null +++ b/tests/Unit/PruneFilesTest.php @@ -0,0 +1,255 @@ + ['include/', 'oldfile.php', 'userdata/', 'gone.png'], + 'expected' => ['INFO', 'setup.php', 'includes/', 'manifest.json'], + 'whitelist' => ['userdata/'], + ]; + + $base = 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(); +}); diff --git a/tests/Unit/SetupCspHooksTest.php b/tests/Unit/SetupCspHooksTest.php index dd0a99c3..91a7e08c 100644 --- a/tests/Unit/SetupCspHooksTest.php +++ b/tests/Unit/SetupCspHooksTest.php @@ -51,7 +51,7 @@ public function test_page_head_emits_theme_css_and_a_nonced_inline_script(): voi thold_page_head(); $output = ob_get_clean(); - $this->assertStringContainsString('themes/modern/main.css', $output); + $this->assertStringContainsString('css/modern.css', $output); $this->assertStringContainsString("