From 2b2bac222b6e9def6da308a50edc4d849cf523ef Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 1 Oct 2026 14:39:42 -0400 Subject: [PATCH 1/2] fix: repoint include/ -> includes/ plugin_hooks on every upgrade check The hook file repoint lived inside the version-change gate in plugin_evidence_upgrade_database(), so installs whose plugin_config.version was already bumped to the current version (but whose plugin_hooks rows were never repointed from the old include/ path) never got healed. With the old include/ directory still present this risks "Cannot redeclare ..." fatals from loading both include/ and includes/; after deleting include/ it produces repeated "SECURITY ERROR: Attempted inclusion of invalid plugin file include/" because the stale hook rows point at a now-missing file. Run the idempotent repoint UPDATE unconditionally (before the version comparison) so broken installs self-heal on the next upgrade check. The WHERE ... file LIKE 'include/%' filter makes it a no-op on healthy installs. Mirrors the intropage fix (Cacti/plugin_intropage#410). --- includes/database.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/includes/database.php b/includes/database.php index 04e580b..efa7a3e 100644 --- a/includes/database.php +++ b/includes/database.php @@ -299,6 +299,15 @@ function plugin_evidence_upgrade_database() { $current = $info['version']; $oldv = db_fetch_cell('SELECT version FROM plugin_config WHERE directory = "evidence"'); + // The plugin's library directory moved from include/ to includes/. Repoint + // any plugin_hooks row still bound to the old path on every check - not only + // during a version-change upgrade - because an install whose version was + // already bumped without its hooks being repointed would otherwise keep + // loading the stale include/ path (or, once include/ is deleted, fail + // Cacti's plugin file-inclusion security check for it) on every page. The + // LIKE filter makes this a no-op on healthy installs. + db_execute("UPDATE plugin_hooks SET file = REPLACE(file, 'include/', 'includes/') WHERE name = 'evidence' AND file LIKE 'include/%'"); + if (!cacti_version_compare($oldv, $current, '=')) { if (cacti_version_compare($oldv, '0.3', '<')) { $data = []; @@ -313,12 +322,9 @@ function plugin_evidence_upgrade_database() { api_plugin_db_table_create('evidence', 'plugin_evidence_snmp_info', $data); } - // The plugin's library directory moved from include/ to includes/; repoint - // any hook still registered against the old path so existing installs load - // them from the new location after upgrade. - db_execute("UPDATE plugin_hooks SET file = REPLACE(file, 'include/', 'includes/') WHERE name = 'evidence' AND file LIKE 'include/%'"); - // Remove files tombstoned in manifest.json (the old include/ tree). - evidence_prune_files(); + // Remove files tombstoned in manifest.json (the old include/ tree). + evidence_prune_files(); + // Set the new version db_execute_prepared("UPDATE plugin_config SET version = ?, author = ?, webpage = ? From ea0fc7f01412de70cf0720312c3e5a59f26c5cbd Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 1 Oct 2026 14:49:55 -0400 Subject: [PATCH 2/2] test: assert the include/ -> includes/ hook repoint runs on up-to-date installs The repoint moved out of the version-change gate, so plugin_evidence_check_config() now issues the idempotent plugin_hooks UPDATE even when the stored version already matches. Update the lifecycle test to pin that contract (exactly one db call: the repoint) instead of asserting no work is done. --- tests/Unit/EvidenceLifecycleTest.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/Unit/EvidenceLifecycleTest.php b/tests/Unit/EvidenceLifecycleTest.php index 488af1d..7425f61 100644 --- a/tests/Unit/EvidenceLifecycleTest.php +++ b/tests/Unit/EvidenceLifecycleTest.php @@ -41,13 +41,23 @@ expect($drops)->toHaveCount(7); }); -it('does nothing when the stored version already matches the plugin version', function () { +it('repoints stale include/ hooks even when the stored version already matches', function () { $info = plugin_evidence_version(); evidence_test_mock_db('db_fetch_cell', 'plugin_config', $info['version']); expect(plugin_evidence_check_config())->toBeTrue(); - expect($GLOBALS['__test_db_calls'])->toBeEmpty(); + + // On an up-to-date install the only work is the idempotent include/ -> + // includes/ plugin_hooks repoint; no schema migration or version write runs. + expect($GLOBALS['__test_db_calls'])->toHaveCount(1); + + $call = $GLOBALS['__test_db_calls'][0]; + + expect($call['fn'])->toBe('db_execute') + ->and($call['sql'])->toContain('UPDATE plugin_hooks') + ->and($call['sql'])->toContain("REPLACE(file, 'include/', 'includes/')") + ->and($call['sql'])->toContain("file LIKE 'include/%'"); }); it('updates the stored plugin_config version when it drifts', function () {