From fa8d8304afe45951c3cddf18368cebee30c6c9d3 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 1 Oct 2026 14:33:27 -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 intropage_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 produced "Cannot redeclare ..." fatals from loading both include/functions.php and includes/functions.php; after deleting include/ it produced repeated "SECURITY ERROR: Attempted inclusion of invalid plugin file include/settings.php" 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. The duplicate copy inside the version gate is removed. --- includes/database.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/includes/database.php b/includes/database.php index b5e7c89..319140b 100644 --- a/includes/database.php +++ b/includes/database.php @@ -222,6 +222,15 @@ function intropage_upgrade_database(): void { $current = $info['version']; $oldv = db_fetch_cell('SELECT version FROM plugin_config WHERE directory = "intropage"'); + // 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 = 'intropage' AND file LIKE 'include/%'"); + if (!cacti_version_compare($oldv, $current, '=')) { if (cacti_version_compare($oldv, '3.0.0', '<=')) { include_once($config['base_path'] . '/plugins/intropage/includes/functions.php'); @@ -369,11 +378,6 @@ function intropage_upgrade_database(): void { ADD COLUMN `height` enum("normal","double","triple") NOT NULL DEFAULT "normal"'); } - // 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 = 'intropage' AND file LIKE 'include/%'"); - // Remove files/directories a previous version left behind (per manifest.json). plugin_intropage_prune_files(); From 5dcea0e2083bcfb035988e282c116404908e505a 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_intropage_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/IntropageLifecycleTest.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/Unit/IntropageLifecycleTest.php b/tests/Unit/IntropageLifecycleTest.php index b9f1a94..b532c6d 100644 --- a/tests/Unit/IntropageLifecycleTest.php +++ b/tests/Unit/IntropageLifecycleTest.php @@ -3,6 +3,8 @@ +-------------------------------------------------------------------------+ | Copyright (C) 2004-2026 The Cacti Group | +-------------------------------------------------------------------------+ + | Cacti: The Complete RRDtool-based Graphing Solution | + +-------------------------------------------------------------------------+ */ /* @@ -45,8 +47,17 @@ expect(plugin_intropage_upgrade())->toBeFalse(); }); -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 () { plugin_intropage_check_config(); - 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/%'"); });