Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a3f8988
Relocate schema + functions to includes/, add db_update_table upgrade
Sep 30, 2026
1e421d0
INFO: set compat to 1.2.32
Sep 30, 2026
36f17c9
CI: prepared version lookup in check_upgrade, allowlist includes/data…
Sep 30, 2026
c910b49
test: cover plugin_maint_check_upgrade; de-measure maint.php web entry
Sep 30, 2026
31fd2f3
fix: restore root functions.php shim for core snmpagent hard-coded in…
TheWitness Sep 30, 2026
f0ee6ac
test: measure includes/functions.php and allowlist the root shim
TheWitness Sep 30, 2026
1bf7042
fix: legacy scalar primary for create, gate version bump on upgrade s…
TheWitness Sep 30, 2026
1071b72
fix(ci): match clover files to the longest changed path, not the first
TheWitness Sep 30, 2026
440f327
fix(upgrade): re-register is_device_in_maintenance hook against inclu…
Sep 30, 2026
f6245b9
INFO: back off compat to 1.2.29
Sep 30, 2026
411eb97
Add manifest.json + upgrade-time file pruning
Sep 30, 2026
f5381ea
fix: add root functions.php shim to manifest 'expected' to fix drift …
TheWitness Sep 30, 2026
f009aef
revert: drop root functions.php shim; core now prefers includes/funct…
TheWitness Sep 30, 2026
4bd568d
Prune phpunit.xml on upgrade and ignore markdown-lint configs
Sep 30, 2026
3de589b
Harden prune against tampered-manifest path traversal
Oct 1, 2026
8612569
docs: align the Project Structure comment column
Oct 1, 2026
407f933
docs/tests: standardize test header and correct manifest-contract notes
Oct 1, 2026
cd0781e
refactor: rename prune helpers to the plugin-slug function prefix
Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ When generating code for this repository:

```
maint/ # Repository root (install to plugins/maint/ in Cacti)
├── locales/ # Internationalization files
├── functions.php # Runtime maintenance checks (plugin_maint_check_* -> plugin_maint_check_schedule)
├── maint.php # Main UI/controller: CRUD schedules, tabbed views, host associations
├── INFO # Plugin metadata (name, version, compat)
├── includes/ # Library/helper files, require_once'd from the entry points
│ ├── database.php # Schema management: table defs + create/upgrade helpers
│ └── functions.php # Runtime maintenance checks (plugin_maint_check_* -> plugin_maint_check_schedule)
├── locales/ # Internationalization files
├── maint.php # Main UI/controller: CRUD schedules, tabbed views, host associations
├── INFO # Plugin metadata (name, version, compat)
├── README.md
└── setup.php # Plugin entry points and hooks (menu, realms, device actions, maintenance hook)
└── setup.php # Plugin entry points and hooks (menu, realms, device actions, maintenance hook)
```

## Naming Conventions
Expand Down Expand Up @@ -77,7 +79,14 @@ arithmetic, strict `===` comparisons).

## Database Operations

Tables are created in `maint_setup_database()` via `api_plugin_db_table_create()`; `plugin_maint_check_schedule()` updates `stime`/`etime` for recurring schedules when windows pass, and schedule listing in `maint.php` uses it to compute "Active" state.
All schema management lives in `includes/database.php` (the thold model), not in `setup.php`. `setup.php`'s
install/upgrade paths `require_once($config['base_path'] . '/plugins/maint/includes/database.php')` and
delegate. Each table is defined once (`maint_schedules_table_data()`, `maint_hosts_table_data()`) and created
via `api_plugin_db_table_create('maint', ...)`; `plugin_maint_check_upgrade()` version-gates against
`plugin_config`, refreshes the schema through `maint_upgrade_tables()` (`db_update_table()` when the table
exists, create fallback when missing), and updates the full `plugin_config` row on a version change.
`plugin_maint_check_schedule()` (in `includes/functions.php`) updates `stime`/`etime` for recurring schedules
when windows pass, and schedule listing in `maint.php` uses it to compute "Active" state.

## Internationalization

Expand Down Expand Up @@ -126,7 +135,7 @@ Document all changes via commit history; use descriptive commit messages referen

- 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

