From 2002771e461c45e0607242df2c8dc5fc590890ee Mon Sep 17 00:00:00 2001 From: Petr Macek Date: Thu, 15 Oct 2026 15:03:26 +0200 Subject: [PATCH 1/4] fix include --- includes/functions.php | 8 +++++++- includes/polling.php | 8 +++++++- phpunit.xml | 41 ----------------------------------------- 3 files changed, 14 insertions(+), 43 deletions(-) delete mode 100644 phpunit.xml diff --git a/includes/functions.php b/includes/functions.php index 6a708d3a..0b6fd084 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -3414,7 +3414,13 @@ function thold_check_threshold(&$thold_data) { // don't alert for this host if it's selected for maintenance if (api_plugin_is_enabled('maint') || in_array('maint', $plugins, true)) { - include_once($config['base_path'] . '/plugins/maint/functions.php'); + if (file_exists(require_once($config['base_path'] . '/plugins/maint/functions.php'))) { + require_once($config['base_path'] . '/plugins/maint/functions.php'); + } else if (file_exists(require_once($config['base_path'] . '/plugins/maint/includes/functions.php'))) { + require_once($config['base_path'] . '/plugins/maint/includes/functions.php'); + } else { + cacti_log('ERROR: cannot include maint functions', false, 'THOLD'); + } } if (api_plugin_is_enabled('maint') && plugin_maint_check_cacti_host($thold_data['host_id'])) { diff --git a/includes/polling.php b/includes/polling.php index f6d8c313..d2562c0e 100644 --- a/includes/polling.php +++ b/includes/polling.php @@ -465,7 +465,13 @@ function thold_update_host_status() { include_once($config['library_path'] . '/snmp.php'); if (api_plugin_is_enabled('maint')) { - include_once($config['base_path'] . '/plugins/maint/functions.php'); + if (file_exists(require_once($config['base_path'] . '/plugins/maint/functions.php'))) { + require_once($config['base_path'] . '/plugins/maint/functions.php'); + } else if (file_exists(require_once($config['base_path'] . '/plugins/maint/includes/functions.php'))) { + require_once($config['base_path'] . '/plugins/maint/includes/functions.php'); + } else { + cacti_log('ERROR: cannot include maint functions', false, 'THOLD'); + } } $ping_failure_count = read_config_option('ping_failure_count'); diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 6ed6b04b..00000000 --- a/phpunit.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - ./tests/Security - ./tests/Unit - ./tests/Integration - - - - - - - includes/functions.php - includes/polling.php - setup.php - - - - From 8389815e0ebeaed0398d55b0e355e07dd68a89c1 Mon Sep 17 00:00:00 2001 From: Petr Macek Date: Thu, 15 Oct 2026 15:15:56 +0200 Subject: [PATCH 2/4] return back deleted file --- phpunit.xml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 phpunit.xml diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..6ed6b04b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,41 @@ + + + + + + + + + ./tests/Security + ./tests/Unit + ./tests/Integration + + + + + + + includes/functions.php + includes/polling.php + setup.php + + + + From ad2ffca937f375de91557b480552c0194293c065 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 15 Oct 2026 09:49:57 -0400 Subject: [PATCH 3/4] Fix maint include guard: test file_exists on path, not require_once return file_exists() was wrapping require_once(), so the first require ran unconditionally and its integer return (1) was passed to file_exists(), which fell through to require the non-existent includes/functions.php and fatalled. Test the path strings directly. --- includes/functions.php | 4 ++-- includes/polling.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 0b6fd084..b04a5f30 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -3414,9 +3414,9 @@ function thold_check_threshold(&$thold_data) { // don't alert for this host if it's selected for maintenance if (api_plugin_is_enabled('maint') || in_array('maint', $plugins, true)) { - if (file_exists(require_once($config['base_path'] . '/plugins/maint/functions.php'))) { + if (file_exists($config['base_path'] . '/plugins/maint/functions.php')) { require_once($config['base_path'] . '/plugins/maint/functions.php'); - } else if (file_exists(require_once($config['base_path'] . '/plugins/maint/includes/functions.php'))) { + } else if (file_exists($config['base_path'] . '/plugins/maint/includes/functions.php')) { require_once($config['base_path'] . '/plugins/maint/includes/functions.php'); } else { cacti_log('ERROR: cannot include maint functions', false, 'THOLD'); diff --git a/includes/polling.php b/includes/polling.php index d2562c0e..51b68988 100644 --- a/includes/polling.php +++ b/includes/polling.php @@ -465,9 +465,9 @@ function thold_update_host_status() { include_once($config['library_path'] . '/snmp.php'); if (api_plugin_is_enabled('maint')) { - if (file_exists(require_once($config['base_path'] . '/plugins/maint/functions.php'))) { + if (file_exists($config['base_path'] . '/plugins/maint/functions.php')) { require_once($config['base_path'] . '/plugins/maint/functions.php'); - } else if (file_exists(require_once($config['base_path'] . '/plugins/maint/includes/functions.php'))) { + } else if (file_exists($config['base_path'] . '/plugins/maint/includes/functions.php')) { require_once($config['base_path'] . '/plugins/maint/includes/functions.php'); } else { cacti_log('ERROR: cannot include maint functions', false, 'THOLD'); From 2c5966793cfbfce58dbe26f3c2f547584be2a273 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 15 Oct 2026 10:00:13 -0400 Subject: [PATCH 4/4] Cover maint include fallback and error branches The maint-include lookup in thold_check_threshold() and thold_update_host_status() gained an includes/functions.php fallback and an error-log branch, but the suite only exercised the plugin-root path. Add MaintIncludeResolutionTest driving all three outcomes for both functions, with cacti-root fixtures for the includes/ layout and the missing-plugin case, so patch coverage of the changed lines reaches 100%. --- .../plugins/maint/includes/functions.php | 3 + .../plugins/thold/css/modern.css | 2 + .../plugins/thold/includes/arrays.php | 3 + tests/Fixtures/cacti-root-no-maint/.gitkeep | 3 + .../plugins/thold/css/modern.css | 2 + .../plugins/thold/includes/arrays.php | 3 + tests/Helpers/ThresholdScenario.php | 8 +- tests/Unit/MaintIncludeResolutionTest.php | 143 ++++++++++++++++++ 8 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/cacti-root-maint-includes/plugins/maint/includes/functions.php create mode 100644 tests/Fixtures/cacti-root-maint-includes/plugins/thold/css/modern.css create mode 100644 tests/Fixtures/cacti-root-maint-includes/plugins/thold/includes/arrays.php create mode 100644 tests/Fixtures/cacti-root-no-maint/.gitkeep create mode 100644 tests/Fixtures/cacti-root-no-maint/plugins/thold/css/modern.css create mode 100644 tests/Fixtures/cacti-root-no-maint/plugins/thold/includes/arrays.php create mode 100644 tests/Unit/MaintIncludeResolutionTest.php diff --git a/tests/Fixtures/cacti-root-maint-includes/plugins/maint/includes/functions.php b/tests/Fixtures/cacti-root-maint-includes/plugins/maint/includes/functions.php new file mode 100644 index 00000000..28247f69 --- /dev/null +++ b/tests/Fixtures/cacti-root-maint-includes/plugins/maint/includes/functions.php @@ -0,0 +1,3 @@ + $overrides + * + * @return ThresholdScenario + */ + private function baseline(array $overrides = []) { + return ThresholdScenario::threshold($overrides + [ + 'thold_type' => 1, + 'bl_type' => 0, + 'bl_pct_up' => 10, + 'bl_pct_down' => 10, + 'bl_ref_time_range' => 3600, + 'bl_fail_trigger' => 1, + ]) + ->alertRecipient('ops@example.org') + ->referenceStatistics(100, 100, 100, 100); + } + + /** + * @param string $name + * + * @return string + */ + private function fixtureRoot($name) { + return dirname(__DIR__) . '/Fixtures/' . $name; + } + + /** + * The newer maint layout keeps functions.php under includes/; a device in a + * maintenance window still resolves it and suppresses the notification. + * + * @return void + */ + public function testCheckThresholdResolvesMaintFromIncludesLayout(): void { + $outcome = $this->baseline(['lastread' => 500]) + ->inMaintenance($this->fixtureRoot('cacti-root-maint-includes')) + ->poll(); + + $this->assertSame(0, $outcome->mailCount()); + } + + /** + * With neither maint layout present the include is skipped and the failure + * is logged rather than fatal. + * + * @return void + */ + public function testCheckThresholdLogsWhenMaintFunctionsMissing(): void { + $this->baseline(['lastread' => 500]) + ->inMaintenance($this->fixtureRoot('cacti-root-no-maint')) + ->poll(); + + $this->assertContains('ERROR: cannot include maint functions', CactiStubs::$log); + } + + /** + * Drive thold_update_host_status() far enough to run the maint include + * block. Downstream work needs a live Cacti, so engine messages are + * swallowed and any throw past the block is ignored. + * + * @param string $cactiRoot + * + * @return void + */ + private function runHostStatus($cactiRoot) { + CactiStubs::willAlwaysReturn('api_plugin_is_enabled', true); + CactiStubs::$configOptions['alert_deadnotify'] = 'on'; + $GLOBALS['config']['base_path'] = $cactiRoot; + + set_error_handler(static fn (): bool => true); + ob_start(); + + try { + thold_update_host_status(); + } catch (\Throwable $e) { + // Reaching the maint include block is the point. + } finally { + ob_end_clean(); + restore_error_handler(); + } + } + + /** + * @return void + */ + public function testHostStatusResolvesMaintFromPluginRoot(): void { + $this->runHostStatus($this->fixtureRoot('cacti-root')); + + $this->assertNotContains('ERROR: cannot include maint functions', CactiStubs::$log); + } + + /** + * @return void + */ + public function testHostStatusResolvesMaintFromIncludesLayout(): void { + $this->runHostStatus($this->fixtureRoot('cacti-root-maint-includes')); + + $this->assertNotContains('ERROR: cannot include maint functions', CactiStubs::$log); + } + + /** + * @return void + */ + public function testHostStatusLogsWhenMaintFunctionsMissing(): void { + $this->runHostStatus($this->fixtureRoot('cacti-root-no-maint')); + + $this->assertContains('ERROR: cannot include maint functions', CactiStubs::$log); + } +}