Files
cacti/lib/rrd.php
T
TheWitnessandci 9facd9b500 security: batch 7 develop - consolidate remaining develop-only GHSA advisories (12 fixes + 8 confirmations) (#8149)
* security: batch 7 develop - consolidate remaining develop-only GHSA advisories (12 fixes + 8 regression confirmations)

Fixes: f7jw (automation preview XSS), rp5g (db_column_exists LIKE), vx2m (automation pivot CASE WHEN), h5wg (plugin DDL identifiers), 929h (aggregate LIKE filter), mfw8 (add_graphs REGEXP), m49v (user_admin sort_column allowlist), mpfm (rrd error-image theme allowlist), m3fh (SNMPv3 control chars), cg45 (remember-me row revoke), xq26 (remember-me retention window), m67r (import xml_path containment).

Confirmed already fixed on develop, pinned with regression tests: 4gph, 977w, mgcq, 5p79, cfhh, wfcf, pjq5, wp33.

Excluded: 227p (1.2.x-only), mx5c (maintainer private branch).

* security: address batch-7 review feedback (lexical-first xml_path containment, session-wide sort allowlist, connection-aware db_column_exists quoting, reject-not-normalize plugin DDL identifiers, behavior-based test names)

* style: apply php-cs-fixer single-quote fix to api_automation pivot builders; add WSL dev-environment note to copilot instructions

* fix phpstan string typing

* test: refresh SQL interpolation baseline for db_column_exists db_qstr change (database.php 9->8)

---------

Co-authored-by: ci <ci@local>
2026-10-02 15:27:34 -04:00

6065 lines
205 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
require_once(__DIR__ . '/rrd_graph_item.php');
use phpseclib4\Crypt\Rijndael;
use phpseclib4\Crypt\RSA;
define('RRD_NL', " \\\n");
define('MAX_FETCH_CACHE_SIZE', 5);
define('RRD_PROXY_END_OF_PACKET', "_EOP_\r\n");
define('RRD_PROXY_END_OF_SEQUENCE', "_EOT_\r\n");
define('RRD_PROXY_TIMEOUT_SECONDS', 10);
define('RRD_PROXY_MAX_KEY_SIZE', 1048576);
define('RRD_PROXY_MAX_RESPONSE_SIZE', 67108864);
define('RRDTOOL_MAX_COMMAND_BYTES', 16 * 1024 * 1024);
define('RRDTOOL_MAX_RESPONSE_BYTES', 128 * 1024 * 1024);
// Bound on in-run RRDtool process relaunches after a crash.
define('RRDTOOL_MAX_RESTARTS', 5);
if (read_config_option('storage_location')) {
global $encryption;
$encryption = true;
}
/**
* Strip control characters from a fully assembled rrdtool command line.
*
* The persistent rrdtool process reads one command per line over a pipe, so a
* CR or LF embedded in any field value (data_source_path, DS min/max, a title,
* a substituted host value, ...) splits the line into a second, attacker chosen
* rrdtool command. RRDtool command lines are single line; the line terminator
* is added at the write site, not here, so removing every C0 control and DEL
* from the assembled command is safe and defeats pipe/newline injection
* regardless of which field carried the payload. Applied after all title and
* placeholder substitution so a later substitution cannot reintroduce a break.
*
* @param string $command_line The assembled rrdtool command line.
*
* @return string The command line with C0 controls and DEL removed.
*/
function rrd_strip_control_chars(string $command_line): string {
$clean = preg_replace('/[\x00-\x1f\x7f]/', '', $command_line);
/* preg_replace() returns null on a PCRE failure. Fall back to removing the
* characters that actually enable pipe injection so a regex engine problem
* never leaves CR/LF/NUL in the command line. */
return $clean ?? str_replace(["\r", "\n", "\0"], '', $command_line);
}
/**
* set the language environment variable for rrdtool functions
*
* @param mixed $lang The desired language to set
*
* @return void
*/
function rrdtool_set_language(mixed $lang = false) : void {
global $prev_lang;
$prev_lang = getenv('LANG');
if ($lang == false) {
putenv('LANG=' . str_replace('-', '_', CACTI_LOCALE) . '.UTF-8');
} else {
putenv('LANG=en_EN.UTF-8');
}
}
/**
* restore the default language environment variable after rrdtool functions
*
* @return void
*/
function rrdtool_reset_language() : void {
global $prev_lang;
putenv('LANG=' . $prev_lang);
}
function rrd_init(bool $output_to_term = true) : 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_init' : '__rrd_init';
return call_user_func_array($function, $args);
}
function __rrd_init(bool $output_to_term = true, mixed $lang = false) : mixed {
// set the rrdtool default font
if (read_config_option('path_rrdtool_default_font')) {
putenv('RRD_DEFAULT_FONT=' . read_config_option('path_rrdtool_default_font'));
}
$path = (string) read_config_option('path_rrdtool');
if (!is_file($path) || !is_executable($path)) {
cacti_log('ERROR: RRDtool executable was not found or is not executable.', false, 'RRDTOOL');
return false;
}
$previous_language = getenv('LANG');
rrdtool_set_language($lang);
$descriptors = [
0 => ['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'], '<?xml')) {
return 'SVG/XML Output OK';
}
return true;
default:
return false;
}
}
/**
* Execute an RRDtool command and return the output.
*
* @param string|array $command_line The RRDtool command to execute. An array is quoted here,
* one element per argument. A caller passing a string quotes
* each variable argument with cacti_escapeshellarg() as it
* builds the line; the assembled line is never quoted
* @param bool $log_to_stdout Whether to echo output to stdout
* @param int $output_flag Output format constant (RRDTOOL_OUTPUT_*)
* @param mixed $rrdtool_pipe An open RRDtool pipe resource, or null
* @param string $logopt Logging context identifier
*
* @return mixed The command output in the requested format
*/
function __rrd_execute(string|array $command_line, bool $log_to_stdout, int $output_flag = RRDTOOL_OUTPUT_STDOUT, mixed $rrdtool_pipe = null, string $logopt = 'WEBLOG') : mixed {
if (is_array($command_line)) {
$command_line = rrdtool_build_command($command_line);
if ($command_line === false) {
return false;
}
}
$command_line = str_replace("\\\n", ' ', $command_line);
/* Defense in depth: after all placeholder and title substitution, strip any
* control characters from the assembled command before it is written to the
* rrdtool pipe or executed. A CR/LF in a field value would otherwise inject
* an independent rrdtool command over the line based pipe protocol. */
$command_line = rrd_strip_control_chars($command_line);
$context = rrdtool_command_log_context($command_line);
cacti_log('CACTI2RRD: ' . $context, $log_to_stdout, $logopt, POLLER_VERBOSITY_DEBUG);
$language = rrdtool_command_language($command_line);
cacti_session_close();
$auto_close = !rrdtool_is_process($rrdtool_pipe);
$process = $auto_close ? __rrd_init(true, $language) : $rrdtool_pipe;
if (!rrdtool_is_process($process)) {
cacti_log('ERROR: RRDtool process unavailable for ' . $context . '.', false, $logopt);
return false;
}
$result = rrdtool_process_command($process, $command_line);
if ($auto_close) {
__rrd_close($process);
}
if (!$result['success']) {
cacti_log('ERROR: RRDtool rejected ' . $context . ': ' . $result['error'], false, $logopt);
}
return rrdtool_format_result($result, $output_flag);
}
function rrdtool_trim_output(string &$output) : void {
global $user_time, $system_time, $real_time;
/**
* When using RRDtool with proc_open for long strings
* and using the '-' to handle standard in from inside
* the process, RRDtool automatically appends stderr
* to stdout for batch programs to parse the output
* string. So, therefore, we have to prune that
* output.
*/
if (CACTI_SERVER_OS == 'win32') {
$output = rtrim($output, "OK \n\r");
} else {
$okpos = strrpos($output, 'OK u:');
if ($okpos !== false) {
$stats = substr($output, $okpos);
$line = trim($stats, ' OK');
$parts = explode(' ', $line);
foreach ($parts as $line) {
$sparts = explode(':', $line);
switch($sparts[0]) {
case 'u':
$user_time = $sparts[1];
break;
case 's':
$system_time = $sparts[1];
break;
case 'r':
$real_time = $sparts[1];
break;
}
}
$output = substr($output, 0, $okpos);
}
}
}
/**
* Execute an RRDtool command through the remote proxy.
*
* @param string|array $command_line RRDtool command string, or one string argument per array element
* @param bool $log_to_stdout Whether to echo log output
* @param int $output_flag Requested RRDTOOL_OUTPUT_* result mode
* @param mixed $rrdp Existing proxy connection tuple, or an empty value
* @param string $logopt Logging context identifier
*
* @return mixed Output in the requested mode, or false when transport/protocol validation fails
*/
function __rrd_proxy_execute(string|array $command_line, bool $log_to_stdout, int $output_flag = RRDTOOL_OUTPUT_STDOUT, mixed $rrdp = '', string $logopt = 'WEBLOG') : mixed {
if (is_array($command_line)) {
$command_line = rrdtool_build_command($command_line);
if ($command_line === false) {
cacti_log('CACTI2RRDP ERROR: Invalid proxy command argument list.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
return false;
}
}
/**
* WIN32: before sending this command off to rrdtool, get rid
* of all of the '\' characters. Unix does not care; win32 does.
* Also make sure to replace all of the fancy "\"s at the end of the line,
* but make sure not to get rid of the "\n"s that are supposed to be
* in there (text format)
*/
$command_line = str_replace([CACTI_PATH_RRA, "\\\n"], ['.', ' '], $command_line);
/* Strip control characters before the command is sent to the rrdtool proxy,
* so a CR/LF in a field value cannot inject a second command at the proxy
* pipe. Mirrors the guard in __rrd_execute() for the local path. */
$command_line = rrd_strip_control_chars($command_line);
// output information to the log file if appropriate
cacti_log('CACTI2RRDP: ' . rrdtool_command_log_context($command_line), $log_to_stdout, $logopt, POLLER_VERBOSITY_DEBUG);
// store the last command to provide rrdtool segfault diagnostics
$last_command = $command_line;
$rrdp_auto_close = false;
if (!$rrdp) {
$rrdp = __rrd_proxy_init($logopt);
$rrdp_auto_close = true;
}
if (!$rrdp) {
cacti_log('CACTI2RRDP ERROR: Unable to connect to RRDtool proxy.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
return false;
} else {
cacti_log('CACTI2RRDP NOTE: Connection to RRDtool proxy has already been established.', $log_to_stdout, $logopt, POLLER_VERBOSITY_DEBUG);
}
if (!is_array($rrdp) || !isset($rrdp[0], $rrdp[1]) || !($rrdp[0] instanceof Socket) || !is_string($rrdp[1])) {
cacti_log('CACTI2RRDP ERROR: Invalid proxy connection state.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
return false;
}
$rrdp_socket = $rrdp[0];
$rrdp_public_key = $rrdp[1];
if (strlen($command_line) >= 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'], '<?xml')) {
return 'SVG/XML Output OK';
}
print $response['output'];
return true;
case RRDTOOL_OUTPUT_RETURN_STDERR:
return $response['raw'];
case RRDTOOL_OUTPUT_BOOLEAN:
return $response['success'];
default:
return false;
}
}
function rrdtool_function_interface_speed(array $data_local) : string {
static $speed_cache = [];
$cache_key = implode(':', [$data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index']]);
if (isset($speed_cache[$cache_key])) {
return $speed_cache[$cache_key];
}
$fields = db_fetch_assoc_prepared('SELECT field_name, field_value
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND snmp_index = ?
AND field_name IN (?, ?)',
[$data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index'], 'ifHighSpeed', 'ifSpeed']
);
$speeds = array_rekey($fields, 'field_name', 'field_value');
$ifHighSpeed = $speeds['ifHighSpeed'] ?? false;
$ifSpeed = $speeds['ifSpeed'] ?? false;
if (!empty($ifHighSpeed)) {
$speed = (int) $ifHighSpeed * 1000000;
} elseif (!empty($ifSpeed)) {
$speed = (int) $ifSpeed;
} else {
$speed = intval(read_config_option('default_interface_speed'));
if (empty($speed)) {
$speed = '10000000000000';
} else {
$speed *= 1000000;
}
}
$speed_cache[$cache_key] = (string) $speed;
return $speed_cache[$cache_key];
}
/**
* Reject an RRD file path that shows directory traversal or a NUL byte, and
* confine it to the RRA directory when that base can be resolved.
*
* data_source_path is stored in the database and flows into every rrdtool
* create/fetch/update/graph command. A '..' segment or a NUL byte would let a
* crafted path escape the RRA directory. The traversal/NUL check is
* unconditional so a legitimate path is never refused. Containment under the
* RRA base is enforced only when both the base and the target (or its parent
* directory, for a not-yet-created file) resolve with realpath(); remote
* storage, where the path is proxy relative, leaves them unresolved and is not
* second-guessed.
*
* @param string $path The candidate RRD file path.
* @param string|null $rra_base RRA base directory; defaults to CACTI_PATH_RRA.
*
* @return bool True when the path is safe to use.
*/
function rrd_check_path(string $path, ?string $rra_base = null) : bool {
if ($path === '' || strpos($path, "\0") !== false) {
return false;
}
// '..' between path boundaries (or at either edge) denotes traversal.
if (preg_match('/(^|[\/\\\\:])\.\.([\/\\\\:]|$)/', $path)) {
return false;
}
if ($rra_base === null) {
$rra_base = defined('CACTI_PATH_RRA') ? (string) CACTI_PATH_RRA : '';
}
// When the RRA base cannot be resolved (e.g. remote/proxy storage), the
// syntactic traversal/NUL checks above are all that apply.
$base = ($rra_base !== '') ? realpath($rra_base) : false;
if ($base === false) {
return true;
}
/* Resolve the target as strictly as the filesystem allows: realpath() when
* the file exists (also collapses symlinks), else realpath() of the nearest
* existing ancestor with the not-yet-created tail appended. A path whose
* whole tail is missing cannot be resolved that way, so fall back to a
* lexical absolute form. A relative path we cannot resolve is refused
* rather than waved through, which was the hole that let a target with a
* non-existent parent skip confinement (the create flow mkdir()s parents). */
$real = realpath($path);
if ($real === false) {
$dir = $path;
$tail = '';
while (true) {
$parent = dirname($dir);
$tail = ($tail === '') ? basename($dir) : basename($dir) . DIRECTORY_SEPARATOR . $tail;
$resolved = realpath($parent);
if ($resolved !== false) {
$real = $resolved . DIRECTORY_SEPARATOR . $tail;
break;
}
if ($parent === $dir) {
// reached the root without an existing ancestor
$real = (strpos($path, DIRECTORY_SEPARATOR) === 0) ? $path : false;
break;
}
$dir = $parent;
}
}
if ($real === false) {
return false;
}
$base = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return strncmp($real . DIRECTORY_SEPARATOR, $base, strlen($base)) === 0;
}
function rrdtool_function_create(int $local_data_id, bool $show_source, mixed $rrdtool_pipe = null) : mixed {
global $data_source_types, $consolidation_functions, $encryption;
include(CACTI_PATH_INCLUDE . '/global_arrays.php');
$data_source_path = get_data_source_path($local_data_id, true);
if (!rrd_check_path($data_source_path)) {
cacti_log("ERROR: Refusing unsafe data source path '$data_source_path' for local_data_id $local_data_id", false, 'RRDTOOL');
return false;
}
/**
* ok, if that passes lets check to make sure an rra does not already
* exist, the last thing we want to do is overright data!
*/
if ($show_source != true) {
if (rrdtool_file_exists($data_source_path, $rrdtool_pipe)) {
return -1;
}
}
/**
* the first thing we must do is make sure there is at least one
* rra associated with this data source... *
* UPDATE: As of version 0.6.6, we are splitting this up into two
* SQL strings because of the multiple DS per RRD support. This is
* not a big deal however since this function gets called once per
* data source
*/
$rras = db_fetch_assoc_prepared('SELECT dtd.rrd_step, dsp.x_files_factor,
dspr.steps, dspr.rows, dspc.consolidation_function_id,
(dspr.rows*dspr.steps) AS rra_order
FROM data_template_data AS dtd
LEFT JOIN data_source_profiles AS dsp
ON dtd.data_source_profile_id=dsp.id
LEFT JOIN data_source_profiles_rra AS dspr
ON dsp.id=dspr.data_source_profile_id
LEFT JOIN data_source_profiles_cf AS dspc
ON dsp.id=dspc.data_source_profile_id
WHERE dtd.local_data_id = ?
AND (dspr.steps IS NOT NULL OR dspr.rows IS NOT NULL)
ORDER BY dspc.consolidation_function_id, rra_order, dspr.steps',
[$local_data_id]
);
// if we find that this DS has no RRA associated; get out
if (cacti_sizeof($rras) <= 0) {
cacti_log("ERROR: There are no RRA's assigned to local_data_id: $local_data_id.");
return false;
}
// create the "--step" line
$create_ds = RRD_NL . '--start 0 --step ' . $rras[0]['rrd_step'] . ' ' . RRD_NL;
/**
* We have to check for Non-Templated Data Source first as they may not include
* a graph. So, for that case, we need the RRDfile to include all data sources
*/
$data_template_id = db_fetch_cell_prepared('SELECT data_template_id
FROM data_local
WHERE id = ?',
[$local_data_id]);
if ($data_template_id > 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;
}
}
function rrdtool_function_update(array $update_cache_array, mixed $rrdtool_pipe = null) : int {
// 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++;
} else {
cacti_log('ERROR: RRDtool update failed for ' . rrdtool_command_log_context('update ' . $rrd_path), false, 'POLLER');
}
}
}
}
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<string>, timestamp?: array{start_time?: int, end_time?: int, step?: int}, values?: array<int, array<int, numeric-string|'U'>>} $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;
include(CACTI_PATH_INCLUDE . '/global_arrays.php');
// 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;
}
}
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');
include(CACTI_PATH_INCLUDE . '/global_arrays.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.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_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
}
}
// +++++++++++++++++++++++ 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 '<pre>' . wordwrap(htmle($source_command_line), 160, PHP_EOL, true) . '</pre>';
print '<span class="textInfo">' . 'RRDtool Command lengths = ' . $source_command_line_lengths . ' characters.</span><br>';
if (CACTI_SERVER_OS == 'win32' && $source_command_line_lengths > 8191) {
print '<pre>' . 'Warning: The Cacti OS is Windows system, RRDtool Command lengths should not exceed 8191 characters.' . '</pre>';
}
} 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']);
} 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']);
}
}
// 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<string, mixed> $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_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]);
// cursory checks on total data sources
if (isset($cacti_info['ds']) && isset($info['ds'])) {
if (cacti_sizeof($cacti_info['ds']) != cacti_sizeof($info['ds'])) {
// Need to warn here about the total number of data sources not matchhing
}
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']);
}
}
}
// if there are data sources missing log that now
foreach ($info['ds'] as $data_source_name => $data_source) {
if (!isset($data_source['seen'])) {
$diff['ds'][$data_source_name]['error'] = __("DS '%s' missing in Cacti definition", $data_source_name);
}
}
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();
}
/**
* print_leaves - A function used by the rrdtool_tune function to output data
*
* @param array $array The array of leaves
*
* @return void
*/
function print_leaves(array $array) : void {
foreach ($array as $key => $line) {
if (!is_array($line)) {
if (CACTI_CLI) {
print $line . PHP_EOL;
} else {
print '<tr><td>' . $line . '</td></tr>';
}
} else {
if ($key === 'tune') {
continue;
}
if ($key === 'resize') {
continue;
}
print_leaves($line);
}
}
}
/**
* 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 The 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 {
$rrd_path = read_config_option('path_rrdtool');
$cmd = [];
if ($show_source && cacti_sizeof($diff)) {
if (!CACTI_CLI) {
html_header([__('Errors Found')]);
}
print_leaves($diff);
if (!CACTI_CLI) {
html_end_box();
}
}
if (isset($diff['tune']) || isset($diff['resize'])) {
if (!CACTI_CLI) {
html_header([__('Optional Tuning Recommendations')]);
}
}
if (isset($diff['tune']) && cacti_sizeof($diff['tune'])) {
// create tune commands
foreach ($diff['tune'] as $line) {
if ($show_source == true) {
if (CACTI_CLI) {
print $rrd_path . ' tune ' . $line . PHP_EOL;
} else {
print '<tr><td>' . $rrd_path . ' tune ' . $line . '</td></tr>';
}
} else {
rrdtool_execute("tune $line", true, RRDTOOL_OUTPUT_STDOUT);
}
}
}
if (isset($diff['resize']) && cacti_sizeof($diff['resize'])) {
// each resize goes into an extra line
foreach ($diff['resize'] as $line) {
if ($show_source == true) {
if (CACTI_CLI) {
print read_config_option('path_rrdtool') . ' resize ' . $line . PHP_EOL;
print __('rename %s to %s', dirname($rrd_file) . '/resize.rrd', $rrd_file) . PHP_EOL;
} else {
print '<tr><td>' . read_config_option('path_rrdtool') . ' resize ' . $line . '</td></tr>';
print '<tr><td>' . __('rename %s to %s', dirname($rrd_file) . '/resize.rrd', $rrd_file) . '</td></tr>';
}
} else {
rrdtool_execute("resize $line", true, RRDTOOL_OUTPUT_STDOUT);
rename(dirname($rrd_file) . '/resize.rrd', $rrd_file);
}
}
}
if (isset($diff['tune']) || isset($diff['resize'])) {
if (!CACTI_CLI) {
html_end_box();
}
}
}
/**
* 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 <DS> 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 <DS> structure as part of the <cdp_prep> tree
rrd_append_cdp_prep_ds($dom, $version);
// add <V>alues to the <database> 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 <DS> 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 <DS> subtree
$new_dom = new DOMDocument;
// pretty print
$new_dom->formatOutput = true;
// this defines the new node structure
$new_dom->loadXML("
<ds>
<name> $name </name>
<type> $type </type>
<minimal_heartbeat> $min_hb </minimal_heartbeat>
<min> $min </min>
<max> $max </max>
<!-- PDP Status -->
<last_ds> $last_ds </last_ds>
<value> 0.0000000000e+00 </value>
<unknown_sec> 0 </unknown_sec>
</ds>");
// 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 <rra> 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 <DS> 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 <DS> subtree
$new_dom = new DOMDocument;
// pretty print
$new_dom->formatOutput = true;
// this defines the new node structure
$new_dom->loadXML("
<ds>
<name> $name </name>
<type> $type </type>
<cdef> $cdef </cdef>
<!-- PDP Status -->
<last_ds> $last_ds </last_ds>
<value> 0.0000000000e+00 </value>
<unknown_sec> 0 </unknown_sec>
</ds>");
// 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 <rra> 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 <DS> subtree to the <CDP_PREP> 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 <cdp_prep><ds> 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 <ds> 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 <rra>' times 'number of <ds>'
if ($cdp_prep_list->length) {
foreach ($cdp_prep_list as $cdp_prep) {
/* $cdp_prep now points to the next <cdp_prep> XML Element
* and append new ds entry at end of <cdp_prep> child list */
$cdp_prep->appendChild($new_ds->cloneNode(true));
}
}
return $dom;
}
/**
* rrd_append_value - append a <V>alue element to the <DATABASE> 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 <cdp_prep><ds> entries
// $itemList = $xpath->query('/rrd/rra/database/row');
$itemList = $dom->getElementsByTagName('row');
// create <V> entry to preserve RRDtool notation
$new_v = $dom->createElement('v', ' NaN ');
// iterate all entries found, equals 'number of <rra>' times 'number of <ds>'
if ($itemList->length) {
foreach ($itemList as $item) {
/* $item now points to the next <row> XML Element
* and append new v entry at end of <row> child list */
$item->appendChild($new_v->cloneNode(true));
}
}
return $dom;
}
/**
* rrd_delete_rra - delete an <RRA> subtree from the <RRD> 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 <RRA> subtree of the <RRD> 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;
}