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'); }