From 99523c47a5535fb84f892b9f4a14f7eed59161f5 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Fri, 8 May 2026 00:59:57 -0700 Subject: [PATCH 1/4] chore(test): gate test-only DB short-circuit behind CACTI_TEST_BOOTSTRAP env The previous `!defined('PHP_TESTING')` guard skipped db_connect_real whenever PHP_TESTING was set anywhere in the include chain. A stray define from a CLI tool or third-party script could disable real DB connection logic in a deployed environment. Switch the guard to require both `PHP_TESTING` and an explicit `CACTI_TEST_BOOTSTRAP=1` env var, and replace the silent-no-connect path with a `Cacti_TestDbSentinel` whose every method throws. Any production code path that accidentally treats the sentinel as a real DB handle will fail loudly instead of silently misbehaving. Signed-off-by: Thomas Vincent --- include/global.php | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/include/global.php b/include/global.php index 572607b1f4..a420cac5a1 100644 --- a/include/global.php +++ b/include/global.php @@ -300,6 +300,19 @@ exit; } +// Test-mode DB sentinel: any method invocation on the sentinel throws, +// so production-mode code paths can never silently treat a non-handle as +// a real connection. Gated by the PHP_TESTING constant AND the +// CACTI_TEST_BOOTSTRAP env var so a stale define alone cannot disable +// real DB connection logic in a deployed environment. +if (!class_exists('Cacti_TestDbSentinel')) { + final class Cacti_TestDbSentinel { + public function __call($name, $args) { + throw new \RuntimeException('PHP_TESTING DB sentinel called: ' . $name); + } + } +} + // set poller mode global $local_db_cnn_id, $remote_db_cnn_id, $conn_mode; @@ -313,8 +326,10 @@ $il = $config['is_web'] ? '' : ''; if ($config['poller_id'] > 1 || isset($rdatabase_hostname)) { - if (!defined('PHP_TESTING')) { + if (!(defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1')) { $local_db_cnn_id = db_connect_real($database_hostname, $database_username, $database_password, $database_default, $database_type, $database_port, $database_retries, $database_ssl, $database_ssl_key, $database_ssl_cert, $database_ssl_ca, $database_ssl_capath, $database_ssl_verify_server_cert); + } else { + $local_db_cnn_id = new Cacti_TestDbSentinel(); } if (!isset($rdatabase_retries)) { @@ -363,12 +378,14 @@ * a remote poller, let's attempt to get back online. */ if ($conn_mode != 'offline') { - if (!defined('PHP_TESTING')) { + if (!(defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1')) { $remote_db_cnn_id = db_connect_real($rdatabase_hostname, $rdatabase_username, $rdatabase_password, $rdatabase_default, $rdatabase_type, $rdatabase_port, $database_retries, $rdatabase_ssl, $rdatabase_ssl_key, $rdatabase_ssl_cert, $rdatabase_ssl_ca, $rdatabase_ssl_capath, $rdatabase_ssl_verify_server_cert); + } else { + $remote_db_cnn_id = new Cacti_TestDbSentinel(); } } - if ($config['is_web'] && is_object($remote_db_cnn_id) && $config['connection'] != 'recovery' && $config['cacti_db_version'] != 'new_install' && !defined('IN_CACTI_INSTALL')) { + if ($config['is_web'] && is_object($remote_db_cnn_id) && !($remote_db_cnn_id instanceof Cacti_TestDbSentinel) && $config['connection'] != 'recovery' && $config['cacti_db_version'] != 'new_install' && !defined('IN_CACTI_INSTALL')) { // Connection worked, so now override the default settings so that it will always utilize the remote connection $database_default = $rdatabase_default; $database_hostname = $rdatabase_hostname; @@ -389,7 +406,7 @@ $config['connection'] = 'offline'; } } else { - if (!defined('PHP_TESTING')) { + if (!(defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1')) { if (!db_connect_real($database_hostname, $database_username, $database_password, $database_default, $database_type, $database_port, $database_retries, $database_ssl, $database_ssl_key, $database_ssl_cert, $database_ssl_ca, $database_ssl_capath, $database_ssl_verify_server_cert)) { print $ps . 'FATAL: Connection to Cacti database failed. Please ensure: ' . $ul; print $li . 'the PHP MySQL module is installed and enabled.' . $il; @@ -408,9 +425,11 @@ exit; } + } else { + $local_db_cnn_id = new Cacti_TestDbSentinel(); } - if (!defined('PHP_TESTING')) { + if (!(defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1')) { if (!db_table_exists('settings') || !db_table_exists('version')) { print $ps . 'FATAL: Connection to Cacti database succeeded but `settings` table not found. Please ensure: ' . $ul; print $li . 'the PHP MySQL module is installed and enabled.' . $il; From 2c2637a50c9b7c5dcd74e4464c470a2950ecbee5 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Fri, 8 May 2026 01:17:01 -0700 Subject: [PATCH 2/4] chore(phpstan): catch up baseline with 11 upstream-detected entries Same as PR-A: appends 11 PHPStan ignoreErrors entries that exist on upstream develop but are not yet baselined, so this branch's CI does not regress on phpstan analyse --level 6. Signed-off-by: Thomas Vincent --- phpstan-baseline.neon | 66 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f90ce8d68c..abe23b7a83 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -41,3 +41,69 @@ parameters: identifier: isset.offset count: 1 path: lib/functions.php + + - + message: '#^Undefined variable\: \$graph_template_item_id$#' + identifier: variable.undefined + count: 1 + path: aggregate_graphs.php + + - + message: '#^Variable \$graph_template_item_id in empty\(\) is never defined\.$#' + identifier: empty.variable + count: 1 + path: aggregate_graphs.php + + - + message: '#^Undefined variable\: \$color_template_item_id$#' + identifier: variable.undefined + count: 1 + path: color_templates.php + + - + message: '#^Variable \$color_template_item_id in empty\(\) is never defined\.$#' + identifier: empty.variable + count: 1 + path: color_templates.php + + - + message: '#^Undefined variable\: \$graph_template_item_id$#' + identifier: variable.undefined + count: 1 + path: graph_templates.php + + - + message: '#^Variable \$graph_template_item_id in empty\(\) is never defined\.$#' + identifier: empty.variable + count: 1 + path: graph_templates.php + + - + message: '#^Undefined variable\: \$graph_template_item_id$#' + identifier: variable.undefined + count: 1 + path: graphs.php + + - + message: '#^Variable \$graph_template_item_id in empty\(\) is never defined\.$#' + identifier: empty.variable + count: 1 + path: graphs.php + + - + message: '#^Offset ''image'' on array\{title\: string, image\: string, id\: ''tree'', url\: ''graph_view\.php…'', selected\?\: true\} in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset + count: 1 + path: lib/html.php + + - + message: '#^Offset ''image'' on array\{title\: string, image\: string, id\: ''list'', url\: ''graph_view\.php…'', selected\?\: true\} in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset + count: 1 + path: lib/html.php + + - + message: '#^Offset ''image'' on array\{title\: string, image\: string, id\: ''preview'', url\: ''graph_view\.php…'', selected\?\: true\} in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset + count: 1 + path: lib/html.php From 38d39ae7b4198a26d8df47dea3da9a19b732c297 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Fri, 8 May 2026 02:09:04 -0700 Subject: [PATCH 3/4] fix(test): apply Copilot review feedback for test DB sentinel guard Signed-off-by: Thomas Vincent --- include/global.php | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/include/global.php b/include/global.php index a420cac5a1..63ae37c5f6 100644 --- a/include/global.php +++ b/include/global.php @@ -305,7 +305,8 @@ // a real connection. Gated by the PHP_TESTING constant AND the // CACTI_TEST_BOOTSTRAP env var so a stale define alone cannot disable // real DB connection logic in a deployed environment. -if (!class_exists('Cacti_TestDbSentinel')) { +// Pass false to class_exists() so this guard never triggers autoload. +if (!class_exists('Cacti_TestDbSentinel', false)) { final class Cacti_TestDbSentinel { public function __call($name, $args) { throw new \RuntimeException('PHP_TESTING DB sentinel called: ' . $name); @@ -313,6 +314,18 @@ public function __call($name, $args) { } } +// Helper for "is this a real DB handle" checks that must reject the sentinel. +// Defined inline so include/global.php remains self-contained. +if (!function_exists('_cacti_is_real_db_conn')) { + function _cacti_is_real_db_conn($x) { + return is_object($x) && !($x instanceof Cacti_TestDbSentinel); + } +} + +// Resolve the test-bootstrap predicate once; both PHP_TESTING and +// CACTI_TEST_BOOTSTRAP=1 must be set for the sentinel branches to engage. +$is_test_bootstrap = (defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1'); + // set poller mode global $local_db_cnn_id, $remote_db_cnn_id, $conn_mode; @@ -326,7 +339,7 @@ public function __call($name, $args) { $il = $config['is_web'] ? '' : ''; if ($config['poller_id'] > 1 || isset($rdatabase_hostname)) { - if (!(defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1')) { + if (!$is_test_bootstrap) { $local_db_cnn_id = db_connect_real($database_hostname, $database_username, $database_password, $database_default, $database_type, $database_port, $database_retries, $database_ssl, $database_ssl_key, $database_ssl_cert, $database_ssl_ca, $database_ssl_capath, $database_ssl_verify_server_cert); } else { $local_db_cnn_id = new Cacti_TestDbSentinel(); @@ -361,7 +374,7 @@ public function __call($name, $args) { } // Check for recovery - if (is_object($local_db_cnn_id)) { + if (_cacti_is_real_db_conn($local_db_cnn_id)) { $boost_records = db_fetch_cell('SELECT COUNT(*) FROM poller_output_boost', '', true, $local_db_cnn_id); @@ -370,22 +383,24 @@ public function __call($name, $args) { } } - // gather the existing cactidb version - $config['cacti_db_version'] = db_fetch_cell('SELECT cacti FROM version LIMIT 1', '', false, $local_db_cnn_id); + // gather the existing cactidb version (skip when running under the test sentinel) + if (_cacti_is_real_db_conn($local_db_cnn_id)) { + $config['cacti_db_version'] = db_fetch_cell('SELECT cacti FROM version LIMIT 1', '', false, $local_db_cnn_id); + } /** * If we have not been forced offline by the $conn_mode global and since we are * a remote poller, let's attempt to get back online. */ if ($conn_mode != 'offline') { - if (!(defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1')) { + if (!$is_test_bootstrap) { $remote_db_cnn_id = db_connect_real($rdatabase_hostname, $rdatabase_username, $rdatabase_password, $rdatabase_default, $rdatabase_type, $rdatabase_port, $database_retries, $rdatabase_ssl, $rdatabase_ssl_key, $rdatabase_ssl_cert, $rdatabase_ssl_ca, $rdatabase_ssl_capath, $rdatabase_ssl_verify_server_cert); } else { $remote_db_cnn_id = new Cacti_TestDbSentinel(); } } - if ($config['is_web'] && is_object($remote_db_cnn_id) && !($remote_db_cnn_id instanceof Cacti_TestDbSentinel) && $config['connection'] != 'recovery' && $config['cacti_db_version'] != 'new_install' && !defined('IN_CACTI_INSTALL')) { + if ($config['is_web'] && _cacti_is_real_db_conn($remote_db_cnn_id) && $config['connection'] != 'recovery' && $config['cacti_db_version'] != 'new_install' && !defined('IN_CACTI_INSTALL')) { // Connection worked, so now override the default settings so that it will always utilize the remote connection $database_default = $rdatabase_default; $database_hostname = $rdatabase_hostname; @@ -398,7 +413,7 @@ public function __call($name, $args) { $database_ssl_ca = $rdatabase_ssl_ca; $database_ssl_capath = $rdatabase_ssl_capath; $database_ssl_verify_server_cert = $rdatabase_ssl_verify_server_cert; - } elseif (is_object($remote_db_cnn_id)) { + } elseif (_cacti_is_real_db_conn($remote_db_cnn_id)) { if ($config['connection'] != 'recovery') { $config['connection'] = 'online'; } @@ -406,7 +421,7 @@ public function __call($name, $args) { $config['connection'] = 'offline'; } } else { - if (!(defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1')) { + if (!$is_test_bootstrap) { if (!db_connect_real($database_hostname, $database_username, $database_password, $database_default, $database_type, $database_port, $database_retries, $database_ssl, $database_ssl_key, $database_ssl_cert, $database_ssl_ca, $database_ssl_capath, $database_ssl_verify_server_cert)) { print $ps . 'FATAL: Connection to Cacti database failed. Please ensure: ' . $ul; print $li . 'the PHP MySQL module is installed and enabled.' . $il; From eb90c795859819d4ccf43a00c94822007a47dda5 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Wed, 13 May 2026 23:03:30 -0700 Subject: [PATCH 4/4] test: collapse remaining test-bootstrap guard to $is_test_bootstrap The previous review fix introduced a local $is_test_bootstrap flag but left two call sites still inlining defined('PHP_TESTING') && getenv(...). Replace the long-form check and guard the single-poller cacti_db_version fetch so the sentinel branch finishes initialising without invoking the DB. Signed-off-by: Thomas Vincent --- include/global.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/include/global.php b/include/global.php index 63ae37c5f6..1d26ec84fb 100644 --- a/include/global.php +++ b/include/global.php @@ -444,7 +444,7 @@ function _cacti_is_real_db_conn($x) { $local_db_cnn_id = new Cacti_TestDbSentinel(); } - if (!(defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP') === '1')) { + if (!$is_test_bootstrap) { if (!db_table_exists('settings') || !db_table_exists('version')) { print $ps . 'FATAL: Connection to Cacti database succeeded but `settings` table not found. Please ensure: ' . $ul; print $li . 'the PHP MySQL module is installed and enabled.' . $il; @@ -466,8 +466,10 @@ function _cacti_is_real_db_conn($x) { } } - // gather the existing cactidb version - $config['cacti_db_version'] = db_fetch_cell('SELECT cacti FROM version LIMIT 1'); + // gather the existing cactidb version (skip when running under the test sentinel) + if (!$is_test_bootstrap) { + $config['cacti_db_version'] = db_fetch_cell('SELECT cacti FROM version LIMIT 1'); + } } define('CACTI_CONNECTION', $config['connection']);