false, 'status' => 0, 'body' => '', 'error' => 'Invalid URL'];
}
$parts = parse_url($url);
if ($parts === false || empty($parts['scheme']) || empty($parts['host'])) {
return ['success' => false, 'status' => 0, 'body' => '', 'error' => 'Invalid URL'];
}
$scheme = strtolower($parts['scheme']);
if (!in_array($scheme, ['https', 'http'], true)) {
return ['success' => false, 'status' => 0, 'body' => '', 'error' => 'Unsupported URL scheme'];
}
if (isset($options['allowed_hosts']) && !in_array($parts['host'], $options['allowed_hosts'], true)) {
return ['success' => false, 'status' => 0, 'body' => '', 'error' => 'Host not allowed'];
}
$safeIps = cacti_http_resolve_safe_ips($parts['host']);
if (empty($safeIps)) {
return ['success' => false, 'status' => 0, 'body' => '', 'error' => 'Target host resolves to a disallowed address'];
}
$ch = curl_init();
$port = $parts['port'] ?? ($scheme === 'https' ? 443 : 80);
// Pin the connection to the address(es) just validated via CURLOPT_RESOLVE,
// so curl's own DNS lookup at connect time cannot return a different
// (private/loopback) address than the one already checked above - the
// classic SSRF-via-DNS-rebinding bypass of a separate preflight check.
// TLS hostname/certificate verification below still runs against
// $parts['host'], unaffected by which address it is pinned to.
$resolve = [];
foreach ($safeIps as $ip) {
$resolve[] = cacti_http_bracket_ipv6($parts['host']) . ':' . $port . ':' . cacti_http_bracket_ipv6($ip);
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_RESOLVE => $resolve,
CURLOPT_TIMEOUT => (int) ($options['timeout'] ?? 10),
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
]);
if (isset($options['body'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $options['body']);
}
if (!empty($options['headers'])) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $options['headers']);
}
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if (!is_string($body)) {
return ['success' => false, 'status' => $status, 'body' => '', 'error' => $error !== '' ? $error : 'Request failed'];
}
return [
'success' => $status >= 200 && $status < 300,
'status' => $status,
'body' => $body,
'error' => $status >= 300 ? 'HTTP ' . $status : ''
];
}
/**
* Resolves a hostname (or validates an IP literal) once and returns its
* addresses only when every one of them is public/routable - never
* loopback/private/link-local/reserved space. Used both to decide whether
* cacti_http() may proceed and, via CURLOPT_RESOLVE, to pin the connection
* to these exact addresses so a second, independent DNS lookup at connect
* time cannot substitute a different (unsafe) address (DNS rebinding).
*
* @param string $host The hostname or IP literal from the target URL.
*
* @return string[] The resolved addresses, or [] if none/any is unsafe.
*/
function cacti_http_resolve_safe_ips(string $host) : array {
// parse_url() can hand back an IPv6 literal wrapped in [brackets]; strip
// them for validation/DNS purposes, they get re-added (where curl
// requires them) when building the CURLOPT_RESOLVE entry below.
$bareHost = trim($host, '[]');
if (filter_var($bareHost, FILTER_VALIDATE_IP)) {
$ips = [$bareHost];
} else {
$records = @dns_get_record($bareHost, DNS_A + DNS_AAAA);
if (!is_array($records)) {
return [];
}
$ips = array_filter(array_map(static function ($record) {
return $record['ip'] ?? ($record['ipv6'] ?? null);
}, $records));
}
if (empty($ips)) {
return [];
}
foreach ($ips as $ip) {
// An IPv4-mapped IPv6 literal (::ffff:x.x.x.x) is structurally valid
// IPv6 and can pass the private/reserved check below on the IPv6
// side while curl still connects via the embedded IPv4 address -
// validate that embedded address instead when present.
$mapped = cacti_http_ipv4_mapped_address($ip);
if (!filter_var($mapped ?? $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return [];
}
}
return array_values($ips);
}
/**
* Extracts the embedded IPv4 address from an IPv4-mapped IPv6 literal
* (the ::ffff:0:0/96 range, e.g. "::ffff:169.254.169.254"), or null if
* $ip is not one.
*
* @param string $ip An IP literal, IPv4 or IPv6.
*
* @return string|null The embedded IPv4 address, or null.
*/
function cacti_http_ipv4_mapped_address(string $ip) : ?string {
$binary = @inet_pton($ip);
if ($binary === false || strlen($binary) !== 16 || substr($binary, 0, 10) !== str_repeat("\x00", 10) || substr($binary, 10, 2) !== "\xff\xff") {
return null;
}
$embedded = inet_ntop(substr($binary, 12, 4));
return $embedded !== false ? $embedded : null;
}
/**
* Wraps an IPv6 address in [brackets] (the syntax CURLOPT_RESOLVE and URLs
* require to disambiguate its colons from a host:port:address separator);
* IPv4 addresses and hostnames are returned unchanged.
*
* @param string $host An IPv6/IPv4 address or hostname, optionally already bracketed.
*
* @return string
*/
function cacti_http_bracket_ipv6(string $host) : string {
$bare = trim($host, '[]');
return filter_var($bare, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '[' . $bare . ']' : $host;
}
/**
* Takes a string of text, truncates it to $max_length and appends
* three periods onto the end
*
* @param string $text the string to evaluate
* @param int $max_length the maximum number of characters the string can contain
*
* @return string the truncated string if len($text) is greater than $max_length, else
* the original string
*/
function title_trim(string $text, int $max_length) : string {
if (strlen($text) > $max_length) {
return mb_substr($text, 0, $max_length) . '...';
} else {
return $text;
}
}
/**
* A quick way to highlight text in a table from general filtering
*
* @param mixed $value The string to filter
* @param string $filter The search term to filter for
* @param string $href The href if you wish to have an anchor returned
* @param string $title The title of the resulting anchor
*
* @return string the filtered string
*/
function filter_value(mixed $value, string $filter, string $href = '', string $title = '') : string {
if ($value == '') {
return '';
}
/* html_escape() resolves the charset and replaces the grave accent itself,
* so the copies that stood here were dead: the charset was never passed on
* and no accent survives the escape for a second pass to find. */
$value = htmle($value);
if ($filter != '') {
$value = preg_replace('#(' . preg_quote($filter) . ')#i', "\\1", $value) ?? $value;
}
if ($href != '') {
$value = '' . $value . '';
}
return $value;
}
/**
* Wrapper to set_user_setting().
*
* @deprecated v1.0
*
* @param string $config_name The name of the configuration setting as specified $settings array
* @param mixed $value The values to be saved
* @param mixed $user The user id, otherwise the session user
*
* @return void
*/
function set_graph_config_option(string $config_name, mixed $value, mixed $user = null) : void {
set_user_setting($config_name, $value, $user);
}
/**
* Wrapper to user_setting_exists
*
* @deprecated v1.0
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
* @param int $user_id The id of the user to check the configuration
* value for
*
* @return bool true if a value exists, false if a value does not exist
*/
function graph_config_value_exists(string $config_name, int $user_id) : bool {
return user_setting_exists($config_name, $user_id);
}
/**
* Wrapper to read_default_user_setting
*
* @deprecated v1.0
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
*
* @return string the default value of the configuration option
*/
function read_default_graph_config_option(string $config_name) : string {
return read_default_user_setting($config_name);
}
/**
* Wrapper to read_user_setting
*
* @deprecated v1.0
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
* @param bool $force
*
* @return string The default value of the configuration option
*/
function read_graph_config_option(string $config_name, bool $force = false) : string {
return read_user_setting($config_name, false, $force);
}
/**
* Sets/updates aLL user settings
*
* @param mixed $user The user id, otherwise the session user
*
* @return void
*/
function save_user_settings(mixed $user = null) : void {
global $settings_user;
// Passed user id, or session id, or else 0
$user ??= $_SESSION[SESS_USER_ID] ?? 0;
foreach ($settings_user as $tab_fields) {
foreach ($tab_fields as $field_name => $field_array) {
// Check every field with a numeric default value and reset it to default if the inputted value is not numeric
if (isset($field_array['default']) && is_numeric($field_array['default']) && !is_numeric(gnrv($field_name))) {
srv($field_name, $field_array['default']);
}
if (isset($field_array['method'])) {
if ($field_array['method'] == 'checkbox') {
set_user_setting($field_name, (isrv($field_name) ? 'on' : ''), $user);
} elseif ($field_array['method'] == 'checkbox_group') {
foreach ($field_array['items'] as $sub_field_name => $sub_field_array) {
set_user_setting($sub_field_name, (isrv($sub_field_name) ? 'on' : ''), $user);
}
} elseif ($field_array['method'] == 'textbox_password') {
if (gnrv($field_name) != gnrv($field_name . '_confirm')) {
$_SESSION[SESS_ERROR_FIELDS][$field_name] = $field_name;
$_SESSION[SESS_FIELD_VALUES][$field_name] = gnrv($field_name);
// Set error 4
$errors[4] = 4;
} elseif (isrv($field_name)) {
set_user_setting($field_name, gnrv($field_name), $user);
}
} elseif ((isset($field_array['items'])) && (is_array($field_array['items']))) {
foreach ($field_array['items'] as $sub_field_name => $sub_field_array) {
if (isrv($sub_field_name)) {
set_user_setting($sub_field_name, gnrv($sub_field_name), $user);
}
}
} elseif (isrv($field_name)) {
set_user_setting($field_name, gnrv($field_name), $user);
}
}
}
}
}
/**
* set_user_setting - sets/updates a user setting with the given value.
*
* @param string $config_name The name of the configuration setting as specified $settings array
* @param mixed $value The values to be saved
* @param mixed $user The user id, otherwise the session user
*
* @return void
*/
function set_user_setting(string $config_name, mixed $value, mixed $user = null) : void {
global $settings_user;
// Passed user id, or session id, or else 0
$user ??= $_SESSION[SESS_USER_ID] ?? 0;
if ($user == 0) {
$mode = isset($_SESSION[SESS_USER_ID]) ? 'WEBUI' : 'POLLER';
cacti_log('NOTE: Attempt to set user setting \'' . $config_name . '\', with no valid user id: ' . cacti_debug_backtrace('', false, false, 0, 1), false, $mode, POLLER_VERBOSITY_MEDIUM);
} elseif (db_table_exists('settings_user')) {
if (strlen($config_name) > 255) {
cacti_log("ERROR: User setting name '$config_name' is too long, will be truncated", false, 'SYSTEM');
}
db_execute_prepared('INSERT INTO settings_user
(user_id, name, value) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)',
[$user, $config_name, $value]);
$_SESSION[OPTIONS_USER][$config_name] = $value;
$settings_user[$config_name]['value'] = $value;
}
}
/**
* Determines if a value exists for the current user/setting specified
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
* @param mixed $user_id The id of the user to check the
* configuration value for
*
* @return bool
*/
function user_setting_exists(string $config_name, mixed $user_id) : bool {
static $exists_user_setting = [];
// We use isset instead of array_key_exists so that
// if we never had a value, we can see if we got on
// but once we know we have one, assume we always
// will
if (!isset($config_name[$exists_user_setting])) {
$value = 0;
if (db_table_exists('settings_user')) {
$value = db_fetch_cell_prepared('SELECT COUNT(*)
FROM settings_user
WHERE name = ?
AND user_id = ?',
[$config_name, $user_id]);
}
$exists_user_setting[$config_name] = ($value !== false && $value > 0);
}
return $exists_user_setting[$config_name];
}
/**
* If a value exists for the current user/setting specified, removes it
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
* @param mixed $user The id of the user to check the
* configuration value for
*
* @return void
*/
function clear_user_setting(string $config_name, mixed $user = null) : void {
// users must have cacti user auth turned on to use this, or the guest account must be active
$effective_uid = $user ?? ($_SESSION[SESS_USER_ID] ?? 0);
if (db_table_exists('settings_user')) {
db_execute_prepared('DELETE FROM settings_user
WHERE name = ?
AND user_id = ?',
[$config_name, $effective_uid]);
}
unset($_SESSION[OPTIONS_USER]);
}
/**
* Finds the default value of a user configuration setting
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
*
* @return string The default value of the configuration option
*/
function read_default_user_setting(string $config_name) : string {
global $settings_user;
$result = '';
foreach ($settings_user as $tab_array) {
if (isset($tab_array[$config_name]['default'])) {
$result = $tab_array[$config_name]['default'];
break;
}
foreach ($tab_array as $field_array) {
if (isset($field_array['items'][$config_name]['default'])) {
$result = $field_array['items'][$config_name]['default'];
break;
}
}
}
return $result;
}
/**
* Finds the current value of a users configuration setting
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
* @param mixed $default The default value is none is set
* @param bool $force Pull the data from the database if true ignoring session
* @param mixed $user The id of the user to read the setting for
*
* @return mixed The current value of the user setting
*/
function read_user_setting(string $config_name, mixed $default = false, bool $force = false, mixed $user = 0) : mixed {
global $config;
// users must have cacti user auth turned on to use this, or the guest account must be active
if ($user == 0 && isset($_SESSION[SESS_USER_ID])) {
$effective_uid = $_SESSION[SESS_USER_ID];
} else {
$effective_uid = $user;
}
if (!$force) {
if (isset($_SESSION[OPTIONS_USER])) {
$user_config_array = $_SESSION[OPTIONS_USER];
}
}
// We use isset instead of array_key_exists so that
// if we never had a value, we can see if we got on
// but once we know we have one, assume we always
// will
if (!isset($user_config_array[$config_name])) {
$db_setting = false;
if (db_table_exists('settings_user')) {
$db_setting = db_fetch_row_prepared('SELECT value
FROM settings_user
WHERE name = ?
AND user_id = ?',
[$config_name, $effective_uid]);
}
if (is_array($db_setting) && cacti_sizeof($db_setting)) {
$user_config_array[$config_name] = $db_setting['value'];
} elseif ($default !== false) {
$user_config_array[$config_name] = $default;
} else {
$user_config_array[$config_name] = read_default_user_setting($config_name);
}
$set_var = CACTI_WEB ? '_SESSION' : 'config';
$set_key = CACTI_WEB ? OPTIONS_USER : 'config_user_options_array';
${$set_var}[$set_key] = $user_config_array;
}
return $user_config_array[$config_name];
}
/**
* Determines of a Cacti setting should be maintained
* on the Remote Data Collector separate from the Main cacti server
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
*
* @return bool true if the setting should be saved locally
*/
function is_remote_path_setting(string $config_name) : bool {
if (POLLER_ID > 1 && (str_contains($config_name, 'path_') || str_contains($config_name, '_path'))) { // @phpstan-ignore-line
return true;
} else {
return false;
}
}
/**
* Sets/updates a cacti config option with the given value.
*
* @param string $config_name The name of the configuration setting as
* specified $settings array in
* 'include/global_settings.php'
* @param mixed $value The values to be saved
* @param bool $remote Push the setting to the remote with
* the exception of path variables
*
* @return void
*/
function set_config_option(string $config_name, mixed $value, bool $remote = false) : void {
global $config;
include_once(CACTI_PATH_LIBRARY . '/poller.php');
if (strlen($config_name) > 255) {
cacti_log("ERROR: Config option name '$config_name' is too long, will be truncated", false, 'SYSTEM');
}
db_execute_prepared('INSERT INTO settings
(name, value) VALUES (?, ?)
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)',
[$config_name, $value]);
if ($remote && !is_remote_path_setting($config_name)) {
$gone_time = read_config_option('poller_interval') * 2;
$pollers = array_rekey(
db_fetch_assoc('SELECT id,
UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_status) AS last_polled
FROM poller
WHERE id > 1
AND disabled=""'),
'id', 'last_polled'
);
$sql = 'INSERT INTO settings (name, value)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE `value`= VALUES(`value`)';
foreach ($pollers as $p => $t) {
if ($t > $gone_time) {
raise_message('poller_' . $p, __esc('Settings save to Data Collector %d skipped due to heartbeat.', $p), MESSAGE_LEVEL_WARN);
} else {
$rcnn_id = poller_connect_to_remote($p);
if ($rcnn_id) {
if (db_execute_prepared($sql, [$config_name, $value], false, $rcnn_id) === false) {
$rcnn_id = false;
}
}
// check if we still have rcnn_id, if it's now become false, we had a problem
if (!$rcnn_id) {
raise_message('poller_' . $p, __esc('Settings save to Data Collector %d Failed.', $p), MESSAGE_LEVEL_ERROR);
}
}
}
}
$set_var = CACTI_WEB ? '_SESSION' : 'config';
$set_key = CACTI_WEB ? OPTIONS_WEB : OPTIONS_CLI;
// Store whatever value we have in the array
if (!isset(${$set_var}[$set_key]) || !is_array(${$set_var}[$set_key])) {
${$set_var}[$set_key] = [];
}
${$set_var}[$set_key][$config_name] = $value;
if (!empty($config['DEBUG_SET_CONFIG_OPTION'])) {
file_put_contents(sys_get_temp_dir() . '/cacti-option.log', get_debug_prefix() . cacti_debug_backtrace($config_name, false, false, 0, 1) . "\n", FILE_APPEND);
}
}
/**
* Determines if a value exists for the current user/setting specified
*
* @param string $config_name The name of the configuration setting as
* specified $settings array in
* 'include/global_settings.php'
*
* @return bool true if a value exists, false if a value does not exist
*/
function config_value_exists(string $config_name) : bool {
static $exists_config_value = [];
// We use isset instead of array_key_exists so that
// if we never had a value, we can see if we got on
// but once we know we have one, assume we always
// will
if (!isset($exists_config_value[$config_name])) {
$value = db_fetch_cell_prepared('SELECT COUNT(*) FROM settings WHERE name = ?', [$config_name]);
$exists_config_value[$config_name] = ($value > 0);
}
return $exists_config_value[$config_name];
}
/**
* Finds the default value of a Cacti configuration setting
*
* @param string $config_name The name of the configuration setting as
* specified $settings_user array in
* 'include/global_settings.php'
*
* @return mixed The default value of the configuration option
*/
function read_default_config_option(string $config_name) : mixed {
global $settings;
if (isset($settings) && is_array($settings)) {
foreach ($settings as $tab_array) {
if (isset($tab_array[$config_name]) && isset($tab_array[$config_name]['default'])) {
return $tab_array[$config_name]['default'];
} else {
foreach ($tab_array as $field_array) {
if (isset($field_array['items']) && isset($field_array['items'][$config_name]) && isset($field_array['items'][$config_name]['default'])) {
return $field_array['items'][$config_name]['default'];
}
}
}
}
}
return null;
}
/**
* Cache the common configuration settings
*
* @return array
*/
function cache_common_config_settings() : array {
global $config;
// $start = microtime(true);
$common_settings = [
'auth_method',
'auth_cache_enabled',
'path_cactilog',
'rrdtool_version',
'log_verbosity',
'log_destination',
'default_image_format',
'default_graph_width',
'default_graph_height',
'default_datechar',
'default_date_format',
'default_poller',
'default_site',
'i18n_language_support',
'i18n_default_language',
'reports_allow_ln',
// Common page rendering
'selective_debug',
'selected_theme',
'min_tree_width',
'max_tree_width',
];
if (CACTI_WEB) {
$extra_settings = [
// Common all pages
'force_https',
'content_security_policy_script',
'content_security_alternate_sources',
'deletion_verification',
// Common graphing
'rrdtool_watermark',
'realtime_cache_path',
'path_rrdtool',
'hide_disabled',
'graph_watermark',
'graph_dateformat',
'font_method',
'date',
'boost_rrd_update_system_enable',
'boost_rrd_update_max_records_per_select',
'boost_rrd_update_enable',
'boost_png_cache_enable',
'remote_storage_method',
];
} else {
$extra_settings = [
// Common polling
'poller_interval',
'snmp_version',
'snmp_username',
'snmp_timeout',
'snmp_community',
'snmp_auth_protocol',
'snmp_security_level',
'snmp_priv_protocol',
'snmp_priv_passphrase',
'snmp_port',
'snmp_password',
'snmp_retries',
'device_threads',
'max_get_size',
'availability_method',
'ping_method',
'ping_port',
'ping_retries',
'ping_timeout',
'path_snmpbulkwalk',
'path_snmpwalk',
'path_snmpget',
'path_spine',
// Common API
'default_template',
'delete_verification',
// Thold
'alert_bl_trigger',
'alert_deadnotify',
'alert_email',
'alert_exempt',
'alert_repeat',
'alert_trigger',
'base_url',
'thold_alert_snmp_warning',
'thold_alert_snmp_normal',
'thold_alert_snmp',
'thold_daemon_debug',
'thold_disable_all',
'thold_log_debug',
'thold_send_text_only',
'thold_show_datasource',
];
}
$common_settings = array_merge($common_settings, $extra_settings);
$settings = array_rekey(
db_fetch_assoc_prepared('SELECT name, value
FROM settings
WHERE name IN (' . trim(str_repeat('?, ', cacti_sizeof($common_settings)),', ') . ')',
$common_settings),
'name', 'value'
);
if (cacti_sizeof($settings)) {
$set_var = CACTI_WEB ? '_SESSION' : 'config';
$set_key = CACTI_WEB ? OPTIONS_WEB : OPTIONS_CLI;
// Store whatever value we have in the array
if (!isset(${$set_var}[$set_key]) || !is_array(${$set_var}[$set_key])) {
${$set_var}[$set_key] = [];
}
foreach ($settings as $name => $value) {
${$set_var}[$set_key][$name] = $value;
}
return ${$set_var};
}
return [];
}
/**
* Finds the current value of a Cacti configuration setting
*
* @param string $config_name The name of the configuration setting as
* specified $settings array in
* 'include/global_settings.php'
* @param bool $force Pull the data from the database if true ignoring session
*
* @return mixed The current value of the configuration option
*/
function read_config_option(string $config_name, bool $force = false) : mixed {
global $config, $database_hostname, $database_default, $database_port, $database_sessions;
$loaded = false;
$set_var = CACTI_WEB ? '_SESSION' : 'config';
$set_key = CACTI_WEB ? OPTIONS_WEB : OPTIONS_CLI;
// Store whatever value we have in the array
if (!isset(${$set_var}[$set_key]) || !is_array(${$set_var}[$set_key])) {
${$set_var}[$set_key] = [];
}
$loaded = isset(${$set_var}[$set_key][$config_name]);
if (!empty($config['DEBUG_READ_CONFIG_OPTION'])) {
file_put_contents(sys_get_temp_dir() . '/cacti-option.log', get_debug_prefix() . cacti_debug_backtrace($config_name, false, false, 0, 1) . "\n", FILE_APPEND);
}
// Do we have a value already stored in the array, or
// do we want to make sure we have the latest value
// from the database?
if (!$loaded || $force) {
// We need to check against the DB, but lets assume default value
// unless we can actually read the DB
$value = read_default_config_option($config_name);
if (!empty($config['DEBUG_READ_CONFIG_OPTION'])) {
file_put_contents(sys_get_temp_dir() . '/cacti-option.log', get_debug_prefix() .
" $config_name: " .
' dh: ' . isset($database_hostname) .
' dp: ' . isset($database_port) .
' dd: ' . isset($database_default) .
' ds: ' . isset($database_sessions["$database_hostname:$database_port:$database_default"]) .
"\n", FILE_APPEND);
if (isset($database_hostname) && isset($database_port) && isset($database_default)) {
file_put_contents(sys_get_temp_dir() . '/cacti-option.log', get_debug_prefix() .
" $config_name: [$database_hostname:$database_port:$database_default]\n", FILE_APPEND);
}
}
// Are the database variables set, and do we have a connection??
// If we don't, we'll only use the default value without storing
// so that we can read the database version later.
if (isset($database_hostname) && isset($database_port) && isset($database_default) &&
isset($database_sessions["$database_hostname:$database_port:$database_default"])) {
// Get the database setting
$db_result = db_fetch_row_prepared('SELECT value FROM settings WHERE name = ?', [$config_name], false);
if (cacti_sizeof($db_result)) {
$value = $db_result['value'];
}
}
// Store whatever value we have in the array
${$set_var}[$set_key][$config_name] = $value;
}
$value = ${$set_var}[$set_key][$config_name];
return $value;
}
/**
* get_selected_theme - checks the user settings and if the user selected
* theme is set, returns it otherwise returns the system default.
*
* @return string the theme name
*/
function get_selected_theme() : string {
global $themes;
// shortcut if theme is set in session
if (isset($_SESSION['selected_theme']) && isset($_SESSION[SESS_USER_ID])) {
if (file_exists(CACTI_PATH_INCLUDE . '/themes/' . $_SESSION['selected_theme'] . '/main.css')) {
return $_SESSION['selected_theme'];
}
}
// default to system selected theme
$theme = read_config_option('selected_theme');
// check for a pre-1.x cacti being upgraded
if ($theme == '' && !db_table_exists('settings_user')) {
return 'modern';
}
// figure out user defined theme
if (isset($_SESSION[SESS_USER_ID])) {
// fetch user defined theme
$user_theme = db_fetch_cell_prepared("SELECT value
FROM settings_user
WHERE name='selected_theme'
AND user_id = ?",
[$_SESSION[SESS_USER_ID]], '', false);
// user has a theme
if (!empty($user_theme)) {
$theme = $user_theme;
}
}
if (isset($_SESSION[SESS_USER_ID]) && !file_exists(CACTI_PATH_INCLUDE . '/themes/' . $theme . '/main.css')) {
foreach ($themes as $t => $name) {
if (file_exists(CACTI_PATH_INCLUDE . '/themes/' . $t . '/main.css')) {
$theme = $t;
db_execute_prepared('UPDATE settings_user
SET value = ?
WHERE user_id = ?
AND name = "selected_theme"',
[$theme, $_SESSION[SESS_USER_ID]]);
break;
}
}
}
if (isset($_SESSION[SESS_USER_ID]) && is_valid_theme($theme, $_SESSION[SESS_USER_ID])) {
$_SESSION['selected_theme'] = (string) $theme;
}
return (string) $theme;
}
/**
* Returns a validated theme name for graph rendering.
*
* @param string $requested Requested theme from input
*
* @return string
*/
function cacti_validate_theme(string $requested) : string {
static $valid_themes = null;
$default = (string) read_config_option('selected_theme');
if ($default === '') {
$default = 'modern';
}
if ($valid_themes === null) {
$valid_themes = [];
$themes_dir = CACTI_PATH_INCLUDE . '/themes';
if (is_dir($themes_dir)) {
$entries = scandir($themes_dir);
if ($entries !== false) {
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$full = $themes_dir . '/' . $entry;
if (is_dir($full) && is_file($full . '/rrdtheme.php')) {
$valid_themes[$entry] = true;
}
}
}
}
}
$requested = basename($requested);
$default = basename($default);
if (isset($valid_themes[$requested])) {
return $requested;
}
// the configured default can itself be stale or poisoned, so re-validate
// it before it reaches an include path; fall back to a theme that ships
return isset($valid_themes[$default]) ? $default : 'modern';
}
/**
* Returns true if a theme is valid
*
* @param mixed $theme
* @param int $set_user
*
* @return bool
*/
function is_valid_theme(mixed &$theme, int $set_user = 0) : bool {
global $themes;
$valid = true;
if ($theme == null || !file_exists(CACTI_PATH_INCLUDE . '/themes/' . $theme . '/main.css')) {
$valid = false;
$user_table = db_table_exists('settings_user');
$theme = read_config_option('selected_theme', true);
if (file_exists(CACTI_PATH_INCLUDE . '/themes/' . $theme . '/main.css')) {
$valid = true;
if ($user_table && $set_user && isset($_SESSION[SESS_USER_ID])) {
db_execute_prepared('UPDATE settings_user
SET value = ?
WHERE user_id = ?
AND name="selected_theme"',
[$theme, $_SESSION[SESS_USER_ID]]);
}
} else {
foreach ($themes as $t => $name) {
if (file_exists(CACTI_PATH_INCLUDE . '/themes/' . $t . '/main.css')) {
$theme = $t;
$valid = true;
if ($user_table && $set_user && isset($_SESSION[SESS_USER_ID])) {
db_execute_prepared('UPDATE settings_user
SET value = ?
WHERE user_id = ?
AND name="selected_theme"',
[$theme, $_SESSION[SESS_USER_ID]]);
}
break;
}
}
}
}
// update session
return $valid;
}
/**
* form_input_validate - validates the value of a form field and Takes the appropriate action if the input
* is not valid
*
* @param mixed $field_value Value of the form field
* @param string $field_name Name of the $_POST field as specified in the HTML
* @param string $regexp_match Optionally enter a regular expression to match the value against
* @param bool $allow_nulls Whether to allow an empty string as a value or not
* @param mixed $message_id The error message to raise in the case of an error
*
* @return mixed - The original $field_value
*/
function form_input_validate(mixed $field_value, string $field_name, string $regexp_match, bool $allow_nulls, mixed $message_id = 3) : mixed {
global $messages;
// write current values to the "field_values" array so we can retain them
$_SESSION[SESS_FIELD_VALUES][$field_name] = $field_value;
if ($allow_nulls == true && $field_value == '') {
return $field_value;
}
$report_message = null;
if ($allow_nulls == false && $field_value == '') {
$report_message = __("Form Validation Failed: Variable '%s' does not allow nulls and variable is null", $field_name);
} elseif ($regexp_match != '' && !preg_match('/' . $regexp_match . '/', $field_value)) {
$report_message = __("Form Validation Failed: Variable '%s' with Value '%s' Failed REGEX '%s'", $field_name, $field_value, $regexp_match);
}
if ($report_message !== null) {
$custom_message = null;
if (read_config_option('log_validation')) {
cacti_log($report_message, false, 'ERROR:');
if (read_config_option('log_validation') == INPUT_VALIDATION_FULL && $message_id == 3) {
$message_id = $field_name;
$custom_message = $report_message;
}
}
$_SESSION[SESS_ERROR_FIELDS][$field_name] = $custom_message ?? $message_id;
raise_message($message_id, $custom_message ?? _('One or more fields failed validation'), MESSAGE_LEVEL_ERROR);
}
return $field_value;
}
/**
* check_changed - determines if a request variable has changed between page loads
*
* @param mixed $request
* @param string $session
*
* @return int true if the value changed between loads
*/
function check_changed(mixed $request, string $session) : int {
if ((isrv($request)) && (isset($_SESSION[$session]))) {
if (gnrv($request) != $_SESSION[$session]) {
return 1;
}
}
return 0;
}
/**
* is_error_message - finds whether an error message has been raised and has not been outputted to the
* user
*
* @return bool whether the messages array contains an error or not
*/
function is_error_message() : bool {
global $messages;
if (isset($_SESSION[SESS_ERROR_FIELDS]) && cacti_sizeof($_SESSION[SESS_ERROR_FIELDS])) {
return true;
} else {
return false;
}
}
/**
* Get the level for the current message
*
* @param array $current_message
*
* @return int
*/
function get_message_level(array $current_message) : int {
$current_level = MESSAGE_LEVEL_NONE;
if (isset($current_message['level'])) {
$current_level = $current_message['level'];
} elseif (isset($current_message['type'])) {
switch ($current_message['type']) {
case 'error':
$current_level = MESSAGE_LEVEL_ERROR;
break;
case 'info':
$current_level = MESSAGE_LEVEL_INFO;
break;
}
}
return $current_level;
}
/**
* Get the title for the current message
*
* @param array $current_message
*
* @return mixed
*/
function get_message_title(array $current_message) : mixed {
$current_title = null;
if (isset($current_message['title'])) {
$current_title = $current_message['title'];
} elseif (isset($current_message['type'])) {
switch ($current_message['type']) {
case 'error':
$current_title = __('Error');
break;
case 'info':
$current_title = __('Information');
break;
}
}
return $current_title;
}
/**
* get_format_message_instance - finds the level of the current message instance
*
* @param mixed $current_message the message instance
*
* @return string a formatted message
*/
function get_format_message_instance(mixed $current_message) : string {
if (is_array($current_message) && isset($current_message['message'])) {
$fmessage = $current_message['message'];
$level = get_message_level($current_message);
} elseif (is_array($current_message)) {
$fmessage = __esc('Message Not Found.');
$level = MESSAGE_LEVEL_ERROR;
} else {
$fmessage = $current_message;
$level = MESSAGE_LEVEL_INFO;
}
$message = match ($level) {
MESSAGE_LEVEL_NONE => '' . $fmessage . '',
MESSAGE_LEVEL_INFO => '' . $fmessage . '',
MESSAGE_LEVEL_WARN => '' . $fmessage . '',
MESSAGE_LEVEL_ERROR => '' . $fmessage . '',
MESSAGE_LEVEL_CSRF => '' . $fmessage . '',
default => '' . $fmessage . '',
};
return $message;
}
/**
* get_message_max_type - finds the message and returns its type
*
* @param mixed $output_messages The messages to check, if null it will check the session messages
*
* @return int The message type
*/
function get_message_max_type(mixed $output_messages = null) : int {
global $messages;
$level = MESSAGE_LEVEL_NONE;
if ($output_messages === null && isset($_SESSION[SESS_MESSAGES])) {
$output_messages = $_SESSION[SESS_MESSAGES];
}
if (is_array($output_messages)) {
foreach ($output_messages as $current_message_id => $current_message) {
$current_level = get_message_level($current_message);
if ($current_level == MESSAGE_LEVEL_NONE && isset($messages[$current_message_id])) {
$current_level = get_message_level($messages[$current_message_id]);
}
if ($current_level != $level && $level != MESSAGE_LEVEL_NONE) {
$level = MESSAGE_LEVEL_MIXED;
} else {
$level = $current_level;
}
}
}
return $level;
}
/**
* Message to be displayed to the user once display_output_messages() is called
*
* @param mixed $message_id ID of the message as defined in $messages in 'include/global_arrays.php'
* @param string $message Text of the message to be displayed
* @param int $message_level Level of the message to be displayed
* @param mixed $message_title Title of the message to be displayed
*
* @return bool
*/
function raise_message(mixed $message_id, string $message = '', int $message_level = MESSAGE_LEVEL_NONE, mixed $message_title = null) : bool {
global $messages, $no_http_headers;
// This function should always exist, if not its an invalid install
if (function_exists('session_status')) {
$need_session = (session_status() == PHP_SESSION_NONE) && (!isset($no_http_headers));
} else {
return false;
}
if (empty($message)) {
if (is_array($messages) && array_key_exists($message_id, $messages)) {
$predefined = $messages[$message_id];
if (isset($predefined['message'])) {
$message = $predefined['message'];
} else {
$message = $predefined;
}
if ($message_level == MESSAGE_LEVEL_NONE) {
$message_level = get_message_level($predefined);
}
$message_title = get_message_title($predefined);
} else {
if (isset($_SESSION[$message_id])) {
/*
* A message was set at the session level
* but rather than just assume it's an error
* lets see if it is
*/
$message_level = MESSAGE_LEVEL_ERROR;
$sessMessage = $_SESSION[$message_id];
// Is the message an array ?
if (is_array($sessMessage)) {
// Do we have the message element to set the text ?
if (!empty($sessMessage['message'])) {
$message = $sessMessage['message'];
}
// Do we have the level element to set the level ?
if (!empty($sessMessage['level'])) {
$message_level = $sessMessage['level'];
}
} else {
$message = $sessMessage;
}
}
// The message is still empty?
if (empty($message)) {
$message = __('Message Not Found.');
$message_level = MESSAGE_LEVEL_ERROR;
}
}
}
if ($need_session) {
cacti_session_start();
}
if (!isset($_SESSION[SESS_MESSAGES])) {
$_SESSION[SESS_MESSAGES] = [];
}
$final_message = ['message' => $message, 'level' => $message_level, 'title' => $message_title];
$final_message['title'] = get_message_title($final_message);
$_SESSION[SESS_MESSAGES][$message_id] = $final_message;
if ($need_session) {
cacti_session_close();
}
return true;
}
/**
* raise_message_javascript - raises a message that will appear in the UI
* as the result of an server side error that can not be captured
* normally.
*
* Note, this function assumes strings are already escaped when being
* called.
*
* @param string $title The title for the dialog title bar
* @param string $header Header section for the message
* @param string $message The actual error message to display
* @param int $level The level to be displayed at
*
* @return void
*/
function raise_message_javascript(string $title, string $header, string $message, int $level = MESSAGE_LEVEL_MIXED) : void {
?>
[
'level' => MESSAGE_LEVEL_ERROR,
'message' => $_SESSION[SESS_MESSAGES]
]
];
} else {
$output_messages = $_SESSION[SESS_MESSAGES];
}
clear_messages();
}
if ($debug_message != '') {
$output_messages['debug_message'] = [
'level' => MESSAGE_LEVEL_NONE,
'message' => $debug_message,
];
debug_log_clear('new_graphs');
}
if (!empty($output_messages)) {
foreach ($output_messages as $current_message_id => $current_message) {
if (!is_array($current_message)) {
$current_message = [
'level' => MESSAGE_LEVEL_ERROR,
'message' => $_SESSION[SESS_MESSAGES],
'title' => null,
];
}
if (!empty($current_message['message'])) {
$current_message['title'] = get_message_title($current_message);
$final_messages[] = [
'id' => $current_message_id,
'level' => $current_message['level'],
'message' => $current_message['message'],
'title' => get_message_title($current_message),
];
} else {
cacti_log("ERROR: Cacti Error Message Id '$current_message_id' Not Defined", false, 'WEBUI');
}
}
}
/**
* Emitted into an inline ';
return get_theme_paths($format, $path, $theme, $file, true, $async ? ' async' : '');
}
/**
* Formatted output of stylesheet include with MD5 hash for uniqueness
*
* @param string $path Path to include
* @param boolean $async Load asynchronously
* @param string|null $theme Theme to use
* @param string|null $file File to include
*
* @return string
*/
function get_md5_include_css(string $path, bool $async = false, string|null $theme = null, string|null $file = null) : string {
$format = '';
return get_theme_paths($format, $path, $theme, $file, true, $async ? ' media=\'print\' online="this.media=\'all\'"' : '');
}
/**
* Formatted output of images include with MD5 hash for uniqueness
*
* @param string $path Path to include
* @param boolean $async Load asynchronously
* @param string|null $theme Theme to use
* @param string|null $file File to include
* @param string|null $rel Rel type output when not null (eg, icon, shortcut icon)
* @param string|null $type Icon type output when not null (eg, image/svg+xml)
* @param string|null $sizes Sizes output when not null (eg, 96x96)
*
* @return string
*/
function get_md5_include_icon(string $path, bool $async = false, string|null $theme = null, string|null $file = null, string|null $rel = null, string|null $type = null, string|null $sizes = null) : string {
if ($type === null) {
$format = '';
} else {
$format = "";
}
if (!empty($rel)) {
$rel = " rel='" . htmlspecialchars($rel) . "'";
}
if (!empty($sizes)) {
$sizes = " sizes='" . htmlspecialchars($sizes) . "'";
}
return get_theme_paths($format, $path, $theme, $file, true, $rel, $sizes);
}
/**
* Is the resource available to be written to?
*
* @param string $path
*
* @return bool
*/
function is_resource_writable(string $path) : bool {
if (empty($path)) {
return false;
}
if ($path[strlen($path) - 1] == '/') {
return is_resource_writable($path . uniqid((string) mt_rand()) . '.tmp');
}
if (file_exists($path)) {
if (is_writable($path)) {
return true;
}
if ($f = @fopen($path, 'a')) {
fclose($f);
return true;
}
return false;
} else {
if (is_writable(dirname($path))) {
return true;
}
if ($f = @fopen($path, 'w')) {
fclose($f);
unlink($path);
return true;
}
}
return false;
}
/**
* Recursively change ownership of files/directories. It should be
* noted that this function exits on the first failure that it finds
* and returns false.
*
* @param string $path Path to be updated
* @param string|integer $uid String or integer of user to set
* @param string|integer $gid String or integer of group to set
*
* @return bool
*/
function recursive_chown(string $path, string|int $uid, string|int $gid) : bool {
$d = opendir($path);
if ($d === false) {
return false;
}
$success = false;
while (($file = readdir($d)) !== false) {
if ($file != '.' && $file != '..') {
$fullpath = $path . '/' . $file;
if (filetype($fullpath) == 'dir') {
return recursive_chown($fullpath, $uid, $gid);
}
$success = chown($fullpath, $uid);
if ($success) {
$success = chgrp($fullpath, $gid);
}
}
if (!$success) {
return false;
}
}
return true;
}
/**
* Verifies that a theme exists. If the theme does exist
* return its name, otherwise returns the default theme.
*
* @param string|null $theme
* @param string $defaultTheme
*
* @return string
*/
function get_validated_theme(string|null $theme, string $defaultTheme) : string {
if (isset($theme) && strlen($theme)) {
$themePath = CACTI_PATH_INCLUDE . '/themes/' . $theme . '/main.css';
if (file_exists($themePath)) {
return $theme;
}
}
return $defaultTheme;
}
/**
* ****************** THIS FUNCTION DOESN'T APPEAR TO BE USED ANYWHERE *********************
*
* Verifies that a language exists. If the language
* does exist, returns its name, otherwise returns
* the default language.
*
* @param string $language
* @param string $defaultLanguage
*
* @return string
*/
function get_validated_language(string $language, string $defaultLanguage) : string {
if ($language != '') {
return $language;
}
return $defaultLanguage;
}
/**
* Attempts to return the currently running user
* via a number of methods.
*
* @return string
*/
function get_running_user() : string {
static $tmp_user = '';
if (empty($tmp_user)) {
if (function_exists('posix_geteuid')) {
$user_info = posix_getpwuid(posix_geteuid());
if ($user_info !== false) {
$tmp_user = $user_info['name'];
}
}
}
if (empty($tmp_user)) {
$tmp_file = tempnam(sys_get_temp_dir(), 'uid');
$f_owner = '';
if (is_resource_writable($tmp_file)) {
if (file_exists($tmp_file)) {
unlink($tmp_file);
}
file_put_contents($tmp_file, 'cacti');
$f_owner = fileowner($tmp_file);
$f_source = 'file';
if (file_exists($tmp_file)) {
unlink($tmp_file);
}
}
if (empty($f_owner) && function_exists('posix_getuid')) {
$f_owner = posix_getuid();
}
if (!empty($f_owner) && function_exists('posix_getpwuid1')) {
$f_array = posix_getpwuid($f_owner);
if (isset($f_array['name'])) {
$tmp_user = $f_array['name'];
}
}
if (empty($tmp_user)) {
exec('id -nu', $o, $r);
if ($r == 0) {
$tmp_user = trim($o[0]);
}
}
// Easy way first
if (empty($tmp_user)) {
$user = get_current_user();
if ($user != '') {
$tmp_user = $user;
}
}
// Fallback method
if (empty($tmp_user)) {
$user = getenv('USERNAME');
if ($user != '') {
$tmp_user = $user;
}
if (empty($tmp_user)) {
$user = getenv('USER');
if ($user != '') {
$tmp_user = $user;
}
}
}
}
return (empty($tmp_user) ? 'apache' : $tmp_user);
}
/**
* Returns a string for debugging purposes
*
* @return string
*/
function get_debug_prefix() : string {
$dateTime = new DateTime('NOW');
$dateTime = $dateTime->format('Y-m-d H:i:s.u');
return sprintf('<[ %s | %7d ]> -- ', $dateTime, getmypid());
}
/**
* Gets the current client addr
*
* This function relies on an administrator to set
* the appropriate proxy headers that are allowed
* in the `config.php` include.
*
* @return string|false
*/
function get_client_addr() : string|false {
global $config, $allowed_proxy_headers;
$proxy_headers = $config['proxy_headers'] ?? null;
if ($proxy_headers === null) {
$last_time = read_config_option('proxy_alert');
if (empty($last_time)) {
// First run — no record yet; log immediately and record today
cacti_log('NOTICE: proxy_headers is not set in config.php; defaulting to false (only REMOTE_ADDR trusted). Set proxy_headers if Cacti is behind a reverse proxy.', false, 'AUTH');
set_config_option('proxy_alert', date('Y-m-d'));
} else {
$last_date = new DateTime($last_time);
$this_date = new DateTime();
$this_diff = $this_date->diff($last_date);
$this_days = $this_diff->format('%a');
if ((int) $this_days >= 1) {
cacti_log('NOTICE: proxy_headers is not set in config.php; defaulting to false (only REMOTE_ADDR trusted). Set proxy_headers if Cacti is behind a reverse proxy.', false, 'AUTH');
set_config_option('proxy_alert', date('Y-m-d'));
}
}
$proxy_headers = false;
}
/* If proxy_headers is true, allow all known headers -- NOT advised
* If proxy_headers is false, allow only REMOTE_ADDR
* IF proxy_headers is an array, filter by known headers
*/
if ($proxy_headers === true) {
$proxy_headers = $allowed_proxy_headers;
} elseif (is_array($proxy_headers) && is_array($allowed_proxy_headers)) {
$proxy_headers = array_intersect($proxy_headers, $allowed_proxy_headers);
}
if (!is_array($proxy_headers)) {
$proxy_headers = [];
}
if (!in_array('REMOTE_ADDR', $proxy_headers, true)) {
$proxy_headers[] = 'REMOTE_ADDR';
}
/**
* A proxy appends to the forwarded chain, so the leftmost entry is client
* supplied. Resolution only honours forwarded headers when the immediate
* peer is listed in $trusted_proxies, and then walks the chain from the
* right. With no trusted proxy configured this yields REMOTE_ADDR.
*/
return cacti_resolve_client_addr($_SERVER, $config['trusted_proxies'] ?? [], $proxy_headers);
}
/**
* cacti_is_https - Determine whether the current request was made over HTTPS.
*
* Checks $_SERVER['HTTPS'] which is set by the web server. Values of 'off',
* '0', '' or unset mean HTTP; anything else (typically 'on' or '1') means HTTPS.
*
* @return bool True when the connection is HTTPS, false otherwise.
*/
function cacti_is_https() : bool {
global $config;
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== '0' && strtolower($_SERVER['HTTPS']) !== 'off') {
return true;
}
// Honour a forwarded proto only when we are configured to trust proxy
// headers, so a client cannot force the Secure flag on a plaintext request.
if (!empty($config['proxy_headers'])) {
$fwd_proto = isset($_SERVER['HTTP_X_FORWARDED_PROTO']) ? strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) : '';
$fwd_ssl = isset($_SERVER['HTTP_X_FORWARDED_SSL']) ? strtolower($_SERVER['HTTP_X_FORWARDED_SSL']) : '';
if ($fwd_proto === 'https' || $fwd_ssl === 'on') {
return true;
}
}
return false;
}
/**
* get_cacti_base_tables - Extracts all the base Cacti tables from the
* cacti.sql file in the base Cacti directory.
*/
function get_cacti_base_tables() : array {
$base_tables = [];
if (file_exists(CACTI_PATH_BASE . '/cacti.sql')) {
$schema = file(CACTI_PATH_BASE . '/cacti.sql');
} else {
return $base_tables;
}
if (cacti_sizeof($schema)) {
foreach (($schema ?: []) as $line) {
if (str_contains($line, 'CREATE TABLE')) {
$table = str_replace(['CREATE TABLE', '`', '(', ' '], '', $line);
$base_tables[] = trim($table);
}
}
}
return $base_tables;
}
function cacti_pton(string $ipaddr) : mixed {
// Strip out the netmask, if there is one.
$subnet_pos = strpos($ipaddr, '/');
if ($subnet_pos) {
$subnet = substr($ipaddr, $subnet_pos + 1);
$ipaddr = substr($ipaddr, 0, $subnet_pos);
} else {
$subnet = null; // No netmask present
}
// Convert address to packed format
$addr = @inet_pton($ipaddr);
if ($addr === false) {
return false;
}
// Maximum netmask length = same as packed address
$len = 8 * strlen($addr);
if (!empty($subnet)) {
if (!is_numeric($subnet)) {
return false;
}
if ($subnet > $len) {
return false;
}
}
if (!is_numeric($subnet)) {
$subnet = $len;
} else {
$subnet = (int)$subnet;
}
// Create a hex expression of the subnet mask
$mask = str_repeat('f',$subnet >> 2);
switch($subnet & 3) {
case 3:
$mask .= 'e';
break;
case 2:
$mask .= 'c';
break;
case 1:
$mask .= '8';
break;
}
$mask = str_pad($mask,$len >> 2,'0');
// Packed representation of netmask
$mask = pack('H*',$mask);
$result = ['ip' => $addr, 'subnet' => $mask];
return $result;
}
function cacti_ntop(mixed $addr) : mixed {
if (empty($addr)) {
return false;
}
if (is_array($addr)) {
foreach ($addr as $ip) {
$addr = $ip;
break;
}
}
return @inet_ntop($addr);
}
function cacti_ntoc(string $subnet, bool $ipv6 = false) : int {
$result = false;
$count = 0;
foreach (str_split($subnet) as $char) {
$i = ord($char);
while (($i & 128) == 128) {
$count++;
$i = ($i << 1) % 256;
}
}
return $count;
}
function cacti_ptoa(string $title, string $addr) : void {
// Let's display it as hexadecimal format
foreach (str_split($addr) as $char) {
print str_pad(dechex(ord($char)),2,'0',STR_PAD_LEFT);
}
}
function cacti_sizeof(mixed $array) : int {
return ($array === false || !is_array($array)) ? 0 : count($array);
}
function cacti_count(mixed $array) : int {
return ($array === false || !is_array($array)) ? 0 : count($array);
}
/**
* cacti_strtolower - Locale-independent lowercase conversion
*
* PHP 8.5 deprecates locale-sensitive strtolower(). This wrapper
* uses mb_strtolower with explicit UTF-8 encoding to avoid
* locale-dependent behavior. All Cacti string comparisons are
* ASCII or UTF-8, never locale-sensitive.
*
* @param string $string The string to convert
*
* @return string The lowercased string
*/
function cacti_strtolower(string $string) : string {
return mb_strtolower($string, 'UTF-8');
}
/**
* cacti_strtoupper - Locale-independent uppercase conversion
*
* PHP 8.5 deprecates locale-sensitive strtoupper(). This wrapper
* uses mb_strtoupper with explicit UTF-8 encoding to avoid
* locale-dependent behavior.
*
* @param string $string The string to convert
*
* @return string The uppercased string
*/
function cacti_strtoupper(string $string) : string {
return mb_strtoupper($string, 'UTF-8');
}
function is_function_enabled(string $name) : bool {
return function_exists($name) &&
!in_array($name, array_map('trim', explode(', ', (string) (ini_get('disable_functions') ?: ''))), true) &&
cacti_strtolower((string) (ini_get('safe_mode') ?: '')) != 1;
}
function is_page_ajax() : bool {
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && cacti_strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
return true;
}
return false;
}
function raise_ajax_permission_denied() : void {
if (is_page_ajax()) {
header('HTTP/1.1 401 ' . __('Permission Denied'));
print __('You are not permitted to access this section of Cacti.') . ' ' . __('If you feel that this is an error. Please contact your Cacti Administrator.');
exit;
}
}
/**
* cacti_session_start - Create a Cacti session from the settings set by the administrator
*
* @return bool
*/
function cacti_session_start() : bool {
global $config;
// initialize php session
if (!function_exists('session_name')) {
die('PHP Session Management is missing, please install PHP Session module');
}
if (!isset($config[CACTI_SESSION_NAME])) {
return false;
}
session_name($config[CACTI_SESSION_NAME]);
if (session_status() === PHP_SESSION_NONE) {
$session_restart = '';
} else {
$session_restart = 're';
}
/** @var array */
$session_options = $config[COOKIE_OPTIONS];
$session_result = session_start($session_options);
if (!$session_result) {
cacti_log('Session "' . session_id() . '" ' . $session_restart . 'start failed! ' . cacti_debug_backtrace('', false, false, 0, 1), false, 'WARNING:');
}
return true;
}
/**
* cacti_session_close - Closes the open Cacti session if it is open
* it can be re-opened afterwards in the case after a long running query
*
* @return void
*/
function cacti_session_close() : void {
session_write_close();
}
/**
* cacti_session_destroy - Destroys the login current session
*
* @return void
*/
function cacti_session_destroy() : void {
session_unset();
session_destroy();
}
/**
* cacti_cookie_set - Allows for settings an arbitrary cookie name and value
* used for CSRF protection.
*
* @param mixed $session
* @param mixed $val
* @param null|mixed $timeout
*
* @return void
*/
function cacti_cookie_set($session, $val, $timeout = null) : void {
global $config;
if (isset($config[COOKIE_OPTIONS][COOKIE_OPTIONS_DOMAIN])) {
$domain = $config[COOKIE_OPTIONS][COOKIE_OPTIONS_DOMAIN];
} else {
$domain = '';
}
$secure = cacti_is_https();
if (version_compare(PHP_VERSION, '7.3', '>=')) {
$options = [
'path' => CACTI_PATH_URL,
'expires' => $timeout ?? (time() + 3600),
'domain' => $domain,
'secure' => $secure,
'httponly' => true,
'samesite' => 'Strict'
];
setcookie((string) $session, $val, $options);
} else {
setcookie((string) $session, $val, ['expires' => time() + 3600, 'path' => CACTI_PATH_URL, 'domain' => $domain, 'secure' => $secure, 'httponly' => true]);
}
}
/**
* cacti_cookie_logout - Clears the Cacti and the 'keep me logged in' cookies
*
* @return void
*/
function cacti_cookie_logout() : void {
global $config;
if (isset($config[COOKIE_OPTIONS][COOKIE_OPTIONS_DOMAIN])) {
$domain = $config[COOKIE_OPTIONS][COOKIE_OPTIONS_DOMAIN];
} else {
$domain = '';
}
$secure = cacti_is_https();
$cookies = [(string) session_name(), (string) session_name() . '_opt', (string) session_name() . '_otp', 'cacti_remembers'];
if (version_compare(PHP_VERSION, '7.3', '>=')) {
$options = [
'path' => CACTI_PATH_URL,
'expires' => time() - 3600,
'domain' => $domain,
'secure' => $secure,
'httponly' => true,
'samesite' => 'Strict'
];
foreach ($cookies as $cookie) {
setcookie($cookie, '', $options);
}
} else {
foreach ($cookies as $cookie) {
setcookie($cookie, '', ['expires' => time() - 3600, 'path' => CACTI_PATH_URL, 'domain' => $domain, 'secure' => $secure, 'httponly' => true]);
}
}
unset($_COOKIE[$config[CACTI_SESSION_NAME]]);
unset($_COOKIE[(string) session_name() . '_otp']);
unset($_COOKIE['cacti_remembers']);
}
/**
* cacti_cookie_session_set - Sets the cacti 'keep me logged in' cookie
*
* @param string $user
* @param int $realm
* @param string $nssecret
*
* @return void
*/
function cacti_cookie_session_set(string $user, int $realm, string $nssecret) : void {
global $config;
if (isset($config[COOKIE_OPTIONS][COOKIE_OPTIONS_DOMAIN])) {
$domain = $config[COOKIE_OPTIONS][COOKIE_OPTIONS_DOMAIN];
} else {
$domain = '';
}
$secure = cacti_is_https();
$_SESSION['cacti_remembers'] = true;
if (version_compare(PHP_VERSION, '7.3', '>=')) {
$options = [
'path' => CACTI_PATH_URL,
'expires' => time() + (86400 * 30),
'domain' => $domain,
'secure' => $secure,
'httponly' => true,
'samesite' => 'Strict'
];
setcookie('cacti_remembers', $user . ',' . $realm . ',' . $nssecret, $options);
} else {
setcookie('cacti_remembers', $user . ',' . $realm . ',' . $nssecret, ['expires' => time() + (86400 * 30), 'path' => CACTI_PATH_URL, 'domain' => $domain, 'secure' => $secure, 'httponly' => true]);
}
}
/**
* cacti_cookie_session_logout - Logs out of Cacti and the remember me session
*
* @return void
*/
function cacti_cookie_session_logout() : void {
global $config;
if (isset($config[COOKIE_OPTIONS][COOKIE_OPTIONS_DOMAIN])) {
$domain = $config[COOKIE_OPTIONS][COOKIE_OPTIONS_DOMAIN];
} else {
$domain = '';
}
$secure = cacti_is_https();
if (version_compare(PHP_VERSION, '7.3', '>=')) {
$options = [
'path' => CACTI_PATH_URL,
'expires' => time() - 3600,
'domain' => $domain,
'secure' => $secure,
'httponly' => true,
'samesite' => 'Strict'
];
setcookie('cacti_remembers', '', $options);
} else {
setcookie('cacti_remembers', '', ['expires' => time() - 3600, 'path' => CACTI_PATH_URL, 'domain' => $domain, 'secure' => $secure, 'httponly' => true]);
}
}
/**
* cacti_browser_zone_set - Set the PHP timezone to the
* browsers timezone if enabled.
*
* @return void
*/
function cacti_browser_zone_set() : void {
if (cacti_browser_zone_enabled()) {
if (isset($_SESSION[SESS_BROWSER_PHP_TZ])) {
ini_set('date.timezone', $_SESSION[SESS_BROWSER_PHP_TZ]);
putenv('TZ=' . $_SESSION[SESS_BROWSER_SYSTEM_TZ]);
}
}
}
/**
* cacti_system_zone_set - Set the PHP timezone to the
* systems timezone.
*
* @return void
*/
function cacti_system_zone_set() : void {
if (cacti_browser_zone_enabled()) {
if (isset($_SESSION[SESS_PHP_TZ])) {
ini_set('date.timezone', $_SESSION[SESS_PHP_TZ]);
putenv('TZ=' . $_SESSION[SESS_SYSTEM_TZ]);
}
}
}
function cacti_browser_zone_enabled() : bool {
$system_setting = read_config_option('client_timezone_support');
if (empty($system_setting)) {
return false;
} else {
$user_setting = read_user_setting('client_timezone_support', '0');
if (empty($user_setting)) {
return false;
}
return true;
}
}
/**
* cacti_time_zone_set - Given an offset in minutes, attempt
* to set a PHP date.timezone. There are some oddballs that
* we have to accommodate.
*
* @param mixed $gmt_offset
*
* @return void
*/
function cacti_time_zone_set(mixed $gmt_offset = null) : void {
if (!cacti_browser_zone_enabled()) {
return;
}
if ($gmt_offset === null) {
if (isset($_COOKIE['CactiTimeZone'])) {
$gmt_offset = $_COOKIE['CactiTimeZone'];
} else {
return;
}
}
$hours = floor($gmt_offset / 60);
$remaining = $gmt_offset % 60;
if (!isset($_SESSION[SESS_PHP_TZ])) {
$_SESSION[SESS_PHP_TZ] = ini_get('date.timezone');
$_SESSION[SESS_SYSTEM_TZ] = getenv('TZ');
}
$zone = timezone_name_from_abbr('', $gmt_offset);
if ($remaining == 0) {
putenv('TZ=GMT' . ($hours > 0 ? '-' : '+') . abs($hours));
$sys_offset = 'GMT' . ($hours > 0 ? '-' : '+') . abs($hours);
if ($zone !== false) {
$php_offset = $zone;
ini_set('date.timezone', $zone);
} else {
// Adding the rounding function as some timezones are Etc/GMT+5.5 which is
// not supported in PHP yet.
$php_offset = 'Etc/GMT' . ($hours > 0 ? '-' : '+') . abs(round($hours));
ini_set('date.timezone', 'Etc/GMT' . ($hours > 0 ? '-' : '+') . abs(round($hours)));
}
$_SESSION[SESS_BROWSER_SYSTEM_TZ] = $sys_offset;
$_SESSION[SESS_BROWSER_PHP_TZ] = $php_offset;
} else {
$time = ($hours > 0 ? '-' : '+') . abs($hours) . ':' . substr('00' . $remaining, -2);
if ($zone === false) {
switch($time) {
case '+3:30':
$zone = 'IRST';
break;
case '+4:30':
$zone = 'IRDT';
break;
case '+5:30':
$zone = 'IST';
break;
case '+5:45':
$zone = 'NPT';
break;
case '+6:30':
$zone = 'CCT';
break;
case '+9:30':
$zone = 'ACST';
break;
case '+10:30':
$zone = 'ACDT';
break;
case '+8:45':
$zone = 'ACWST';
break;
case '+12:45':
$zone = 'CHAST';
break;
case '+13:45':
$zone = 'CHADT';
break;
case '-3:30':
$zone = 'NST';
break;
case '-2:30':
$zone = 'NDT';
break;
case '-9:30':
$zone = 'MART';
break;
}
if ($zone !== false) {
$zone = timezone_name_from_abbr($zone);
}
}
$php_offset = $zone;
$sys_offset = 'GMT' . $time;
putenv('TZ=GMT' . $time);
if ($zone != '') {
ini_set('date.timezone', $zone);
}
$_SESSION[SESS_BROWSER_SYSTEM_TZ] = $sys_offset;
$_SESSION[SESS_BROWSER_PHP_TZ] = $php_offset;
}
}
function debounce_run_notification(mixed $id, int $frequency = 7200) : bool {
$key = 'debounce_' . md5($id);
// debounce admin emails
$last = read_config_option($key);
$now = time();
// default to unset
$last_timestamp = '';
if ($last != '' && is_numeric($last)) {
$last_timestamp = $last;
} elseif ($last != '') {
$last = json_decode($last, true);
if (isset($last['timestamp'])) {
$last_timestamp = $last['timestamp'];
} else {
$last_timestamp = '';
}
}
if (empty($last_timestamp) || $now - $last_timestamp > $frequency) {
$current = [
'id' => $id,
'timestamp' => $now,
'frequency' => $frequency
];
set_config_option($key, json_encode($current));
return true;
}
return false;
}
/**
* Return an array of sorted and unique IDs
*
* @param mixed $ids
* @param bool $shouldExplode
*
* @return array
*/
function cacti_unique_ids(mixed $ids, bool $shouldExplode = true) : array {
if ($shouldExplode && is_string($ids)) {
$ids = explode(',', str_replace(' ', '', $ids));
}
if (!is_array($ids)) {
$ids = [$ids];
}
$ids = array_map('intval', array_filter(array_unique($ids)));
sort($ids);
return $ids;
}
/**
* Record a log of depreciated use
*
* @param string $text text to insert
*
* @return void
*/
function cacti_depreciated(string $text) : void {
cacti_debug_backtrace('WARN Depreciated use of ' . $text . ' at ');
}
function substring_index(string $subject, string $delim, int $count) : string {
if ($delim === '') {
return $subject;
}
if ($count < 0) {
return implode($delim, array_slice(explode($delim, $subject), $count));
} else {
return implode($delim, array_slice(explode($delim, $subject), 0, $count));
}
}
function cacti_format_ipv6_colon(string $address) : string {
if (!filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return $address;
}
if (str_contains($address, '[')) {
return $address;
}
if (str_contains($address, ':')) {
return '[' . $address . ']';
}
return ($address);
}
function text_substitute(mixed $text, bool $isHtml = true, bool $includeStandard = true,
mixed $extraSubstitutions = null, mixed $extraMatches = null) : string {
if (!empty($text)) {
/** @var callable $parser */
$parser = 'text_regex_parser' . ($isHtml ? '_html' : '');
$extraSubstitutions ??= [];
$extraMatches ??= [];
// Get parts for text substitution
$extra_search = array_keys($extraSubstitutions);
$extra_values = array_values($extraSubstitutions);
$regex_array = $includeStandard ? text_get_regex_array($extraMatches) : $extraMatches;
if (!empty($regex_array)) {
$regex_complete = '';
foreach ($regex_array as $regex_setting) {
$regex_text = $regex_setting['regex'] ?? '';
if (!empty($regex_text)) {
$regex_complete .= (strlen($regex_complete) ? ')|(' : '') . $regex_text;
} else {
cacti_log('WARNING: Bad regex search: ' . json_encode($regex_setting), false, 'UTIL');
}
}
$regex_complete = '~(' . $regex_complete . ')~';
if (is_array($text)) {
foreach ($text as &$line) {
$line = text_substitute_line($line, $regex_complete, $parser, $extra_search, $extra_values);
}
} else {
$text = text_substitute_line($text, $regex_complete, $parser, $extra_search, $extra_values);
}
}
}
return (string) $text;
}
function text_substitute_line(string $source, string $regex, callable $parser, array $search, array $values) : string {
$result = $source;
if (!empty($source)) {
if (!empty($regex)) {
$result = preg_replace_callback($regex, $parser, $result) ?? $result;
}
if (!empty($values) && cacti_sizeof($values) == cacti_sizeof($search)) {
$result = str_replace($search, $values, $result);
}
}
return $result;
}
function text_get_regex_array(mixed $extraSubstitutions = []) : array {
static $regex_array = [];
static $regex_extra = [];
if ($extraSubstitutions !== null) {
$regex_extra = $extraSubstitutions;
}
if (!cacti_sizeof($regex_array)) {
$regex_array = [
1 => ['name' => 'DS', 'regex' => '( DS\[)([, \d]+)(\])', 'func' => 'text_regex_datasource'],
2 => ['name' => 'DQ', 'regex' => '( DQ\[)([, \d]+)(\])', 'func' => 'text_regex_dataquery'],
3 => ['name' => 'Device', 'regex' => '( Device\[)([, \d]+)(\])', 'func' => 'text_regex_device'],
4 => ['name' => 'Poller', 'regex' => '( Poller\[)([, \d]+)(\])', 'func' => 'text_regex_poller'],
5 => ['name' => 'RRA', 'regex' => "([_\/])(\d+)(\.rrd')", 'func' => 'text_regex_rra'],
6 => ['name' => 'GT', 'regex' => '( GT\[)([, \d]+)(\])', 'func' => 'text_regex_graphtemplates'],
7 => ['name' => 'Graph', 'regex' => '( Graph\[)([, \d]+)(\])', 'func' => 'text_regex_graphs'],
8 => ['name' => 'Graphs', 'regex' => '( Graphs\[)([, \d]+)(\])', 'func' => 'text_regex_graphs'],
9 => ['name' => 'User', 'regex' => '( User\[)([, \d]+)(\])', 'func' => 'text_regex_users'],
10 => ['name' => 'User', 'regex' => '( Users\[)([, \d]+)(\])', 'func' => 'text_regex_users'],
11 => ['name' => 'Rule', 'regex' => '( Rule\[)([, \d]+)(\])', 'func' => 'text_regex_rule'],
];
// We will currently issue two hooks, one for the clog portion for backwards
// compatibility and one for new name. In the future, the old hook will be
// marked depreciated and removed.
$regex_array = api_plugin_hook_function('clog_regex_array', $regex_array);
$regex_array = api_plugin_hook_function('text_regex_array', $regex_array);
}
return array_merge($regex_array, $regex_extra);
}
function text_regex_replace(int $id, string $link, string $url, array $matches, array $cache) : string {
if ($link) {
return $matches[1] . '' . (isset($cache[$id]) ? htmle($cache[$id]) : $id) . '' . $matches[3];
} else {
return $matches[1] . ($cache[$id] ?? $id) . $matches[3];
}
}
function text_regex_parser_html(array $matches) : string {
return text_regex_parser($matches, true);
}
function text_regex_parser(array $matches, mixed $link = false) : string {
$result = $matches[0];
$match = $matches[0];
$key_match = -1;
for ($index = 1; $index < cacti_sizeof($matches); $index++) {
if ($match == $matches[$index]) {
$key_match = $index;
break;
}
}
if ($key_match != -1) {
$key_setting = (int) (($key_match - 1) / 4);
$regex_array = text_get_regex_array();
if (cacti_sizeof($regex_array)) {
if (array_key_exists($key_setting, $regex_array)) {
$regex_setting = $regex_array[$key_setting];
$rekey_array = [];
for ($j = 0; $j < 4; $j++) {
$rekey_array[$j] = $matches[$key_match + $j];
}
if (function_exists($regex_setting['func'])) {
$result = call_user_func_array($regex_setting['func'], [$rekey_array, $link]);
} else {
$result = $match;
}
}
}
}
return $result;
}
function text_regex_device(array $matches, mixed $link = false) : string {
static $host_cache = null;
if (!cacti_sizeof($host_cache)) {
$host_cache[0] = __('System Device');
}
$result = $matches[0];
$dev_ids = cacti_unique_ids($matches[2]);
if (cacti_sizeof($dev_ids)) {
$result = '';
foreach ($dev_ids as $id) {
if (!isset($host_cache[$id])) {
$host_cache[$id] = db_fetch_cell_prepared('SELECT description
FROM host
WHERE id = ?',
[$id]
);
}
$result .= text_regex_replace($id, $link, 'host.php?action=edit&id=%s', $matches, $host_cache);
}
}
return $result;
}
function text_regex_datasource(array $matches, mixed $link = false) : string {
static $gr_cache = null;
$result = $matches[0];
$ds_ids = cacti_unique_ids($matches[2]);
if (cacti_sizeof($ds_ids)) {
$result = '';
$graph_rows = [];
foreach ($ds_ids as $ds) {
if (!isset($gr_cache[$ds])) {
$gr_cache[$ds] = array_rekey(
db_fetch_assoc_prepared('SELECT DISTINCT
gti.local_graph_id AS id
FROM graph_templates_item AS gti
INNER JOIN data_template_rrd AS dtr
ON gti.task_item_id=dtr.id
WHERE gti.local_graph_id > 0
AND dtr.local_data_id = ?',
[$ds]
),
'id', 'id'
);
}
$graph_rows = array_merge($graph_rows, $gr_cache[$ds]);
}
$graph_results = '';
if (cacti_sizeof($graph_rows)) {
$graph_ids = implode(',', $graph_rows);
$graph_array = [0 => '', 1 => ' Graphs[', 2 => $graph_ids, 3 => ']'];
$graph_results = text_regex_graphs($graph_array, $link);
}
$result = $matches[1];
$ds_titles = get_data_source_titles($ds_ids);
$sep = '';
$ds_matches = $matches;
$ds_matches[1] = $ds_matches[3] = '';
foreach ($ds_ids as $ds_id) {
$result .= $sep . text_regex_replace($ds_id, $link, 'data_sources.php?action=ds_edit&id=%s', $ds_matches, $ds_titles);
$sep = ', ';
}
$result .= $matches[3];
if (!empty($graph_results)) {
$result .= ', ' . $graph_results;
}
}
return $result;
}
function text_regex_poller(array $matches, mixed $link = false) : string {
static $poller_cache = null;
if (!cacti_sizeof($poller_cache)) {
$poller_cache = array_rekey(
db_fetch_assoc('SELECT id, name
FROM poller'),
'id',
'name'
);
}
$result = $matches[0];
$poller_ids = cacti_unique_ids($matches[2]);
if (cacti_sizeof($poller_ids)) {
$result = '';
foreach ($poller_ids as $poller_id) {
$result .= text_regex_replace($poller_id, $link, 'pollers.php?action=edit&id=%s', $matches, $poller_cache);
}
}
return $result;
}
function text_regex_dataquery(array $matches, mixed $link = false) : string {
static $query_cache = null;
if (!cacti_sizeof($query_cache)) {
$query_cache = array_rekey(
db_fetch_assoc('SELECT id, name
FROM snmp_query'),
'id',
'name'
);
}
$result = $matches[0];
$query_ids = cacti_unique_ids($matches[2]);
if (cacti_sizeof($query_ids)) {
$result = '';
foreach ($query_ids as $query_id) {
$result .= text_regex_replace($query_id, $link, 'data_queries.php?action=edit&id=%s', $matches, $query_cache);
}
}
return $result;
}
function text_regex_rra(array $matches, mixed $link = false) : string {
$result = $matches[0];
$local_data_ids = $matches[2];
if (strlen($local_data_ids)) {
$datasource_array = [0 => '', 1 => ' DS[', 2 => $local_data_ids, 3 => ']'];
$datasource_result = text_regex_datasource($datasource_array, $link);
if (strlen($datasource_result)) {
$result .= ' ' . $datasource_result;
}
}
return $result;
}
function text_regex_graphs(array $matches, mixed $link = false) : string {
static $graph_cache = null;
$result = $matches[0];
$graph_ids = cacti_unique_ids($matches[2]);
if (cacti_sizeof($graph_ids)) {
$result = '';
$graph_add = CACTI_PATH_URL . 'graph_view.php?page=1&style=selective&action=preview&graph_add=';
$title = '';
$i = 0;
foreach ($graph_ids as $id) {
if (!isset($graph_cache[$id])) {
$graph_cache[$id] = db_fetch_cell_prepared('SELECT title_cache AS title
FROM graph_templates_graph AS gtg
WHERE local_graph_id = ?',
[$id]
);
}
}
$i = 0;
foreach ($graph_ids as $id) {
$graph_add .= ($i > 0 ? '%2C' : '') . $id;
$title .= ($title != '' ? ', ' : '') . htmle((isset($graph_cache[$id]) ? htmle($graph_cache[$id]) : (string) $id));
$i++;
}
if ($link) {
$result .= $matches[1] . "' . $title . '' . $matches[3];
} else {
$result .= $title . $matches[3];
}
}
return $result;
}
function text_regex_graphtemplates(array $matches, mixed $link = false) : string {
static $templates_cache = null;
if (!cacti_sizeof($templates_cache)) {
$templates_cache = array_rekey(
db_fetch_assoc('SELECT id, name
FROM graph_templates'),
'id',
'name'
);
}
$result = $matches[0];
$ids = cacti_unique_ids($matches[2]);
if (cacti_sizeof($ids)) {
$result = '';
foreach ($ids as $id) {
$result .= text_regex_replace($id, $link, 'graph_templates.php?action=template_edit&id=%s', $matches, $templates_cache);
}
}
return $result;
}
function text_regex_users(array $matches, mixed $link = false) : string {
static $users_cache = null;
$result = $matches[0];
$user_ids = cacti_unique_ids($matches[2]);
if (cacti_sizeof($user_ids)) {
$result = '';
foreach ($user_ids as $id) {
if (!isset($users_cache[$id])) {
$users_cache[$id] = db_fetch_cell_prepared('SELECT username
FROM user_auth
WHERE id = ?',
[$id]
);
}
}
foreach ($user_ids as $id) {
$result .= text_regex_replace($id, $link, 'user_admin.php?action=user_edit&tab=general&id=%s', $matches, $users_cache);
}
}
return $result;
}
function text_regex_rule(array $matches, mixed $link = false) : string {
static $rules_cache = null;
if (!cacti_sizeof($rules_cache)) {
$rules_cache = array_rekey(
db_fetch_assoc('SELECT id, name
FROM automation_graph_rules'),
'id', 'name'
);
}
$result = $matches[0];
$dev_ids = cacti_unique_ids($matches[2]);
if (cacti_sizeof($dev_ids)) {
$result = '';
foreach ($dev_ids as $rule_id) {
$result .= text_regex_replace($rule_id, $link, 'automation_graph_rules.php?action=edit&id=%s', $matches, $rules_cache);
}
}
return $result;
}
function get_keyup_delay() : int {
$keyup_delay = intval(read_user_setting('autocomplete_delay'));
if ($keyup_delay < 500) {
$keyup_delay = 500;
}
return $keyup_delay;
}
function cacti_unserialize(string $strobj) : mixed {
return unserialize($strobj, ['allowed_classes' => false]);
}
function detect_cpu_cores() : int {
$cpu_cores = 0;
if (CACTI_SERVER_OS === 'win32') {
$output = shell_exec('powershell -Command "Get-WmiObject Win32_Processor | Select-Object NumberOfLogicalProcessors"');
if (!is_null($output) && $output !== false) {
preg_match_all('/\d+/', $output, $matches);
$cpu_cores = array_sum($matches[0]);
}
} elseif (substr_count(cacti_strtolower(PHP_OS), 'darwin')) {
$cpu_cores = shell_exec('sysctl -n hw.ncpu');
} else {
if (file_exists('/usr/bin/nproc')) {
$cpu_cores = shell_exec('/usr/bin/nproc');
} elseif (file_exists('/bin/nproc')) {
$cpu_cores = shell_exec('/bin/nproc');
} else {
$output = shell_exec('nproc');
if (!is_null($output) && $output !== false) {
$cpu_cores = $output;
}
}
}
return intval(trim((string) $cpu_cores));
}
/**
* wrapper function to emulate pecl stats if it's not installed
*
* @param array $items A list of items to calculate the standard deviation for
* @param bool $sample
*
* @return mixed False on failure, double otherwise
*/
if (!function_exists('stats_standard_deviation')) {
function stats_standard_deviation(array $items, bool $sample = false) : mixed {
$sum = 0;
$total_items = 0;
// remove NaN entries from the data set
if (cacti_sizeof($items)) {
foreach ($items as $key => $value) {
if (is_int($value) || is_float($value)) {
$total_items++;
$sum += $value;
} else {
unset($items[$key]);
}
}
}
if (($sample && $total_items === 1) || $total_items === 0) {
return false;
}
$mean = $sum / $total_items;
$carry = 0.0;
foreach ($items as $val) {
$d = ((float) $val) - $mean;
$carry += $d * $d;
}
if ($sample) {
--$total_items;
}
return sqrt($carry / $total_items);
}
}
/**
* cacti_normalize_windows_path - folds a Windows path into a comparable form
*
* @param mixed $path The path to normalize
*
* @return string The lower cased, forward slashed path
*/
function cacti_normalize_windows_path(mixed $path) : string {
$lower = strtolower((string) $path);
/**
* Long-path prefixes. Strip \\?\UNC\ first so the remaining \\ is
* preserved for UNC share comparison; then strip bare \\?\ which only
* wraps drive-letter paths for filesystem APIs.
*/
if (strpos($lower, '\\\\?\\unc\\') === 0) {
$lower = '\\\\' . substr($lower, 8);
} elseif (strpos($lower, '\\\\?\\') === 0) {
$lower = substr($lower, 4);
}
$lower = str_replace('\\', '/', $lower);
// drop trailing slashes except for a lone '/', the drive-root case
if (strlen($lower) > 1) {
$lower = rtrim($lower, '/');
}
return $lower;
}
/**
* cacti_path_is_within - checks that a resolved path sits under a base directory
*
* @param string $candidate The path to test
* @param string $base The directory it must stay within
* @param bool|null $windows Force Windows comparison rules, null to detect
*
* @return bool True when the candidate resolves inside the base
*/
function cacti_path_is_within(string $candidate, string $base, ?bool $windows = null) : bool {
$resolved = realpath($candidate);
if ($resolved === false) {
return false;
}
$base_resolved = realpath($base);
if ($base_resolved === false) {
return false;
}
if ($windows ?? (DIRECTORY_SEPARATOR === '\\')) {
$resolved = cacti_normalize_windows_path($resolved);
$base_resolved = cacti_normalize_windows_path($base_resolved);
}
return strpos($resolved, $base_resolved . '/') === 0 || $resolved === $base_resolved;
}
/**
* validate_relative_path_within - validates an untrusted relative path
*
* Rejects absolute paths, drive letters, empty or dot segments, and symlinked
* segments under the base, then confirms the deepest existing ancestor resolves
* inside the base. A not-yet-created entry (including nested directories that
* an archive restore has not built yet) is allowed, mirroring
* data_source_path_within_rra().
*
* @param mixed $path The untrusted relative path
* @param string $base_dir The base directory the path must stay within
*
* @return mixed The validated absolute path, or false when invalid
*/
function validate_relative_path_within(mixed $path, string $base_dir) : mixed {
if (!is_string($path) || $path === '' || strpos($path, "\0") !== false) {
return false;
}
$normalized = str_replace('\\', '/', $path);
if ($normalized === '' || $normalized[0] === '/' || preg_match('/^[a-zA-Z]:\//', $normalized)) {
return false;
}
$parts = [];
foreach (explode('/', $normalized) as $part) {
if ($part === '' || $part === '.' || $part === '..') {
return false;
}
$parts[] = $part;
}
$base_real = realpath($base_dir);
if ($base_real === false) {
return false;
}
$candidate = $base_real . '/' . implode('/', $parts);
// block symlink pivots under writable base paths, and remember the deepest
// ancestor that actually exists
$walk = $base_real;
$deepest_existing = $base_real;
foreach ($parts as $part) {
$walk .= '/' . $part;
// is_link() before file_exists(): file_exists() follows the link and is
// false for a dangling symlink, so a symlink segment whose target does
// not exist yet would otherwise be accepted.
if (is_link($walk)) {
return false;
}
if (file_exists($walk)) {
$deepest_existing = $walk;
}
}
/**
* A not-yet-created entry is judged by its deepest existing ancestor, so a
* nested archive directory that has not been created yet is not mistaken
* for an escape. Every segment is a plain name and no walked segment is a
* symlink, so the full candidate stays within the base.
*/
$resolved = realpath($deepest_existing);
return $resolved !== false && cacti_path_is_within($resolved, $base_real) ? $candidate : false;
}
/**
* Log an administrative action performed on a user account.
*
* @param mixed $user Username of the affected account.
* @param mixed $action Action that was performed.
*
* @return bool Whether the message was written to the Cacti log.
*/
function log_user_action($user, $action) {
if (!is_string($user) || !is_string($action)) {
return false;
}
$clean = static function (string $value) : string {
return preg_replace('/[\x00-\x1f\x7f]/', '', $value) ?? '';
};
$user = $clean($user);
$action = $clean($action);
if ($user === '' || $action === '') {
return false;
}
if (!empty($_SESSION['sess_user_id'])) {
$acting_user_id = $_SESSION['sess_user_id'];
$acting_user = get_username($acting_user_id);
if (!is_string($acting_user) || $acting_user === '') {
$acting_user = 'user ID ' . $acting_user_id;
}
} elseif (PHP_SAPI === 'cli') {
$acting_user = get_execution_user();
if ($acting_user === '') {
$acting_user = 'unknown user';
}
$acting_user .= ' (CLI)';
} else {
$acting_user = 'unknown user';
}
$acting_user = $clean($acting_user);
return cacti_log("NOTE: User '$user' was $action by user '$acting_user'", false, PHP_SAPI === 'cli' ? 'AUTHC' : 'AUTH');
}