Expand All @@ -150,6 +159,11 @@ existing code or adding new code, not just in dedicated cleanup passes:
- **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.
- **File inclusion uses `require`/`require_once`.** Always use `require`/`require_once` (never
`include`/`include_once`) so a missing dependency fails fast and loudly. Keep library/helper files
(e.g. `functions.php`, `includes/database.php`) under `includes/` and reference them from that path;
entry points (`maint.php`, `setup.php`) stay in the plugin root. A hook registered for a function that
lives in `includes/functions.php` must pass `'includes/functions.php'` as its file argument.
- **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
Expand All @@ -158,3 +172,7 @@ existing code or adding new code, not just in dedicated cleanup passes:
line, `@param` lines, a blank comment line, then `@return`. Infer parameter/return types from
actual usage; don't change the function's real type-hints in the same pass (let static analysis
flag mismatches separately). Skip vendored third-party library files.

## File manifest & upgrade pruning

The plugin ships a root `manifest.json` with three arrays: `tombstones` (files/directories older versions shipped that have since moved or been removed), `expected` (the top-level files and directories that ship today, directories written with a trailing `/`), and `whitelist` (paths holding user data that must never be touched). Keep `expected` current: CI runs `tests/bin/validate-manifest.php`, which fails on any drift between `expected` and the real top-level tree (it ignores `tests/`, `phpunit.xml`, `.git*`, `.md*`, and whitelisted paths). Custom customer CSS/theme files belong in `expected`, and stylesheets live in `css/` (not `themes/`). On upgrade, `maint_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 changes: 3 additions & 0 deletions .github/workflows/plugin-ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ jobs:
- name: Check PHP version
run: php -v

- name: Validate plugin manifest (expected-file drift)
run: php cacti/plugins/maint/tests/bin/validate-manifest.php

- name: Run apt-get update
run: sudo apt-get update

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

--- develop ---

* refactor: Move schema management into includes/database.php (the thold model) and relocate functions.php into includes/; setup.php delegates via require_once, and plugin_maint_check_upgrade() now refreshes the schema via db_update_table(), re-registers the is_device_in_maintenance hook against includes/functions.php, and updates the full plugin_config row on a version change. Switches every file inclusion from include/include_once to require/require_once
* dev: Enforce patch coverage of changed lines in CI and remove the inert COMPOSER_ROOT_VERSION env from the Pest step
* feature: Add a standard Cacti search filter to the Maintenance Schedules list that also matches the hostname/description/location/site name of associated Thold devices and the hostname/description of associated Servcheck tests
* security: Add a version-safe CSP nonce (`plugin_maint_csp_nonce()`) to every inline `<script>` tag so pages stay compatible with Cacti's Content-Security-Policy nonce enforcement, while falling back cleanly on older Cacti releases that lack the `CactiSecureHeaders` class
Expand Down
2 changes: 1 addition & 1 deletion INFO
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ longname = Maintenance Scheduler
author = The Cacti Group
email =
homepage = http://cacti.net
compat = 1.2.0
compat = 1.2.29
capabilities = online_view:1, online_mgmt:1, offline_view:0, offline_mgmt:0, remote_collect:0
137 changes: 137 additions & 0 deletions includes/database.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/

/**
* The plugin_maint_schedules table definition (one row per maintenance
* schedule), shared by the create path (api_plugin_db_table_create()) and
* the upgrade path (db_update_table()) so both stay in sync from a single
* definition.
*
* @return array<string, mixed> The table definition array.
*/
function maint_schedules_table_data(): array {
$data = [];
$data['columns'][] = ['name' => 'id', 'type' => 'int(11)', 'NULL' => false, 'auto_increment' => true];
$data['columns'][] = ['name' => 'enabled', 'type' => 'varchar(3)', 'NULL' => false, 'default' => 'on'];
$data['columns'][] = ['name' => 'name', 'type' => 'varchar(128)', 'NULL' => true];
$data['columns'][] = ['name' => 'mtype', 'type' => 'int(11)', 'NULL' => false];
$data['columns'][] = ['name' => 'stime', 'type' => 'int(22)', 'NULL' => false];
$data['columns'][] = ['name' => 'etime', 'type' => 'int(22)', 'NULL' => false];
$data['columns'][] = ['name' => 'minterval', 'type' => 'int(11)', 'NULL' => false];
$data['primary'] = ['id'];
Comment thread
TheWitness marked this conversation as resolved.
$data['keys'][] = ['name' => 'mtype', 'columns' => ['mtype']];
$data['keys'][] = ['name' => 'enabled', 'columns' => ['enabled']];
$data['type'] = 'InnoDB';
$data['comment'] = 'Maintenance Schedules';

return $data;
}

/**
* The plugin_maint_hosts table definition (maps devices/tests to a
* maintenance schedule), shared by the create path
* (api_plugin_db_table_create()) and the upgrade path (db_update_table()).
*
* @return array<string, mixed> The table definition array.
*/
function maint_hosts_table_data(): array {
$data = [];
$data['columns'][] = ['name' => 'type', 'type' => 'int(6)', 'NULL' => false];
$data['columns'][] = ['name' => 'host', 'type' => 'int(12)', 'NULL' => false];
$data['columns'][] = ['name' => 'schedule', 'type' => 'int(12)', 'NULL' => false];
$data['primary'] = ['type', 'schedule', 'host'];
$data['keys'][] = ['name' => 'type', 'columns' => ['type']];
$data['keys'][] = ['name' => 'schedule', 'columns' => ['schedule']];
$data['type'] = 'InnoDB';
$data['comment'] = 'Maintenance Schedules Hosts';

return $data;
}

/**
* Creates this plugin's database tables (plugin_maint_schedules and
* plugin_maint_hosts) through Cacti's tracked plugin table API. Called
* from plugin_maint_install() during installation, and re-run (safely, as
* a no-op for already-applied changes) from maint_upgrade_tables() during
* upgrades.
*
* @return void
*/
function maint_setup_database(): void {
api_plugin_db_table_create('maint', 'plugin_maint_schedules', maint_create_definition(maint_schedules_table_data()));
api_plugin_db_table_create('maint', 'plugin_maint_hosts', maint_create_definition(maint_hosts_table_data()));
}

/**
* Returns a copy of a table definition with its array primary key
* normalized to the legacy backtick-joined scalar form. Older Cacti
* releases (1.2.0) concatenate data['primary'] straight into the CREATE in
* api_plugin_db_table_create(), so an array would render PRIMARY KEY
* (`Array`) and break a fresh install; the array form is retained in the
* *_table_data() helpers for db_update_table(), which requires it.
*
* @param array<string, mixed> $data The table definition (array primary).
*
* @return array<string, mixed> The definition with a scalar primary key.
*/
function maint_create_definition(array $data): array {
if (isset($data['primary']) && is_array($data['primary'])) {
$data['primary'] = implode('`,`', $data['primary']);
}

return $data;
}

/**
* Refreshes this plugin's tables to their current definition on upgrade:
* db_update_table() diffs the live schema against each definition and
* issues the exact ALTER when the table already exists, otherwise the
* table is created outright. Called from plugin_maint_check_upgrade() when
* the stored version changes.
*
* @return bool True when every table reconciled; false when any
* db_update_table()/api_plugin_db_table_create() reported an
* explicit failure, so the caller can skip the version bump
* and retry on the next request.
*/
function maint_upgrade_tables(): bool {
$success = true;

foreach ([
'plugin_maint_schedules' => maint_schedules_table_data(),
'plugin_maint_hosts' => maint_hosts_table_data(),
] as $table => $data) {
if (db_table_exists($table)) {
$result = db_update_table($table, $data);
} else {
$result = api_plugin_db_table_create('maint', $table, maint_create_definition($data));
}

if ($result === false) {
$success = false;
}
}

return $success;
}
File renamed without changes.
4 changes: 2 additions & 2 deletions maint.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
global $config;

chdir('../../');
include_once('./include/auth.php');
include_once($config['base_path'] . '/plugins/maint/functions.php');
require_once('./include/auth.php');
require_once($config['base_path'] . '/plugins/maint/includes/functions.php');

define('MAINT_HOST_FILTER_LOC_ANY', '__any'); // internal value unlikely in data
define('MAINT_HOST_FILTER_LOC_NONE', '__none');
Expand Down
17 changes: 17 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"tombstones": [],
"expected": [
".pre-commit-config.yml",
"CHANGELOG.md",
"INFO",
"LICENSE",
"README.md",
"includes/",
"index.php",
"locales/",
"maint.php",
"manifest.json",
"setup.php"
],
"whitelist": []
}
3 changes: 1 addition & 2 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
-->
<source>
<include>
<file>functions.php</file>
<file>maint.php</file>
<file>includes/functions.php</file>
<file>setup.php</file>
</include>
</source>
Expand Down
Loading
Loading