Files
TheWitnessandCopilot 5afe0f8f68 Add one-click RRDfile repair (Apply Suggested Fixes) to Data Source Info mode (#8401)
* Add one-click RRDfile repair to Data Source Info mode

The Data Source Info mode already compares the RRDfile against the Cacti
definition and prints the rrdtool tune recommendations (add a missing Data
Source with DS:name:type:heartbeat:min:max, remove an extra one with DEL:name,
and correct the type, heartbeat, minimum and maximum). It did not, however,
offer a way to apply them from the page the way data_debug.php does.

- Add an "Apply Suggested Fixes" link to the "RRDtool Tune Info" box header when
  the web server can write the RRDfile; when it cannot, the header falls back to
  "Run Fix from Command Line" and the commands remain listed for manual use.
- Add a ds_repair action that runs the same tune recommendations server-side
  (gated on data_source_authorized() and on the RRDfile being writable, and
  skipping any recommendation carrying control characters), logs each command to
  DSDEBUG, then reloads the Data Source so the result is visible. This mirrors
  data_debug.php's run_repair / "Apply Suggested Fixes".
- Harden the Info-mode guard so a missing/unreadable RRDfile (rrdtool info
  returning false) no longer triggers an array-offset warning.

* Address review: POST-guard ds_repair and harden repair result check

- Route the "Apply Suggested Fixes" action through a CSRF-protected POST: the
  link now uses the cactiPostAction handler (href="#", data-url) instead of a
  GET navigation, and ds_repair is added to the $bad_actions guard in
  include/global.php so a tokenless request fails closed with 405. ds_repair is
  added to the curated CSRF action test for regression coverage.
- Use a strict comparison on the rrdtool_execute() result: it can return null
  on a failed exec/proxy connection, and null == '' would have reported a failed
  repair as "All RRDfile repairs succeeded."
- Refresh the sink inventory baseline for the new header redirects in ds_repair.

* Add request-coverage probe for the ds_repair RRDfile repair handler

The Data Source Info mode repair handler ds_repair() is a POST action that the
page-level e2e suites do not drive (it needs a Data Source whose RRDfile
mismatches its Cacti definition). Add a request-coverage fixture that loads the
handler in isolation via cacti_fixture_source_function(), stubs its I/O
boundaries, and drives every branch (unauthorized, unreadable RRDfile, no RRA,
nothing to tune, not writable, a control-character recommendation that is
skipped, and both tune success and failure). Register it in
run_request_coverage.sh for data_sources.php changes so the changed-line
coverage gate sees the handler exercised.

* Address review: use defined helpers and boolean repair result

- Replace the undefined cacti_has_control_chars() check with a comparison
  against rrd_strip_control_chars() (loaded from lib/rrd.php) to skip any
  tune recommendation that carries control characters.
- Execute each tune with RRDTOOL_OUTPUT_BOOLEAN and treat a strict true as
  success so proxy storage (which returns the raw 'OK u:...' status under
  RRDTOOL_OUTPUT_RETURN_STDERR) is not misreported as a failure.
- Add the new translatable strings to locales/po/cacti.pot and reuse the
  existing failure msgid.
- Update the request-coverage fixture to the new boundary contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 20:47:31 -04:00

883 lines
31 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/*
!!! IMPORTANT !!!
The following defaults are not to be altered. Please refer to
include/config.php for user configurable settings.
test
*/
global $config;
$config = [];
// Workaround for PHPStan scanning
global $rdatabase_default, $rdatabase_hostname, $rdatabase_password,
$rdatabase_port, $rdatabase_type, $rdatabase_username;
// Workaround End
// define if cacti is in CLI mode
define('CACTI_CLI', (php_sapi_name() == 'cli'));
define('CACTI_WEB', (php_sapi_name() != 'cli'));
define('CACTI_SERVER_OS', (str_starts_with(PHP_OS, 'WIN')) ? 'win32' : 'unix');
define('CACTI_ERR_2NULL', (str_starts_with(PHP_OS, 'WIN')) ? ' 2> NULL' : ' 2> /dev/null');
if (defined('CACTI_CLI_ONLY') && CACTI_WEB) {
die('<br><strong>This script is only meant to run at the command line.</strong>');
}
// this should be auto-detected, set it manually if needed
$config['cacti_server_os'] = (str_starts_with(PHP_OS, 'WIN')) ? 'win32' : 'unix';
$config['is_web'] = CACTI_WEB;
// load cacti version from file
$cacti_version_file = __DIR__ . '/cacti_version';
if (!file_exists($cacti_version_file)) {
die('ERROR: failed to find cacti version file');
}
$cacti_version = file_get_contents($cacti_version_file, false);
if ($cacti_version === false) {
die('ERROR: failed to load cacti version file');
}
$cacti_version = trim($cacti_version);
// define documentation table of contents
define('CACTI_DOCUMENTATION_TOC', 'docs/Table-of-Contents.html');
// By default, we assume that it is not
// an AJAX request.
global $is_request_ajax;
$is_request_ajax = false;
// If HTTP_X_REQUESTED_WITH is equal to xmlhttprequest
// We assume this is an ajax call
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strcasecmp($_SERVER['HTTP_X_REQUESTED_WITH'], 'xmlhttprequest') == 0) {
$is_request_ajax = true;
} elseif (isset($_REQUEST['header']) && $_REQUEST['header'] == 'false') {
$is_request_ajax = true;
} elseif (isset($_REQUEST['headercontent'])) {
$is_request_ajax = false;
}
// Default database settings
$database_type = 'mysql';
$database_default = 'cacti';
$database_hostname = 'localhost';
$database_username = 'cactiuser';
$database_password = 'cactiuser';
$database_port = 3306;
$database_retries = 2;
$database_ssl = false;
$database_ssl_key = '';
$database_ssl_cert = '';
$database_ssl_ca = '';
$database_ssl_capath = '';
$database_ssl_verify_server_cert = true;
$database_persist = true;
// Default session name - Session name must contain alpha characters
$cacti_session_name = 'Cacti';
if (isset($_COOKIE['CactiTab'])) {
$cacti_session_name = 'CactiTabId:' . $_COOKIE['CactiTab'];
}
// define default url path
$url_path = '/cacti/';
// disable log rotation setting
$disable_log_rotation = false;
// Include configuration, or use the defaults
if (file_exists(__DIR__ . '/config.php')) {
if (!is_readable(__DIR__ . '/config.php')) {
die('Configuration file include/config.php is present, but unreadable.' . PHP_EOL);
}
include(__DIR__ . '/config.php');
}
// Combined test-bootstrap gate: PHP_TESTING alone is not enough. The
// CACTI_TEST_BOOTSTRAP env var (local_only so per-request SAPI params under
// FPM/FastCGI cannot set it) must also be present. A stray PHP_TESTING define
// in a deployed environment therefore cannot swap to the default config, skip
// schema validation, or disable real DB connection logic. Fail-closed.
if (!function_exists('cacti_is_test_bootstrap')) {
function cacti_is_test_bootstrap(): bool {
return defined('PHP_TESTING') && getenv('CACTI_TEST_BOOTSTRAP', true) === '1';
}
}
if (cacti_is_test_bootstrap() && file_exists(__DIR__ . '/config.php.dist')) {
if (!is_readable(__DIR__ . '/config.php.dist')) {
die('Configuration file include/config.php is present, but unreadable.' . PHP_EOL);
}
include(__DIR__ . '/config.php.dist');
}
if (isset($config['cacti_version'])) { // @phpstan-ignore-line
die('Invalid include/config.php file detected.' . PHP_EOL);
}
// Define global paths
require_once(__DIR__ . '/global_path.php');
// Should we allow proxy ip headers?
$config['proxy_headers'] = $proxy_headers ?? [];
// Reverse proxies allowed to assert a forwarded, pre-authenticated user
$config['trusted_proxies'] = $trusted_proxies ?? [];
// Set the poller_id
if (isset($poller_id)) {
$config['poller_id'] = $poller_id;
define('POLLER_ID', $poller_id);
} else {
$config['poller_id'] = 1;
define('POLLER_ID', 1);
}
$db_var_defaults = [
'database_type' => 'mysql',
'database_default' => null,
'database_hostname' => null,
'database_username' => null,
'database_password' => null,
'database_port' => '3306',
'database_retries' => 2,
'database_ssl' => false,
'database_ssl_key' => '',
'database_ssl_cert' => '',
'database_ssl_ca' => '',
'database_ssl_capath' => '',
'database_ssl_verify_server_cert' => true,
];
$db_var_prefixes = [''];
if ($config['poller_id'] > 1 || isset($rdatabase_hostname)) {
$db_var_prefixes[] = 'r';
}
$db_missing_vars = '';
foreach ($db_var_prefixes as $db_var_prefix) {
foreach ($db_var_defaults as $db_var_name => $db_var_default) {
$db_var_full = $db_var_prefix . $db_var_name;
if (!isset(${$db_var_full})) {
if ($db_var_default !== null) {
${$db_var_full} = $db_var_default;
} else {
$db_missing_vars .= (($db_missing_vars == '') ? 'missing ' : ', ') . $db_var_full;
}
}
}
}
if (!empty($db_missing_vars)) {
die("config.php is $db_missing_vars" . PHP_EOL);
}
// set the local for international users
setlocale(LC_CTYPE, 'en_US.UTF-8');
$colors = [];
// required for Windows
if ($config['cacti_server_os'] == 'win32') {
putenv('MIB_DIRS=c:/usr/share/snmp/mibs');
}
if (!empty($path_csrf_secret)) {
$config['path_csrf_secret'] = $path_csrf_secret;
} else {
$config['path_csrf_secret'] = CACTI_PATH_INCLUDE . '/vendor/csrf/csrf-secret.php';
}
define('CACTI_CSRF_SECRET', $config['path_csrf_secret']);
// built-in snmp support
if ((isset($php_snmp_support) && $php_snmp_support == false) || !function_exists('snmpget')) {
$config['php_snmp_support'] = false;
} else {
$config['php_snmp_support'] = class_exists('SNMP');
}
define('CACTI_PHP_SNMP', $config['php_snmp_support']);
// PHP binary location
if (isset($php_path)) {
$config['php_path'] = $php_path;
}
// Set various debug fields
$config['DEBUG_READ_CONFIG_OPTION'] = defined('DEBUG_READ_CONFIG_OPTION');
$config['DEBUG_READ_CONFIG_OPTION_DB_OPEN'] = defined('DEBUG_READ_CONFIG_OPTION_DB_OPEN');
$config['DEBUG_SQL_CMD'] = defined('DEBUG_SQL_CMD');
$config['DEBUG_SQL_FLOW'] = defined('DEBUG_SQL_FLOW');
$config['DEBUG_SQL_CONNECT'] = defined('DEBUG_SQL_CONNECT');
// check for an empty database port
if (empty($database_port)) { // @phpstan-ignore-line
$database_port = 3306;
}
if (isset($input_whitelist)) {
$config['input_whitelist'] = $input_whitelist;
define('CACTI_WHITELIST', $input_whitelist);
}
// define any additional paths as constants
foreach ($config as $key => $value) {
if (str_ends_with($key, '_path')) {
$path_name = substr($key, 0, -5);
$constant_name = 'CACTI_PATH_' . mb_strtoupper($path_name);
if (!defined($constant_name)) {
define($constant_name, $value);
}
}
}
if (isset($i18n_handler)) {
$config['i18n_language_handler'] = $i18n_handler;
}
if (isset($i18n_force_language)) {
$config['i18n_force_language'] = $i18n_force_language;
}
if (isset($i18n_log)) {
$config['i18n_log'] = $i18n_log;
}
if (isset($i18n_text_log)) {
$config['i18n_text_log'] = $i18n_text_log;
}
/* Shared object cache directory (see $config['cache_dir'] in config.php).
* Default to a private cache/ under the install. If it cannot be created or is
* not writable the shared cache simply degrades to a process-local tier; never
* fall back to a world-writable shared temp directory, which would let a local
* user plant predictable cache files for the poller to read. */
if (empty($config['cache_dir'])) {
$config['cache_dir'] = $config['base_path'] . '/cache';
}
if (!is_dir($config['cache_dir'])) {
@mkdir($config['cache_dir'], 0770, true);
}
// include base modules
require_once(CACTI_PATH_LIBRARY . '/database.php');
require_once(CACTI_PATH_LIBRARY . '/functions.php');
require_once(CACTI_PATH_LIBRARY . '/graph_template_input.php');
require_once(CACTI_PATH_LIBRARY . '/renderer.php');
require_once(CACTI_PATH_LIBRARY . '/headers_secure.php');
require_once(CACTI_PATH_INCLUDE . '/global_constants.php');
define('CACTI_VERSION', format_cacti_version($cacti_version, CACTI_VERSION_FORMAT_SHORT));
define('CACTI_VERSION_FULL', format_cacti_version($cacti_version, CACTI_VERSION_FORMAT_FULL));
require_once(CACTI_PATH_LIBRARY . '/htmx.php');
require_once(CACTI_PATH_LIBRARY . '/html.php');
require_once(CACTI_PATH_LIBRARY . '/html_utility.php');
require_once(CACTI_PATH_LIBRARY . '/html_validate.php');
require_once(CACTI_PATH_LIBRARY . '/html_filter.php');
$filename = get_current_page();
if (isset($no_http_headers) && $no_http_headers == true) {
$config['is_web'] = false;
if (!str_contains(php_sapi_name(), 'cli') || !isset($_SERVER['argv'])) {
print 'FATAL: This file can only be called from the command line.' . PHP_EOL;
exit;
}
}
if ($config['is_web'] && ini_get('session.auto_start') == 1) {
print 'FATAL: PHP setting session.auto_start NOT supported. Disable in your php.ini file and then restart your Web Service' . PHP_EOL;
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.
// Pass false to class_exists() so this guard never triggers autoload.
if (!class_exists('Cacti_TestDbSentinel', false)) {
final class Cacti_TestDbSentinel {
/**
* @param string $name
* @param array<int,mixed> $args
* @return never
*/
public function __call($name, $args) {
throw new \RuntimeException('PHP_TESTING DB sentinel called: ' . $name);
}
}
}
// 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(mixed $x): bool {
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 = cacti_is_test_bootstrap();
// set poller mode
global $local_db_cnn_id, $remote_db_cnn_id, $conn_mode;
$config['connection'] = 'online';
$ps = $config['is_web'] ? '<p>' : '';
$sp = $config['is_web'] ? '</p>' : PHP_EOL;
$ul = $config['is_web'] ? '<ul>' : PHP_EOL;
$li = $config['is_web'] ? '<li>' : PHP_EOL . ' - ';
$lu = $config['is_web'] ? '</ul>' : '';
$il = $config['is_web'] ? '</li>' : '';
/* Git checkouts and interrupted upgrades do not ship include/vendor. Without
this check the autoload require below produces a bare fatal error. */
// The installer can repair a missing vendor tree before application classes
// are loaded. All normal entry points still fail closed with an actionable
// message rather than emitting a raw require() fatal.
$vendor_autoload = CACTI_PATH_INCLUDE . '/vendor/autoload.php';
if (!is_file($vendor_autoload) && !defined('IN_CACTI_INSTALL')) {
print $ps . 'FATAL: The include/vendor directory is not populated. Cacti requires its Composer dependencies before it can start. From the Cacti directory, run:' . $sp;
print $ps . 'composer install' . $sp;
print $ps . 'Then reload this page. See the README for details.' . $sp;
exit(1);
}
if ($config['poller_id'] > 1 || isset($rdatabase_hostname)) {
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();
}
if (!isset($rdatabase_retries)) {
$rdatabase_retries = 2;
}
if (!isset($rdatabase_ssl)) {
$rdatabase_ssl = false;
}
if (!isset($rdatabase_ssl_key)) {
$rdatabase_ssl_key = false;
}
if (!isset($rdatabase_ssl_cert)) {
$rdatabase_ssl_cert = false;
}
if (!isset($rdatabase_ssl_ca)) {
$rdatabase_ssl_ca = false;
}
if (!isset($rdatabase_ssl_capath)) {
$rdatabase_ssl_capath = false;
}
if (!isset($rdatabase_ssl_verify_server_cert)) {
$rdatabase_ssl_verify_server_cert = true;
}
// Check for recovery
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);
if ($boost_records > 0) {
$config['connection'] = 'recovery';
}
}
// gather the existing cactidb version (skip when running under the test sentinel).
// Seed '' first so the key is always set, matching pre-sentinel behavior where a
// failed db_fetch_cell returned ''; a remote poller with the local DB down then
// reads a defined value instead of raising an 'Undefined array key' warning.
$config['cacti_db_version'] = '';
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 (!$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'] && _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;
$database_username = $rdatabase_username;
$database_password = $rdatabase_password;
$database_port = $rdatabase_port;
$database_ssl = $rdatabase_ssl;
$database_ssl_key = $rdatabase_ssl_key;
$database_ssl_cert = $rdatabase_ssl_cert;
$database_ssl_ca = $rdatabase_ssl_ca;
$database_ssl_capath = $rdatabase_ssl_capath;
$database_ssl_verify_server_cert = $rdatabase_ssl_verify_server_cert;
} elseif (_cacti_is_real_db_conn($remote_db_cnn_id)) {
if ($config['connection'] != 'recovery') {
$config['connection'] = 'online';
}
} else {
$config['connection'] = 'offline';
}
} else {
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;
print $li . 'the database is running.' . $il;
print $li . 'the credentials in config.php are valid.' . $il;
print $lu . $sp;
if (isrv('display_db_errors') && !empty($config['DATABASE_ERROR'])) { // @phpstan-ignore-line
print $ps . 'The following database errors occurred: ' . $ul;
foreach ($config['DATABASE_ERROR'] as $e) { // @phpstan-ignore-line
print $li . $e['Code'] . ': ' . $e['Error'] . $il;
}
print $lu . $sp;
}
exit;
}
} else {
$local_db_cnn_id = new Cacti_TestDbSentinel();
}
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;
print $li . 'the database is running.' . $il;
print $li . 'the cacti.sql has been imported.' . $il;
print $li . 'the credentials in config.php are valid and correct.' . $il;
print $lu . $sp;
if (isrv('display_db_errors') && !empty($config['DATABASE_ERROR'])) { // @phpstan-ignore-line
print $ps . 'The following database errors occurred: ' . $ul;
foreach ($config['DATABASE_ERROR'] as $e) { // @phpstan-ignore-line
print $li . $e['Code'] . ': ' . $e['Error'] . $il;
}
print $lu . $sp;
}
exit;
}
}
// gather the existing cactidb version (skip when running under the test sentinel).
// Seed '' first so the key is always set even under the test bootstrap.
$config['cacti_db_version'] = '';
if (!$is_test_bootstrap) {
$config['cacti_db_version'] = db_fetch_cell('SELECT cacti FROM version LIMIT 1');
}
}
define('CACTI_CONNECTION', $config['connection']);
// check cacti log is available
$log_filename = cacti_log_file();
if (!is_resource_writable($log_filename)) {
print $ps . 'FATAL: System log file is not available for writing. Please ensure: ' . $ul;
print $li . 'the log folder is correctly set.' . $il;
if (CACTI_CLI) {
print $li . 'the script was run as the website user. ' . $il;
}
print $li . 'the log folder is writable by the website user.' . $il;
print $li . 'there is enough disk space.' . $il;
print $lu . $sp;
if (CACTI_CLI) {
print $ps . 'To run as the website user, use sudo -u <website user> php -q <script file>' . $sp;
}
print $ps . 'Log: ' . $log_filename . $sp;
}
// prime the most popular config settings
cache_common_config_settings();
if ($config['poller_id'] > 1) {
$timezone = db_fetch_cell_prepared('SELECT timezone
FROM poller
WHERE id = ?',
[$config['poller_id']]);
if ($timezone != '') {
db_execute_prepared('SET time_zone = ?', [$timezone]);
}
if (db_column_exists('poller', 'log_level')) {
$poller_log_level = db_fetch_cell_prepared('SELECT log_level
FROM poller
WHERE id = ?',
[$config['poller_id']]);
if ($poller_log_level > 0) {
define('POLLER_LOG_LEVEL', $poller_log_level);
} else {
define('POLLER_LOG_LEVEL', '-1');
}
} else {
define('POLLER_LOG_LEVEL', '-1');
}
} else {
define('POLLER_LOG_LEVEL', '-1');
}
if (!defined('IN_CACTI_INSTALL')) {
set_error_handler('CactiErrorHandler');
register_shutdown_function('CactiShutdownHandler');
}
// verify the cacti database is initialized before moving past here
db_cacti_initialized($config['is_web']);
if ($config['is_web']) {
if (read_config_option('force_https') == 'on') {
if (!cacti_is_https()) {
/* SERVER_NAME mirrors the client Host header under the common Apache
* default UseCanonicalName Off, so validating its format alone still
* lets an attacker redirect to another valid-looking hostname (open
* redirect / cache poisoning). Prefer the admin-configured base_url,
* the trusted source Cacti already uses for absolute URLs, and only
* fall back to the validated request host when it is not set. */
$https_host = cacti_force_https_host();
/* without a trusted, admin-configured base_url there is no host we
* can redirect to safely: SERVER_NAME mirrors the client Host header
* under the common Apache default UseCanonicalName Off, so falling
* back to it would reopen the same open-redirect/cache-poisoning
* issue this check exists to close. Fail closed instead. */
$location = ($https_host !== '') ? cacti_build_https_redirect_url(
$https_host,
$_SERVER['REQUEST_URI'] ?? '',
CACTI_PATH_URL
) : '';
if ($location === '') {
http_response_code(400);
exit;
}
header('Location: ' . $location);
exit;
}
}
// set the maximum post size
ini_set('post_max_size', '8M');
// add additional cookie directives
ini_set('session.cookie_httponly', true);
ini_set('session.cookie_path', CACTI_PATH_URL);
ini_set('session.use_strict_mode', true);
$options = [
COOKIE_OPTIONS_HTTPONLY => true,
COOKIE_OPTIONS_PATH => CACTI_PATH_URL,
COOKIE_OPTIONS_STRICT => true
];
if (isset($cacti_cookie_domain) && $cacti_cookie_domain != '') {
ini_set('session.cookie_domain', $cacti_cookie_domain);
$options[COOKIE_OPTIONS_DOMAIN] = $cacti_cookie_domain;
}
// SameSite php7.3+ behavior
if (version_compare(PHP_VERSION, '7.3', '>=')) {
ini_set('session.cookie_samesite', 'Strict');
$options[COOKIE_OPTIONS_SAMESITE] = 'Strict';
}
if (cacti_is_https()) {
ini_set('session.cookie_secure', true);
$options[COOKIE_OPTIONS_SECURE] = true;
}
$config[COOKIE_OPTIONS] = $options;
$config[CACTI_SESSION_NAME] = $cacti_session_name;
if (isset($cacti_db_session) && $cacti_db_session && db_table_exists('sessions') && CACTI_CONNECTION == 'online') {
include(__DIR__ . '/session.php');
} else {
$cacti_db_session = false;
}
// we don't want these pages cached
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('X-Frame-Options: SAMEORIGIN');
// increased web hardening
$alternates = CactiSecureHeaders::normalizeAlternateSources(read_config_option('content_security_alternate_sources'));
$script_src = CactiSecureHeaders::scriptSrc($alternates);
$report_uri = CactiSecureHeaders::getCspMode() === 'nonce-enforce' ?
CactiSecureHeaders::reportUriDirective() : '';
header("Content-Security-Policy: default-src *; img-src 'self' https://api.qrserver.com $alternates data: blob:; style-src 'self' 'unsafe-inline' $alternates; $script_src; frame-ancestors 'self' $alternates; worker-src 'self' $alternates;$report_uri");
$report_script_src = CactiSecureHeaders::reportOnlyScriptSrc($alternates);
if ($report_script_src !== '') {
$report_uri = CactiSecureHeaders::reportUriDirective();
header("Content-Security-Policy-Report-Only: default-src *; img-src 'self' https://api.qrserver.com $alternates data: blob:; style-src 'self' 'unsafe-inline' $alternates; $report_script_src; frame-ancestors 'self' $alternates; worker-src 'self' $alternates;$report_uri");
}
// prevent IE from silently rejects cookies sent from third party sites.
header('P3P: CP="CAO PSA OUR"');
// header() replaces by default, so a second Cache-Control here would drop
// no-store and leave every authenticated page cacheable
header('Cache-Control: no-store, no-cache, must-revalidate');
cacti_session_start();
// make sure to start only Cacti session at a time
if (!isset($_SESSION[CACTI_CWD])) {
$_SESSION[CACTI_CWD] = CACTI_PATH_BASE;
} else {
if ($_SESSION[CACTI_CWD] != CACTI_PATH_BASE) {
cacti_session_destroy();
}
}
// Sanitize the http referer
if (isset($_SERVER['HTTP_REFERER'])) {
$_SERVER['HTTP_REFERER'] = sanitize_uri($_SERVER['HTTP_REFERER']);
}
}
// emulate 'register_globals' = 'off' if turned on
if ((bool)ini_get('register_globals')) {
$not_unset = ['_GET', '_POST', '_COOKIE', '_SERVER', '_SESSION', '_ENV', '_FILES', 'database_type', 'database_default', 'database_hostname', 'database_username', 'database_password', 'config', 'colors'];
/* Not only will array_merge give a warning if a parameter is not an array, it will
* actually fail. So we check if HTTP_SESSION_VARS has been initialised. */
if (!isset($_SESSION)) {
$_SESSION = [];
}
// Merge all into one extremely huge array; unset this later
$input = array_merge($_GET, $_POST, $_COOKIE, $_SERVER, $_SESSION, $_ENV, $_FILES);
unset($input['input']);
unset($input['not_unset']);
foreach ($input as $var => $val) {
if (!in_array($var, $not_unset, true)) {
unset(${$var});
}
}
unset($input);
}
define('CACTI_DATE_TIME_FORMAT', date_time_format());
require_once(CACTI_PATH_INCLUDE . '/global_languages.php');
define('CACTI_VERSION_BRIEF', get_cacti_version_text(false, CACTI_VERSION));
define('CACTI_VERSION_BRIEF_FULL', get_cacti_version_text(false, CACTI_VERSION_FULL));
define('CACTI_VERSION_TEXT', get_cacti_version_text(true, CACTI_VERSION));
define('CACTI_VERSION_TEXT_FULL', get_cacti_version_text(true, CACTI_VERSION_FULL));
define('CACTI_VERSION_TEXT_CLI', get_cacti_cli_version(true, CACTI_VERSION_FULL));
require_once(CACTI_PATH_LIBRARY . '/auth.php');
require_once(CACTI_PATH_LIBRARY . '/plugins.php');
require_once(CACTI_PATH_INCLUDE . '/plugins.php');
require_once(CACTI_PATH_INCLUDE . '/global_arrays.php');
require_once(CACTI_PATH_INCLUDE . '/global_settings.php');
require_once(CACTI_PATH_INCLUDE . '/global_form.php');
require_once(CACTI_PATH_LIBRARY . '/html_form.php');
require_once(CACTI_PATH_LIBRARY . '/html_filter.php');
require_once(CACTI_PATH_LIBRARY . '/variables.php');
require_once(CACTI_PATH_LIBRARY . '/mib_cache.php');
require_once(CACTI_PATH_LIBRARY . '/poller.php');
require_once(CACTI_PATH_LIBRARY . '/snmpagent.php');
require_once(CACTI_PATH_LIBRARY . '/aggregate.php');
require_once(CACTI_PATH_LIBRARY . '/api_automation.php');
require_once(CACTI_PATH_INCLUDE . '/domain.php');
// The CSRF guard resolves Symfony classes as include/csrf.php is evaluated,
// so the autoloader has to be registered first. During a fresh install the
// vendor directory may not exist yet; CactiCsrfGuard degrades in that case.
if (is_file($vendor_autoload)) {
require_once($vendor_autoload);
}
require_once(CACTI_PATH_INCLUDE . '/csrf.php');
// csrf-magic ran this from the bottom of its own file, so including the
// library was what protected the request. The guard is invoked explicitly.
csrf_startup();
if ($config['is_web']) {
// raise a message and perform a page refresh if we've changed modes
if ($config['poller_id'] > 1) {
if (isset($_SESSION['connection_mode'])) {
$previous_mode = $_SESSION['connection_mode'];
$reload = false;
cacti_log('Connection: ' . CACTI_CONNECTION . ', Previous Mode: ' . $previous_mode . ', Page: ' . $_SERVER['SCRIPT_NAME'], false, 'WEBUI', POLLER_VERBOSITY_DEBUG);
if (CACTI_CONNECTION == 'online' && ($previous_mode != CACTI_CONNECTION)) {
$reload = true;
$message = __('The Main Data Collector has returned to an Online Status');
$level = MESSAGE_LEVEL_INFO;
} elseif (CACTI_CONNECTION != 'online' && $previous_mode == 'online') {
$reload = true;
$message = __('The Main Data Collector has gone to an Offline or Recovering Status');
$level = MESSAGE_LEVEL_ERROR;
}
if ($reload) {
$_SESSION['connection_mode'] = CACTI_CONNECTION;
raise_message('connection_state', $message, $level);
session_destroy();
print '<div style="display:none">cactiRemoteState</div>';
exit;
}
} else {
cacti_log('Connection: ' . CACTI_CONNECTION . ', Previous Mode: notset', false, 'WEBUI', POLLER_VERBOSITY_DEBUG);
$previous_mode = CACTI_CONNECTION;
$_SESSION['connection_mode'] = CACTI_CONNECTION;
}
}
if (isrv('newtheme')) {
$newtheme = gnrv('newtheme');
$newtheme_css = __DIR__ . "/themes/$newtheme/main.css";
if (is_valid_theme($newtheme)) {
$_SESSION['selected_theme'] = $newtheme;
}
}
if (isrv('csrf_timeout')) {
raise_message('csrf_ptimeout');
}
// check for save actions using GET
if (isrv('action')) {
$action = gnrv('action');
// GHSA-w3qw-762w-6g9v: an array-form action[] makes every scalar
// $action == $bad comparison below false, slipping a state-changing
// action past this guard. A real action is never an array here.
if (is_array($action)) {
header('Allow: POST');
http_response_code(405);
exit;
}
// State-changing actions must arrive by POST with a CSRF token. Their
// links use cactiPostAction so the query fields and token are submitted
// in the request body instead of remaining cross-origin GET targets.
$bad_actions = [
'save', 'update_data', 'changepassword',
'item_remove', 'item_moveup', 'item_movedown',
'item_remove_gsv', 'item_remove_dssv',
'item_moveup_gsv', 'item_moveup_dssv',
'item_movedown_gsv', 'item_movedown_dssv',
'delete_node', 'create_node', 'rename_node', 'move_node', 'copy_node',
'gt_remove', 'query_remove', 'remove', 'change_leaf',
'moveup', 'movedown', 'tree_up', 'tree_down',
'move_page_up', 'move_page_down', 'rrd_add', 'rrd_remove',
'ds_repair',
'accept',
];
foreach ($bad_actions as $bad) {
if ($action == $bad && ($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
// Preserve the legacy warning for form actions. Item-action links
// can also come from plugins outside this repository; crawlers may
// discover those URLs, but they must still fail closed.
if (in_array($bad, ['save', 'update_data', 'changepassword'], true)) {
cacti_log(sprintf('WARNING: Attempt to use GET method for POST operations in page %s from IP %s', $filename, get_client_addr()), false, 'WEBUI');
}
header('Allow: POST');
http_response_code(405);
exit;
}
if ($action == $bad && !isset($_POST['__csrf_magic'])) {
http_response_code(403);
exit;
}
}
}
cacti_time_zone_set();
}
api_plugin_hook('config_insert');
// set config cacti_version for plugins
$config['cacti_version'] = CACTI_VERSION;