From 731838655b42c909ed4a5da3dcd57602a74e7744 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 1 Oct 2026 11:18:36 -0400 Subject: [PATCH 1/3] fix: load Thold's relocated function library and guard the poller Thold moved thold_functions.php to includes/functions.php. intropage's panellib/thold.php still include_once()'d the old plugins/thold/thold_functions.php path and thold_collect() then called get_allowed_thresholds() unguarded, fataling the intropage poller (and the cacti integration CI) with an undefined function. Add intropage_load_thold_functions() (new path preferred, legacy fallback), repoint all four include sites to it, and skip thold_collect() when the threshold API is unavailable. --- CHANGELOG.md | 1 + panellib/thold.php | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6752a..002deb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ --- develop --- +* issue: Load Thold's relocated function library from `includes/functions.php` (with a legacy `thold_functions.php` fallback) and skip the threshold poller when Thold's API is unavailable, so intropage no longer fatals after Thold moved its functions out of the plugin root * dev: Rename the plugin's stylesheet directory from `themes/` to `css/`; the old directory is tombstoned in `manifest.json` and removed on upgrade * dev: Move the plugin's library directory from `include/` to `includes/` and re-register the affected hooks on upgrade (a `plugin_hooks` file repoint in `intropage_upgrade_database()`) so existing installations load them from the new path * dev: Measure CI coverage with xdebug instead of pcov so the plugin's own sources are instrumented (pcov auto-scopes to the Composer root and skipped cacti/plugins/, leaving the patch-coverage gate with nothing to measure) diff --git a/panellib/thold.php b/panellib/thold.php index 1db4fce..fa85410 100644 --- a/panellib/thold.php +++ b/panellib/thold.php @@ -24,6 +24,36 @@ +-------------------------------------------------------------------------+ */ +/** + * Loads the Thold plugin's function library. Thold relocated its helpers + * from the legacy plugins/thold/thold_functions.php to + * plugins/thold/includes/functions.php, so prefer the new path and fall + * back to the old one for older Thold installs. + * + * @return bool True when a Thold function library was found and loaded. + * + * @global array $config Cacti global configuration array; used to resolve + * the plugin base path. + */ +function intropage_load_thold_functions(): bool { + global $config; + + $candidates = [ + $config['base_path'] . '/plugins/thold/includes/functions.php', + $config['base_path'] . '/plugins/thold/thold_functions.php', + ]; + + foreach ($candidates as $file) { + if (file_exists($file)) { + include_once($file); + + return true; + } + } + + return false; +} + /** * Registers the 'thold' panel category and its 'Last Threshold Events' * and 'Threshold' (trend graph) panels with the panel library. Called @@ -239,7 +269,7 @@ function graph_thold($panel, $user_id, $timespan = 0): void { $panel['alarm'] = 'green'; - include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); + intropage_load_thold_functions(); $graph = [ 'line' => [ @@ -374,7 +404,7 @@ function graph_thold($panel, $user_id, $timespan = 0): void { function graph_thold_detail(): array { global $config, $sql_where; - include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); + intropage_load_thold_functions(); $panel = [ 'name' => __('Threshold Details', 'intropage'), @@ -498,7 +528,7 @@ function graph_thold_detail(): array { function thold_event_detail(): array { global $config; - include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); + intropage_load_thold_functions(); $panel = [ 'name' => __('Last Threshold Events', 'intropage'), @@ -592,7 +622,11 @@ function thold_collect(): void { // update in poller $users = get_user_list(); - include_once($config['base_path'] . '/plugins/thold/thold_functions.php'); + // Skip quietly when Thold is absent or does not expose its threshold API, + // rather than fataling the intropage poller. + if (!intropage_load_thold_functions() || !function_exists('get_allowed_thresholds')) { + return; + } foreach ($users as $user) { $t_brea = 0; From 414c65c65b40638a7445a8884f8816c034d19fe0 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 1 Oct 2026 11:35:24 -0400 Subject: [PATCH 2/3] test: cover intropage_load_thold_functions() and Thold panel fallbacks Adds patch-coverage for the new Thold loader helper (new path, legacy fallback, and absent branches) and the four panellib/thold.php call sites by invoking the panel renderers with Thold not installed. --- tests/Unit/TholdFunctionLoaderTest.php | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/Unit/TholdFunctionLoaderTest.php diff --git a/tests/Unit/TholdFunctionLoaderTest.php b/tests/Unit/TholdFunctionLoaderTest.php new file mode 100644 index 0000000..9c7ca41 --- /dev/null +++ b/tests/Unit/TholdFunctionLoaderTest.php @@ -0,0 +1,104 @@ +toBeTrue(); +}); + +it('falls back to the legacy thold_functions.php path', function () { + $base = intropage_thold_sandbox('legacy'); + mkdir($base . '/plugins/thold', 0777, true); + file_put_contents($base . '/plugins/thold/thold_functions.php', "toBeTrue(); +}); + +it('returns false when no Thold library is present', function () { + $base = intropage_thold_sandbox('none'); + mkdir($base . '/plugins/thold', 0777, true); + + expect(intropage_load_thold_functions())->toBeFalse(); +}); + +it('renders the thold graph panel as grey when Thold is not installed', function () { + intropage_thold_sandbox('graph'); + $GLOBALS['__intropage_saved_panel'] = array(); + + $panel = array('id' => 1, 'refresh' => 300, 'alarm' => 'green'); + graph_thold($panel, 1); + + expect($GLOBALS['__intropage_saved_panel']['alarm'])->toBe('grey'); +}); + +it('renders the thold detail panels when Thold is not installed', function () { + intropage_thold_sandbox('detail'); + + $detail = graph_thold_detail(); + $events = thold_event_detail(); + + expect($detail['alarm'])->toBe('grey'); + expect($events['alarm'])->toBe('yellow'); +}); + +it('skips thold_collect when the Thold threshold API is unavailable', function () { + intropage_thold_sandbox('collect'); + $GLOBALS['__test_db_calls'] = array(); + + thold_collect(); + + expect($GLOBALS['__test_db_calls'])->toBeEmpty(); +}); From d5cc1bd54c065e6ed6165b92d5905dda03372d20 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 1 Oct 2026 11:42:38 -0400 Subject: [PATCH 3/3] test: use real intropage helpers and set is_web for Thold panel coverage The suite already loads includes/functions.php, so rely on the real get_user_list()/save_panel_result() (require functions.php here too for load-order safety) instead of colliding stubs, set \['is_web']=false so get_user_list() takes its non-web branch without a notice, and assert on the recorded db calls. --- tests/Unit/TholdFunctionLoaderTest.php | 31 +++++++++++++------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/Unit/TholdFunctionLoaderTest.php b/tests/Unit/TholdFunctionLoaderTest.php index 9c7ca41..0485cb2 100644 --- a/tests/Unit/TholdFunctionLoaderTest.php +++ b/tests/Unit/TholdFunctionLoaderTest.php @@ -10,6 +10,7 @@ * renderers' behaviour when the Thold plugin is absent, in panellib/thold.php. */ +require_once __DIR__ . '/../../includes/functions.php'; require_once __DIR__ . '/../../panellib/thold.php'; if (!function_exists('api_plugin_is_enabled')) { @@ -24,20 +25,10 @@ function api_plugin_user_realm_auth($file) { } } -if (!function_exists('save_panel_result')) { - function save_panel_result($panel, $user_id) { - $GLOBALS['__intropage_saved_panel'] = $panel; - } -} - -if (!function_exists('get_user_list')) { - function get_user_list() { - return array(); - } -} - beforeEach(function () { $GLOBALS['__intropage_thold_base_restore'] = $GLOBALS['config']['base_path']; + $GLOBALS['__test_db_calls'] = array(); + $GLOBALS['config']['is_web'] = false; }); afterEach(function () { @@ -76,12 +67,17 @@ function intropage_thold_sandbox(string $suffix): string { it('renders the thold graph panel as grey when Thold is not installed', function () { intropage_thold_sandbox('graph'); - $GLOBALS['__intropage_saved_panel'] = array(); $panel = array('id' => 1, 'refresh' => 300, 'alarm' => 'green'); graph_thold($panel, 1); - expect($GLOBALS['__intropage_saved_panel']['alarm'])->toBe('grey'); + $updates = array_values(array_filter($GLOBALS['__test_db_calls'], function ($call) { + return $call['fn'] === 'db_execute_prepared' + && stripos($call['sql'], 'UPDATE plugin_intropage_panel_data') !== false; + })); + + expect($updates)->toHaveCount(1); + expect($updates[0]['params'][1])->toBe('grey'); }); it('renders the thold detail panels when Thold is not installed', function () { @@ -96,9 +92,12 @@ function intropage_thold_sandbox(string $suffix): string { it('skips thold_collect when the Thold threshold API is unavailable', function () { intropage_thold_sandbox('collect'); - $GLOBALS['__test_db_calls'] = array(); thold_collect(); - expect($GLOBALS['__test_db_calls'])->toBeEmpty(); + $trend_writes = array_filter($GLOBALS['__test_db_calls'], function ($call) { + return isset($call['sql']) && stripos($call['sql'], 'plugin_intropage_trends') !== false; + }); + + expect($trend_writes)->toBeEmpty(); });