['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'] ]; $pipes = []; try { $process = proc_open([$path, '-'], $descriptors, $pipes, null, null, ['bypass_shell' => true]); } finally { if ($previous_language === false) { putenv('LANG'); } else { putenv('LANG=' . $previous_language); } } if (!is_resource($process) || count($pipes) !== 3) { cacti_log('ERROR: Unable to launch RRDtool without a command shell.', false, 'RRDTOOL'); return false; } stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); return (object) [ 'process' => $process, 'stdin' => $pipes[0], 'stdout' => $pipes[1], 'stderr' => $pipes[2], 'alive' => true, 'restarts' => 0, 'output_to_term' => $output_to_term ]; } /** * Configure bounded blocking I/O for an RRDtool proxy socket. * * @param Socket $socket Connected or unconnected proxy socket * @param int $seconds Read and write timeout in seconds * * @return bool Whether both socket options were applied */ function rrdtool_proxy_set_timeouts(Socket $socket, int $seconds = RRD_PROXY_TIMEOUT_SECONDS) : bool { if ($seconds < 1) { return false; } $timeout = ['sec' => $seconds, 'usec' => 0]; return @socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout) && @socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout); } /** * Connect to an RRDtool proxy with an explicit deadline. * * @param Socket $socket Proxy socket * @param string $server IPv4 or IPv6 address * @param int $port TCP port * @param int $seconds Connection timeout in seconds * * @return bool Whether the connection completed successfully */ function rrdtool_proxy_connect(Socket $socket, string $server, int $port, int $seconds = RRD_PROXY_TIMEOUT_SECONDS) : bool { if ($seconds < 1 || !@socket_set_nonblock($socket)) { return false; } $connected = @socket_connect($socket, $server, $port); if (!$connected) { $error = socket_last_error($socket); $in_progress = []; foreach (['SOCKET_EINPROGRESS', 'SOCKET_EALREADY', 'SOCKET_EWOULDBLOCK', 'SOCKET_EAGAIN'] as $constant) { if (defined($constant)) { $in_progress[] = constant($constant); } } if (!in_array($error, array_unique($in_progress), true)) { @socket_set_block($socket); return false; } $read = []; $write = [$socket]; $except = [$socket]; $ready = @socket_select($read, $write, $except, $seconds, 0); if ($ready !== 1 || $except !== []) { @socket_set_block($socket); return false; } $socket_error = @socket_get_option($socket, SOL_SOCKET, SO_ERROR); if ($socket_error !== 0) { @socket_set_block($socket); return false; } } return @socket_set_block($socket); } /** * Write an entire RRDtool proxy frame. * * socket_write() may complete only part of a write. This helper retries until * all bytes are transferred, and treats zero-byte or failed writes as errors. * * @param Socket $socket Proxy socket * @param string $data Complete encoded frame * * @return bool Whether every byte was written */ function rrdtool_proxy_write(Socket $socket, string $data) : bool { $length = strlen($data); $written = 0; while ($written < $length) { set_error_handler(static fn () : bool => true); try { $result = socket_write($socket, substr($data, $written)); } finally { restore_error_handler(); } if ($result === false || $result === 0) { return false; } $written += $result; } return true; } /** * Read one bounded, terminator-delimited RRDtool proxy frame. * * @param Socket $socket Proxy socket * @param string $terminator Protocol frame terminator * @param int $max_size Maximum buffered frame size * * @return string|false Frame contents without the terminator, or false */ function rrdtool_proxy_read_frame(Socket $socket, string $terminator, int $max_size) : string|false { if ($terminator === '' || $max_size < 1) { return false; } $input = ''; while (true) { $remaining = $max_size + strlen($terminator) - strlen($input); $recv = @socket_read($socket, min(100000, $remaining), PHP_BINARY_READ); if ($recv === false || $recv === '') { return false; } $input .= $recv; $position = strpos($input, $terminator); if ($position !== false) { return substr($input, 0, $position); } if (strlen($input) > $max_size) { return false; } } } /** * Decode gzip data without leaking malformed-peer warnings into graph output. * * @param string $input Compressed packet * @param int $max_size Maximum decoded size * * @return string|false Decoded packet, or false when invalid or oversized */ function rrdtool_proxy_gzdecode(string $input, int $max_size) : string|false { if ($max_size < 1) { return false; } set_error_handler(static fn () : bool => true); try { return gzdecode($input, $max_size); } finally { restore_error_handler(); } } /** * Decode and validate a complete RRDtool proxy response. * * The terminal status must occur at the end of the final protocol packet. * Status-like text in graph or xport payload data is therefore not treated as * a protocol boundary. * * @param string $frame Encoded frame without the sequence terminator * @param int $max_size Maximum decoded response size * * @return array{output:string,status:string,success:bool,raw:string}|false */ function rrdtool_proxy_decode_response(string $frame, int $max_size = RRD_PROXY_MAX_RESPONSE_SIZE) : array|false { if ($max_size < 1 || strlen($frame) > $max_size) { return false; } $packets = explode(RRD_PROXY_END_OF_PACKET, $frame); if (end($packets) === '') { array_pop($packets); } if ($packets === []) { return false; } $decoded_size = 0; $decoded = []; foreach ($packets as $packet) { $transaction = decrypt($packet); if ($transaction === false) { return false; } if (str_starts_with($transaction, "\x1f\x8b")) { $transaction = rrdtool_proxy_gzdecode($transaction, $max_size - $decoded_size); if ($transaction === false) { return false; } } $decoded_size += strlen($transaction); $decoded[] = $transaction; } $last_packet = array_pop($decoded); if (!preg_match('/(?:^|\r?\n)(OK u(?::[^\r\n]*)?|ERROR:[^\r\n]*)\r?\n?\z/D', $last_packet, $matches, PREG_OFFSET_CAPTURE)) { return false; } $status_offset = $matches[1][1]; $status = $matches[1][0]; $packet_output = substr($last_packet, 0, $status_offset); $output = implode('', $decoded) . rtrim($packet_output, "\r\n"); $raw = $output . ($output === '' ? '' : "\n") . $status; return [ 'output' => $output, 'status' => $status, 'success' => str_starts_with($status, 'OK u'), 'raw' => $raw ]; } function __rrd_proxy_init(string $logopt = 'WEBLOG') : mixed { global $encryption; $encryption = true; $load_balancing = read_config_option('rrdp_load_balancing') == 'on' ? true : false; $portp = intval(read_config_option('rrdp_port')); $portb = intval(read_config_option('rrdp_port_backup')); $servera = read_config_option('rrdp_server'); $serverb = read_config_option('rrdp_server_backup'); if ($load_balancing) { $rrdp_id = random_int(1,2); $server = ($rrdp_id == 1) ? $servera : $serverb; $port = ($rrdp_id == 1) ? $portp : $portb; } else { $server = read_config_option('rrdp_server'); $port = intval(read_config_option('rrdp_port')); $rrdp_id = 1; } // Detect address family based on server address for IPv6 support $socket_family = filter_var(trim($server, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET; $rrdp_socket = @socket_create($socket_family, SOCK_STREAM, SOL_TCP); if ($rrdp_socket === false) { cacti_log('CACTI2RRDP ERROR: Unable to create socket to connect to RRDtool Proxy Server', false, $logopt, POLLER_VERBOSITY_LOW); return false; } if (!rrdtool_proxy_set_timeouts($rrdp_socket)) { cacti_log('CACTI2RRDP ERROR: Unable to configure socket time-outs.', false, $logopt, POLLER_VERBOSITY_LOW); @socket_close($rrdp_socket); return false; } // Strip brackets from IPv6 addresses for socket_connect $connect_server = trim($server, '[]'); $rrdp = rrdtool_proxy_connect($rrdp_socket, $connect_server, $port); if ($rrdp === false) { // log entry ... cacti_log('CACTI2RRDP ERROR: Unable to connect to RRDtool Proxy Server #' . $rrdp_id, false, $logopt, POLLER_VERBOSITY_LOW); if ($load_balancing) { // Close the failed socket before creating a new one for the backup server @socket_close($rrdp_socket); $rrdp_id = ($rrdp_id == 1) ? 2 : 1; $server = ($rrdp_id == 1) ? $servera : $serverb; $port = ($rrdp_id == 1) ? $portp : $portb; // Re-detect address family for backup server (may differ from primary) $socket_family = filter_var(trim($server, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET; $connect_server = trim($server, '[]'); $rrdp_socket = @socket_create($socket_family, SOCK_STREAM, SOL_TCP); if ($rrdp_socket === false) { cacti_log('CACTI2RRDP ERROR: Unable to create socket to connect to RRDtool Proxy Server #' . $rrdp_id, false, $logopt, POLLER_VERBOSITY_LOW); return false; } if (!rrdtool_proxy_set_timeouts($rrdp_socket)) { cacti_log('CACTI2RRDP ERROR: Unable to configure backup socket time-outs.', false, $logopt, POLLER_VERBOSITY_LOW); @socket_close($rrdp_socket); return false; } $rrdp = rrdtool_proxy_connect($rrdp_socket, $connect_server, $port); if ($rrdp === false) { cacti_log('CACTI2RRDP ERROR: Unable to connect to RRDtool Proxy Server #' . $rrdp_id, false, $logopt, POLLER_VERBOSITY_LOW); @socket_close($rrdp_socket); return false; } } else { @socket_close($rrdp_socket); return false; } } $rrdp_fingerprint = ($rrdp_id == 1) ? read_config_option('rrdp_fingerprint') : read_config_option('rrdp_fingerprint_backup'); if (!rrdtool_proxy_write($rrdp_socket, read_config_option('rsa_public_key') . RRD_PROXY_END_OF_SEQUENCE)) { cacti_log('CACTI2RRDP ERROR: Public RSA Key Exchange - unable to send local key.', false, $logopt, POLLER_VERBOSITY_LOW); @socket_close($rrdp_socket); return false; } // read public key being returned by the proxy server $rrdp_public_key = rrdtool_proxy_read_frame($rrdp_socket, RRD_PROXY_END_OF_SEQUENCE, RRD_PROXY_MAX_KEY_SIZE); if ($rrdp_public_key === false || trim($rrdp_public_key) === '') { cacti_log('CACTI2RRDP ERROR: Public RSA Key Exchange failed - no key received.', false, $logopt, POLLER_VERBOSITY_LOW); @socket_close($rrdp_socket); return false; } $rrdp_public_key = trim($rrdp_public_key); try { $public = RSA::loadPublicKey($rrdp_public_key); } catch (Throwable $e) { cacti_log('CACTI2RRDP ERROR: Public RSA Key Exchange returned an invalid key.', false, $logopt, POLLER_VERBOSITY_LOW); @socket_close($rrdp_socket); return false; } $fingerprint = $public->getFingerprint('md5'); if (!is_string($rrdp_fingerprint) || !hash_equals($rrdp_fingerprint, $fingerprint)) { cacti_log('CACTI2RRDP ERROR: Mismatch RSA Fingerprint.', false, $logopt, POLLER_VERBOSITY_LOW); @socket_close($rrdp_socket); return false; } else { $rrdproxy = [$rrdp_socket, $rrdp_public_key]; // set the rrdtool default font if (read_config_option('path_rrdtool_default_font')) { rrdtool_execute('setenv RRD_DEFAULT_FONT ' . cacti_escapeshellarg((string) read_config_option('path_rrdtool_default_font')), false, RRDTOOL_OUTPUT_NULL, $rrdproxy, $logopt); } return $rrdproxy; } } function rrd_close() : void { global $config; $args = func_get_args(); $local_storage = (isset($config['local_storage']) && $config['local_storage'] === true) ? true : false; $function = ($local_storage === false && read_config_option('storage_location')) ? '__rrd_proxy_close' : '__rrd_close'; call_user_func_array($function, $args); } function __rrd_close(mixed $rrdtool_pipe) : void { if (rrdtool_is_process($rrdtool_pipe)) { if ($rrdtool_pipe->alive && is_resource($rrdtool_pipe->stdin)) { rrdtool_write_all($rrdtool_pipe->stdin, "quit\n"); } foreach (['stdin', 'stdout', 'stderr'] as $stream) { if (is_resource($rrdtool_pipe->{$stream})) { fclose($rrdtool_pipe->{$stream}); } } if (is_resource($rrdtool_pipe->process)) { proc_close($rrdtool_pipe->process); } $rrdtool_pipe->alive = false; } elseif (is_resource($rrdtool_pipe)) { // Compatibility with a pipe opened by an older plugin. pclose($rrdtool_pipe); } } /** * Determine whether a value is a local RRDtool process handle. * * @param mixed $process Candidate process handle * * @return bool Whether the value contains every required process resource */ function rrdtool_is_process(mixed $process) : bool { return is_object($process) && isset($process->process, $process->stdin, $process->stdout, $process->stderr, $process->alive); } /** * Write a complete command to RRDtool, including when fwrite() is partial. * * @param mixed $stream Writable process stream * @param string $data Complete bytes to write * * @return bool Whether every byte was written and flushed */ function rrdtool_write_all(mixed $stream, string $data) : bool { if (!is_resource($stream)) { return false; } $offset = 0; $length = strlen($data); while ($offset < $length) { $written = @fwrite($stream, substr($data, $offset)); if ($written === false || $written === 0) { return false; } $offset += $written; } return fflush($stream); } /** * Convert Cacti's shell-quoted arguments to RRDtool stdin quoting. * * RRDtool's interactive parser accepts double-quoted arguments, but shell * escaping for an apostrophe (for example, 'one'\''s') is not valid in that * parser. This converter recognizes only the single-quoted form produced by * escapeshellarg(); it does not attempt to interpret arbitrary shell syntax. * * @return string|false The stdin-safe command, or false for an invalid command */ function rrdtool_prepare_stdin_command(string $command_line) : string|false { if (strlen($command_line) > RRDTOOL_MAX_COMMAND_BYTES || strpbrk($command_line, "\0\r\n") !== false) { return false; } if (!preg_match('/^\s*([A-Za-z][A-Za-z0-9-]*)\b/', $command_line, $matches)) { return false; } $commands = [ 'create', 'update', 'updatev', 'graph', 'graphv', 'dump', 'restore', 'last', 'lastupdate', 'first', 'info', 'list', 'fetch', 'tune', 'resize', 'xport', 'flushcached', 'ls', 'cd', 'mkdir', 'pwd' ]; if (!in_array(strtolower($matches[1]), $commands, true)) { return false; } $prepared = ''; $length = strlen($command_line); for ($offset = 0; $offset < $length; $offset++) { if ($command_line[$offset] !== "'") { $prepared .= $command_line[$offset]; continue; } $value = ''; $closed = false; $offset++; while ($offset < $length) { $quote = strpos($command_line, "'", $offset); if ($quote === false) { return false; } $value .= substr($command_line, $offset, $quote - $offset); $offset = $quote + 1; if (substr($command_line, $offset, 3) === "\\''") { $value .= "'"; $offset += 3; continue; } $closed = true; $offset--; break; } if (!$closed) { return false; } /** * RRDtool control codes such as \: and \n must reach the graph parser * verbatim. Escaping the backslash leaves a legend like "IO Wait\:" * ending in a dangling control code, which RRDtool refuses, so only the * delimiter is escaped here. A value ending in a backslash, or holding * one immediately before a double quote, has no unambiguous form inside * a quoted token and is rejected instead. */ if (str_ends_with($value, '\\') || str_contains($value, '\\"')) { return false; } $prepared .= '"' . str_replace('"', '\\"', $value) . '"'; } return $prepared; } function __rrd_proxy_close(mixed $rrdp) : void { if (is_array($rrdp) && isset($rrdp[0], $rrdp[1]) && $rrdp[0] instanceof Socket) { try { rrdtool_proxy_write($rrdp[0], encrypt('quit', $rrdp[1]) . RRD_PROXY_END_OF_SEQUENCE); } catch (Throwable $e) { // The connection still has to be closed when request encoding fails. } finally { @socket_shutdown($rrdp[0], 2); @socket_close($rrdp[0]); } } } function encrypt(string $output, string $rsa_key) : string { global $encryption; if ($encryption) { /** @var \phpseclib4\Crypt\RSA\PublicKey $public loadPublicKey() returns the RSA public key implementation, which exposes encrypt() */ $public = RSA::loadPublicKey($rsa_key); $aes = new Rijndael('cbc'); $aes_key = random_bytes(32); $aes->setKey($aes_key); $aes->setIV(str_repeat("\0", 16)); $ciphertext = base64_encode($aes->encrypt($output)); $aes_key = base64_encode($public->encrypt($aes_key)); $aes_key_length = str_pad(dechex(strlen($aes_key)), 3, '0', STR_PAD_LEFT); return $aes_key_length . $aes_key . $ciphertext; } else { return $output; } } /** * Decrypt one encrypted RRDtool proxy packet. * * @param string $input Encoded packet * @param string $rsa_private_key Local RSA private key * * @return string|false Plaintext, or false when the packet is malformed */ function rrdtool_proxy_decrypt(string $input, string $rsa_private_key) : string|false { if (strlen($input) < 4 || !ctype_xdigit(substr($input, 0, 3))) { return false; } $aes_key_length = hexdec(substr($input, 0, 3)); if ($aes_key_length < 1 || strlen($input) <= 3 + $aes_key_length) { return false; } $aes_key = base64_decode(substr($input, 3, $aes_key_length), true); $ciphertext = base64_decode(substr($input, 3 + $aes_key_length), true); if ($aes_key === false || $aes_key === '' || $ciphertext === false) { return false; } try { /** @var \phpseclib4\Crypt\RSA\PrivateKey $private loadPrivateKey() returns the RSA private key implementation, which exposes decrypt() */ $private = RSA::loadPrivateKey($rsa_private_key); $aes = new Rijndael('cbc'); $aes_key = $private->decrypt($aes_key); if (!is_string($aes_key) || $aes_key === '') { return false; } // phpseclib 2 truncated oversized Rijndael keys to 256 bits. if (strlen($aes_key) > 32) { $aes_key = substr($aes_key, 0, 32); } $aes->setKey($aes_key); $aes->setIV(str_repeat("\0", 16)); return $aes->decrypt($ciphertext); } catch (Throwable $e) { return false; } } function decrypt(string $input) : string|false { global $encryption; if (!$encryption) { return $input; } return rrdtool_proxy_decrypt($input, (string) read_config_option('rsa_private_key')); } function rrdtool_execute() : mixed { global $config; $args = func_get_args(); $local_storage = (isset($config['local_storage']) && $config['local_storage'] === true) ? true : false; $function = ($local_storage === false && read_config_option('storage_location')) ? '__rrd_proxy_execute' : '__rrd_execute'; return call_user_func_array($function, $args); } /** * Build a non-sensitive identifier for an RRDtool command. * * RRDtool command lines can contain paths, graph labels, device metadata, and * credentials substituted by legacy templates. Logging the complete command * therefore exposes considerably more than is needed to correlate failures. * The operation, byte count, and digest retain that correlation without * copying command arguments into the log. * * @param string $command_line The complete RRDtool command line * * @return string A safe operation/length/digest tuple */ function rrdtool_command_log_context(string $command_line) : string { $command = 'unknown'; $trimmed = ltrim($command_line, " \t"); if (preg_match('/^([A-Za-z0-9_-]+)/', $trimmed, $matches)) { $command = strtolower($matches[1]); } return sprintf( 'command=%s bytes=%d sha256=%s', $command, strlen($command_line), hash('sha256', $command_line) ); } /** * Build an RRDtool command from a trusted operation and isolated arguments. * * @param array $command One operation followed by zero or more arguments * * @return string|false A command suitable for stdin conversion, or false */ function rrdtool_build_command(array $command) : string|false { $operation = array_shift($command); if (!is_string($operation) || $operation === '') { return false; } foreach ($command as $argument) { if (!is_string($argument)) { return false; } } return $operation . ($command === [] ? '' : ' ' . implode(' ', array_map('cacti_escapeshellarg', $command))); } /** * Select the locale inherited by a newly launched RRDtool process. * * RRDtool formats fetch/info/xport numbers for the current locale. Cacti's * parsers require a decimal point for those machine-readable responses. * * @return string|false English for numeric output, otherwise the Cacti locale */ function rrdtool_command_language(string $command_line) : string|false { $operation = strtolower(strtok(ltrim($command_line), " \t") ?: ''); return in_array($operation, ['fetch', 'info', 'xport'], true) ? 'en' : false; } /** * Execute one command over an RRDtool stdin process and wait for its status. * * @return array{success: bool, output: string, error: string} */ /** * Relaunch a died RRDtool process in place. * * The poller opens one process per run and reuses it for every update, so a * process that dies partway through would otherwise take the rest of the run * with it. The replacement is written back into the same object so every holder * of the handle recovers. * * @param object $process RRDtool process handle to revive * * @return bool Whether a working process is available afterwards */ function rrdtool_process_restart(object $process) : bool { if (!rrdtool_is_process($process)) { return false; } $attempts = isset($process->restarts) ? (int) $process->restarts : 0; if ($attempts >= RRDTOOL_MAX_RESTARTS) { return false; } foreach (['stdin', 'stdout', 'stderr'] as $pipe) { if (isset($process->$pipe) && is_resource($process->$pipe)) { fclose($process->$pipe); } } if (isset($process->process) && is_resource($process->process)) { proc_close($process->process); } $replacement = __rrd_init((bool) ($process->output_to_term ?? false)); if (!rrdtool_is_process($replacement)) { $process->alive = false; return false; } $process->process = $replacement->process; $process->stdin = $replacement->stdin; $process->stdout = $replacement->stdout; $process->stderr = $replacement->stderr; $process->alive = true; $process->restarts = $attempts + 1; cacti_log(sprintf('WARNING: Detected RRDtool Crash, restarted the process (attempt %d of %d).', $attempts + 1, RRDTOOL_MAX_RESTARTS), false, 'RRDTOOL'); if (function_exists('debounce_run_notification')) { debounce_run_notification('rrdtool_command_crash'); } return true; } function rrdtool_process_command(object $process, string $command_line, float $timeout = 60.0) : array { $result = ['success' => false, 'output' => '', 'error' => 'RRDtool process is unavailable.']; if (!rrdtool_is_process($process)) { return $result; } // A died process used to be reopened and the command retried; without that // every remaining update in the run fails. if (!$process->alive && !rrdtool_process_restart($process)) { return $result; } $prepared_command = rrdtool_prepare_stdin_command($command_line); if ($prepared_command === false) { $result['error'] = 'RRDtool command contains an invalid operation, quote, or framing character.'; return $result; } if (!rrdtool_write_all($process->stdin, $prepared_command . "\n")) { $result['error'] = 'Unable to write the complete RRDtool command.'; $process->alive = false; return $result; } $deadline = microtime(true) + max(0.1, $timeout); $stdout = ''; $stderr = ''; $status = null; while (microtime(true) < $deadline) { $read = []; if (is_resource($process->stdout) && !feof($process->stdout)) { $read[] = $process->stdout; } if (is_resource($process->stderr) && !feof($process->stderr)) { $read[] = $process->stderr; } if ($read === []) { break; } $write = null; $except = null; $ready = @stream_select($read, $write, $except, 0, 200000); if ($ready === false) { $result['error'] = 'Unable to read the RRDtool response.'; break; } foreach ($read as $stream) { $chunk = fread($stream, 1048576); if ($chunk === false) { continue; } if ($stream === $process->stdout) { $stdout .= $chunk; } else { $stderr .= $chunk; } } if (strlen($stdout) + strlen($stderr) > RRDTOOL_MAX_RESPONSE_BYTES) { $result['error'] = 'RRDtool response exceeded the configured safety limit.'; $process->alive = false; if (is_resource($process->process)) { proc_terminate($process->process); } return $result; } if (preg_match('/(OK(?: u:[^\r\n]*)?|ERROR:[^\r\n]*)\r?\n?$/', $stdout, $matches, PREG_OFFSET_CAPTURE)) { $status = $matches[1][0]; $stdout = substr($stdout, 0, $matches[0][1]); break; } } $result['output'] = $stdout; $result['error'] = trim($stderr); if ($status === null) { if ($result['error'] === '') { $result['error'] = 'RRDtool did not return a terminal status before the timeout.'; } $process->alive = false; if (is_resource($process->process)) { proc_terminate($process->process); } return $result; } if (str_starts_with($status, 'ERROR:')) { $result['error'] = trim($status . ($result['error'] !== '' ? "\n" . $result['error'] : '')); return $result; } $result['success'] = true; return $result; } /** * Convert a process result to the long-standing RRDTOOL_OUTPUT_* contract. */ function rrdtool_format_result(array $result, int $output_flag) : mixed { if (!$result['success']) { if ($output_flag === RRDTOOL_OUTPUT_RETURN_STDERR) { return $result['error']; } if ($output_flag === RRDTOOL_OUTPUT_STDERR && $result['error'] !== '') { print $result['error']; } return false; } switch ($output_flag) { case RRDTOOL_OUTPUT_NULL: case RRDTOOL_OUTPUT_BOOLEAN: return true; case RRDTOOL_OUTPUT_STDOUT: case RRDTOOL_OUTPUT_GRAPH_DATA: return $result['output']; case RRDTOOL_OUTPUT_RETURN_STDERR: return $result['error'] !== '' ? $result['error'] : $result['output']; case RRDTOOL_OUTPUT_STDERR: if ($result['error'] !== '') { print $result['error']; } if (substr($result['output'], 1, 3) === 'PNG') { return 'OK'; } if (str_starts_with($result['output'], '= 8192) { $compressed = gzencode($command_line, 1); if ($compressed !== false) { $command_line = $compressed; } } try { $frame = encrypt($command_line, $rrdp_public_key) . RRD_PROXY_END_OF_SEQUENCE; } catch (Throwable $e) { cacti_log('CACTI2RRDP ERROR: Unable to encode the proxy request.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW); if ($rrdp_auto_close) { __rrd_proxy_close($rrdp); } return false; } if (!rrdtool_proxy_write($rrdp_socket, $frame)) { cacti_log('CACTI2RRDP ERROR: Data Transfer - unable to write complete request.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW); if ($rrdp_auto_close) { __rrd_proxy_close($rrdp); } return false; } $frame = rrdtool_proxy_read_frame($rrdp_socket, RRD_PROXY_END_OF_SEQUENCE, RRD_PROXY_MAX_RESPONSE_SIZE); if ($frame === false) { cacti_log('CACTI2RRDP ERROR: Data Transfer - timed out, closed, or exceeded the response limit.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW); if ($rrdp_auto_close) { __rrd_proxy_close($rrdp); } return false; } $response = rrdtool_proxy_decode_response($frame); if ($response === false) { cacti_log('CACTI2RRDP ERROR: Malformed, undecodable, or unterminated proxy response.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW); if ($rrdp_auto_close) { __rrd_proxy_close($rrdp); } return false; } cacti_log('RRDP: ' . $response['raw'], $log_to_stdout, $logopt, POLLER_VERBOSITY_DEBUG); if ($rrdp_auto_close) { __rrd_proxy_close($rrdp); } switch ($output_flag) { case RRDTOOL_OUTPUT_NULL: return $response['success'] ? 'OK' : false; case RRDTOOL_OUTPUT_STDOUT: return $response['success'] ? rtrim($response['output']) : false; case RRDTOOL_OUTPUT_GRAPH_DATA: // Graph output is binary. The proxy decoder has already removed the // protocol terminator, so trimming here could corrupt valid image bytes. return $response['success'] ? $response['output'] : false; case RRDTOOL_OUTPUT_STDERR: if (!$response['success']) { print $response['raw']; return false; } if (substr($response['output'], 1, 3) == 'PNG') { return 'OK'; } if (str_starts_with($response['output'], 'GIF87')) { return 'OK'; } if (str_starts_with($response['output'], ' 0) { $data_sources = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.rrd_heartbeat, dtr.rrd_minimum, dtr.rrd_maximum, dtr.data_source_type_id FROM data_template_rrd AS dtr INNER JOIN graph_templates_item AS gti ON dtr.id = gti.task_item_id WHERE local_data_id = ? ORDER BY local_data_template_rrd_id', [$local_data_id] ); } else { $data_sources = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.rrd_heartbeat, dtr.rrd_minimum, dtr.rrd_maximum, dtr.data_source_type_id FROM data_template_rrd AS dtr WHERE local_data_id = ? ORDER BY local_data_template_rrd_id', [$local_data_id] ); } /** * ONLY make a new DS entry if: * * - There are multiple data sources and this item is not the main one. * - There are only one data source (then use it) */ if (cacti_sizeof($data_sources)) { $data_local = db_fetch_row_prepared('SELECT host_id, snmp_query_id, snmp_index FROM data_local WHERE id = ?', [$local_data_id] ); $speed = is_array($data_local) ? rrdtool_function_interface_speed($data_local) : (string) intval(read_config_option('default_interface_speed')) * 1000000; foreach ($data_sources as $data_source) { // use the cacti ds name by default or the user defined one, if entered $data_source_name = get_data_source_item_name($data_source['id']); // Trim the data source maximum $data_source['rrd_maximum'] = trim($data_source['rrd_maximum']); if ($data_source['rrd_maximum'] == 'U') { // in case no maximum is given, use "Undef" value $data_source['rrd_maximum'] = 'U'; } elseif (str_contains($data_source['rrd_maximum'], '|query_')) { // in case a query variable is given, evaluate it if ($data_source['rrd_maximum'] == '|query_ifSpeed|' || $data_source['rrd_maximum'] == '|query_ifHighSpeed|') { $data_source['rrd_maximum'] = $speed; } elseif (is_array($data_local)) { $data_source['rrd_maximum'] = substitute_snmp_query_data($data_source['rrd_maximum'], $data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index']); } } elseif ($data_source['rrd_maximum'] != 'U' && (int)$data_source['rrd_maximum'] <= (int)$data_source['rrd_minimum']) { // max > min required, but take care of an "Undef" value if ($data_source['data_source_type_id'] == 1 || $data_source['data_source_type_id'] == 4) { $data_source['rrd_maximum'] = 'U'; } else { $data_source['rrd_maximum'] = (int)$data_source['rrd_minimum'] + 1; } } // min==max==0 won't work with rrdtool if ($data_source['rrd_minimum'] == 0 && $data_source['rrd_maximum'] == 0) { $data_source['rrd_maximum'] = 'U'; } // the bounds are about to be concatenated onto an rrdtool command line, so // never let anything but a number or the literal 'U' reach that sink foreach (['rrd_minimum', 'rrd_maximum'] as $bound) { if ($data_source[$bound] !== 'U' && !is_numeric($data_source[$bound])) { cacti_log("ERROR: Non-numeric $bound '" . $data_source[$bound] . "' for data source '$data_source_name' local_data_id " . $local_data_id, false, 'RRDTOOL'); $data_source[$bound] = 'U'; } } $create_ds .= "DS:$data_source_name:" . $data_source_types[$data_source['data_source_type_id']] . ':' . $data_source['rrd_heartbeat'] . ':' . $data_source['rrd_minimum'] . ':' . $data_source['rrd_maximum'] . RRD_NL; } } $create_rra = ''; // loop through each available RRA for this DS foreach ($rras as $rra) { $create_rra .= 'RRA:' . $consolidation_functions[$rra['consolidation_function_id']] . ':' . $rra['x_files_factor'] . ':' . $rra['steps'] . ':' . $rra['rows'] . RRD_NL; } $owner_id = 0; $group_id = 0; if (CACTI_SERVER_OS != 'win32') { $owner_id = fileowner(CACTI_PATH_RRA); $group_id = filegroup(CACTI_PATH_RRA); } /** * check for structured path configuration, if in place verify directory * exists and if not create it. */ if (read_config_option('extended_paths') == 'on') { if (read_config_option('storage_location')) { if (rrdtool_execute('is_dir ' . cacti_escapeshellarg(dirname($data_source_path)), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'POLLER') === false) { if (rrdtool_execute('mkdir ' . cacti_escapeshellarg(dirname($data_source_path)), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'POLLER') === false) { cacti_log("ERROR: Unable to create directory '" . dirname($data_source_path) . "'", false); } } } elseif (!is_dir(dirname($data_source_path))) { if (CACTI_WEB == false || is_writable(CACTI_PATH_RRA)) { if (mkdir(dirname($data_source_path), 0775, true)) { if (CACTI_SERVER_OS != 'win32' && posix_getuid() == 0) { $success = true; $paths = explode('/', str_replace(CACTI_PATH_RRA, '/', dirname($data_source_path))); $spath = ''; foreach ($paths as $path) { if ($path == '') { continue; } $spath .= '/' . $path; $powner_id = fileowner(CACTI_PATH_RRA . $spath); $pgroup_id = filegroup(CACTI_PATH_RRA . $spath); if ($powner_id !== false && $owner_id !== false && $powner_id != $owner_id) { $success = chown(CACTI_PATH_RRA . $spath, $owner_id); } if ($pgroup_id !== false && $group_id !== false && $pgroup_id != $group_id && $success) { $success = chgrp(CACTI_PATH_RRA . $spath, $group_id); } if (!$success) { cacti_log("ERROR: Unable to set directory permissions for '" . CACTI_PATH_RRA . $spath . "'", false); break; } } } } else { cacti_log("ERROR: Unable to create directory '" . dirname($data_source_path) . "'", false); } } else { cacti_log("WARNING: Poller has not created structured path '" . dirname($data_source_path) . "' yet.", false); } } } if ($show_source == true) { return read_config_option('path_rrdtool') . ' create' . RRD_NL . cacti_escapeshellarg($data_source_path) . $create_ds . $create_rra; } else { $success = rrdtool_execute('create ' . cacti_escapeshellarg($data_source_path) . " $create_ds$create_rra", true, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe, 'POLLER'); if (CACTI_SERVER_OS != 'win32' && posix_getuid() == 0) { if (file_exists($data_source_path)) { chown($data_source_path, (int) $owner_id); chgrp($data_source_path, (int) $group_id); } else { cacti_log('WARNING: RRDCreate failed for ' . rrdtool_command_log_context('create ' . $data_source_path), false, 'POLLER'); } } return $success; } } /** * @param-out array> $successful_updates Per-timestamp acknowledgements. */ function rrdtool_function_update(array $update_cache_array, mixed $rrdtool_pipe = null, array &$successful_updates = []) : int { $track_updates = func_num_args() >= 3; $successful_updates = []; // lets count the number of rrd files processed $rrds_processed = 0; foreach ($update_cache_array as $rrd_path => $rrd_fields) { if (cacti_sizeof($rrd_fields['times'])) { $file_exists = rrdtool_file_exists($rrd_path, $rrdtool_pipe); ksort($rrd_fields['times']); if ($file_exists === false) { rrdtool_function_create($rrd_fields['local_data_id'], false, $rrdtool_pipe); } foreach ($rrd_fields['times'] as $update_time => $field_array) { if (empty($update_time)) { // default the rrdupdate time to now $rrd_update_values = 'N:'; } else { $rrd_update_values = $update_time . ':'; } $rrd_update_template = ''; foreach ($field_array as $field_name => $value) { if ($rrd_update_template != '') { $rrd_update_template .= ':'; $rrd_update_values .= ':'; } $rrd_update_template .= $field_name; // if we have "invalid data", give rrdtool an Unknown (U) if (!isset($value) || !is_numeric($value)) { $value = 'U'; } $rrd_update_values .= $value; } if (cacti_version_compare(get_rrdtool_version(), '1.5', '>=')) { $update_options = '--skip-past-updates'; } else { $update_options = ''; } $update_result = rrdtool_execute('update ' . cacti_escapeshellarg($rrd_path) . " $update_options --template $rrd_update_template $rrd_update_values", true, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe, 'POLLER'); if ($update_result !== false) { $rrds_processed++; if ($track_updates) { $successful_updates[(string) $rrd_path][(int) $update_time] = true; } } else { cacti_log('ERROR: RRDtool update failed for ' . rrdtool_command_log_context('update ' . $rrd_path), false, 'POLLER'); // Do not advance this file past a failed timestamp before it can be retried. break; } } } } return $rrds_processed; } function rrdtool_function_tune(array $rrd_tune_array) : void { global $data_source_types; include(CACTI_PATH_INCLUDE . '/global_arrays.php'); $data_source_name = get_data_source_item_name($rrd_tune_array['data_source_id']); $data_source_type = $data_source_types[$rrd_tune_array['data-source-type']]; $data_source_path = get_data_source_path($rrd_tune_array['data_source_id'], true); $rrd_tune = ''; if ($rrd_tune_array['heartbeat'] != '') { $rrd_tune .= ' --heartbeat ' . cacti_escapeshellarg($data_source_name . ':' . $rrd_tune_array['heartbeat']); } if ($rrd_tune_array['minimum'] != '') { $rrd_tune .= ' --minimum ' . cacti_escapeshellarg($data_source_name . ':' . $rrd_tune_array['minimum']); } if ($rrd_tune_array['maximum'] != '') { $rrd_tune .= ' --maximum ' . cacti_escapeshellarg($data_source_name . ':' . $rrd_tune_array['maximum']); } if ($rrd_tune_array['data-source-type'] != '') { $rrd_tune .= ' --data-source-type ' . cacti_escapeshellarg($data_source_name . ':' . $data_source_type); } if ($rrd_tune_array['data-source-rename'] != '') { $rrd_tune .= ' --data-source-rename ' . cacti_escapeshellarg($data_source_name . ':' . $rrd_tune_array['data-source-rename']); } if ($rrd_tune != '') { if (file_exists($data_source_path) == true) { $result = rrdtool_execute( 'tune ' . cacti_escapeshellarg($data_source_path) . $rrd_tune, false, RRDTOOL_OUTPUT_BOOLEAN, null, 'WEBLOG' ); if ($result === false) { cacti_log('ERROR: Unable to tune ' . rrdtool_command_log_context('tune ' . $data_source_path) . '.', false, 'WEBLOG'); } } } } /** * Convert RRDtool fetch output into Cacti's timestamp-indexed representation. * * RRDtool emits one bucket after the requested end. The graph window is * end-inclusive, so that bucket must not reach percentile and summation users. * * @param string $output Raw RRDtool fetch output * @param int $normalized_end_time Absolute requested end timestamp * @param bool $show_unknown Preserve unknown values as 'U' * * @return array Parsed data source names, timestamps, effective step, and values */ function rrdtool_parse_fetch_output(string $output, int $normalized_end_time, bool $show_unknown = false) : array { /** @var array{data_source_names?: list, timestamp?: array{start_time?: int, end_time?: int, step?: int}, values?: array>} $fetch_array */ $fetch_array = []; $lines = explode("\n", $output); $first = true; $previous_timestamp = null; $last_timestamp = null; $effective_step = 0; foreach ($lines as $line) { $line = trim($line); if ($first) { if ($line == '') { continue; } $names = preg_split('/\s+/', $line); if ($names === false) { return []; } $fetch_array['data_source_names'] = $names; $first = false; continue; } if ($line == '' || !str_contains($line, ':')) { continue; } [$raw_timestamp, $raw_data] = explode(':', $line, 2); if (!is_numeric(trim($raw_timestamp))) { continue; } $timestamp = (int) trim($raw_timestamp); if ($previous_timestamp !== null) { $observed_step = $timestamp - $previous_timestamp; if ($observed_step > 0 && ($effective_step == 0 || $observed_step < $effective_step)) { $effective_step = $observed_step; } } if ($timestamp > $normalized_end_time) { continue; } $previous_timestamp = $timestamp; $last_timestamp = $timestamp; $data = preg_split('/\s+/', trim($raw_data)); if ($data === false) { continue; } if (!isset($fetch_array['timestamp']['start_time'])) { $fetch_array['timestamp']['start_time'] = $timestamp; } foreach ($data as $index => $number) { if (cacti_strtolower($number) == 'nan' || cacti_strtolower($number) == '-nan') { if ($show_unknown) { $fetch_array['values'][$index][$timestamp] = 'U'; } } elseif (is_numeric($number)) { $fetch_array['values'][$index][$timestamp] = $number; } elseif ($show_unknown) { $fetch_array['values'][$index][$timestamp] = 'U'; } } } if ($last_timestamp !== null) { $fetch_array['timestamp']['end_time'] = $last_timestamp; $fetch_array['timestamp']['step'] = $effective_step; } return $fetch_array; } /** * rrdtool_function_fetch - given a data source, return all of its data in an array * * @param int $local_data_id The data source to fetch data for * @param int $start_time The start time to use for the data calculation. this value can * either be absolute (unix timestamp) or relative (to now) * @param int $end_time The end time to use for the data calculation. this value can * either be absolute (unix timestamp) or relative (to now) * @param int $resolution The accuracy of the data measured in seconds * @param bool $show_unknown Show unknown 'NAN' values in the output as 'U' * @param string $rrdtool_file Don't force Cacti to calculate the file * @param string $cf Specify the consolidation function to use * @param mixed $rrdtool_pipe A pipe to an rrdtool command * * @return array An array containing all data in this data source broken down * by each data source item. the maximum of all data source items is included in * an item called 'nth_percentile_maximum'. The array will look as follows: * * $fetch_array['data_source_names'][0] = 'ds1' * $fetch_array['data_source_names'][1] = 'ds2' * $fetch_array['data_source_names'][2] = 'nth_percentile_maximum' * $fetch_array['start_time'] = $timestamp; * $fetch_array['end_time'] = $timestamp; * $fetch_array['values'][$dsindex1][...] = $value; * $fetch_array['values'][$dsindex2][...] = $value; * $fetch_array['values'][$nth_index][...] = $value; * * Again, the 'nth_percentile_maximum' will have the maximum value amongst all the * data sources for each set of data. So, if you have traffic_in and traffic_out, * each member element in the array will have the maximum of traffic_in and traffic_out * in it. */ function rrdtool_function_fetch(int $local_data_id, int $start_time, int $end_time, int $resolution = 0, bool $show_unknown = false, string $rrdtool_file = '', string $cf = 'AVERAGE', mixed $rrdtool_pipe = null) : array { include_once(CACTI_PATH_LIBRARY . '/boost.php'); // validate local data id if (empty($local_data_id) && $rrdtool_file == '') { return []; } $time = time(); $normalized_start_time = $start_time < 0 ? $time + $start_time : $start_time; $normalized_end_time = $end_time < 0 ? $time + $end_time : $end_time; // initialize fetch array $fetch_array = []; // check if we have been passed a file instead of local data source to look up if ($rrdtool_file == '') { $data_source_path = get_data_source_path($local_data_id, true); } else { $data_source_path = $rrdtool_file; } // Find the correct resolution if ($resolution == 0) { $resolution = rrdtool_function_get_resstep($local_data_id, $normalized_start_time, $normalized_end_time, 'res'); } // update the rrdfile if performing a fetch boost_fetch_cache_check($local_data_id, $rrdtool_pipe); // rrdtool consolidation function is a fixed keyword set; never pass it through unchecked if (!in_array($cf, ['AVERAGE', 'MIN', 'MAX', 'LAST'], true)) { $cf = 'AVERAGE'; } // build and run the rrdtool fetch command with all of our data $cmd_line = 'fetch ' . cacti_escapeshellarg($data_source_path) . " $cf -s $normalized_start_time -e $normalized_end_time"; if ($resolution > 0) { $cmd_line .= " -r $resolution"; } $output = rrdtool_execute($cmd_line, false, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe); if (!is_string($output)) { return $fetch_array; } return rrdtool_parse_fetch_output($output, $normalized_end_time, $show_unknown); } function rrd_function_process_graph_options(int $graph_start, int $graph_end, array &$graph, array &$graph_data_array) : string { global $image_types; // define some variables $scale = ''; $rigid = ''; $unit_value = ''; $version = get_rrdtool_version(); $unit_exponent_value = ''; if ($graph['auto_scale'] == 'on') { switch ($graph['auto_scale_opts']) { case '1': // autoscale ignores lower, upper limit $scale = '--alt-autoscale' . RRD_NL; break; case '2': // autoscale-max, accepts a given lower limit $scale = '--alt-autoscale-max' . RRD_NL; if (is_numeric($graph['lower_limit'])) { $scale .= '--lower-limit=' . cacti_escapeshellarg((string) $graph['lower_limit']) . RRD_NL; } break; case '3': // autoscale-min, accepts a given upper limit $scale = '--alt-autoscale-min' . RRD_NL; if (is_numeric($graph['upper_limit'])) { $scale .= '--upper-limit=' . cacti_escapeshellarg((string) $graph['upper_limit']) . RRD_NL; } break; case '4': // auto_scale with limits $scale = '--alt-autoscale' . RRD_NL; if (is_numeric($graph['upper_limit'])) { $scale .= '--upper-limit=' . cacti_escapeshellarg((string) $graph['upper_limit']) . RRD_NL; } if (is_numeric($graph['lower_limit'])) { $scale .= '--lower-limit=' . cacti_escapeshellarg((string) $graph['lower_limit']) . RRD_NL; } break; } } else { if ($graph['upper_limit'] != '') { $scale = '--upper-limit=' . cacti_escapeshellarg($graph['upper_limit']) . RRD_NL; } if ($graph['lower_limit'] != '') { $scale .= '--lower-limit=' . cacti_escapeshellarg($graph['lower_limit']) . RRD_NL; } } if ($graph['auto_scale_log'] == 'on') { $scale .= '--logarithmic' . RRD_NL; } // --units=si only defined for logarithmic y-axis scaling, even if it doesn't hurt on linear graphs if ($graph['scale_log_units'] == 'on' && $graph['auto_scale_log'] == 'on') { $scale .= '--units=si' . RRD_NL; } if ($graph['auto_scale_rigid'] == 'on') { $rigid = '--rigid' . RRD_NL; } if ($graph['unit_value'] != '') { $unit_value = '--y-grid=' . cacti_escapeshellarg($graph['unit_value']) . RRD_NL; } if (preg_match('/^[0-9]+$/', $graph['unit_exponent_value'])) { $unit_exponent_value = '--units-exponent=' . cacti_escapeshellarg($graph['unit_exponent_value']) . RRD_NL; } /* * optionally you can specify and array that overrides some of the db's values, lets set * that all up here */ // override: graph height (in pixels) if (isset($graph_data_array['graph_height'])) { $graph_height = $graph_data_array['graph_height']; } else { $graph_height = $graph['height']; } // override: graph width (in pixels) if (isset($graph_data_array['graph_width'])) { $graph_width = $graph_data_array['graph_width']; } else { $graph_width = $graph['width']; } // override: skip drawing the legend? if (isset($graph_data_array['graph_nolegend'])) { $graph_legend = '--no-legend' . RRD_NL; } else { $graph_legend = ''; } // export options if (isset($graph_data_array['export'])) { $graph_opts = cacti_escapeshellarg((string) $graph_data_array['export_filename']) . RRD_NL; } else { if (empty($graph_data_array['output_filename'])) { $graph_opts = '-' . RRD_NL; } else { $graph_opts = cacti_escapeshellarg((string) $graph_data_array['output_filename']) . RRD_NL; } } if (isset($graph_data_array['image_format']) && $graph_data_array['image_format'] == 'png') { $graph['image_format_id'] = 1; } // basic graph options $graph_opts .= '--imgformat=' . $image_types[$graph['image_format_id']] . RRD_NL . '--start=' . $graph_start . RRD_NL . '--end=' . $graph_end . RRD_NL; $graph_opts .= '--pango-markup ' . RRD_NL; if (read_config_option('rrdtool_watermark') == 'on') { $graph_opts .= '--disable-rrdtool-tag ' . RRD_NL; } // activate --add-jsontime option for graphv only and RRDtool version 1.8.0 and above if (isset($graph_data_array['graphv'])) { $rrdversion = get_rrdtool_version(); if (cacti_version_compare($rrdversion, '1.8', '>=')) { $graph_opts .= '--add-jsontime ' . RRD_NL; } } $graph_opts .= rrd_graph_axis_options($graph, $version); foreach ($graph as $key => $value) { if (is_string($value)) { $value = rrdtool_resolve_graph_text($value, $graph); } switch($key) { case 'title_cache': if (!empty($value)) { $graph_opts .= '--title=' . cacti_escapeshellarg(htmle($value)) . RRD_NL; } break; case 'alt_y_grid': if ($value == CHECKED) { $graph_opts .= '--alt-y-grid' . RRD_NL; } break; case 'unit_value': if (!empty($value)) { $graph_opts .= '--y-grid=' . cacti_escapeshellarg($value) . RRD_NL; } break; case 'unit_exponent_value': if (preg_match('/^[0-9]+$/', $value)) { $graph_opts .= '--units-exponent=' . $value . RRD_NL; } break; case 'height': if (isset($graph_data_array['graph_height']) && preg_match('/^[0-9]+$/', $graph_data_array['graph_height'])) { $graph_opts .= '--height=' . $graph_data_array['graph_height'] . RRD_NL; } else { $graph_opts .= '--height=' . $value . RRD_NL; } break; case 'width': if (isset($graph_data_array['graph_width']) && preg_match('/^[0-9]+$/', $graph_data_array['graph_width'])) { $graph_opts .= '--width=' . $graph_data_array['graph_width'] . RRD_NL; } else { $graph_opts .= '--width=' . $value . RRD_NL; } break; case 'graph_nolegend': if (isset($graph_data_array['graph_nolegend'])) { $graph_opts .= '--no-legend' . RRD_NL; } else { $graph_opts .= ''; } break; case 'base_value': if ($value == 1000 || $value == 1024) { $graph_opts .= '--base=' . $value . RRD_NL; } break; case 'vertical_label': if (!empty($value)) { $graph_opts .= '--vertical-label=' . cacti_escapeshellarg(htmle($value)) . RRD_NL; } break; case 'slope_mode': if ($value == CHECKED) { $graph_opts .= '--slope-mode' . RRD_NL; } break; case 'right_axis': if (!empty($value)) { $graph_opts .= '--right-axis ' . cacti_escapeshellarg($value) . RRD_NL; } break; case 'right_axis_label': if (!empty($value)) { $graph_opts .= '--right-axis-label ' . cacti_escapeshellarg($value) . RRD_NL; } break; case 'right_axis_format': if (!empty($value)) { $format = $graph['right_axis_format_text']; $graph_opts .= '--right-axis-format ' . cacti_escapeshellarg(trim(str_replace('%s', '', $format))) . RRD_NL; } break; case 'left_axis_format': if (!empty($value)) { $format = $graph['left_axis_format_text']; $graph_opts .= '--left-axis-format ' . cacti_escapeshellarg(trim(str_replace('%s', '', $format))) . RRD_NL; } break; case 'no_gridfit': if ($value == CHECKED) { $graph_opts .= '--no-gridfit' . RRD_NL; } break; case 'unit_length': if (!empty($value)) { $graph_opts .= '--units-length ' . cacti_escapeshellarg($value) . RRD_NL; } break; case 'tab_width': if (!empty($value)) { $graph_opts .= '--tabwidth ' . cacti_escapeshellarg($value) . RRD_NL; } break; case 'dynamic_labels': if ($value == CHECKED) { $graph_opts .= '--dynamic-labels' . RRD_NL; } break; case 'force_rules_legend': if ($value == CHECKED) { $graph_opts .= '--force-rules-legend' . RRD_NL; } break; case 'legend_position': if (cacti_version_compare($version, '1.4', '>=')) { if (!empty($value)) { $graph_opts .= '--legend-position ' . cacti_escapeshellarg($value) . RRD_NL; } } break; case 'legend_direction': if (cacti_version_compare($version, '1.4', '>=')) { if (!empty($value)) { $graph_opts .= '--legend-direction ' . cacti_escapeshellarg($value) . RRD_NL; } } break; case 'left_axis_formatter': if (cacti_version_compare($version, '1.4', '>=')) { if (!empty($value)) { $graph_opts .= '--left-axis-formatter ' . cacti_escapeshellarg($value) . RRD_NL; } } break; case 'right_axis_formatter': if (cacti_version_compare($version, '1.4', '>=')) { if (!empty($value)) { $graph_opts .= '--right-axis-formatter ' . cacti_escapeshellarg($value) . RRD_NL; } } break; } } $graph_opts .= "$rigid" . trim("$scale$unit_value$unit_exponent_value$graph_legend", "\n\r " . RRD_NL) . RRD_NL; // add a date to the graph legend $graph_opts .= rrdtool_function_format_graph_date($graph_data_array); // process theme and font styling options $graph_opts .= rrdtool_function_theme_font_options($graph_data_array); /* NOTE: title, vertical-label and right-axis-label perform |query_*| * substitution before cacti_escapeshellarg above, so a device-supplied * value cannot break out of the quoted RRDtool argument. */ $watermark = str_replace("'", '"', read_config_option('graph_watermark')); if ($watermark != '') { $graph_opts .= '--watermark ' . cacti_escapeshellarg($watermark) . RRD_NL; } // if the user desires to hide RRDtools warker, set it if (read_config_option('graph_watermark_rrd') != '') { $graph_opts .= '--disable-rrdtool-tag' . RRD_NL; } return $graph_opts; } /** * Resolve variables in one graph text value before the value is encoded for * RRDtool or a process transport. * * Graph text must never expand monitoring credentials. Apart from leaking * secrets into graph output and debug logs, inserting a replacement after * quoting would allow the replacement to escape its argument boundary. * * @param string $value Unresolved graph text * @param array $graph Graph context * @param array $graph_item Optional graph-item context * @param callable|null $resolver Test seam for the substitution provider * * @return string Resolved graph text with sensitive variables removed */ function rrdtool_resolve_graph_text(string $value, array $graph, array $graph_item = [], ?callable $resolver = null) : string { $value = preg_replace( '/\|host_snmp_(?:community|username|password|priv_passphrase|context|engine_id)\|/i', '', $value ) ?? ''; $resolver ??= 'rrd_substitute_host_query_data'; return (string) $resolver($value, $graph, $graph_item); } /** * Return the established graph failure response for an invalid CDEF. * * @param array $graph_data_array Graph output options * @param int $cdef_id Invalid CDEF identifier * @param int $graph_id Graph identifier * @param callable|null $error_image Test seam for the image renderer * * @return string|false */ function rrdtool_invalid_cdef_response(array $graph_data_array, int $cdef_id, int $graph_id, ?callable $error_image = null) : string|false { $message = __('ERROR: Invalid CDEF %d for graph %d.', $cdef_id, $graph_id); if (isset($graph_data_array['export_csv'])) { return false; } if (isset($graph_data_array['get_error']) || isset($graph_data_array['print_source'])) { return $message; } $error_image ??= 'rrdtool_create_error_image'; return $error_image($message); } function rrdtool_function_graph(int $local_graph_id, mixed $rra_id, array $graph_data_array, mixed $rrdtool_pipe = null, mixed &$xport_meta = [], int $user = 0) : mixed { global $consolidation_functions, $graph_item_types, $encryption; include_once(CACTI_PATH_LIBRARY . '/cdef.php'); include_once(CACTI_PATH_LIBRARY . '/vdef.php'); include_once(CACTI_PATH_LIBRARY . '/graph_variables.php'); include_once(CACTI_PATH_LIBRARY . '/boost.php'); include_once(CACTI_PATH_LIBRARY . '/xml.php'); /* prevent command injection * This function prepares an rrdtool graph statement to be executed by the web server. * We have to take care, that the attacker does not insert shell code. * As some rrdtool parameters accept "Cacti variables", we have to perform the * variable substitution prior to vulnerability checks. * We will enclose all parameters in quotes and substitute quotation marks within * those parameters. */ /* before we do anything; make sure the user has permission to view this graph, if not then get out */ if ($user > 0) { if (!is_graph_allowed($local_graph_id, $user)) { return 'GRAPH ACCESS DENIED'; } } if (getenv('LANG') == '') { putenv('LANG=' . str_replace('-', '_', CACTI_LOCALE) . '.UTF-8'); } /* several callers pass '' when no rra is selected; boost_graph_set_file() expects int|null and throws a TypeError on a string. Only whole numbers are archive ids; anything else falls back to best-fit selection */ $rra_id = (is_int($rra_id) || ctype_digit((string)$rra_id)) ? (int)$rra_id : null; // check the purge the boost poller output cache, and check for a live image file if caching is enabled $graph_data = boost_graph_cache_check($local_graph_id, $rra_id, $rrdtool_pipe, $graph_data_array, false); if ($graph_data !== false) { return $graph_data; } if (empty($graph_data_array['graph_start'])) { $graph_data_array['graph_start'] = -86400; } if (empty($graph_data_array['graph_end'])) { $main_last_run = intval(read_config_option('poller_lastrun_1')); $now_time = time(); $default_delta = intval(read_config_option('poller_interval')) * -1; if ($main_last_run > 0) { $delta_time = $main_last_run - $now_time; // DST time change detected if ($delta_time > 0) { $delta_time = $default_delta; } } else { $delta_time = $default_delta; } $graph_data_array['graph_end'] = $delta_time; } $local_data_ids = array_rekey( db_fetch_assoc_prepared('SELECT dtr.local_data_id FROM graph_templates_item AS gti INNER JOIN data_template_rrd AS dtr ON gti.task_item_id = dtr.id WHERE dtr.local_data_id > 0 AND gti.local_graph_id = ?', [$local_graph_id]), 'local_data_id', 'local_data_id' ); $ds_step = rrdtool_function_get_resstep($local_data_ids, $graph_data_array['graph_start'], $graph_data_array['graph_end'], 'step'); /* if no rra was specified, we need to figure out which one RRDtool will choose using * "best-fit" resolution fit algorithm */ if (empty($rra_id)) { if ($graph_data_array['graph_start'] == 0 || $graph_data_array['graph_end'] == 0) { $rra['rows'] = 600; $rra['steps'] = 1; $rra['timespan'] = 86400; } else { // get a list of RRAs related to this graph $rras = get_associated_rras($local_graph_id); if (cacti_sizeof($rras)) { foreach ($rras as $unchosen_rra) { // the timespan specified in the RRA "timespan" field may not be accurate $real_timespan = ($ds_step * $unchosen_rra['steps'] * $unchosen_rra['rows']); // make sure the current start/end times fit within each RRA's timespan if ($graph_data_array['graph_end'] - $graph_data_array['graph_start'] <= $real_timespan && time() - $graph_data_array['graph_start'] <= $real_timespan) { // is this RRA better than the already chosen one? if (isset($rra) && $unchosen_rra['steps'] < $rra['steps']) { $rra = $unchosen_rra; } elseif (!isset($rra)) { $rra = $unchosen_rra; } } } } if (!isset($rra)) { $rra['rows'] = 600; $rra['steps'] = 1; $rra['timespan'] = 86400; } } } else { $rra = db_fetch_row_prepared('SELECT dspr.rows, dsp.step, dspr.steps FROM data_source_profiles_rra AS dspr INNER JOIN data_source_profiles AS dsp ON dspr.data_source_profile_id=dsp.id WHERE dspr.id = ?', [$rra_id] ); if (is_array($rra) && isset($rra['steps'])) { $rra['timespan'] = $rra['rows'] * $rra['step'] * $rra['steps']; } else { $rra = []; $rra['timespan'] = 86400; $rra['steps'] = 1; $rra['rows'] = 600; } } if (!isset($graph_data_array['export_realtime']) && isset($rra['steps'])) { $rra_seconds = max(1, (int) $ds_step * (int) $rra['steps']); } else { $rra_seconds = 5; } $graph = db_fetch_row_prepared('SELECT gl.id AS local_graph_id, gl.host_id, gl.snmp_query_id, gl.snmp_index, gtg.title_cache, gtg.vertical_label, gtg.vertical_label_angle, gtg.slope_mode, gtg.auto_scale, gtg.auto_scale_opts, gtg.auto_scale_log, gtg.scale_log_units, gtg.auto_scale_rigid, gtg.auto_padding, gtg.base_value, gtg.upper_limit, gtg.lower_limit, gtg.height, gtg.width, gtg.image_format_id, gtg.unit_value, gtg.unit_exponent_value, gtg.alt_y_grid, gtg.right_axis, gtg.right_axis_label, gtg.right_axis_label_angle, gtg.right_axis_range, gtg.right_axis_format, gtg.no_gridfit, gtg.unit_length, gtg.tab_width, gtg.dynamic_labels, gtg.force_rules_legend, gtg.legend_position, gtg.legend_direction, gtg.right_axis_formatter, gtg.left_axis_format, gtg.left_axis_formatter, right_gprint.gprint_text AS right_axis_format_text, left_gprint.gprint_text AS left_axis_format_text FROM graph_templates_graph AS gtg INNER JOIN graph_local AS gl ON gl.id=gtg.local_graph_id LEFT JOIN graph_templates_gprint AS right_gprint ON right_gprint.id = gtg.right_axis_format LEFT JOIN graph_templates_gprint AS left_gprint ON left_gprint.id = gtg.left_axis_format WHERE gtg.local_graph_id = ?', [$local_graph_id] ); // handle the case where the graph has been deleted if (!cacti_sizeof($graph)) { return false; } // lets make that sql query... $graph_items = db_fetch_assoc_prepared('SELECT gti.id AS graph_templates_item_id, gti.cdef_id, gti.vdef_id, gti.text_format, gti.value, gti.hard_return, gti.consolidation_function_id, gti.graph_type_id, gtgp.gprint_text, colors.hex, colors2.hex AS hex2, gti.alpha, gti.alpha2, gti.gradheight, gti.line_width, gti.dashes, gti.shift, gti.dash_offset, gti.textalign, dl.host_id AS data_source_host_id, dl.snmp_query_id, dl.snmp_index, gti.legend, dtr.id AS data_template_rrd_id, dl.id AS local_data_id, dtr.rrd_minimum, dtr.rrd_maximum, dtr.data_source_name, dtr.local_data_template_rrd_id, dtd.rrd_step FROM graph_templates_item AS gti LEFT JOIN data_template_rrd AS dtr ON gti.task_item_id=dtr.id LEFT JOIN data_template_data AS dtd ON dtd.local_data_id = dtr.local_data_id LEFT JOIN data_local AS dl ON dl.id = dtr.local_data_id LEFT JOIN colors ON gti.color_id = colors.id LEFT JOIN colors AS colors2 ON gti.color2_id = colors2.id LEFT JOIN graph_templates_gprint AS gtgp ON gti.gprint_id = gtgp.id WHERE gti.local_graph_id = ? ORDER BY gti.sequence', [$local_graph_id] ); // variables for use below $graph_defs = ''; $txt_graph_items = ''; $pad_number = 0; // override: graph start time if ($graph_data_array['graph_start'] == 0) { $graph_start = -($rra['timespan']); } else { $graph_start = $graph_data_array['graph_start']; } // override: graph end time if ($graph_data_array['graph_end'] == 0) { $graph_end = -($rra_seconds); } else { $graph_end = $graph_data_array['graph_end']; } // +++++++++++++++++++++++ GRAPH OPTIONS +++++++++++++++++++++++ if (!isset($graph_data_array['export_csv'])) { $graph_opts = rrd_function_process_graph_options($graph_start, $graph_end, $graph, $graph_data_array); } else { // basic export options $export_step = max(1, $rra_seconds); $export_rows = (int) ceil(abs($graph_end - $graph_start) / $export_step) + 10; $graph_opts = '--start=' . $graph_start . RRD_NL . '--end=' . $graph_end . RRD_NL . '--maxrows=' . max(10000, $export_rows) . RRD_NL; } // +++++++++++++++++++++++ LEGEND: MAGIC +++++++++++++++++++++++ $realtimeCachePath = read_config_option('realtime_cache_path'); $dateTimeFormat = read_config_option('graph_dateformat'); $cactiLastDate = read_config_option('date'); if (empty($cactiLastDate)) { $cactiLastDate = date('Y-m-d H:i:s'); } $dateTime = date($dateTimeFormat, strtotime($cactiLastDate)); // the following fields will be searched for graph variables $variable_fields = [ 'text_format' => [ 'process_no_legend' => false ], 'value' => [ 'process_no_legend' => true ], 'cdef_cache' => [ 'process_no_legend' => true ], 'vdef_cache' => [ 'process_no_legend' => true ] ]; $i = 0; $j = 0; $nth = 0; $sum = 0; $last_graph_cf = []; $legends = []; $defs = []; $graph_variables = []; $cf_ds_cache = []; $cdef_cache = []; $vdef_cache = []; if (cacti_sizeof($graph_items)) { // we need to add a new column 'cf_reference', so unless PHP 5 is used, this foreach syntax is required foreach ($graph_items as $key => $graph_item) { // mimic the old behavior: LINE[123], AREA and STACK items use the CF specified in the graph item switch ($graph_item['graph_type_id']) { case GRAPH_ITEM_TYPE_LINE1: case GRAPH_ITEM_TYPE_LINE2: case GRAPH_ITEM_TYPE_LINE3: case GRAPH_ITEM_TYPE_LINESTACK: case GRAPH_ITEM_TYPE_TIC: case GRAPH_ITEM_TYPE_AREA: case GRAPH_ITEM_TYPE_STACK: $graph_cf = generate_graph_best_cf($graph_item['local_data_id'], $graph_item['consolidation_function_id']); /* remember the last CF for this data source for use with GPRINT * if e.g. an AREA/AVERAGE and a LINE/MAX is used * we will have AVERAGE first and then MAX, depending on GPRINT sequence */ $last_graph_cf[$graph_item['data_source_name']][$graph_item['data_template_rrd_id']] = $graph_cf; // remember this for second foreach loop $graph_items[$key]['cf_reference'] = $graph_cf; break; case GRAPH_ITEM_TYPE_GPRINT: /* ATTENTION! * the 'CF' given on graph_item edit screen for GPRINT is indeed NOT a real 'CF', * but an aggregation function * see 'man rrdgraph_data' for the correct VDEF based notation * so our task now is to 'guess' the very graph_item, this GPRINT is related to * and to use that graph_item's CF */ if (isset($last_graph_cf[$graph_item['data_source_name']][$graph_item['data_template_rrd_id']])) { $graph_cf = $last_graph_cf[$graph_item['data_source_name']][$graph_item['data_template_rrd_id']]; // remember this for second foreach loop $graph_items[$key]['cf_reference'] = $graph_cf; } else { $graph_cf = generate_graph_best_cf($graph_item['local_data_id'], $graph_item['consolidation_function_id']); // remember this for second foreach loop $graph_items[$key]['cf_reference'] = $graph_cf; } break; case GRAPH_ITEM_TYPE_GPRINT_AVERAGE: case GRAPH_ITEM_TYPE_GPRINT_LAST: case GRAPH_ITEM_TYPE_GPRINT_MAX: case GRAPH_ITEM_TYPE_GPRINT_MIN: $graph_cf = $graph_item['consolidation_function_id']; $graph_items[$key]['cf_reference'] = $graph_cf; break; default: // all other types are based on the best matching CF $graph_cf = generate_graph_best_cf($graph_item['local_data_id'], $graph_item['consolidation_function_id']); // remember this for second foreach loop $graph_items[$key]['cf_reference'] = $graph_cf; break; } $cf_ds_key = "{$graph_item['data_template_rrd_id']}:{$graph_cf}"; if ($graph_item['local_data_id'] > 0 && !isset($cf_ds_cache[$cf_ds_key])) { // use a user-specified ds path if one is entered if (isset($graph_data_array['export_realtime'])) { $data_source_path = $realtimeCachePath . '/user_' . hash('sha256', session_id() ?: '') . '_' . $graph_item['local_data_id'] . '.rrd'; } else { $data_source_path = get_data_source_path($graph_item['local_data_id'], true); } if (!rrdtool_file_exists($data_source_path, $rrdtool_pipe)) { if (read_config_option('log_verbosity') >= POLLER_VERBOSITY_DEBUG || isset($graph_data_array['get_error'])) { cacti_log("WARNING: RRD file '$data_source_path' does not exist", false, 'GRAPH'); } if (isset($graph_data_array['export_csv'])) { return false; } // Both get_error (STDERR output) and print_source (HTML debug view) // are text-output modes; returning PNG bytes in either would produce // garbage in the browser or CLI. Return a human-readable error instead. if (isset($graph_data_array['get_error']) || isset($graph_data_array['print_source'])) { return __('ERROR: RRD file does not exist: %s', $data_source_path); } return rrdtool_create_error_image(__('The Cacti Poller has not run yet.')); } // FOR WIN32: Escape all colon for drive letters (ex. D\:/path/to/rra) $data_source_path = rrdtool_escape_string($data_source_path); if ($data_source_path != '') { /** * NOTE: (Update) Data source DEF names are created using the graph_item_id; then passed * to a function that matches the digits with letters. rrdtool likes letters instead * of numbers in DEF names; especially with CDEFs. CDEFs are created * the same way, except a 'cdef' is put on the beginning of the hash */ $def = generate_graph_def_name(intval($i)); $defs[] = $def; $graph_defs .= "DEF:$def=" . cacti_escapeshellarg($data_source_path) . ':' . cacti_escapeshellarg($graph_item['data_source_name'], true) . ':' . $consolidation_functions[$graph_cf] . ':step=' . max(1, $rra_seconds) . RRD_NL; $cf_ds_cache[$cf_ds_key] = $i; if ($graph_item['legend'] == '') { $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]] = ucwords(str_replace('_', ' ', $graph_item['data_source_name'])) . ' (' . $consolidation_functions[$graph_cf] . ')'; } else { $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]] = $graph_item['legend']; } $i++; } } // cache cdef value here to support data query variables in the cdef string $cdef = empty($graph_item['cdef_id']) ? '' : get_cdef($graph_item['cdef_id']); if ($cdef === null) { cacti_log('ERROR: Invalid CDEF ' . $graph_item['cdef_id'] . ' for graph ' . $local_graph_id . '; graph rendering aborted.', true, 'RRD'); return rrdtool_invalid_cdef_response($graph_data_array, (int) $graph_item['cdef_id'], $local_graph_id); } $graph_item['cdef_cache'] = $cdef; $graph_items[$j]['cdef_cache'] = $cdef; // cache vdef value here if (empty($graph_item['vdef_id'])) { $graph_item['vdef_cache'] = ''; $graph_items[$j]['vdef_cache'] = ''; } else { $vdef = get_vdef($graph_item['vdef_id']); $graph_item['vdef_cache'] = $vdef; $graph_items[$j]['vdef_cache'] = $vdef; } // +++++++++++++++++++++++ LEGEND: TEXT SUBSTITUTION (<>) +++++++++++++++++++++++ // note the current item_id for easy access $graph_item_id = $graph_item['graph_templates_item_id']; /* loop through each field that we want to substitute values for: currently: text format and value */ foreach ($variable_fields as $field_name => $field_array) { // certain fields do not require values when the legend is not to be shown if ($field_array['process_no_legend'] == false && isset($graph_data_array['graph_nolegend'])) { continue; } $graph_variables[$field_name][$graph_item_id] = $graph_item[$field_name]; $search = []; $replace = []; // date/time substitution if (strstr($graph_variables[$field_name][$graph_item_id], '|date_time|')) { $search[] = '|date_time|'; $replace[] = $dateTime; } // data source title substitution if (strstr($graph_variables[$field_name][$graph_item_id], '|data_source_title|')) { $search[] = '|data_source_title|'; $replace[] = get_data_source_title($graph_item['local_data_id']); } // data query variables $graph_variables[$field_name][$graph_item_id] = rrdtool_resolve_graph_text($graph_variables[$field_name][$graph_item_id], $graph, $graph_item); // Nth percentile if (preg_match_all('/\|([0-9]{1,2}):(bits|bytes):(\d):(current|total|max|total_peak|all_max_current|all_max_peak|aggregate_max|aggregate_sum|aggregate_sum_peak|aggregate_current|aggregate_current_peak|aggregate_peak|aggregate):(\d)?\|/', $graph_variables[$field_name][$graph_item_id], $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $search[] = $match[0]; $value = variable_nth_percentile($match, $graph, $graph_item, $graph_items, $graph_start, $graph_end, $rra_seconds); $replace[] = $value; if ($field_name == 'value') { $xport_meta['NthPercentile'][$nth]['format'] = $match[0]; $xport_meta['NthPercentile'][$nth]['value'] = str_replace($match[0], $value, $graph_variables[$field_name][$graph_item_id]); $nth++; } } } // bandwidth summation if (preg_match_all('/\|sum:(\d|auto):(current|total|atomic):(\d):(\d+|auto)\|/', $graph_variables[$field_name][$graph_item_id], $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $search[] = $match[0]; $value = variable_bandwidth_summation($match, $graph, $graph_item, $graph_items, $graph_start, $graph_end, $rra['steps'], $ds_step); $replace[] = $value; if ($field_name == 'text_format') { $xport_meta['Summation'][$sum]['format'] = $match[0]; $xport_meta['Summation'][$sum]['value'] = str_replace($match[0], $value, $graph_variables[$field_name][$graph_item_id]); $sum++; } } } if (cacti_count($search)) { $graph_variables[$field_name][$graph_item_id] = str_replace($search, $replace, $graph_variables[$field_name][$graph_item_id]); } } /* if we are not displaying a legend there is no point in us even processing the auto padding, text format stuff. */ if (!isset($graph_data_array['graph_nolegend'])) { // set hard return variable if selected (\n) if ($graph_item['hard_return'] == 'on') { $hardreturn[$graph_item_id] = '\\n'; } else { $hardreturn[$graph_item_id] = ''; } } $j++; } } // +++++++++++++++++++++++ LEGEND: AUTO PADDING (<>) +++++++++++++++++++++++ if (cacti_sizeof($graph_items)) { /* we need to add a new column 'cf_reference', so unless PHP 5 is used, this foreach * syntax is required */ foreach ($graph_items as $graph_item) { // note the current item_id for easy access $graph_item_id = $graph_item['graph_templates_item_id']; /* if we are not displaying a legend there is no point in us even processing the * auto padding, text format stuff. */ if (!isset($graph_data_array['graph_nolegend'])) { /* PADDING: remember this is not perfect! its main use is for the basic graph setup of: * AREA - GPRINT-CURRENT - GPRINT-AVERAGE - GPRINT-MAXIMUM \n * of course it can be used in other situations, however may not work as intended. * If you have any additions to this small piece of code, feel free to send them to me. */ if ($graph['auto_padding'] == 'on') { // only applies to AREA, STACK and LINEs if (preg_match('/(AREA|STACK|LINE[123])/', $graph_item_types[$graph_item['graph_type_id']])) { $text_format_length = mb_strlen(trim($graph_variables['text_format'][$graph_item_id]), 'UTF-8'); if ($text_format_length > $pad_number) { $pad_number = $text_format_length; } } } } } } // +++++++++++++++++++++++ LEGEND: AUTO PADDING (<>) +++++++++++++++++++++++ // +++++++++++++++++++++++ GRAPH ITEMS: CDEF +++++++++++++++++++++++ $i = 0; // hack for rrdtool 1.2.x support $graph_item_stack_type = ''; $graph_vdefs = []; $stacked_columns = []; if (cacti_sizeof($graph_items)) { foreach ($graph_items as $graph_item) { // Restore the $cf_reference from the first pass of the graph items $graph_cf = $graph_item['cf_reference']; $cf_ds_key = "{$graph_item['data_template_rrd_id']}:{$graph_cf}"; /** * first we need to check if there is a DEF for the current data source/cf combination. if so, * we will use that */ if (isset($cf_ds_cache[$cf_ds_key])) { $cf_id = $graph_item['consolidation_function_id']; } else { /* if there is not a DEF defined for the current data source/cf combination, then we will have to improvise. choose the first available cf in the following order: AVERAGE, MAX, MIN, LAST */ if (isset($cf_ds_cache["{$graph_item['data_template_rrd_id']}:1"])) { $cf_id = 1; // CF: AVERAGE } elseif (isset($cf_ds_cache["{$graph_item['data_template_rrd_id']}:3"])) { $cf_id = 3; // CF: MAX } elseif (isset($cf_ds_cache["{$graph_item['data_template_rrd_id']}:2"])) { $cf_id = 2; // CF: MIN } elseif (isset($cf_ds_cache["{$graph_item['data_template_rrd_id']}:4"])) { $cf_id = 4; // CF: LAST } else { $cf_id = 1; // CF: AVERAGE } $cf_ds_key = "{$graph_item['data_template_rrd_id']}:{$cf_id}"; } // +++++++++++++++++++++++ GRAPH ITEMS: CDEF START +++++++++++++++++++++++ /** * make cdef string here; a note about CDEFs in cacti. A CDEF is neither unique to a * data source of global cdef, but is unique when those two variables combine. */ $cdef_graph_defs = ''; $special_def = 1; $cdef_cache_key = "{$graph_item['cdef_id']}:{$graph_item['data_template_rrd_id']}:{$cf_id}"; if ($graph_item['cdef_id'] > 0 && !isset($cdef_cache[$cdef_cache_key])) { /** @var string $cdef_string */ $cdef_string = $graph_variables['cdef_cache'][$graph_item['graph_templates_item_id']]; $magic_item = []; $already_seen = []; $sources_seen = []; $count_all_ds_dups = 0; $count_all_ds_nodups = 0; $count_similar_ds_dups = 0; $count_similar_ds_nodups = 0; // if any of those magic variables are requested ... if (preg_match('/(ALL_DATA_SOURCES_(NO)?DUPS|SIMILAR_DATA_SOURCES_(NO)?DUPS)/', $cdef_string) || preg_match('/(COUNT_ALL_DS_(NO)?DUPS|COUNT_SIMILAR_DS_(NO)?DUPS)/', $cdef_string)) { // now walk through each case to initialize array if (preg_match('/ALL_DATA_SOURCES_DUPS/', $cdef_string)) { $magic_item['ALL_DATA_SOURCES_DUPS'] = ''; } if (preg_match('/ALL_DATA_SOURCES_NODUPS/', $cdef_string)) { $magic_item['ALL_DATA_SOURCES_NODUPS'] = ''; } if (preg_match('/SIMILAR_DATA_SOURCES_DUPS/', $cdef_string)) { $magic_item['SIMILAR_DATA_SOURCES_DUPS'] = ''; } if (preg_match('/SIMILAR_DATA_SOURCES_NODUPS/', $cdef_string)) { $magic_item['SIMILAR_DATA_SOURCES_NODUPS'] = ''; } if (preg_match('/COUNT_ALL_DS_DUPS/', $cdef_string)) { $magic_item['COUNT_ALL_DS_DUPS'] = ''; } if (preg_match('/COUNT_ALL_DS_NODUPS/', $cdef_string)) { $magic_item['COUNT_ALL_DS_NODUPS'] = ''; } if (preg_match('/COUNT_SIMILAR_DS_DUPS/', $cdef_string)) { $magic_item['COUNT_SIMILAR_DS_DUPS'] = ''; } if (preg_match('/COUNT_SIMILAR_DS_NODUPS/', $cdef_string)) { $magic_item['COUNT_SIMILAR_DS_NODUPS'] = ''; } // loop over all graph items foreach ($graph_items as $gi_check) { // only work on graph items, omit GRPINTs, COMMENTs and stuff if ((preg_match('/(AREA|STACK|LINE[123])/', $graph_item_types[$gi_check['graph_type_id']])) && (!empty($gi_check['data_template_rrd_id']))) { // if the user screws up CF settings, PHP will generate warnings if left unchecked $magic_cf_ds_key = "{$gi_check['data_template_rrd_id']}:{$cf_id}"; // matching consolidation function? if (isset($cf_ds_cache[$magic_cf_ds_key])) { $def_name = generate_graph_def_name($cf_ds_cache[$magic_cf_ds_key]); // do we need ALL_DATA_SOURCES_DUPS? if (isset($magic_item['ALL_DATA_SOURCES_DUPS'])) { $magic_item['ALL_DATA_SOURCES_DUPS'] .= ($count_all_ds_dups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,$def_name,$def_name,UN,0,$def_name,IF,IF"; // convert unknowns to '0' first } // do we need COUNT_ALL_DS_DUPS? if (isset($magic_item['COUNT_ALL_DS_DUPS'])) { $magic_item['COUNT_ALL_DS_DUPS'] .= ($count_all_ds_dups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,1,$def_name,UN,0,1,IF,IF"; // convert unknowns to '0' first } $count_all_ds_dups++; // check if this item also qualifies for NODUPS if (!isset($already_seen[$def_name])) { if (isset($magic_item['ALL_DATA_SOURCES_NODUPS'])) { $magic_item['ALL_DATA_SOURCES_NODUPS'] .= ($count_all_ds_nodups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,$def_name,$def_name,UN,0,$def_name,IF,IF"; // convert unknowns to '0' first } if (isset($magic_item['COUNT_ALL_DS_NODUPS'])) { $magic_item['COUNT_ALL_DS_NODUPS'] .= ($count_all_ds_nodups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,1,$def_name,UN,0,1,IF,IF"; // convert unknowns to '0' first } $count_all_ds_nodups++; $already_seen[$def_name] = true; } // check for SIMILAR data sources if ($graph_item['data_source_name'] == $gi_check['data_source_name']) { // do we need SIMILAR_DATA_SOURCES_DUPS? if (isset($magic_item['SIMILAR_DATA_SOURCES_DUPS']) && ($graph_item['data_source_name'] == $gi_check['data_source_name'])) { $magic_item['SIMILAR_DATA_SOURCES_DUPS'] .= ($count_similar_ds_dups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,$def_name,$def_name,UN,0,$def_name,IF,IF"; // convert unknowns to '0' first } // do we need COUNT_SIMILAR_DS_DUPS? if (isset($magic_item['COUNT_SIMILAR_DS_DUPS']) && ($graph_item['data_source_name'] == $gi_check['data_source_name'])) { $magic_item['COUNT_SIMILAR_DS_DUPS'] .= ($count_similar_ds_dups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,1,$def_name,UN,0,1,IF,IF"; // convert unknowns to '0' first } $count_similar_ds_dups++; // check if this item also qualifies for NODUPS if (!isset($sources_seen[$gi_check['data_template_rrd_id']])) { if (isset($magic_item['SIMILAR_DATA_SOURCES_NODUPS'])) { $magic_item['SIMILAR_DATA_SOURCES_NODUPS'] .= ($count_similar_ds_nodups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,$def_name,$def_name,UN,0,$def_name,IF,IF"; // convert unknowns to '0' first } if (isset($magic_item['COUNT_SIMILAR_DS_NODUPS']) && ($graph_item['data_source_name'] == $gi_check['data_source_name'])) { $magic_item['COUNT_SIMILAR_DS_NODUPS'] .= ($count_similar_ds_nodups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,1,$def_name,UN,0,1,IF,IF"; // convert unknowns to '0' first } $count_similar_ds_nodups++; $sources_seen[$gi_check['data_template_rrd_id']] = true; } } // SIMILAR data sources } // matching consolidation function? } // only work on graph items, omit GRPINTs, COMMENTs and stuff } // loop over all graph items /* if there is only one item to total, don't even bother with the summation. * Otherwise cdef=a,b,c,+,+ is fine. */ if ($count_all_ds_dups > 1 && isset($magic_item['ALL_DATA_SOURCES_DUPS'])) { $magic_item['ALL_DATA_SOURCES_DUPS'] .= str_repeat(',+', ($count_all_ds_dups - 2)) . ',+'; } if ($count_all_ds_nodups > 1 && isset($magic_item['ALL_DATA_SOURCES_NODUPS'])) { $magic_item['ALL_DATA_SOURCES_NODUPS'] .= str_repeat(',+', ($count_all_ds_nodups - 2)) . ',+'; } if ($count_similar_ds_dups > 1 && isset($magic_item['SIMILAR_DATA_SOURCES_DUPS'])) { $magic_item['SIMILAR_DATA_SOURCES_DUPS'] .= str_repeat(',+', ($count_similar_ds_dups - 2)) . ',+'; } if ($count_similar_ds_nodups > 1 && isset($magic_item['SIMILAR_DATA_SOURCES_NODUPS'])) { $magic_item['SIMILAR_DATA_SOURCES_NODUPS'] .= str_repeat(',+', ($count_similar_ds_nodups - 2)) . ',+'; } if ($count_all_ds_dups > 1 && isset($magic_item['COUNT_ALL_DS_DUPS'])) { $magic_item['COUNT_ALL_DS_DUPS'] .= str_repeat(',+', ($count_all_ds_dups - 2)) . ',+'; } if ($count_all_ds_nodups > 1 && isset($magic_item['COUNT_ALL_DS_NODUPS'])) { $magic_item['COUNT_ALL_DS_NODUPS'] .= str_repeat(',+', ($count_all_ds_nodups - 2)) . ',+'; } if ($count_similar_ds_dups > 1 && isset($magic_item['COUNT_SIMILAR_DS_DUPS'])) { $magic_item['COUNT_SIMILAR_DS_DUPS'] .= str_repeat(',+', ($count_similar_ds_dups - 2)) . ',+'; } if ($count_similar_ds_nodups > 1 && isset($magic_item['COUNT_SIMILAR_DS_NODUPS'])) { $magic_item['COUNT_SIMILAR_DS_NODUPS'] .= str_repeat(',+', ($count_similar_ds_nodups - 2)) . ',+'; } } // allow automatic rate calculations on raw gauge data if (isset($graph_item['local_data_id'])) { $polling_interval = $graph_item['rrd_step']; } else { $polling_interval = read_config_option('poller_interval'); } $cdef_string = str_replace('CURRENT_DATA_SOURCE_PI', $polling_interval, $cdef_string); $cdef_string = str_replace('CURRENT_DATA_SOURCE', generate_graph_def_name($cf_ds_cache[$cf_ds_key] ?? 0), $cdef_string); // allow automatic rate calculations on raw gauge data $cdef_string = str_replace('ALL_DATA_SOURCES_DUPS_PI', $polling_interval, $cdef_string); // ALL|SIMILAR_DATA_SOURCES(NO)?DUPS are to be replaced here if (isset($magic_item['ALL_DATA_SOURCES_DUPS'])) { $cdef_string = str_replace('ALL_DATA_SOURCES_DUPS', $magic_item['ALL_DATA_SOURCES_DUPS'], $cdef_string); } // allow automatic rate calculations on raw gauge data $cdef_string = str_replace('ALL_DATA_SOURCES_NODUPS_PI', $polling_interval, $cdef_string); if (isset($magic_item['ALL_DATA_SOURCES_NODUPS'])) { $cdef_string = str_replace('ALL_DATA_SOURCES_NODUPS', $magic_item['ALL_DATA_SOURCES_NODUPS'], $cdef_string); } // allow automatic rate calculations on raw gauge data $cdef_string = str_replace('SIMILAR_DATA_SOURCES_DUPS_PI', $polling_interval, $cdef_string); if (isset($magic_item['SIMILAR_DATA_SOURCES_DUPS'])) { $cdef_string = str_replace('SIMILAR_DATA_SOURCES_DUPS', $magic_item['SIMILAR_DATA_SOURCES_DUPS'], $cdef_string); } $cdef_string = str_replace('SIMILAR_DATA_SOURCES_NODUPS_PI', $polling_interval, $cdef_string); if (isset($magic_item['SIMILAR_DATA_SOURCES_NODUPS'])) { $cdef_string = str_replace('SIMILAR_DATA_SOURCES_NODUPS', $magic_item['SIMILAR_DATA_SOURCES_NODUPS'], $cdef_string); } // COUNT_ALL|SIMILAR_DATA_SOURCES(NO)?DUPS are to be replaced here if (isset($magic_item['COUNT_ALL_DS_DUPS'])) { $cdef_string = str_replace('COUNT_ALL_DS_DUPS', $magic_item['COUNT_ALL_DS_DUPS'], $cdef_string); } if (isset($magic_item['COUNT_ALL_DS_NODUPS'])) { $cdef_string = str_replace('COUNT_ALL_DS_NODUPS', $magic_item['COUNT_ALL_DS_NODUPS'], $cdef_string); } if (isset($magic_item['COUNT_SIMILAR_DS_DUPS'])) { $cdef_string = str_replace('COUNT_SIMILAR_DS_DUPS', $magic_item['COUNT_SIMILAR_DS_DUPS'], $cdef_string); } if (isset($magic_item['COUNT_SIMILAR_DS_NODUPS'])) { $cdef_string = str_replace('COUNT_SIMILAR_DS_NODUPS', $magic_item['COUNT_SIMILAR_DS_NODUPS'], $cdef_string); } // data source item variables $cdef_string = str_replace('CURRENT_DS_MINIMUM_VALUE', (empty($graph_item['rrd_minimum']) ? '0' : $graph_item['rrd_minimum']), $cdef_string); $cdef_string = str_replace('CURRENT_DS_MAXIMUM_VALUE', (empty($graph_item['rrd_maximum']) ? '0' : $graph_item['rrd_maximum']), $cdef_string); $cdef_string = str_replace('CURRENT_GRAPH_MINIMUM_VALUE', (empty($graph['lower_limit']) ? '0' : $graph['lower_limit']), $cdef_string); $cdef_string = str_replace('CURRENT_GRAPH_MAXIMUM_VALUE', (empty($graph['upper_limit']) ? '0' : $graph['upper_limit']), $cdef_string); // special data source types for prediction/trending $def_name = generate_graph_def_name($special_def); if (preg_match('/CURRENT_DS_LSLSLOPE/', $cdef_string)) { $vdef_name = 'vdef' . $def_name . 'slope'; if (!isset($graph_vdefs[$vdef_name])) { $graph_vdefs[$vdef_name] = "$vdef_name=$def_name,LSLSLOPE"; } $cdef_string = str_replace('CURRENT_DS_LSLSLOPE', $vdef_name, $cdef_string); $special_def++; } elseif (preg_match('/CURRENT_DS_LSLINT/', $cdef_string)) { $vdef_name = 'vdef' . $def_name . 'int'; if (!isset($graph_vdefs[$vdef_name])) { $graph_vdefs[$vdef_name] = "$vdef_name=$def_name,LSLINT"; } $cdef_string = str_replace('CURRENT_DS_LSLINT', $vdef_name, $cdef_string); $special_def++; } elseif (preg_match('/CURRENT_DS_LSLCORREL/', $cdef_string)) { $vdef_name = 'vdef' . $def_name . 'correl'; if (!isset($graph_vdefs[$vdef_name])) { $graph_vdefs[$vdef_name] = "$vdef_name=$def_name,LSLCORREL"; } $cdef_string = str_replace('CURRENT_DS_LSLCORREL', $vdef_name, $cdef_string); $special_def++; } elseif (preg_match('/CURRENT_DS_LSLFUNCTION/', $cdef_string)) { $vdefslope_name = 'vdef' . $def_name . 'slope'; $vdefint_name = 'vdef' . $def_name . 'int'; if (!isset($graph_vdefs[$vdefslope_name])) { $graph_vdefs[$vdefslope_name] = "$vdefslope_name=$def_name,LSLSLOPE"; } if (!isset($graph_vdefs[$vdefint_name])) { $graph_vdefs[$vdefint_name] = "$vdefint_name=$def_name,LSLINT"; } $cdef_string = str_replace('CURRENT_DS_LSLFUNCTION', "$def_name,POP,COUNT,$vdefslope_name,*,$vdefint_name,+" , $cdef_string); $special_def++; } // generate the custom prediction vdef string foreach ($graph_vdefs as $vdef_name => $vdef_def) { if ($vdef_def != '') { $graph_defs .= "VDEF:$vdef_def" . RRD_NL; } $graph_vdefs[$vdef_name] = ''; } if ((str_contains($cdef_string, '|query_ifHighSpeed|')) || (str_contains($cdef_string, '|query_ifSpeed|'))) { $local_data = db_fetch_row_prepared('SELECT * FROM data_local WHERE id = ?', [$graph_item['local_data_id']]); $speed = is_array($local_data) ? rrdtool_function_interface_speed($local_data) : '0'; $cdef_string = str_replace(['|query_ifHighSpeed|', '|query_ifSpeed|'], [$speed, $speed], $cdef_string); } // replace query variables in cdefs $cdef_string = rrdtool_resolve_graph_text($cdef_string, $graph, $graph_item); // aggregate graphs can produce an empty RPN expression for GPRINT items // whose consolidation function does not match the data source; skip them // rather than emitting a bare "CDEF:cdefX=" which rrdtool rejects. if ($cdef_string === '') { cacti_log('Empty CDEF string for graph ' . $graph['local_graph_id'] . '; skipping.', true, 'RRD', POLLER_VERBOSITY_DEBUG); continue; } // make the initial 'virtual' cdef name: 'cdef' + [a,b,c,d...] $cdef_graph_defs .= 'CDEF:cdef' . generate_graph_def_name(intval($i)) . '='; // prohibit command injection and provide platform specific quoting $cdef_graph_defs .= cacti_escapeshellarg(sanitize_cdef($cdef_string), true); $cdef_graph_defs .= " \\\n"; // the CDEF cache is so we do not create duplicate CDEF's on a graph $cdef_cache[$cdef_cache_key] = $i; } // add the cdef string to the end of the def string $graph_defs .= $cdef_graph_defs; // +++++++++++++++++++++++ GRAPH ITEMS: CDEFs END +++++++++++++++++++++++ // +++++++++++++++++++++++ GRAPH ITEMS: VDEFs START +++++++++++++++++++++++ $vdef_graph_defs = ''; $vdef_cache_key = "{$graph_item['vdef_id']}:{$graph_item['cdef_id']}:{$graph_item['data_template_rrd_id']}:{$cf_id}"; if ($graph_item['vdef_id'] > 0 && !isset($vdef_cache[$vdef_cache_key])) { $vdef_string = $graph_variables['vdef_cache'][$graph_item['graph_templates_item_id']]; // do we refer to a CDEF within this VDEF? if ($graph_item['cdef_id'] > 0) { // 'calculated' VDEF: use (cached) CDEF as base, only way to get calculations into VDEFs $vdef_string = 'cdef' . str_replace('CURRENT_DATA_SOURCE', generate_graph_def_name($cdef_cache[$cdef_cache_key]), $vdef_string); } else { // 'pure' VDEF: use DEF as base $vdef_string = str_replace('CURRENT_DATA_SOURCE', generate_graph_def_name($cf_ds_cache[$cf_ds_key]), $vdef_string); } // TODO: It would be possible to refer to a CDEF, but that's all. So ALL_DATA_SOURCES_NODUPS and stuff can't be used directly! // $vdef_string = str_replace('ALL_DATA_SOURCES_NODUPS', $magic_item['ALL_DATA_SOURCES_NODUPS'], $vdef_string); // $vdef_string = str_replace('ALL_DATA_SOURCES_DUPS', $magic_item['ALL_DATA_SOURCES_DUPS'], $vdef_string); // $vdef_string = str_replace('SIMILAR_DATA_SOURCES_NODUPS', $magic_item['SIMILAR_DATA_SOURCES_NODUPS'], $vdef_string); // $vdef_string = str_replace('SIMILAR_DATA_SOURCES_DUPS', $magic_item['SIMILAR_DATA_SOURCES_DUPS'], $vdef_string); // make the initial 'virtual' vdef name $vdef_graph_defs .= 'VDEF:vdef' . generate_graph_def_name($i) . '='; $vdef_graph_defs .= cacti_escapeshellarg(sanitize_cdef($vdef_string)); $vdef_graph_defs .= " \\\n"; /* the VDEF cache is so we do not create duplicate VDEFs on a graph, * but take info account, that same VDEF may use different CDEFs * so index over VDEF_ID, CDEF_ID per DATA_TEMPLATE_RRD_ID, lvm */ $vdef_cache[$vdef_cache_key] = $i; } // add the cdef string to the end of the def string $graph_defs .= $vdef_graph_defs; // +++++++++++++++++++++++ GRAPH ITEMS: VDEFs END +++++++++++++++++++++++ // note the current item_id for easy access $graph_item_id = $graph_item['graph_templates_item_id']; /* we put this in a variable so it can be manipulated before mainly used if we want to skip it, like below */ $current_graph_item_type = $graph_item_types[$graph_item['graph_type_id']]; /** * If this graph item has a data source... get a DEF name for it, or the cdef if that applies * to this graph item */ if ($graph_item['cdef_id'] == 0) { if (isset($cf_ds_cache[$cf_ds_key])) { $data_source_name = generate_graph_def_name($cf_ds_cache[$cf_ds_key]); } else { $data_source_name = ''; } } else { $data_source_name = 'cdef' . generate_graph_def_name($cdef_cache[$cdef_cache_key]); } /** * If this graph item has a data source... get a DEF name for it, or the vdef if that applies * to this graph item . */ if ($graph_item['vdef_id'] > 0 && isset($vdef_cache[$vdef_cache_key])) { $data_source_name = 'vdef' . generate_graph_def_name($vdef_cache[$vdef_cache_key]); } // to make things easier... if there is no text format set; set blank text if (!isset($graph_variables['text_format'][$graph_item_id])) { $graph_variables['text_format'][$graph_item_id] = ''; } if (!isset($hardreturn[$graph_item_id])) { $hardreturn[$graph_item_id] = ''; } // +++++++++++++++++++++++ GRAPH ITEMS +++++++++++++++++++++++ /* most of the calculations have been done above. now we have for print everything out in an RRDtool-friendly fashion */ $need_rrd_nl = true; // initialize color support $graph_item_color_code = ''; if (!empty($graph_item['hex'])) { $graph_item_color_code = '#' . $graph_item['hex']; $graph_item_color_code .= rrd_graph_item_alpha($graph_item['alpha']); } $rrdversion = get_rrdtool_version(); if (cacti_version_compare($rrdversion, '1.8', '>=')) { if (!empty($graph_item['hex2']) && ($graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_AREA || $graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_STACK)) { $graph_item_color_code2 = '#' . $graph_item['hex2']; $graph_item_color_code2 .= rrd_graph_item_alpha($graph_item['alpha2']); } else { $graph_item_color_code2 = ''; } } else { $graph_item_color_code2 = ''; } // initialize dash support $dash = ''; if ($graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_LINE1 || $graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_LINE2 || $graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_LINE3 || $graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_LINESTACK || $graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_HRULE || $graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_VRULE) { $dashes = rrd_graph_item_number_list($graph_item['dashes']); if ($dashes !== '') { $dash .= ':dashes=' . $dashes; } $dash_offset = strip_alpha($graph_item['dash_offset']); if (!empty($dash_offset)) { $dash .= ':dash-offset=' . $dash_offset; } } if (!isset($graph_data_array['export_csv'])) { switch($graph_item['graph_type_id']) { case GRAPH_ITEM_TYPE_COMMENT: if (!isset($graph_data_array['graph_nolegend'])) { $comments = []; $comment_arg = rrdtool_resolve_graph_text($graph_variables['text_format'][$graph_item_id], $graph, $graph_item); // Check for a wrapping comment $max = read_config_option('max_title_length') - 20; if (strlen($comment_arg) > $max) { $comments = explode("\n", wordwrap($comment_arg, $max)); } else { $comments[] = $comment_arg; } foreach ($comments as $comment) { // next, compute the argument of the COMMENT statement and perform injection counter measures if (trim($comment) == '') { // an empty COMMENT must be treated with care $comment = cacti_escapeshellarg(' ' . $hardreturn[$graph_item_id]); } else { $comment = cacti_escapeshellarg(rrdtool_escape_string(htmle($comment)) . $hardreturn[$graph_item_id]); } // create rrdtool specific command line $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $comment . ' '; } } break; case GRAPH_ITEM_TYPE_TEXTALIGN: if (!isset($graph_data_array['graph_nolegend'])) { $textalign = rrd_graph_item_textalign($graph_item['textalign']); if ($textalign !== '') { $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $textalign; } } break; case GRAPH_ITEM_TYPE_GPRINT: $text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id]), false); if ($graph_item['vdef_id'] == '0') { $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . ':' . $consolidation_functions[$graph_item['consolidation_function_id']] . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } else { $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } break; case GRAPH_ITEM_TYPE_GPRINT_AVERAGE: if (!isset($graph_data_array['graph_nolegend'])) { $text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])); if ($graph_item['vdef_id'] == '0') { $txt_graph_items .= 'GPRINT:' . $data_source_name . ':AVERAGE:' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } else { $txt_graph_items .= 'GPRINT:' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } } break; case GRAPH_ITEM_TYPE_GPRINT_LAST: if (!isset($graph_data_array['graph_nolegend'])) { $text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])); if ($graph_item['vdef_id'] == '0') { $txt_graph_items .= 'GPRINT:' . $data_source_name . ':LAST:' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } else { $txt_graph_items .= 'GPRINT:' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } } break; case GRAPH_ITEM_TYPE_GPRINT_MAX: if (!isset($graph_data_array['graph_nolegend'])) { $text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])); if ($graph_item['vdef_id'] == '0') { $txt_graph_items .= 'GPRINT:' . $data_source_name . ':MAX:' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } else { $txt_graph_items .= 'GPRINT:' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } } break; case GRAPH_ITEM_TYPE_GPRINT_MIN: if (!isset($graph_data_array['graph_nolegend'])) { $text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])); if ($graph_item['vdef_id'] == '0') { $txt_graph_items .= 'GPRINT:' . $data_source_name . ':MIN:' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } else { $txt_graph_items .= 'GPRINT:' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' '; } } break; case GRAPH_ITEM_TYPE_AREA: if ($graph_variables['text_format'][$graph_item_id] != '') { $text_format = rrdtool_escape_string(htmle(str_pad($graph_variables['text_format'][$graph_item_id], $pad_number))); } elseif (isset($graph_data_array['graph_nolegend'])) { if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) { $text_format = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]]; } else { $text_format = ''; } } else { $text_format = ''; } if (read_config_option('enable_rrdtool_gradient_support') == 'on') { // End color is a 40% (0.4) darkened (negative number) version of the original color $end_color = colourBrightness('#' . $graph_item['hex'], -0.4); $item_alpha = rrd_graph_item_alpha($graph_item['alpha']); $txt_graph_items .= gradient($data_source_name, $graph_item_color_code, $end_color . $item_alpha, cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]), 20, false, $item_alpha); } else { $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . $graph_item_color_code . $graph_item_color_code2 . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]); if ($graph_item_color_code2 != '') { $gradheight = strip_alpha($graph_item['gradheight']); if ($gradheight !== false) { $txt_graph_items .= ':gradheight=' . $gradheight; } } } if ($graph_item['shift'] == CHECKED && abs($graph_item['value']) > 0) { // create a SHIFT statement $shift_value = strip_alpha($graph_item['value']); if ($shift_value !== false) { $txt_graph_items .= RRD_NL . 'SHIFT:' . $data_source_name . ':' . $shift_value; } } $text_format = trim(preg_replace('/[^a-z0-9 _()]/i', '', $text_format)); $xport_meta['legend'][$text_format] = $graph_item_color_code; break; case GRAPH_ITEM_TYPE_STACK: if ($graph_variables['text_format'][$graph_item_id] != '') { $text_format = rrdtool_escape_string(htmle(str_pad($graph_variables['text_format'][$graph_item_id], $pad_number))); } elseif (isset($graph_data_array['graph_nolegend'])) { if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) { $text_format = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]]; } else { $text_format = ''; } } else { $text_format = ''; } $txt_graph_items .= 'AREA:' . $data_source_name . $graph_item_color_code . $graph_item_color_code2 . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]) . ':STACK'; if ($graph_item_color_code2 != '') { $gradheight = strip_alpha($graph_item['gradheight']); if ($gradheight !== false) { $txt_graph_items .= ':gradheight=' . $gradheight; } } if ($graph_item['shift'] == CHECKED && $graph_item['value'] > 0) { // create a SHIFT statement $shift_value = strip_alpha($graph_item['value']); if ($shift_value !== false) { $txt_graph_items .= RRD_NL . 'SHIFT:' . $data_source_name . ':' . $shift_value; } } $text_format = trim(preg_replace('/[^a-z0-9 _()]/i', '', $text_format)); $xport_meta['legend'][$text_format] = $graph_item_color_code; break; case GRAPH_ITEM_TYPE_LINE1: case GRAPH_ITEM_TYPE_LINE2: case GRAPH_ITEM_TYPE_LINE3: if ($graph_variables['text_format'][$graph_item_id] != '') { $text_format = rrdtool_escape_string(htmle(str_pad($graph_variables['text_format'][$graph_item_id], $pad_number))); } elseif (isset($graph_data_array['graph_nolegend'])) { if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) { $text_format = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]]; } else { $text_format = ''; } } else { $text_format = ''; } $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . $graph_item_color_code . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]) . $dash; if ($graph_item['shift'] == CHECKED && $graph_item['value'] > 0) { // create a SHIFT statement $shift_value = strip_alpha($graph_item['value']); if ($shift_value !== false) { $txt_graph_items .= RRD_NL . 'SHIFT:' . $data_source_name . ':' . $shift_value; } } $text_format = trim(preg_replace('/[^a-z0-9 _()]/i', '', $text_format)); $xport_meta['legend'][$text_format] = $graph_item_color_code; break; case GRAPH_ITEM_TYPE_LINESTACK: if ($graph_variables['text_format'][$graph_item_id] != '') { $text_format = rrdtool_escape_string(htmle(str_pad($graph_variables['text_format'][$graph_item_id], $pad_number))); } elseif (isset($graph_data_array['graph_nolegend'])) { if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) { $text_format = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]]; } else { $text_format = ''; } } else { $text_format = ''; } $line_width = strip_alpha($graph_item['line_width']); $txt_graph_items .= 'LINE' . ($line_width === false ? '1' : $line_width) . ':' . $data_source_name . $graph_item_color_code . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]) . ':STACK' . $dash; if ($graph_item['shift'] == CHECKED && $graph_item['value'] > 0) { // create a SHIFT statement $shift_value = strip_alpha($graph_item['value']); if ($shift_value !== false) { $txt_graph_items .= RRD_NL . 'SHIFT:' . $data_source_name . ':' . $shift_value; } } $text_format = trim(preg_replace('/[^a-z0-9 _()]/i', '', $text_format)); $xport_meta['legend'][$text_format] = $graph_item_color_code; break; case GRAPH_ITEM_TYPE_TIC: $tick_fraction = strip_alpha($graph_item['value']); $_fraction = (empty($graph_item['graph_type_id']) || $tick_fraction === false ? '' : (':' . $tick_fraction)); $_legend = ':' . cacti_escapeshellarg(rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])) . $hardreturn[$graph_item_id]); $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . $graph_item_color_code . $_fraction . $_legend; break; case GRAPH_ITEM_TYPE_HRULE: // perform variable substitution; if this does not return a number, rrdtool will FAIL! $substitute = strip_alpha(rrdtool_resolve_graph_text($graph_variables['value'][$graph_item_id], $graph, $graph_item)); $text_format = rrdtool_escape_string(htmle(rrdtool_resolve_graph_text($graph_variables['text_format'][$graph_item_id], $graph, $graph_item))); // don't break rrdtool if the strip_alpha() returns false if ($substitute !== false) { $graph_variables['value'][$graph_item_id] = $substitute; } else { $graph_variables['value'][$graph_item_id] = '0'; } $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $graph_variables['value'][$graph_item_id] . $graph_item_color_code . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]) . '' . $dash; break; case GRAPH_ITEM_TYPE_VRULE: if (substr_count($graph_item['value'], ':')) { $value_array = explode(':', $graph_item['value']); if ($value_array[0] < 0) { $value = date('U') - (-3600 * (int) $value_array[0]) - 60 * (int) $value_array[1]; } else { $mktime = mktime((int) $value_array[0], (int) $value_array[1], 0); $value = date('U', $mktime !== false ? $mktime : 0); } $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $value . $graph_item_color_code . ':' . cacti_escapeshellarg(rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])) . $hardreturn[$graph_item_id]) . $dash; } elseif (is_numeric($graph_item['value'])) { $value = $graph_item['value']; $txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $value . $graph_item_color_code . ':' . cacti_escapeshellarg(rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])) . $hardreturn[$graph_item_id]) . $dash; } break; default: $need_rrd_nl = false; } } else { /* rrdtool xport only accepts DEF/CDEF references; a VDEF backed * item would emit XPORT:vdefNN and fail the whole command */ if ($graph_item['vdef_id'] == 0 && preg_match('/^(AREA|AREA:STACK|LINE[123]|STACK)$/', $graph_item_types[$graph_item['graph_type_id']])) { $legend_name = ''; // give all export items a name if (trim($graph_variables['text_format'][$graph_item_id]) == '') { if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) { $legend_name = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]]; } } else { if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) { $legend_name = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]]; } else { $legend_name = $graph_variables['text_format'][$graph_item_id]; } } if ($legend_name == '') { $legend_name = 'col' . $j . '-' . $data_source_name; } $stacked_columns[$legend_name] = ($graph_item_types[$graph_item['graph_type_id']] == 'STACK') ? 1 : 0; $j++; $txt_graph_items .= 'XPORT:' . cacti_escapeshellarg($data_source_name) . ':' . str_replace(':', '', cacti_escapeshellarg($legend_name)); } else { $need_rrd_nl = false; } } $i++; if (($i < cacti_sizeof($graph_items)) && ($need_rrd_nl)) { $txt_graph_items .= RRD_NL; } } } if (!isset($graph_data_array['export_csv']) || $graph_data_array['export_csv'] != true) { $graph_array = api_plugin_hook_function('rrd_graph_graph_options', ['graph_opts' => $graph_opts, 'graph_defs' => $graph_defs, 'txt_graph_items' => $txt_graph_items, 'graph_id' => $local_graph_id, 'start' => $graph_start, 'end' => $graph_end, 'defs' => $defs]); $graph_array = add_unknown_data($graph_array, $xport_meta); $graph_array = add_business_hours($graph_array, $xport_meta); if (!empty($graph_array)) { $graph_defs = $graph_array['graph_defs']; $txt_graph_items = $graph_array['txt_graph_items']; $graph_opts = $graph_array['graph_opts']; $defs = $graph_array['defs']; } // either print out the source or pass the source onto rrdtool to get us a nice PNG if (isset($graph_data_array['print_source'])) { if (isset($graph_data_array['graphv'])) { $graph = 'graphv'; } else { $graph = 'graph'; } $source_command_line = read_config_option('path_rrdtool') . " $graph " . $graph_opts . $graph_defs . $txt_graph_items; $source_command_line_lengths = strlen(str_replace("\\\n", ' ', $source_command_line)); print '
' . wordwrap(htmle($source_command_line), 160, PHP_EOL, true) . '
'; print '' . 'RRDtool Command lengths = ' . $source_command_line_lengths . ' characters.
'; if (CACTI_SERVER_OS == 'win32' && $source_command_line_lengths > 8191) { print '
' . 'Warning: The Cacti OS is Windows system, RRDtool Command lengths should not exceed 8191 characters.' . '
'; } } else { if (isset($graph_data_array['graphv'])) { $graph = 'graphv'; } else { $graph = 'graph'; } if (isset($graph_data_array['get_error'])) { return rrdtool_execute("graph $graph_opts$graph_defs$txt_graph_items", false, RRDTOOL_OUTPUT_STDERR); } if (isset($graph_data_array['export'])) { rrdtool_execute("graph $graph_opts$graph_defs$txt_graph_items", false, RRDTOOL_OUTPUT_NULL, $rrdtool_pipe); return true; } if (isset($graph_data_array['export_realtime'])) { $output_flag = RRDTOOL_OUTPUT_GRAPH_DATA; $output = rrdtool_execute("graph $graph_opts$graph_defs$txt_graph_items", false, $output_flag, $rrdtool_pipe); if (!is_string($output) || !rrdtool_atomic_write( (string) $graph_data_array['export_realtime'], $output, (string) read_config_option('realtime_cache_path') )) { cacti_log('ERROR: Unable to write realtime graph output safely.', false, 'RRDTOOL'); return false; } return $output; } else { $graph_data_array = boost_prep_graph_array($graph_data_array); if (!isset($graph_data_array['output_flag'])) { $output_flag = RRDTOOL_OUTPUT_GRAPH_DATA; } else { $output_flag = $graph_data_array['output_flag']; } $output = rrdtool_execute("$graph $graph_opts$graph_defs$txt_graph_items", false, $output_flag, $rrdtool_pipe); boost_graph_set_file($output, $local_graph_id, $rra_id); return $output; } } } else { $output_flag = RRDTOOL_OUTPUT_STDOUT; $xport_array = rrdxport2array(rrdtool_execute("xport $graph_opts$graph_defs$txt_graph_items", false, $output_flag, $rrdtool_pipe)); if (!isset($xport_array['meta'])) { cacti_log('WARNING: RRDtool xport returned no valid data for Local Graph ID ' . $local_graph_id, false, 'EXPORT'); return []; } // add host and graph information $xport_array['meta']['stacked_columns'] = $stacked_columns; $xport_array['meta']['title_cache'] = is_array($graph) ? $graph['title_cache'] : ''; $xport_array['meta']['vertical_label'] = is_array($graph) ? $graph['vertical_label'] : ''; $xport_array['meta']['local_graph_id'] = $local_graph_id; $xport_array['meta']['host_id'] = is_array($graph) ? $graph['host_id'] : 0; return $xport_array; } return false; } /** * Atomically write generated RRDtool output beneath an approved directory. * * @param string $path Destination file * @param string $contents Complete generated output * @param string $allowed_root Configured directory that owns the destination * * @return bool Whether the complete file was safely replaced */ function rrdtool_atomic_write(string $path, string $contents, string $allowed_root) : bool { $root = realpath($allowed_root); $parent = realpath(dirname($path)); if ($root === false || $parent === false) { return false; } $rootPrefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if ($parent !== $root && !str_starts_with($parent . DIRECTORY_SEPARATOR, $rootPrefix)) { return false; } $temporary = tempnam($parent, '.cacti-rrd-'); if ($temporary === false) { return false; } try { $written = file_put_contents($temporary, $contents, LOCK_EX); if ($written !== strlen($contents) || !chmod($temporary, 0644)) { return false; } return rename($temporary, $path); } finally { if (is_file($temporary)) { unlink($temporary); } } } function rrdtool_escape_string(string $text, bool $ignore_percent = true) : string { if ($ignore_percent) { return str_replace(['"', ':'], ['\"', '\:'], $text); } else { return str_replace(['"', ':', '%'], ['\"', '\:', '%%'], $text); } } function rrdtool_function_xport(int $local_graph_id, int $rra_id, array $xport_data_array, array &$xport_meta, int $user = 0) : mixed { return rrdtool_function_graph($local_graph_id, $rra_id, $xport_data_array, null, $xport_meta, $user); } function rrdtool_function_format_graph_date(array &$graph_data_array) : string { global $datechar; $graph_legend = ''; // setup date format $date_fmt = read_user_setting('default_date_format',read_config_option('default_date_format')); $dateCharSetting = read_user_setting('default_datechar',read_config_option('default_datechar')); $datecharacter = $datechar[$dateCharSetting]; switch ($date_fmt) { case GD_MO_D_Y: $graph_date = 'm' . $datecharacter . 'd' . $datecharacter . 'Y H:i:s'; break; case GD_MN_D_Y: $graph_date = 'M' . $datecharacter . 'd' . $datecharacter . 'Y H:i:s'; break; case GD_D_MO_Y: $graph_date = 'd' . $datecharacter . 'm' . $datecharacter . 'Y H:i:s'; break; case GD_D_MN_Y: $graph_date = 'd' . $datecharacter . 'M' . $datecharacter . 'Y H:i:s'; break; case GD_Y_MO_D: $graph_date = 'Y' . $datecharacter . 'm' . $datecharacter . 'd H:i:s'; break; case GD_Y_MN_D: $graph_date = 'Y' . $datecharacter . 'M' . $datecharacter . 'd H:i:s'; break; default: $graph_date = 'Y' . $datecharacter . 'M' . $datecharacter . 'd H:i:s'; break; } if (CACTI_WEB) { cacti_time_zone_set(); } // display the timespan for zoomed graphs if ((isset($graph_data_array['graph_start'])) && (isset($graph_data_array['graph_end']))) { if (($graph_data_array['graph_start'] < 0) && ($graph_data_array['graph_end'] < 0)) { $graph_legend = 'COMMENT:"From ' . str_replace(':', '\:', date($graph_date, time() + $graph_data_array['graph_start'])) . ' To ' . str_replace(':', '\:', date($graph_date, time() + $graph_data_array['graph_end'])) . '\\c"' . RRD_NL . 'COMMENT:" \\n"' . RRD_NL; } elseif (($graph_data_array['graph_start'] >= 0) && ($graph_data_array['graph_end'] >= 0)) { $graph_legend = 'COMMENT:"From ' . str_replace(':', '\:', date($graph_date, $graph_data_array['graph_start'])) . ' To ' . str_replace(':', '\:', date($graph_date, $graph_data_array['graph_end'])) . '\\c"' . RRD_NL . 'COMMENT:" \\n"' . RRD_NL; } } if (CACTI_WEB) { cacti_system_zone_set(); } return $graph_legend; } function rrdtool_function_theme_font_options(array &$graph_data_array) : string { global $rrdcolors, $rrdfonts, $rrdborder; // implement theme colors $graph_opts = ''; $themefonts = []; $themecolors = 'rrdcolors'; $themeborder = 'rrdborder'; if (isset($graph_data_array['graph_theme'])) { $theme = cacti_validate_theme((string) $graph_data_array['graph_theme']); } else { $theme = cacti_validate_theme(get_selected_theme()); } $rrdtheme = CACTI_PATH_INCLUDE . '/themes/' . $theme . '/rrdtheme.php'; if (file_exists($rrdtheme) && is_readable($rrdtheme)) { $rrdversion = get_rrdtool_version(); include($rrdtheme); if (isset($_COOKIE['CactiColorMode']) && in_array($_COOKIE['CactiColorMode'], ['dark', 'light', 'dark-dimmed'], true)) { $themecolors = 'rrdcolors_' . $_COOKIE['CactiColorMode']; $themeborder = 'rrdborder_' . $_COOKIE['CactiColorMode']; if (!isset(${$themecolors}) || !is_array(${$themecolors})) { // @phpstan-ignore-line $themecolors = 'rrdcolors'; $themeborder = 'rrdborder'; } } if (isset(${$themecolors}) && is_array(${$themecolors})) { // @phpstan-ignore-line foreach (${$themecolors} as $colortag => $color) { // @phpstan-ignore-line $graph_opts .= '--color ' . cacti_strtoupper($colortag) . '#' . cacti_strtoupper($color) . RRD_NL; } } if (isset(${$themeborder}) && cacti_version_compare($rrdversion, '1.4', '>=')) { $graph_opts .= '--border ' . ${$themeborder} . RRD_NL; // @phpstan-ignore-line } if (isset($rrdfonts)) { $themefonts = $rrdfonts; } } // title fonts $graph_opts .= rrdtool_function_set_font('title', ((!empty($graph_data_array['graph_nolegend'])) ? $graph_data_array['graph_nolegend'] : ''), $themefonts); // axis fonts $graph_opts .= rrdtool_function_set_font('axis', '', $themefonts); // legend fonts $graph_opts .= rrdtool_function_set_font('legend', '', $themefonts); // unit fonts $graph_opts .= rrdtool_function_set_font('unit', '', $themefonts); // watermark fonts if (isset($rrdversion) && cacti_version_compare($rrdversion, '1.3', '>')) { $graph_opts .= rrdtool_function_set_font('watermark', '', $themefonts); } return $graph_opts; } function rrdtool_set_font(string $type, string $no_legend = '', array $themefonts = []) : string { return rrdtool_function_set_font($type, $no_legend, $themefonts); } function rrdtool_function_set_font(string $type, string $no_legend, array $themefonts) : string { if (read_config_option('font_method') == 0) { if (read_user_setting('custom_fonts') == 'on') { $font = read_user_setting($type . '_font'); $size = read_user_setting($type . '_size'); } else { $font = read_config_option($type . '_font'); $size = read_config_option($type . '_size'); } } elseif (isset($themefonts[$type]['font']) && isset($themefonts[$type]['size'])) { $font = $themefonts[$type]['font']; $size = $themefonts[$type]['size']; } else { return ''; } if ($font != '') { /* verifying all possible pango font params is too complex to be tested here * so we only escape the font */ $font = cacti_escapeshellarg($font); } if ($type == 'title') { if (!empty($no_legend)) { $size *= .70; } elseif (($size <= 4) || !is_numeric($size)) { $size = 12; } } elseif (($size <= 4) || !is_numeric($size)) { $size = 8; } return '--font ' . cacti_strtoupper($type) . ':' . floatval($size) . ':' . $font . RRD_NL; } function rrd_substitute_host_query_data(string $txt_graph_item, array $graph, array $graph_item) : string { // replace host variables in graph elements $host_id = 0; if (!preg_match('/(\|query_|\|host_|\|input_|\|poller_|\|site_|\|stream_)/', $txt_graph_item)) { return $txt_graph_item; } if (empty($graph['host_id'])) { // if graph has no associated host determine host_id from graph item data source if (!empty($graph_item['data_source_host_id'])) { $host_id = $graph_item['data_source_host_id']; } elseif (isset($graph_item['local_data_id']) && !empty($graph_item['local_data_id'])) { $host_id = db_fetch_cell_prepared('SELECT host_id FROM data_local WHERE id = ?', [$graph_item['local_data_id']]); } } else { $host_id = $graph['host_id']; } $txt_graph_item = substitute_host_data($txt_graph_item, '|', '|', $host_id); // replace query variables in graph elements if (str_contains($txt_graph_item, '|query_')) { if (isset($graph_item['snmp_query_id'])) { $txt_graph_item = substitute_snmp_query_data($txt_graph_item, $host_id, $graph_item['snmp_query_id'], $graph_item['snmp_index'], 0, true); } elseif (isset($graph['snmp_query_id'])) { $txt_graph_item = substitute_snmp_query_data($txt_graph_item, $host_id, $graph['snmp_query_id'], $graph['snmp_index'], 0, true); } } // replace query variables in graph elements if (str_contains($txt_graph_item, '|input_') && isset($graph_item['local_data_id'])) { return substitute_data_input_data($txt_graph_item, $graph, $graph_item['local_data_id']); } if (str_contains($txt_graph_item, '|poller_') && isset($graph_item['local_data_id'])) { return substitute_poller_data($txt_graph_item, $graph, $graph_item['local_data_id']); } if (str_contains($txt_graph_item, '|site_') && isset($graph_item['local_data_id'])) { return substitute_site_data($txt_graph_item, $graph, $graph_item['local_data_id']); } if (str_contains($txt_graph_item, '|stream_') && isset($graph_item['local_data_id'])) { if (function_exists('stream_substitute_query_data')) { return stream_substitute_query_data($txt_graph_item, $graph, $graph_item['local_data_id']); } } else { return $txt_graph_item; } return ''; } function rrdtool_function_get_resstep(mixed $local_data_ids, int $graph_start, int $graph_end, string $type = 'res') : int { if (!is_array($local_data_ids)) { $local_data_ids = [$local_data_ids]; } $local_data_ids = array_values(array_unique($local_data_ids, SORT_REGULAR)); $time = time(); if ($graph_start < 0) { $graph_start = $time + $graph_start; } if ($graph_end < 0) { $graph_end = $time + $graph_end; } if (!cacti_sizeof($local_data_ids)) { return 0; } $placeholders = implode(', ', array_fill(0, cacti_sizeof($local_data_ids), '?')); $resolutions = db_fetch_assoc_prepared("SELECT dtd.local_data_id, dsp.step, dspr.steps, dspr.rows, dspr.timespan FROM data_source_profiles AS dsp INNER JOIN data_source_profiles_rra AS dspr ON dspr.data_source_profile_id = dsp.id INNER JOIN data_template_data AS dtd ON dtd.data_source_profile_id = dsp.id WHERE dtd.local_data_id IN ($placeholders) ORDER BY dtd.local_data_id, dsp.step, dspr.steps ASC", $local_data_ids); $resolutions_by_data_source = []; foreach ($resolutions as $resolution) { $resolutions_by_data_source[$resolution['local_data_id']][] = $resolution; } foreach ($local_data_ids as $local_data_id) { foreach ($resolutions_by_data_source[$local_data_id] ?? [] as $resolution) { if ($graph_start > ($time - ($resolution['step'] * $resolution['steps'] * $resolution['rows']))) { return $type == 'res' ? $resolution['step'] * $resolution['steps'] : $resolution['step']; } } } return 0; } /** * rrdtool_file_exists - given a data source path check either * the local file system of the rrdtool proxy to see if the * data source path exists. * * @param string $data_source_path The data source rrdfile path * @param mixed $rrdtool_pipe The rrdtool pipe if available * * @return bool A boolean to tell if the file exists */ function rrdtool_file_exists(string $data_source_path, mixed $rrdtool_pipe = null) : bool { if (read_config_option('storage_location')) { if (!rrdtool_execute('file_exists ' . cacti_escapeshellarg($data_source_path), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'POLLER')) { return false; } } elseif (!file_exists($data_source_path)) { return false; } return true; } /** * rrdtool_function_info - given a data source id, return rrdtool info array * * @param int $local_data_id Data source id * @param mixed $rrdtool_pipe The rrdtool pipe if available * * @return mixed An array containing all data from rrdtool info command */ function rrdtool_function_info(int $local_data_id, mixed $rrdtool_pipe = null) : mixed { // Get the path to rrdtool file $data_source_path = get_data_source_path($local_data_id, true); if (!rrdtool_file_exists($data_source_path, $rrdtool_pipe)) { return false; } // Execute rrdtool info command $cmd_line = ' info ' . cacti_escapeshellarg($data_source_path); $output = rrdtool_execute($cmd_line, RRDTOOL_OUTPUT_NULL, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe); if (!is_string($output) || $output == '') { return false; } // Hack for i18n if (str_contains($output, ',')) { $output = str_replace(',', '.', $output); } // Parse the output $matches = []; /** @var array $rrd_info */ $rrd_info = ['rra' => [], 'ds' => []]; $output = explode("\n", $output); foreach ($output as $line) { $line = trim($line); if (preg_match('/^ds\[(\S+)\]\.(\S+) = (\S+)$/', $line, $matches)) { $rrd_info['ds'][$matches[1]][$matches[2]] = trim($matches[3], '"'); } elseif (preg_match('/^rra\[(\S+)\]\.(\S+)\[(\S+)\]\.(\S+) = (\S+)$/', $line, $matches)) { $rrd_info['rra'][$matches[1]][$matches[2]][$matches[3]][$matches[4]] = trim($matches[5], '"'); } elseif (preg_match('/^rra\[(\S+)\]\.(\S+) = (\S+)$/', $line, $matches)) { $rrd_info['rra'][$matches[1]][$matches[2]] = trim($matches[3], '"'); } elseif (preg_match("/^(\S+) = \"(\S+)\"$/", $line, $matches)) { $rrd_info[$matches[1]] = trim($matches[2], '"'); } elseif (preg_match('/^(\S+) = (\S+)$/', $line, $matches)) { $rrd_info[$matches[1]] = trim($matches[2], '"'); } } $output = ''; $matches = []; // Return parsed values return $rrd_info; } function rrdtool_function_info_from_ds(int $data_source_id) : array { global $data_source_types, $consolidation_functions; // construct the skeleton of the info array $info_array['rra'] = []; $info_array['ds'] = []; $info_array['step'] = 0; $info_array['filename'] = clean_up_path(get_data_source_path($data_source_id, true)); // get cacti header information for given data source id $cacti_header_array = db_fetch_row_prepared('SELECT local_data_template_data_id, rrd_step, data_source_profile_id FROM data_template_data WHERE local_data_id = ?', [$data_source_id]); $data_template_id = db_fetch_cell_prepared('SELECT data_template_id FROM data_local WHERE id = ?', [$data_source_id]); // get cacti DS information if ($data_template_id > 0) { $cacti_ds_array = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.data_source_type_id, dtr.rrd_heartbeat, dtr.rrd_maximum, dtr.rrd_minimum FROM data_template_rrd AS dtr INNER JOIN graph_templates_item AS gti ON dtr.id = gti.task_item_id WHERE local_data_id = ?', [$data_source_id]); } else { $cacti_ds_array = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.data_source_type_id, dtr.rrd_heartbeat, dtr.rrd_maximum, dtr.rrd_minimum FROM data_template_rrd AS dtr WHERE local_data_id = ?', [$data_source_id]); } if (cacti_sizeof($cacti_header_array) && cacti_sizeof($cacti_ds_array)) { $info_array['step'] = $cacti_header_array['rrd_step']; // get cacti RRA information $cacti_rra_array = db_fetch_assoc_prepared('SELECT dspc.consolidation_function_id AS cf, dsp.x_files_factor AS xff, dsp.heartbeat, dspr.steps AS steps, dspr.rows AS `rows` FROM data_source_profiles AS dsp INNER JOIN data_source_profiles_cf AS dspc ON dsp.id = dspc.data_source_profile_id INNER JOIN data_source_profiles_rra AS dspr ON dsp.id = dspr.data_source_profile_id WHERE dsp.id = ? ORDER BY dspc.consolidation_function_id, dspr.steps', [$cacti_header_array['data_source_profile_id']]); // get the data source information $data_local = db_fetch_row_prepared('SELECT host_id, snmp_query_id, snmp_index FROM data_local WHERE id = ?', [$data_source_id]); // get the speed just in case we have replacement data $speed = is_array($data_local) ? rrdtool_function_interface_speed($data_local) : '0'; // complete the 'ds' component of the info array foreach ($cacti_ds_array as $index => $ds) { $info_array['ds'][$ds['data_source_name']]['index'] = $index; $info_array['ds'][$ds['data_source_name']]['type'] = $data_source_types[$ds['data_source_type_id']]; $info_array['ds'][$ds['data_source_name']]['min'] = $ds['rrd_minimum']; if ($ds['rrd_maximum'] == '|query_ifSpeed|' || $ds['rrd_maximum'] == '|query_ifHighSpeed|') { $info_array['ds'][$ds['data_source_name']]['max'] = $speed; } else { $info_array['ds'][$ds['data_source_name']]['max'] = $ds['rrd_maximum']; } $info_array['ds'][$ds['data_source_name']]['minimal_heartbeat'] = $ds['rrd_heartbeat']; } // complete the 'rra' component of the info array if (cacti_sizeof($cacti_rra_array)) { foreach ($cacti_rra_array as $index => $rra) { foreach ($cacti_ds_array as $ds) { $info_array['rra'][$index]['cf'] = $consolidation_functions[$rra['cf']]; $info_array['rra'][$index]['rows'] = $rra['rows']; $info_array['rra'][$index]['xff'] = $rra['xff']; $info_array['rra'][$index]['steps'] = $rra['steps']; } } return $info_array; } else { return []; } } else { return []; } } /** * rrdtool_function_contains_cf verifies if the RRDfile contains the 'MAX' consolidation function * * @param int $local_data_id The id of the data source * @param string $cf The consolidation function to search for * @param mixed $rrdtool_pipe The rrdtool pipe if available * * @return bool true or false depending on the result */ function rrdtool_function_contains_cf(int $local_data_id, string $cf, mixed $rrdtool_pipe = null) : bool { $info = rrdtool_function_info($local_data_id, $rrdtool_pipe); if (cacti_sizeof($info)) { if (isset($info['rra'])) { foreach ($info['rra'] as $ds) { if ($ds['cf'] == $cf) { return true; } } } } return false; } /** * rrdtool_tune_ds_add_spec - build an rrdtool 'tune' DS specification used to add a data source * that the Graph requires but that is missing from the RRDfile. A non-numeric minimum or maximum * falls back to 'U', and a 0/0 range becomes 0:U since RRDtool requires the maximum to exceed the * minimum. Adding a data source with 'tune' requires RRDtool 1.5 or above. * * @param string $ds_name The data source name * @param string $type The RRDtool data source type * @param mixed $heartbeat The minimal heartbeat for the data source * @param mixed $min The minimum acceptable value or 'U' * @param mixed $max The maximum acceptable value or 'U' * * @return string The 'DS:name:type:heartbeat:min:max' specification */ function rrdtool_tune_ds_add_spec(string $ds_name, string $type, $heartbeat, $min, $max) : string { $min = is_numeric($min) ? $min : 'U'; $max = is_numeric($max) ? $max : 'U'; if ($min !== 'U' && $max !== 'U' && (float) $min == 0 && (float) $max == 0) { $max = 'U'; } return 'DS:' . $ds_name . ':' . $type . ':' . $heartbeat . ':' . $min . ':' . $max; } /** * rrdtool_cacti_compare_ds - records data source count, add and remove recommendations between the * Cacti definition and the RRDfile into the supplied diff array. Data sources the Graph requires * but the RRDfile lacks are added with 'DS:'; data sources the RRDfile carries but the Graph no * longer requires are removed with 'DEL:'. * * @param array $diff The diff array to populate, passed by reference * @param string $filename The RRDfile path * @param array $cacti_ds The data sources Cacti expects, keyed by name * @param array $file_ds The data sources present in the RRDfile, keyed by name * * @return void */ function rrdtool_cacti_compare_ds(array &$diff, string $filename, array $cacti_ds, array $file_ds) : void { $cacti_count = cacti_sizeof($cacti_ds); $file_count = cacti_sizeof($file_ds); if ($cacti_count > $file_count) { $diff['ds_count'] = __('The Graph requires %d Data Sources but the RRDfile only includes %d. The missing Data Sources will be added.', $cacti_count, $file_count); } elseif ($cacti_count < $file_count) { $diff['ds_count'] = __('The Graph requires %d Data Sources but the RRDfile includes %d. The extra Data Sources will be removed.', $cacti_count, $file_count); } foreach ($cacti_ds as $name => $ds) { if (!isset($file_ds[$name])) { $diff['ds'][$name]['error'] = __("Data Source '%s' is required by the Graph but is missing from the RRDfile and will be added", $name); $diff['tune'][] = cacti_escapeshellarg($filename) . ' ' . cacti_escapeshellarg(rrdtool_tune_ds_add_spec($name, $ds['type'], $ds['minimal_heartbeat'], $ds['min'], $ds['max'])); } } foreach ($file_ds as $name => $ds) { if (!isset($cacti_ds[$name])) { $diff['ds'][$name]['error'] = __("Data Source '%s' is included in the RRDfile but is not required by the Graph and will be removed", $name); $diff['tune'][] = cacti_escapeshellarg($filename) . ' ' . cacti_escapeshellarg('DEL:' . $name); } } } /** * rrdtool_cacti_compare - compares cacti information to rrd file information * * @param int $data_source_id The id of the data source * @param array $info The rrdtool info as an array * * @return array Array build like $info defining html class in case of error */ function rrdtool_cacti_compare(int $data_source_id, array &$info) : array { global $data_source_types, $consolidation_functions; /** * This function will be passed the RRDtool info rrdfile output * formatted as an array with multiple indexes. They include: * * 'ds' => An array of data source items including name, min/max, heartbeat, and type * 'rra' => An array of consolidation functions, rows, xff and other misc data from * the RRDfile. * * The the $cacti_info array includes the same structure, but from the current database * settings, which may differ from the original rrdfile that is on disk. * * Once we have these two pieces of information, we can do a comparison and provide some * guidance as to how to correct the format of the RRDfile. Some of the possible * differences can result in what would effectively be missing data, so care must be taken * when providing these recommendations. */ $cacti_info = rrdtool_function_info_from_ds($data_source_id); // create an empty diff array to hold the differences $diff = []; // initialize some additional variables $cacti_rra_id = -1; $cacti_rra = -1; if ($cacti_info['step'] != $info['step']) { $diff['step'] = __("The required RRDfile step size is '%s' but observed step is '%s'", $cacti_info['step'], $info['step']); } /* ----------------------------------------------------------------------------------- * data source information -----------------------------------------------------------------------------------*/ if (cacti_sizeof($cacti_info) && cacti_sizeof($info)) { // get the heartbeat from the profile $profile_heartbeat = db_fetch_cell_prepared('SELECT heartbeat FROM data_source_profiles AS dsp INNER JOIN data_template_data AS dtd ON dsp.id = dtd.data_source_profile_id WHERE dtd.local_data_id = ?', [$data_source_id]); $file_ds_before = isset($info['ds']) ? $info['ds'] : []; if (isset($cacti_info['ds']) && isset($info['ds'])) { foreach ($cacti_info['ds'] as $data_source_name => $data_source) { /** * This should be a rare case today where the RRDfile does not * have the data source, but Cacti does. */ if (!isset($info['ds'][$data_source_name])) { // cacti knows this ds, but the rrd file does not $info['ds'][$data_source_name]['type'] = $data_source['type']; $info['ds'][$data_source_name]['minimal_heartbeat'] = $data_source['minimal_heartbeat']; $info['ds'][$data_source_name]['min'] = $data_source['min']; $info['ds'][$data_source_name]['max'] = $data_source['max']; $info['ds'][$data_source_name]['seen'] = true; continue; } /** * Accommodate a Cacti bug where the heartbeat was not * propagated. */ if ($data_source['minimal_heartbeat'] != $profile_heartbeat) { cacti_log(sprintf('NOTE: Incorrect Data Source heartbeat found and corrected for Local Data ID %s and Data Source \'%s\'', $data_source_id, $data_source_name), false, 'DSDEBUG'); db_execute_prepared('UPDATE data_template_rrd SET rrd_heartbeat = ? WHERE local_data_id = ? AND data_source_name = ?', [$profile_heartbeat, $data_source_id, $data_source_name]); $cacti_info['ds'][$data_source_name]['minimal_heartbeat'] = $profile_heartbeat; } // verity that all data sources are seen if (isset($info['ds'][$data_source_name])) { $info['ds'][$data_source_name]['seen'] = true; } // check the data source type if ($data_source['type'] != $info['ds'][$data_source_name]['type']) { $diff['ds'][$data_source_name]['type'] = __("Type for Data Source '%s' should be '%s'", $data_source_name, $data_source['type']); $diff['tune'][] = cacti_escapeshellarg($info['filename']) . ' --data-source-type ' . cacti_escapeshellarg($data_source_name . ':' . $data_source['type']); } // check the minimal heartbeat if ($data_source['minimal_heartbeat'] != $info['ds'][$data_source_name]['minimal_heartbeat']) { $diff['ds'][$data_source_name]['minimal_heartbeat'] = __("Heartbeat for Data Source '%s' should be '%s'", $data_source_name, $data_source['minimal_heartbeat']); $diff['tune'][] = cacti_escapeshellarg($info['filename']) . ' --heartbeat ' . cacti_escapeshellarg($data_source_name . ':' . $data_source['minimal_heartbeat']); } // check the minimums and maximums and correct for legacy behavior if ($data_source['min'] != $info['ds'][$data_source_name]['min']) { if (($data_source['min'] == '0' || $data_source['max'] == 'U') && $info['ds'][$data_source_name]['min'] == 'NaN') { $info['ds'][$data_source_name]['min'] = 'U'; } } if ($data_source['min'] != $info['ds'][$data_source_name]['min']) { $diff['ds'][$data_source_name]['min'] = __("RRD minimum for Data Source '%s' should be '%s'", $data_source_name, $data_source['min']); $diff['tune'][] = cacti_escapeshellarg($info['filename']) . ' --minimum ' . cacti_escapeshellarg($data_source_name . ':' . $data_source['min']); } } } rrdtool_cacti_compare_ds($diff, $info['filename'], isset($cacti_info['ds']) ? $cacti_info['ds'] : [], $file_ds_before); if (isset($cacti_info['rra']) && isset($info['rra'])) { $resize = true; // assume a resize operation as long as no rra duplicates are found // scan cacti rra information for duplicates of (CF, STEPS) foreach ($cacti_info['rra'] as $index => $rra) { $cf = $rra['cf']; $steps = $rra['steps']; foreach ($cacti_info['rra'] as $cacti_rra_id => $cacti_rra) { if ($cf == $cacti_rra['cf'] && $steps == $cacti_rra['steps'] && ($index != $cacti_rra_id)) { $diff['rra'][$index]['error'] = __("Cacti RRA '%s' has same CF/steps (%s, %s) as '%s'", $index, $cf, $steps, $cacti_rra_id); $diff['rra'][$cacti_rra_id]['error'] = __("Cacti RRA '%s' has same CF/steps (%s, %s) as '%s'", $cacti_rra_id, $cf, $steps, $index); $resize = false; } } } // scan file rra information for duplicates of (CF, PDP_PER_ROWS) foreach ($info['rra'] as $index => $rra) { $cf = $rra['cf']; $steps = $rra['pdp_per_row']; $i = 0; foreach ($info['rra'] as $file_rra_id => $file_rra) { if (($cf == $file_rra['cf']) && ($steps == $file_rra['pdp_per_row']) && ($index != $file_rra_id)) { $diff['rra'][$i]['error'] = __("File RRA '%s' has same CF/steps (%s, %s) as '%s'", $index, $cf, $steps, $file_rra_id); $diff['rra'][$file_rra_id]['error'] = __("File RRA '%s' has same CF/steps (%s, %s) as '%s'", $file_rra_id, $cf, $steps, $index); $resize = false; } $i++; } } foreach ($cacti_info['rra'] as $cacti_rra_id => $cacti_rra) { /** * find matching rra info from rrd file * do NOT assume, that rra sequence is kept ($cacti_rra_id != $file_rra_id may happen)! * Match is assumed, if CF and STEPS/PDP_PER_ROW match; so go for it */ foreach ($info['rra'] as $file_rra_id => $file_rra) { // in case of mismatch, $file_rra['pdp_per_row'] might not be defined if (!isset($file_rra['pdp_per_row'])) { $file_rra['pdp_per_row'] = 0; } // correct issue with older rrdtools $file_rra['cf'] = trim($file_rra['cf'], '"'); if ($cacti_rra['cf'] == $file_rra['cf'] && $cacti_rra_id == $file_rra_id) { if (isset($info['rra'][$file_rra_id]['seen'])) { continue; } // mark both rra id's as seen to avoid printing them as non-matching $info['rra'][$file_rra_id]['seen'] = true; $cacti_info['rra'][$cacti_rra_id]['seen'] = true; if ($cacti_rra['steps'] != $file_rra['pdp_per_row']) { $diff['rra'][$file_rra_id]['pdp_per_row_error'] = __("The pdp_per_row of '%s' is invalid for RRA '%s' should be '%s'. Consider deleting and allowing Cacti to re-create RRDfile.", $file_rra['pdp_per_row'], $cacti_rra_id, $cacti_rra['steps']); } if ($cacti_rra['xff'] != $file_rra['xff']) { $diff['rra'][$file_rra_id]['xff'] = __("The XFF for Cacti RRA id is '%s' but should be '%s'", $cacti_rra_id, $cacti_rra['xff']); } if ($cacti_rra['rows'] != $file_rra['rows'] && $resize) { $diff['rra'][$file_rra_id]['rows'] = __("The number of rows for Cacti RRA id '%s' is incorrect. The number of rows are '%s' but should be '%s'", $cacti_rra_id, $file_rra['rows'], $cacti_rra['rows']); if ($cacti_rra['rows'] > $file_rra['rows']) { $diff['resize'][] = cacti_escapeshellarg($info['filename']) . ' ' . $cacti_rra_id . ' GROW ' . ($cacti_rra['rows'] - $file_rra['rows']); } else { $diff['resize'][] = cacti_escapeshellarg($info['filename']) . ' ' . $cacti_rra_id . ' SHRINK ' . ($file_rra['rows'] - $cacti_rra['rows']); } } } } } // if cacti knows an rra that has no match, consider this as an error if ($cacti_rra_id >= 0 && $cacti_rra >= 0 && !isset($cacti_info['rra'][$cacti_rra_id]['seen'])) { // add to info array for printing, the index $cacti_rra_id has no real meaning $info['rra']['cacti_' . $cacti_rra_id]['cf'] = $cacti_rra['cf']; $info['rra']['cacti_' . $cacti_rra_id]['steps'] = $cacti_rra['steps']; $info['rra']['cacti_' . $cacti_rra_id]['xff'] = $cacti_rra['xff']; $info['rra']['cacti_' . $cacti_rra_id]['rows'] = $cacti_rra['rows']; $diff['rra']['cacti_' . $cacti_rra_id]['error'] = __("The RRA '%s' missing in the existing Cacti RRDfile", $cacti_rra_id); } } // if the rrd file has an rra that has no cacti match, consider this as an error foreach ($info['rra'] as $file_rra_id => $file_rra) { if (!isset($info['rra'][$file_rra_id]['seen'])) { $diff['rra'][$file_rra_id]['error'] = __("RRA '%s' missing in Cacti definition", $file_rra_id); } } } return $diff; } /** * rrdtool_info2html - take output from rrdtool info array and build html table * * @param array $info_array An array of rrdtool info data * @param array $diff An array of differences between definition and current rrd file settings * * @return void */ function rrdtool_info2html(array $info_array, array $diff = []) : void { include_once(CACTI_PATH_LIBRARY . '/time.php'); html_start_box(__('RRD File Information'), '100%', false, 3, 'center', ''); // header data $header_items = [ ['display' =>__('Header'), 'align' => 'left'], ['display' => '', 'align' => 'left'] ]; html_header($header_items, 1); // add human readable timestamp if (isset($info_array['last_update'])) { $info_array['last_update'] .= ' [' . date(CACTI_DATE_TIME_FORMAT, $info_array['last_update']) . ']'; } $loop = [ 'filename' => $info_array['filename'], 'rrd_version' => $info_array['rrd_version'], 'step' => $info_array['step'], 'last_update' => $info_array['last_update']]; foreach ($loop as $key => $value) { form_alternate_row($key, true); form_selectable_cell($key, 'key'); form_selectable_cell($value, 'value', '', ((isset($diff[$key]) ? 'color:red' : ''))); form_end_row(); } html_end_box(); // data sources $header_items = [ ['display' => __('Data Source Items'), 'align' => 'left'], ['display' => __('Type'), 'align' => 'left'], ['display' => __('Minimal Heartbeat'), 'align' => 'right'], ['display' => __('Min'), 'align' => 'right'], ['display' => __('Max'), 'align' => 'right'], ['display' => __('Last DS'), 'align' => 'right'], ['display' => __('Value'), 'align' => 'right'], ['display' => __('Unknown Sec'), 'align' => 'right'] ]; html_start_box('', '100%', false, 3, 'center', ''); html_header($header_items, 1); if (cacti_sizeof($info_array['ds'])) { foreach ($info_array['ds'] as $key => $value) { form_alternate_row('line' . $key, true); form_selectable_cell($key, 'name', '', (isset($diff['ds'][$key]['error']) ? 'color:red' : '')); form_selectable_cell(($value['type'] ?? ''), 'type', '', (isset($diff['ds'][$key]['type']) ? 'color:red' : '')); form_selectable_cell(($value['minimal_heartbeat'] ?? ''), 'minimal_heartbeat', '', (isset($diff['ds'][$key]['minimal_heartbeat']) ? 'color:red, text-align:right' : 'text-align:right')); if (isset($value['min'])) { if ($value['min'] == 'U') { form_selectable_cell($value['min'], 'min', '', 'right'); } elseif (is_numeric($value['min'])) { form_selectable_cell(number_format_i18n($value['min']), 'min', '', 'right'); } else { form_selectable_cell($value['min'], 'min', '', 'color:red;text-align:right'); } } else { form_selectable_cell(__('Unknown'), 'min', '', 'color:red;text-align:right'); } if (isset($value['max'])) { if ($value['max'] == 'U' || $value['max'] == 'NaN') { form_selectable_cell($value['max'], 'max', '', 'right'); } elseif (is_numeric($value['max'])) { form_selectable_cell(number_format_i18n($value['max']), 'max', '', 'right'); } else { form_selectable_cell($value['max'], 'max', '', 'color:red;text-align:right'); } } else { form_selectable_cell(__('Unknown'), 'max', '', 'color:red;text-align:right'); } form_selectable_cell(((isset($value['last_ds']) && is_numeric($value['last_ds']) ? number_format_i18n($value['last_ds']) : ($value['last_ds'] ?? ''))), 'last_ds', '', 'text-align:right'); form_selectable_cell(((isset($value['value']) ? (is_numeric($value['value']) ? number_format_i18n($value['value']) : $value['value']) : '')), 'value', '', 'text-align:right'); form_selectable_cell(((isset($value['unknown_sec']) && is_numeric($value['unknown_sec']) ? number_format_i18n($value['unknown_sec']) : ($value['unknown_sec'] ?? ''))), 'unknown_sec', '', 'text-align:right'); form_end_row(); } } html_end_box(); // round robin archive $header_items = [ ['display' => __('Round Robin Archive'), 'align' => 'left'], ['display' => __('Consolidation Function'), 'align' => 'left'], ['display' => __('Rows'), 'align' => 'right'], ['display' => __('Cur Row'), 'align' => 'right'], ['display' => __('PDP per Row'), 'align' => 'right'], ['display' => __('X-Files Factor'), 'align' => 'right'], ['display' => __('CDP Prep Value (0)'), 'align' => 'right'], ['display' => __('CDP Unknown Data points (0)'), 'align' => 'right'] ]; html_start_box('', '100%', false, 3, 'center', ''); html_header($header_items, 1); if (cacti_sizeof($info_array['rra'])) { foreach ($info_array['rra'] as $key => $value) { form_alternate_row('line_' . $key, true); form_selectable_cell($key, 'name', '', (isset($diff['rra'][$key]['error']) ? 'color:red' : '')); form_selectable_cell(($value['cf'] ?? ''), 'cf'); form_selectable_cell(($value['rows'] ?? ''), 'rows', '', (isset($diff['rra'][$key]['rows']) ? 'color:red;text-align:right' : 'text-align:right')); form_selectable_cell(($value['cur_row'] ?? ''), 'cur_row', '', 'text-align:right'); form_selectable_cell(($value['pdp_per_row'] ?? ''), 'pdp_per_row', '', 'text-align:right'); form_selectable_cell((isset($value['xff']) ? floatval($value['xff']) : ''), 'xff', '', (isset($diff['rra'][$key]['xff']) ? 'color:red;text-align:right' : 'text-align:right')); form_selectable_cell((isset($value['cdp_prep'][0]['value']) ? ((cacti_strtolower($value['cdp_prep'][0]['value']) == 'nan') ? $value['cdp_prep'][0]['value'] : floatval($value['cdp_prep'][0]['value'])) : ''), 'value', '', 'text-align:right'); form_selectable_cell(($value['cdp_prep'][0]['unknown_datapoints'] ?? ''), 'unknown_datapoints', '', 'text-align:right'); form_end_row(); } } html_end_box(); } /** * rrdtool_tune_print_leaves - a helper used by rrdtool_tune_print_cli() and * rrdtool_tune_print_html() to recursively output the leaf nodes of an rrdtool * 'diff' array * * @param array $array The array of leaves * @param bool $html Emit div rows for the web UI, or plain text for the CLI * * @return void */ function rrdtool_tune_print_leaves(array $array, bool $html = true) : void { foreach ($array as $key => $line) { if (!is_array($line)) { if ($html) { print "
" . $line . '
'; } else { print $line . PHP_EOL; } } else { if ($key === 'tune' || $key === 'resize') { continue; } rrdtool_tune_print_leaves($line, $html); } } } /** * rrdtool_tune_execute - apply the rrdtool tune/resize commands for a repair * * @param string $rrd_file The rrd file name * @param array $diff The array of discrepancies between cacti settings and rrd file info * @param callable|null $executor Test seam for RRDtool execution * * @return void */ function rrdtool_tune_execute(string $rrd_file, array $diff, ?callable $executor = null) : void { $executor ??= 'rrdtool_execute'; if (isset($diff['tune']) && cacti_sizeof($diff['tune'])) { foreach ($diff['tune'] as $line) { $executor("tune $line", true, RRDTOOL_OUTPUT_STDOUT); } } if (isset($diff['resize']) && cacti_sizeof($diff['resize'])) { foreach ($diff['resize'] as $line) { $executor("resize $line", true, RRDTOOL_OUTPUT_STDOUT); rename(dirname($rrd_file) . '/resize.rrd', $rrd_file); } } } /** * rrdtool_tune_print_cli - print the rrdtool tune/resize source commands as plain text * * @param string $rrd_file The rrd file name * @param array $diff The array of discrepancies between cacti settings and rrd file info * * @return void */ function rrdtool_tune_print_cli(string $rrd_file, array $diff) : void { $rrd_path = read_config_option('path_rrdtool'); if (cacti_sizeof($diff)) { rrdtool_tune_print_leaves($diff, false); } if (isset($diff['tune']) && cacti_sizeof($diff['tune'])) { foreach ($diff['tune'] as $line) { print $rrd_path . ' tune ' . $line . PHP_EOL; } } if (isset($diff['resize']) && cacti_sizeof($diff['resize'])) { foreach ($diff['resize'] as $line) { print $rrd_path . ' resize ' . $line . PHP_EOL; print __('rename %s to %s', dirname($rrd_file) . '/resize.rrd', $rrd_file) . PHP_EOL; } } } /** * rrdtool_tune_print_html - render the rrdtool tune/resize output as balanced div boxes * * @param string $rrd_file The rrd file name * @param array $diff The array of discrepancies between cacti settings and rrd file info * * @return void */ function rrdtool_tune_print_html(string $rrd_file, array $diff) : void { $rrd_path = read_config_option('path_rrdtool'); if (cacti_sizeof($diff)) { html_start_box(__('Errors Found'), '100%', true, 3, 'center', ''); rrdtool_tune_print_leaves($diff, true); html_end_box(true, true); } if ((isset($diff['tune']) && cacti_sizeof($diff['tune'])) || (isset($diff['resize']) && cacti_sizeof($diff['resize']))) { html_start_box(__('Optional Tuning Recommendations'), '100%', true, 3, 'center', ''); if (isset($diff['tune'])) { foreach ($diff['tune'] as $line) { print "
" . $rrd_path . ' tune ' . $line . '
'; } } if (isset($diff['resize'])) { foreach ($diff['resize'] as $line) { print "
" . $rrd_path . ' resize ' . $line . '
'; print "
" . __('rename %s to %s', dirname($rrd_file) . '/resize.rrd', $rrd_file) . '
'; } } html_end_box(true, true); } } /** * rrdtool_tune - create rrdtool tune/resize commands html+cli enabled * * @param string $rrd_file The rrd file name * @param array $diff The array of discrepancies between cacti settings and rrd file info * @param bool $show_source Only show text+commands, or execute all commands; execute is for cli mode only! * * @return void */ function rrdtool_tune(string $rrd_file, array $diff, bool $show_source = true) : void { if (!$show_source) { rrdtool_tune_execute($rrd_file, $diff); } elseif (CACTI_CLI) { rrdtool_tune_print_cli($rrd_file, $diff); } else { rrdtool_tune_print_html($rrd_file, $diff); } } /** * rrd_check - Given a data source id, check the rrdtool file to the data source definition * * @param int $data_source_id The data source id * * @return void An array containing issues with the rrdtool file definition vs data source */ function rrd_check(int $data_source_id) : void { global $rrd_tune_array, $data_source_types; $data_source_name = get_data_source_item_name($rrd_tune_array['data_source_id']); $data_source_type = $data_source_types[$rrd_tune_array['data-source-type']]; $data_source_path = get_data_source_path($rrd_tune_array['data_source_id'], true); } /** * rrd_repair - Given a data source id, update the rrdtool file to match the data source definition * * @param int $data_source_id The data source id * * @return void */ function rrd_repair(int $data_source_id) : void { global $rrd_tune_array, $data_source_types; $data_source_name = get_data_source_item_name($rrd_tune_array['data_source_id']); $data_source_type = $data_source_types[$rrd_tune_array['data-source-type']]; $data_source_path = get_data_source_path($rrd_tune_array['data_source_id'], true); } /** * Load an RRDtool dump as XML without network/entity access or PHP warnings. * * @param string $file Existing RRD file * @param mixed $pipe Optional RRDtool pipe * @param callable|null $executor Test seam for RRDtool execution * * @return DOMDocument|false Parsed dump, or false when execution/parsing fails */ function rrdtool_dump_document(string $file, mixed $pipe = null, ?callable $executor = null) : DOMDocument|false { $executor ??= 'rrdtool_execute'; $output = $executor( 'dump ' . cacti_escapeshellarg($file), false, RRDTOOL_OUTPUT_STDOUT, $pipe, 'UTIL' ); if (!is_string($output) || trim($output) === '') { return false; } $dom = new DOMDocument(); $previous = libxml_use_internal_errors(true); try { return $dom->loadXML($output, LIBXML_NONET) ? $dom : false; } finally { libxml_clear_errors(); libxml_use_internal_errors($previous); } } /** * Restore a modified RRD XML document to a validated replacement file. * * The original RRD remains untouched until RRDtool has produced a non-empty * replacement in a private sibling directory. The final rename is atomic on * supported local filesystems, and every temporary artifact is removed on * both success and failure. * * @param DOMDocument $dom Modified RRDtool XML document * @param string $file Existing RRD file to replace * @param mixed $pipe Optional RRDtool pipe * @param callable|null $executor Test seam for RRDtool execution * @param callable|null $renamer Test seam for the final atomic rename * * @return bool Whether a validated replacement was installed */ function rrdtool_restore_document(DOMDocument $dom, string $file, mixed $pipe = null, ?callable $executor = null, ?callable $renamer = null) : bool { $directory = realpath(dirname($file)); $metadata = @stat($file); if ($directory === false || !is_array($metadata) || !is_writable($file) || !is_writable($directory)) { return false; } $placeholder = tempnam($directory, '.cacti-rrd-restore-'); if ($placeholder === false || !unlink($placeholder) || !mkdir($placeholder, 0700)) { return false; } $xml_file = $placeholder . DIRECTORY_SEPARATOR . 'source.xml'; $new_file = $placeholder . DIRECTORY_SEPARATOR . 'replacement.rrd'; $executor ??= 'rrdtool_execute'; $renamer ??= 'rename'; try { $bytes = $dom->save($xml_file); if ($bytes === false || $bytes < 1 || !chmod($xml_file, 0600)) { return false; } $result = $executor( 'restore -f ' . cacti_escapeshellarg($xml_file) . ' ' . cacti_escapeshellarg($new_file), false, RRDTOOL_OUTPUT_STDOUT, $pipe, 'UTIL' ); if ($result === false || !is_file($new_file) || filesize($new_file) < 1) { return false; } if (!chmod($new_file, $metadata['mode'] & 0777)) { return false; } if (function_exists('posix_geteuid') && posix_geteuid() === 0) { if (!chown($new_file, $metadata['uid']) || !chgrp($new_file, $metadata['gid'])) { return false; } } return $renamer($new_file, $file) === true; } finally { if (is_file($xml_file)) { unlink($xml_file); } if (is_file($new_file)) { unlink($new_file); } if (is_dir($placeholder)) { rmdir($placeholder); } } } /** * rrd_datasource_add - add a (list of) datasource(s) to an (array of) rrd file(s) * * @param array $file_array The array of rrd files * @param array $ds_array The array of datasource parameters * @param bool $debug Debug mode * * @return mixed Success or error message */ function rrd_datasource_add(array $file_array, array $ds_array, bool $debug) : mixed { global $data_source_types, $consolidation_functions; $rrdtool_pipe = rrd_init(); try { // iterate all given rrd files foreach ($file_array as $file) { $dom = rrdtool_dump_document($file, $rrdtool_pipe); if (!$dom instanceof DOMDocument) { $check['err_msg'] = __('Error while parsing the XML of rrdtool dump'); return $check; } /* rrdtool dump depends on rrd file version: * version 0001 => RRDtool 1.0.x * version 0003 => RRDtool 1.2.x, 1.3.x, 1.4.x, 1.5.x, 1.6.x */ $version_node = $dom->getElementsByTagName('version')->item(0); $version = $version_node !== null ? trim((string) $version_node->nodeValue) : RRD_FILE_VERSION3; // now start XML processing foreach ($ds_array as $ds) { // first, append the structure in the rrd header if ($ds['type'] === $data_source_types[5]) { rrd_append_compute_ds($dom, $version, $ds['name'], $ds['type'], $ds['cdef']); } else { rrd_append_ds($dom, $version, $ds['name'], $ds['type'], $ds['heartbeat'], $ds['min'], $ds['max']); } // now work on the structure as part of the tree rrd_append_cdp_prep_ds($dom, $version); // add alues to the tree rrd_append_value($dom); } if ($debug) { print $dom->saveXML(); } elseif (!rrdtool_restore_document($dom, $file, $rrdtool_pipe)) { $check['err_msg'] = __('ERROR: Unable to install validated RRDfile replacement for %s', $file); return $check; } else { cacti_log('Added Data Source(s) to RRDfile: ' . $file, false, 'UTIL'); } } } finally { rrd_close($rrdtool_pipe); } return true; } /** * rrd_rra_delete - delete a (list of) rra(s) from an (array of) rrd file(s) * * @param array $file_array An array of rrd files * @param array $rra_array An array of rra parameters * @param bool $debug Debug mode * * @return mixed True for success (bool) or error message (array) */ function rrd_rra_delete(array $file_array, array $rra_array, bool $debug) : mixed { $rrdtool_pipe = rrd_init(); try { // iterate all given rrd files foreach ($file_array as $file) { $dom = rrdtool_dump_document($file, $rrdtool_pipe); if (!$dom instanceof DOMDocument) { $check['err_msg'] = __('Error while parsing the XML of RRDtool dump'); return $check; } // now start XML processing foreach ($rra_array as $rra) { rrd_delete_rra($dom, $rra); } if ($debug) { print $dom->saveXML(); } elseif (!rrdtool_restore_document($dom, $file, $rrdtool_pipe)) { $check['err_msg'] = __('ERROR: Unable to install validated RRDfile replacement for %s', $file); return $check; } else { cacti_log('Deleted RRA(s) from RRDfile: ' . $file, false, 'UTIL'); } } } finally { rrd_close($rrdtool_pipe); } return true; } /** * rrd_rra_clone - clone a (list of) rra(s) from an (array of) rrd file(s) * * @param array $file_array And array of rrd files * @param string $cf The new consolidation function * @param array $rra_array An array of rra parameters * @param bool $debug Debug mode * * @return mixed Success (bool) or error message (array) */ function rrd_rra_clone(array $file_array, string $cf, array $rra_array, bool $debug) : mixed { $rrdtool_pipe = rrd_init(); try { // iterate all given rrd files foreach ($file_array as $file) { $dom = rrdtool_dump_document($file, $rrdtool_pipe); if (!$dom instanceof DOMDocument) { $check['err_msg'] = __('Error while parsing the XML of RRDtool dump'); return $check; } // now start XML processing foreach ($rra_array as $rra) { rrd_copy_rra($dom, $cf, $rra); } if ($debug) { print $dom->saveXML(); } elseif (!rrdtool_restore_document($dom, $file, $rrdtool_pipe)) { $check['err_msg'] = __('ERROR: Unable to install validated RRDfile replacement for %s', $file); return $check; } else { cacti_log('Cloned RRA(s) in RRDfile: ' . $file, false, 'UTIL'); } } } finally { rrd_close($rrdtool_pipe); } return true; } /** * rrd_append_ds - appends a subtree to an RRD XML structure * * @param object $dom The DOM object, where the RRD XML is stored * @param string $version The rrd file version * @param string $name Name of the new ds * @param string $type Type of the new ds * @param int $min_hb Heartbeat of the new ds * @param string $min Min value of the new ds or [NaN|U] * @param string $max Max value of the new ds or [NaN|U] * * @return object Modified DOM */ function rrd_append_ds(object $dom, string $version, string $name, string $type, int $min_hb, string $min, string $max) : object { // rrdtool version dependencies if ($version === RRD_FILE_VERSION1) { $last_ds = 'U'; } elseif ($version === RRD_FILE_VERSION3) { $last_ds = 'UNKN'; } else { $last_ds = 'UNKN'; } // create subtree $new_dom = new DOMDocument; // pretty print $new_dom->formatOutput = true; // this defines the new node structure $new_dom->loadXML(" $name $type $min_hb $min $max $last_ds 0.0000000000e+00 0 "); // create a node element from new document $new_node = $new_dom->getElementsByTagName('ds')->item(0); // echo $new_dom->saveXML(); # print new node // get XPATH notation required for positioning // $xpath = new DOMXPath($dom); /* get XPATH for entry where new node will be inserted * which is the entry */ // $insert = $xpath->query('/rrd/rra')->item(0); $insert = $dom->getElementsByTagName('rra')->item(0); if ($insert !== null && $new_node !== null) { // import the new node $new_node = $dom->importNode($new_node, true); // and insert it at the correct place $insert->parentNode->insertBefore($new_node, $insert); } return $dom; } /** * rrd_append_compute_ds - COMPUTE DS: appends a subtree to an RRD XML structure * * @param object $dom The DOM object, where the RRD XML is stored * @param string $version The rrd file version * @param string $name Name of the new ds * @param string $type Type of the new ds * @param int $cdef The cdef rpn used for COMPUTE * * @return object Modified DOM */ function rrd_append_compute_ds(object $dom, string $version, string $name, string $type, int $cdef) : object { // rrdtool version dependencies if ($version === RRD_FILE_VERSION1) { $last_ds = 'U'; } elseif ($version === RRD_FILE_VERSION3) { $last_ds = 'UNKN'; } else { $last_ds = 'UNKN'; } // create subtree $new_dom = new DOMDocument; // pretty print $new_dom->formatOutput = true; // this defines the new node structure $new_dom->loadXML(" $name $type $cdef $last_ds 0.0000000000e+00 0 "); // create a node element from new document $new_node = $new_dom->getElementsByTagName('ds')->item(0); // get XPATH notation required for positioning // $xpath = new DOMXPath($dom); /* get XPATH for entry where new node will be inserted * which is the entry */ // $insert = $xpath->query('/rrd/rra')->item(0); $insert = $dom->getElementsByTagName('rra')->item(0); if ($insert !== null && $new_node !== null) { // import the new node $new_node = $dom->importNode($new_node, true); // and insert it at the correct place $insert->parentNode->insertBefore($new_node, $insert); } return $dom; } /** * rrd_append_cdp_prep_ds - append a subtree to the subtrees of a RRD XML structure * * @param object $dom The DOM object, where the RRD XML is stored * @param string $version The rrd file version * * @return object The modified DOM object */ function rrd_append_cdp_prep_ds(object $dom, string $version) : object { // get all entries // $cdp_prep_list = $xpath->query('/rrd/rra/cdp_prep'); $rra_node = $dom->getElementsByTagName('rra')->item(0); if ($rra_node === null) { return $dom; } $cdp_prep_list = $rra_node->getElementsByTagName('cdp_prep'); // get XPATH notation required for positioning // $xpath = new DOMXPath($dom); // get XPATH for source entry // $src_ds = $xpath->query('/rrd/rra/cdp_prep/ds')->item(0); $cdp_prep_first = $cdp_prep_list->item(0); if ($cdp_prep_first === null) { return $dom; } $src_ds = $cdp_prep_first->getElementsByTagName('ds')->item(0); if ($src_ds === null) { return $dom; } // clone the source ds entry to preserve RRDtool notation $new_ds = $src_ds->cloneNode(true); // rrdtool version dependencies if ($version === RRD_FILE_VERSION3) { $new_ds->getElementsByTagName('primary_value')->item(0)->nodeValue = ' NaN '; $new_ds->getElementsByTagName('secondary_value')->item(0)->nodeValue = ' NaN '; } // the new node always has default entries $new_ds->getElementsByTagName('value')->item(0)->nodeValue = ' NaN '; $new_ds->getElementsByTagName('unknown_datapoints')->item(0)->nodeValue = ' 0 '; // iterate all entries found, equals 'number of ' times 'number of ' if ($cdp_prep_list->length) { foreach ($cdp_prep_list as $cdp_prep) { /* $cdp_prep now points to the next XML Element * and append new ds entry at end of child list */ $cdp_prep->appendChild($new_ds->cloneNode(true)); } } return $dom; } /** * rrd_append_value - append a alue element to the subtrees of a RRD XML structure * * @param object $dom The DOM object, where the RRD XML is stored * * @return object The modified DOM object */ function rrd_append_value(object $dom) : object { if (!$dom instanceof DOMDocument) { return $dom; } // get XPATH notation required for positioning // $xpath = new DOMXPath($dom); // get all entries // $itemList = $xpath->query('/rrd/rra/database/row'); $itemList = $dom->getElementsByTagName('row'); // create entry to preserve RRDtool notation $new_v = $dom->createElement('v', ' NaN '); // iterate all entries found, equals 'number of ' times 'number of ' if ($itemList->length) { foreach ($itemList as $item) { /* $item now points to the next XML Element * and append new v entry at end of child list */ $item->appendChild($new_v->cloneNode(true)); } } return $dom; } /** * rrd_delete_rra - delete an subtree from the XML structure * * @param object $dom The DOM document, where the RRD XML is stored * @param array $rra_parm A single rra parameter set, given by the user * * @return object The modified DOM object */ function rrd_delete_rra(object $dom, array $rra_parm) : object { if (!$dom instanceof DOMDocument) { return $dom; } // find all RRA DOMNodes $rras = $dom->getElementsByTagName('rra'); // iterate all entries found $nb = $rras->length; for ($pos = 0; $pos < $nb; $pos++) { // retrieve all RRA DOMNodes one by one $rra = $rras->item($pos); $cf = $rra->getElementsByTagName('cf')->item(0)->nodeValue; $pdp_per_row = $rra->getElementsByTagName('pdp_per_row')->item(0)->nodeValue; $xff = $rra->getElementsByTagName('xff')->item(0)->nodeValue; $rows = $rra->getElementsByTagName('row')->length; if ($cf == $rra_parm['cf'] && $pdp_per_row == $rra_parm['pdp_per_row'] && $xff == $rra_parm['xff'] && $rows == $rra_parm['rows']) { print (__('RRA (CF=%s, ROWS=%d, PDP_PER_ROW=%d, XFF=%1.2f) removed from RRD file', $cf, $rows, $pdp_per_row, $xff)) . PHP_EOL; // we need the parentNode for removal operation $parent = $rra->parentNode; $parent->removeChild($rra); break; // do NOT accidentally remove more than one element, else loop back to forth } } return $dom; } /** * rrd_copy_rra - clone an subtree of the XML structure, replacing cf * * @param object $dom The DOM document, where the RRD XML is stored * @param string $cf New consolidation function * @param array $rra_parm A single rra parameter set, given by the user * * @return object The modified DOM object */ function rrd_copy_rra(object $dom, string $cf, array $rra_parm) : object { if (!$dom instanceof DOMDocument) { return $dom; } // find all RRA DOMNodes $rras = $dom->getElementsByTagName('rra'); // iterate all entries found $nb = $rras->length; for ($pos = 0; $pos < $nb; $pos++) { // retrieve all RRA DOMNodes one by one $rra = $rras->item($pos); $_cf = $rra->getElementsByTagName('cf')->item(0)->nodeValue; $_pdp_per_row = $rra->getElementsByTagName('pdp_per_row')->item(0)->nodeValue; $_xff = $rra->getElementsByTagName('xff')->item(0)->nodeValue; $_rows = $rra->getElementsByTagName('row')->length; if ($_cf == $rra_parm['cf'] && $_pdp_per_row == $rra_parm['pdp_per_row'] && $_xff == $rra_parm['xff'] && $_rows == $rra_parm['rows']) { print (__('RRA (CF=%s, ROWS=%d, PDP_PER_ROW=%d, XFF=%1.2f) adding to RRD file', $cf, $_rows, $_pdp_per_row, $_xff)) . PHP_EOL; // we need the parentNode for append operation $parent = $rra->parentNode; // get a clone of the matching RRA $new_rra = $rra->cloneNode(true); // and find the 'old' cf // $old_cf = $new_rra->getElementsByTagName('cf')->item(0); // now replace old cf with new one // $old_cf->childNodes->item(0)->replaceData(0,20,$cf); if ($new_rra instanceof DOMElement) { $new_rra->getElementsByTagName('cf')->item(0)->nodeValue = $cf; } // append new rra entry at end of the list $parent->appendChild($new_rra); break; // do NOT accidentally clone more than one element, else loop back to forth } } return $dom; } function rrdtool_replacement_legend(int $local_graph_id) : array { $graph_data = db_fetch_assoc_prepared('SELECT gti.legend, dtr.data_source_name, gti.consolidation_function_id, c.hex, gti.alpha, gti.text_format FROM graph_templates_item AS gti INNER JOIN data_template_rrd AS dtr ON gti.task_item_id = dtr.id LEFT JOIN colors AS c ON gti.color_id = c.id WHERE gti.local_graph_id = ? AND graph_type_id IN (7,8,2,4,5,6,20) ORDER BY gti.sequence', [$local_graph_id]); $is_agg = db_fetch_cell_prepared('SELECT COUNT(*) FROM aggregate_graphs WHERE local_graph_id = ?', [$local_graph_id]); $legend = []; if (cacti_sizeof($graph_data)) { foreach ($graph_data as $g) { if ($is_agg && $g['text_format'] != '') { $agg_prefix = $g['text_format'] . ' - '; } else { $agg_prefix = ''; } if ($g['legend'] != '') { $legend[] = ['legend' => $agg_prefix . $g['legend'], 'color' => '#' . $g['hex'] . $g['alpha']]; } else { if ($g['data_source_name'] == '') { $g['data_source_name'] = 'Unknown'; } $g['data_source_name'] = ucwords(str_replace('_', ' ', $g['data_source_name'])); switch($g['consolidation_function_id']) { case 1: $g['data_source_name'] .= ' (AVG)'; break; case 2: $g['data_source_name'] .= ' (MIN)'; break; case 3: $g['data_source_name'] .= ' (MAX)'; break; case 4: $g['data_source_name'] .= ' (LAST)'; break; } $legend[] = ['legend' => $agg_prefix . $g['data_source_name'], 'color' => '#' . $g['hex'] . $g['alpha']]; } } } return $legend; } function rrdtool_parse_error(string $string) : string { if (preg_match('/ERROR. opening \'(.*)\': (No such|Permiss).*/', $string, $matches)) { if (cacti_sizeof($matches) >= 2) { $filename = (string) $matches[1]; $rra_name = basename($filename); $rra_path = dirname($filename) . '/'; if (!is_resource_writable($rra_path)) { $message = __('Website does not have write access to %s, may be unable to create/update RRDs', 'folder'); $rra_name = str_replace(CACTI_PATH_RRA . '/', '', $rra_path); $rra_path = ''; } else { $found = stripos($filename, CACTI_PATH_BASE); if ($found > 0 || $found === 0) { $rra_file = str_replace(CACTI_PATH_RRA . '/', '', $filename); $rra_name = basename($rra_file); $rra_path = dirname($rra_file); } else { $rra_name = basename($filename); $rra_path = __('(Custom)'); } if (!is_resource_writable($filename)) { $message = __('Website does not have write access to %s, may be unable to create/update RRDs', 'data file'); } else { $message = __('Failed to open data file, poller may not have run yet'); } $rra_path = '(' . __('RRA Folder') . ': ' . ((empty($rra_path) || $rra_path == '.') ? __('Root') : $rra_path) . ')'; } $string = $message . ":\n\0x27\n" . $rra_name; if (!empty($rra_path)) { $string .= "\n" . $rra_path; } } } return $string; } function rrdtool_create_error_image(string $string, mixed $width = '', mixed $height = '') : string { global $dejavu_paths; $string = rrdtool_parse_error($string); // put image in buffer ob_start(); $image_data = false; $font_color = '000000'; $font_size = 8; $back_color = 'F3F3F3'; $shadea = 'CBCBCB'; $shadeb = '999999'; if (CACTI_SERVER_OS == 'unix') { foreach ($dejavu_paths as $dejavupath) { if (file_exists($dejavupath . '/DejaVuSans.ttf')) { $font_file = $dejavupath . '/DejaVuSans.ttf'; break; } } } else { $font_file = 'C:/Windows/Fonts/Arial.ttf'; } // GHSA-mpfm-qhvj-5879: get_selected_theme() bypasses the theme allowlist, so validate it before it builds an include path. $themefile = CACTI_PATH_INCLUDE . '/themes/' . cacti_validate_theme(get_selected_theme()) . '/rrdtheme.php'; if (file_exists($themefile) && is_readable($themefile)) { include($themefile); if (isset($rrdfonts['legend']['size'])) { $font_size = $rrdfonts['legend']['size']; } if (isset($rrdcolors['font'])) { $font_color = $rrdcolors['font']; } if (isset($rrdcolors['canvas'])) { $back_color = $rrdcolors['canvas']; } if (isset($rrdcolors['shadea'])) { $shadea = $rrdcolors['shadea']; } if (isset($rrdcolors['shadeb'])) { $shadeb = $rrdcolors['shadeb']; } } $image = imagecreatetruecolor(450, 200); imagesavealpha($image, true); // create a transparent color $transparent = imagecolorallocatealpha($image, 0, 0, 0, 127); if ($transparent !== false) { imagefill($image, 0, 0, $transparent); } // background the entire image with the frame $shadeb_parsed = sscanf($shadeb, '%02x%02x%02x'); $red = min(255, max(0, (int) ($shadeb_parsed[0] ?? 0))); $green = min(255, max(0, (int) ($shadeb_parsed[1] ?? 0))); $blue = min(255, max(0, (int) ($shadeb_parsed[2] ?? 0))); $shadeb_color = imagecolorallocate($image, $red, $green, $blue); if ($shadeb_color !== false) { imagefill($image, 0, 0, $shadeb_color); } // set the background color $shadea_parsed = sscanf($shadea, '%02x%02x%02x'); $red = min(255, max(0, (int) ($shadea_parsed[0] ?? 0))); $green = min(255, max(0, (int) ($shadea_parsed[1] ?? 0))); $blue = min(255, max(0, (int) ($shadea_parsed[2] ?? 0))); $shadea_color = imagecolorallocate($image, $red, $green, $blue); if ($shadea_color !== false) { imagefilledrectangle($image, 1, 1, 448, 198, $shadea_color); } // set the background color $back_parsed = sscanf($back_color, '%02x%02x%02x'); $red = min(255, max(0, (int) ($back_parsed[0] ?? 0))); $green = min(255, max(0, (int) ($back_parsed[1] ?? 0))); $blue = min(255, max(0, (int) ($back_parsed[2] ?? 0))); $back_color_alloc = imagecolorallocate($image, $red, $green, $blue); if ($back_color_alloc !== false) { imagefilledrectangle($image, 2, 2, 447, 197, $back_color_alloc); } // allocate the image $logo = imagecreatefrompng(CACTI_PATH_IMAGES . '/cacti_error_image.png'); // merge the two images if ($logo !== false) { imagecopy($image, $logo, 0, 0, 0, 0, 450, 200); } // set the background color $font_parsed = sscanf($font_color, '%02x%02x%02x'); $red = min(255, max(0, (int) ($font_parsed[0] ?? 0))); $green = min(255, max(0, (int) ($font_parsed[1] ?? 0))); $blue = min(255, max(0, (int) ($font_parsed[2] ?? 0))); $text_color = imagecolorallocate($image, $red, $green, $blue); // see the size of the string $string = trim($string); $maxstring = intval(ceil((450 - (125 + 10)) / ($font_size / 0.9))); $stringlen = strlen($string) * $font_size; $padding = 5; if ($stringlen > $maxstring) { $cstring = wordwrap($string, $maxstring, "\n", true); $strings = explode("\n", $cstring); $strings = array_reverse($strings); $lines = cacti_sizeof($strings); } elseif (strlen(trim($string)) == 0) { $strings = [__('Unknown RRDtool Error')]; $lines = 1; } else { $strings = [$string]; $lines = 1; } // setup the text position, image is 450x200, we start at 125 pixels from the left $xpos = 125; $texth = ($lines * $font_size + (($lines - 1) * $padding)); $ypos = round((200 / 2) + ($texth / 2),0); if ($text_color === false) { $text_color = 0; } // set the font of the image if (isset($font_file) && file_exists($font_file) && is_readable($font_file) && function_exists('imagettftext')) { foreach ($strings as $string) { if (trim($string) != '') { if (@imagettftext($image, $font_size, 0, $xpos, intval($ypos), $text_color, $font_file, $string) === false) { cacti_log('TTF text overlay failed'); } $ypos -= ($font_size + $padding); } } } else { foreach ($strings as $string) { if (trim($string) != '') { if (@imagestring($image, $font_size, $xpos, intval($ypos), $string, $text_color) === false) { cacti_log('Text overlay failed'); } $ypos -= ($font_size + $padding); } } } if ($width != '' && $height != '') { $nimage = imagecreatetruecolor($width, $height); imagecopyresized($nimage, $image, 0, 0, 0, 0, $width, $height, 450, 200); // create the image imagepng($image); } else { // create the image imagepng($image); } // get the image from the buffer $image_data = ob_get_contents(); // flush the buffer ob_end_clean(); return $image_data !== false ? $image_data : ''; } /** * gradient - Add gradient support for AREA type charts. This function adds several CDEF with different shading * * @param string $vname The data source name * @param string $start_color The start color for the gradient * @param string $end_color The end color for the gradient * @param string $label Any label attached to it * @param mixed $steps Defaults to 20 * @param mixed $lower Defaults to false * @param string $alpha Alpha channel to be used * * @return string The additional CDEF/AREA command lines for rrdtool * * License: GPLv2 * Original Code: https://github.com/lingej/pnp4nagios/blob/master/share/pnp/application/helpers/rrd.php */ function gradient(string $vname = '', string $start_color = '#0000a0', string $end_color = '#f0f0f0', string $label = '', mixed $steps = 20, mixed $lower = false, string $alpha = 'FF') : string { if (preg_match('/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i', $start_color, $matches)) { $r1 = hexdec($matches[1]); $g1 = hexdec($matches[2]); $b1 = hexdec($matches[3]); } else { $r1 = $g1 = $b1 = 0; } if (preg_match('/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i', $end_color, $matches)) { $r2 = hexdec($matches[1]); $g2 = hexdec($matches[2]); $b2 = hexdec($matches[3]); } else { $r2 = $g2 = $b2 = 0; } $diff_r = $r2 - $r1; $diff_g = $g2 - $g1; $diff_b = $b2 - $b1; $spline = ''; $spline_vname = 'var_' . substr(sha1((string) random_int(0, mt_getrandmax())), 1, 4); $vnamet = $vname . substr(sha1((string) random_int(0, mt_getrandmax())), 1, 4); if (preg_match('/^([0-9]{1,3})%$/', $lower, $matches)) { $lower = $matches[1]; $spline .= sprintf('CDEF:%s_min=%s,100,/,%d,* ' . RRD_NL, $vnamet, $vname, $lower); } elseif (preg_match('/^([0-9]+)$/', $lower, $matches)) { $lower = $matches[1]; $spline .= sprintf('CDEF:%s_min=%s,%d,- ' . RRD_NL, $vnamet, $vname, $lower); } else { $lower = 0; $spline .= sprintf('CDEF:%s_min=%s,%s,- ' . RRD_NL, $vnamet, $vname, $vname); } for ($i = $steps; $i > 0; $i--) { $spline .= sprintf('CDEF:%s%d=%s,%s_min,-,%d,/,%d,*,%s_min,+ ' . RRD_NL, $spline_vname, $i, $vname, $vnamet, $steps, $i, $vnamet); } // We don't use alpha blending for the area right now $alpha = 'FF'; for ($i = $steps; $i > 0; $i--) { $factor = $i / $steps; $r = round($r1 + $diff_r * $factor); $g = round($g1 + $diff_g * $factor); $b = round($b1 + $diff_b * $factor); if ($i == $steps && $label != '' && strlen($label) > 2) { $spline .= sprintf('AREA:%s%d#%02X%02X%02X%s:%s ' . RRD_NL, $spline_vname, $i, $r, $g, $b, $alpha, $label); } else { $spline .= sprintf('AREA:%s%d#%02X%02X%02X%s ' . RRD_NL, $spline_vname, $i, $r, $g, $b, $alpha); } } $spline .= sprintf('AREA:%s%d#%02X%02X%02X%s ' . RRD_NL, $spline_vname, $steps, $r2, $g2, $b2, '00'); return $spline; } /** * colourBrightness - Adjust the brightness of a hex color for gradient charts. * Positive percent lightens; negative percent darkens. * * @param string $hex The hex representation of a color (with or without leading #) * @param float $percent Brightness adjustment: decimal in [-1, 1] (e.g. 0.4 = +40%) * or coerced integer in [-100, 100] (e.g. 40 = +40%). Values * outside [-100, 100] are normalized then clamped to [-1, 1]. * NOTE: values in the open interval (1.0, 2.0) are treated as * integers and divided by 100 (e.g. 1.5 → 0.015). The value * 1.0 itself is NOT divided — it means 100% original color * (the identity). There is no decimal representation of "1% * brighter"; use 0.01 instead. * * @return string The adjusted color in the same format as the input * * License: GPLv2 * Original Code: http://www.barelyfitz.com/projects/csscolor/ */ function colourBrightness(string $hex, float $percent) : string { // Work out if hash given $hash = ''; if (stristr($hex, '#')) { $hex = str_replace('#', '', $hex); $hash = '#'; } // / HEX TO RGB $rgb = [hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2))]; // // CALCULATE if (abs($percent) > 1) { $percent = $percent / 100; } $percent = max(-1.0, min(1.0, $percent)); for ($i = 0; $i < 3; $i++) { // See if brighter or darker if ($percent > 0) { // Lighter $rgb[$i] = round($rgb[$i] * $percent) + round(255 * (1 - $percent)); } else { // Darker $positivePercent = abs($percent); $rgb[$i] = round($rgb[$i] * (1 - $positivePercent)); } if ($rgb[$i] < 0) { $rgb[$i] = 0; } // In case rounding up causes us to go to 256 if ($rgb[$i] > 255) { $rgb[$i] = 255; } } // // RBG to Hex $hex = ''; for ($i = 0; $i < 3; $i++) { // Convert the decimal digit to hex $hexDigit = dechex((int) $rgb[$i]); // Add a leading zero if necessary if (strlen($hexDigit) == 1) { $hexDigit = '0' . $hexDigit; } // Append to the hex string $hex .= $hexDigit; } return $hash . $hex; } /** * add_unknown_data - Add unknown data area fills to graphs if selected by the admin * * @param array $graph_array The graph_array data containing all rrdtool graph options * @param array $xport_meta If the graph unknown data is true, set that in the * xport meta output. * * @return array The graph_array modified to include unknown data ranges */ function add_unknown_data(array $graph_array, array &$xport_meta) : array { // add the cdef for unknown data if (read_config_option('graph_unknown_data') == 'on') { if (isset($xport_meta['ignoreItems'])) { $xport_meta['ignoreItems'] += 1; } else { $xport_meta['ignoreItems'] = 1; } if (cacti_sizeof($graph_array['defs'])) { $tmp_def = ''; $pluscnt = cacti_sizeof($graph_array['defs']) - 1; foreach ($graph_array['defs'] as $d) { $tmp_def .= ($tmp_def != '' ? ',' : '') . "$d,UN"; } $graph_array['graph_defs'] .= "CDEF:unknowndata='$tmp_def" . str_repeat(',+', $pluscnt); $graph_array['graph_defs'] .= ",INF,UNKN,IF'" . RRD_NL; $ud_color = intval(read_config_option('graph_unknown_color')); if ($ud_color > 0) { $ud_color = get_color($ud_color); } else { $ud_color = '8F9286'; } $ud_opacity = read_config_option('graph_unknown_opacity'); if ($ud_opacity == '') { $ud_opacity = '7F'; } $ucolor = $ud_color . $ud_opacity; $graph_array['graph_defs'] .= "AREA:unknowndata#$ucolor:" . RRD_NL; } } return $graph_array; } /** * add_business_hours - Add business hours highlight support for all rrdtool based charts * * @param array $data The graph_array data containing all rrdtool graph options * @param mixed $xport_meta * * @return array The graph_array containing AREA definitions for the business hours * */ function add_business_hours(array $data, mixed &$xport_meta) : array { if (read_config_option('business_hours_enable') == 'on' && gnrv('business_hours') == 'true') { if ($data['start'] < 0) { $bh_graph_start = time() + $data['start']; $bh_graph_end = time() + $data['end']; } else { $bh_graph_start = $data['start']; $bh_graph_end = $data['end']; } preg_match('/(\d+)\:(\d+)/',read_config_option('business_hours_start'), $bh_start_matches); preg_match('/(\d+)\:(\d+)/',read_config_option('business_hours_end'), $bh_end_matches); // Convert all the responses to integers $bh_start_matches = array_map('intval', $bh_start_matches); $bh_end_matches = array_map('intval', $bh_end_matches); $bh_start_matches[1] = $bh_start_matches[1] ?? 0; $bh_start_matches[2] = $bh_start_matches[2] ?? 0; $bh_end_matches[1] = $bh_end_matches[1] ?? 0; $bh_end_matches[2] = $bh_end_matches[2] ?? 0; $start_bh_time = mktime($bh_start_matches[1], $bh_start_matches[2], 0, intval(date('m', $bh_graph_start)), intval(date('d', $bh_graph_start)), intval(date('Y', $bh_graph_start))); $end_bh_time = mktime($bh_end_matches[1], $bh_end_matches[2], 0, intval(date('m', $bh_graph_end)), intval(date('d', $bh_graph_end)), intval(date('Y', $bh_graph_end))); if ($start_bh_time === false || $end_bh_time === false) { return $data; } if ($start_bh_time < $bh_graph_start) { if ($start_bh_time < $end_bh_time) { $start_bh_time = $bh_graph_start; } else { $next_day_time = mktime($bh_start_matches[1], $bh_start_matches[2], 0, intval(date('m', $bh_graph_start)), intval(date('d', $bh_graph_start)) + 1, intval(date('Y', $bh_graph_start))); if ($next_day_time !== false) { $start_bh_time = $next_day_time; } } } // Get the number of days: $datediff = $bh_graph_end - $bh_graph_start; $num_of_days = round($datediff / (60 * 60 * 24)) + 1; if ($num_of_days <= read_config_option('business_hours_max_days')) { for ($day = 0; $day < $num_of_days; $day++) { $current_start_bh_time = mktime($bh_start_matches[1], $bh_start_matches[2], 0, intval(date('m', $start_bh_time)), intval(date('d', $start_bh_time)) + $day, intval(date('Y', $start_bh_time))); $current_end_bh_time = mktime($bh_end_matches[1], $bh_end_matches[2], 0, intval(date('m', $start_bh_time)), intval(date('d', $start_bh_time)) + $day, intval(date('Y', $start_bh_time))); if ($current_start_bh_time === false || $current_end_bh_time === false) { continue; } if ($current_start_bh_time < $bh_graph_start) { $current_start_bh_time = $bh_graph_start; } if ($current_end_bh_time > $bh_graph_end) { $current_end_bh_time = $bh_graph_end; } $data['graph_defs'] .= 'CDEF:officehours' . $day . '=a,POP,TIME,' . $current_start_bh_time . ',LT,1,0,IF,TIME,' . $current_end_bh_time . ',GT,1,0,IF,MAX,0,GT,0,1,IF' . RRD_NL; $data['graph_defs'] .= 'CDEF:dslimit' . $day . '=INF,officehours' . $day . ',*' . RRD_NL; $bh_color = intval(read_config_option('business_hours_color')); if (empty($bh_color)) { $bh_color = 'CCCCCC'; } else { $bh_color = get_color($bh_color); if (empty($bh_color)) { $bh_color = 'CCCCCC'; } } $bh_opacity = read_config_option('business_hours_opacity'); if (empty($bh_opacity)) { $bh_opacity = '7F'; } if (preg_match('/[0-9A-Fa-f]{2}/', $bh_opacity)) { $bh_color .= $bh_opacity; } else { $bh_color .= '7F'; } if (date('N', $current_start_bh_time) < 6) { $data['graph_defs'] .= 'AREA:dslimit' . $day . '#' . $bh_color . RRD_NL; } if (date('N', $current_start_bh_time) > 5 && read_config_option('business_hours_hide_weekends') == '') { $data['graph_defs'] .= 'AREA:dslimit' . $day . '#' . $bh_color . RRD_NL; } if (isset($xport_meta['ignoreItems'])) { $xport_meta['ignoreItems'] += 1; } else { $xport_meta['ignoreItems'] = 1; } } } } return $data; }