diff --git a/cacti.sql b/cacti.sql index cad6762d4..997dc4c9e 100644 --- a/cacti.sql +++ b/cacti.sql @@ -67,6 +67,12 @@ CREATE TABLE `aggregate_graph_templates_graph` ( `lower_limit` varchar(20) NOT NULL default '0', `t_vertical_label` char(2) default '', `vertical_label` varchar(200) default '', + t_vertical_label_angle char(2) default '', + vertical_label_angle varchar(3) NOT NULL default '', + t_right_axis_label_angle char(2) default '', + right_axis_label_angle varchar(3) NOT NULL default '', + t_right_axis_range char(2) default '', + right_axis_range varchar(64) NOT NULL default '', `t_slope_mode` char(2) default '', `slope_mode` char(2) default 'on', `t_auto_scale` char(2) default '', @@ -1846,6 +1852,12 @@ CREATE TABLE `graph_templates_graph` ( lower_limit varchar(20) NOT NULL default '0', t_vertical_label char(2) default '', vertical_label varchar(200) default NULL, + t_vertical_label_angle char(2) default '', + vertical_label_angle varchar(3) NOT NULL default '', + t_right_axis_label_angle char(2) default '', + right_axis_label_angle varchar(3) NOT NULL default '', + t_right_axis_range char(2) default '', + right_axis_range varchar(64) NOT NULL default '', t_slope_mode char(2) default '', slope_mode char(2) default 'on', t_auto_scale char(2) default '', diff --git a/graph_templates.php b/graph_templates.php index 537bb20e8..e72a63e15 100644 --- a/graph_templates.php +++ b/graph_templates.php @@ -245,6 +245,12 @@ function form_save() : void { $save2['lower_limit'] = CactiValidator::validateInput(gnrv('lower_limit'), 'lower_limit', [new Assert\Regex('/^(-?([0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+)([eE][+\-]?[0-9]+)?|U)\z/')], 3); $save2['t_vertical_label'] = CactiValidator::validateInput((isrv('t_vertical_label') ? gnrv('t_vertical_label') : ''), 't_vertical_label', [], 3); $save2['vertical_label'] = CactiValidator::validateInput(gnrv('vertical_label'), 'vertical_label', [], 3); + $save2['t_vertical_label_angle'] = CactiValidator::validateInput((isrv('t_vertical_label_angle') ? gnrv('t_vertical_label_angle') : ''), 't_vertical_label_angle', [new Assert\Choice(['', 'on'])], 3); + $save2['vertical_label_angle'] = CactiValidator::validateRrdGraphAxisInput((isrv('vertical_label_angle') ? gnrv('vertical_label_angle') : ''), 'vertical_label_angle'); + $save2['t_right_axis_label_angle'] = CactiValidator::validateInput((isrv('t_right_axis_label_angle') ? gnrv('t_right_axis_label_angle') : ''), 't_right_axis_label_angle', [new Assert\Choice(['', 'on'])], 3); + $save2['right_axis_label_angle'] = CactiValidator::validateRrdGraphAxisInput((isrv('right_axis_label_angle') ? gnrv('right_axis_label_angle') : ''), 'right_axis_label_angle'); + $save2['t_right_axis_range'] = CactiValidator::validateInput((isrv('t_right_axis_range') ? gnrv('t_right_axis_range') : ''), 't_right_axis_range', [new Assert\Choice(['', 'on'])], 3); + $save2['right_axis_range'] = CactiValidator::validateRrdGraphAxisInput((isrv('right_axis_range') ? gnrv('right_axis_range') : ''), 'right_axis_range'); $save2['t_slope_mode'] = CactiValidator::validateInput((isrv('t_slope_mode') ? gnrv('t_slope_mode') : ''), 't_slope_mode', [], 3); $save2['slope_mode'] = CactiValidator::validateInput((isrv('slope_mode') ? gnrv('slope_mode') : ''), 'slope_mode', [], 3); $save2['t_auto_scale'] = CactiValidator::validateInput((isrv('t_auto_scale') ? gnrv('t_auto_scale') : ''), 't_auto_scale', [], 3); diff --git a/graphs.php b/graphs.php index a01a767d4..dffe05620 100644 --- a/graphs.php +++ b/graphs.php @@ -397,6 +397,9 @@ function form_save() : void { $save2['upper_limit'] = CactiValidator::validateInput(gnrv('upper_limit'), 'upper_limit', [new Assert\Regex('/^(-?([0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+)([eE][+\-]?[0-9]+)?|U)\z/')], 3); $save2['lower_limit'] = CactiValidator::validateInput(gnrv('lower_limit'), 'lower_limit', [new Assert\Regex('/^(-?([0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+)([eE][+\-]?[0-9]+)?|U)\z/')], 3); $save2['vertical_label'] = CactiValidator::validateInput(gnrv('vertical_label'), 'vertical_label', [], 3); + $save2['vertical_label_angle'] = CactiValidator::validateRrdGraphAxisInput((isrv('vertical_label_angle') ? gnrv('vertical_label_angle') : ''), 'vertical_label_angle'); + $save2['right_axis_label_angle'] = CactiValidator::validateRrdGraphAxisInput((isrv('right_axis_label_angle') ? gnrv('right_axis_label_angle') : ''), 'right_axis_label_angle'); + $save2['right_axis_range'] = CactiValidator::validateRrdGraphAxisInput((isrv('right_axis_range') ? gnrv('right_axis_range') : ''), 'right_axis_range'); $save2['slope_mode'] = CactiValidator::validateInput((isrv('slope_mode') ? gnrv('slope_mode') : ''), 'slope_mode', [], 3); $save2['auto_scale'] = CactiValidator::validateInput((isrv('auto_scale') ? gnrv('auto_scale') : ''), 'auto_scale', [], 3); $save2['auto_scale_opts'] = CactiValidator::validateInput(gnrv('auto_scale_opts'), 'auto_scale_opts', [], 3); diff --git a/include/global_form.php b/include/global_form.php index 95c05af34..c67f9dfcc 100644 --- a/include/global_form.php +++ b/include/global_form.php @@ -773,6 +773,13 @@ $struct_graph = [ 'description' => __('Should be set to 1024 for memory and 1000 for traffic measurements.'), 'size' => '12' ], + 'vertical_label_angle' => [ + 'friendly_name' => __('Vertical Label Angle (--vertical-label-angle)'), + 'method' => 'drop_array', + 'array' => ['' => __('RRDtool Default'), '0' => '0', '90' => '90', '180' => '180', '270' => '270'], + 'default' => '', + 'description' => __('Label rotation in degrees counter clockwise. Requires RRDtool 1.11 or newer. Blank uses the RRDtool default. Angles other than 90 and 270 may overlap nearby content.'), + ], 'slope_mode' => [ 'friendly_name' => __('Slope Mode (--slope-mode)'), 'method' => 'checkbox', @@ -912,6 +919,21 @@ $struct_graph = [ 'size' => '30', 'description' => __('The label for the right axis.'), ], + 'right_axis_label_angle' => [ + 'friendly_name' => __('Right Axis Label Angle (--right-axis-label-angle)'), + 'method' => 'drop_array', + 'array' => ['' => __('RRDtool Default'), '0' => '0', '90' => '90', '180' => '180', '270' => '270'], + 'default' => '', + 'description' => __('Right label rotation in degrees counter clockwise. Requires RRDtool 1.11 or newer. Blank follows the vertical label angle. Angles other than 90 and 270 may overlap nearby content.'), + ], + 'right_axis_range' => [ + 'friendly_name' => __('Right Axis Label Range (--right-axis-range)'), + 'method' => 'textbox', + 'max_length' => '64', + 'default' => '', + 'size' => '20', + 'description' => __('Only show right axis labels within min:max, for example 0:100. Either bound may be blank. This does not change the axis scale. Requires RRDtool 1.11 or newer. Blank leaves labels unrestricted.'), + ], 'right_axis_format' => [ 'friendly_name' => __('Right Axis Format (--right-axis-format <format>)'), 'method' => 'drop_sql', diff --git a/install/upgrades/1_3_0.php b/install/upgrades/1_3_0.php index 8d64778d6..918cc0633 100644 --- a/install/upgrades/1_3_0.php +++ b/install/upgrades/1_3_0.php @@ -83,6 +83,13 @@ function upgrade_to_1_3_0() : void { db_install_add_column('user_domains_ldap', ['name' => 'tls_certificate', 'type' => 'tinyint(3)', 'unsigned' => true, 'NULL' => false, 'default' => '3']); } + foreach (['graph_templates_graph', 'aggregate_graph_templates_graph'] as $table) { + foreach (['vertical_label_angle' => 3, 'right_axis_label_angle' => 3, 'right_axis_range' => 64] as $field => $length) { + db_install_add_column($table, ['name' => 't_' . $field, 'type' => 'char(2)', 'default' => '']); + db_install_add_column($table, ['name' => $field, 'type' => 'varchar(' . $length . ')', 'NULL' => false, 'default' => '']); + } + } + db_install_add_column('graph_templates_graph', ['name' => 't_left_axis_format', 'type' => 'char(2)', 'default' => '', 'after' => 'right_axis_formatter']); db_install_add_column('graph_templates_graph', ['name' => 'left_axis_format', 'type' => 'mediumint(8)', 'NULL' => true, 'after' => 't_left_axis_format']); diff --git a/lib/CactiValidator.php b/lib/CactiValidator.php index 9c92c7cd5..e93b471f0 100644 --- a/lib/CactiValidator.php +++ b/lib/CactiValidator.php @@ -18,6 +18,8 @@ use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +require_once(__DIR__ . '/rrd_graph_axis.php'); + class CactiValidator { private static ?ValidatorInterface $validator = null; @@ -186,6 +188,17 @@ class CactiValidator { ]); } + /** Apply the same axis validation to graph, template and aggregate saves. */ + public static function validateRrdGraphAxisInput(mixed $value, string $name): mixed { + return self::validateInput($value, $name, [new Assert\Callback( + static function (mixed $value, \Symfony\Component\Validator\Context\ExecutionContextInterface $context) use ($name): void { + if (!rrd_graph_axis_option_is_valid($name, $value)) { + $context->buildViolation(__('Invalid RRDtool axis option.'))->addViolation(); + } + } + )], 3); + } + /** * Common input validation helper. * Populates $_SESSION with the value and any error states. diff --git a/lib/api_aggregate.php b/lib/api_aggregate.php index 4da91e386..b7e734689 100644 --- a/lib/api_aggregate.php +++ b/lib/api_aggregate.php @@ -22,6 +22,8 @@ +-------------------------------------------------------------------------+ */ +require_once(__DIR__ . '/CactiValidator.php'); + /** * Create or update aggregate graph. * Save all graph definitions, but omit graph items. Wipe out host_id and graph_template_id. @@ -575,37 +577,40 @@ function aggregate_graph_items_save(array $items, string $table) : bool { */ function aggregate_validate_graph_params(array $posted, bool $has_override = false) : array { $check_post_params = [ - 'alt_y_grid' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'auto_padding' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'auto_scale' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'auto_scale_log' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'auto_scale_opts' => ['type' => 'int', 'allow_empty' => false, 'default' => 0, 'regex' => ''], - 'auto_scale_rigid' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'base_value' => ['type' => 'int', 'allow_empty' => true, 'default' => 0, 'regex' => '^[0-9]+$'], - 'dynamic_labels' => ['type' => 'str', 'allow_empty' => true, 'default' => 0, 'regex' => ''], - 'force_rules_legend' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'grouping' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'height' => ['type' => 'int', 'allow_empty' => false, 'default' => 0, 'regex' => '^[0-9]+$'], - 'image_format_id' => ['type' => 'int', 'allow_empty' => false, 'default' => 0, 'regex' => ''], - 'left_axis_format' => ['type' => 'int', 'allow_empty' => true, 'default' => '', 'regex' => '^[0-9]+$'], - 'left_axis_formatter' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'legend_direction' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'legend_position' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'lower_limit' => ['type' => 'int', 'allow_empty' => true, 'default' => 0, 'regex' => '^(-?([0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+)([eE][+\-]?[0-9]+)?)|U$'], - 'no_gridfit' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'right_axis' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'right_axis_format' => ['type' => 'int', 'allow_empty' => true, 'default' => '', 'regex' => '^[0-9]+$'], - 'right_axis_formatter' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'right_axis_label' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'scale_log_units' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'slope_mode' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'tab_width' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => '^[0-9]+$'], - 'unit_exponent_value' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'unit_length' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'unit_value' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'upper_limit' => ['type' => 'int', 'allow_empty' => true, 'default' => 0, 'regex' => '^(-?([0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+)([eE][+\-]?[0-9]+)?)|U$'], - 'vertical_label' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], - 'width' => ['type' => 'int', 'allow_empty' => false, 'default' => 0, 'regex' => '^[0-9]+$'] + 'alt_y_grid' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'auto_padding' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'auto_scale' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'auto_scale_log' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'auto_scale_opts' => ['type' => 'int', 'allow_empty' => false, 'default' => 0, 'regex' => ''], + 'auto_scale_rigid' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'base_value' => ['type' => 'int', 'allow_empty' => true, 'default' => 0, 'regex' => '^[0-9]+$'], + 'dynamic_labels' => ['type' => 'str', 'allow_empty' => true, 'default' => 0, 'regex' => ''], + 'force_rules_legend' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'grouping' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'height' => ['type' => 'int', 'allow_empty' => false, 'default' => 0, 'regex' => '^[0-9]+$'], + 'image_format_id' => ['type' => 'int', 'allow_empty' => false, 'default' => 0, 'regex' => ''], + 'left_axis_format' => ['type' => 'int', 'allow_empty' => true, 'default' => '', 'regex' => '^[0-9]+$'], + 'left_axis_formatter' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'legend_direction' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'legend_position' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'lower_limit' => ['type' => 'int', 'allow_empty' => true, 'default' => 0, 'regex' => '^(-?([0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+)([eE][+\-]?[0-9]+)?)|U$'], + 'no_gridfit' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'right_axis' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'right_axis_format' => ['type' => 'int', 'allow_empty' => true, 'default' => '', 'regex' => '^[0-9]+$'], + 'right_axis_formatter' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'right_axis_label_angle' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'right_axis_range' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'vertical_label_angle' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'right_axis_label' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'scale_log_units' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'slope_mode' => ['type' => 'bool', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'tab_width' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => '^[0-9]+$'], + 'unit_exponent_value' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'unit_length' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'unit_value' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'upper_limit' => ['type' => 'int', 'allow_empty' => true, 'default' => 0, 'regex' => '^(-?([0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+)([eE][+\-]?[0-9]+)?)|U$'], + 'vertical_label' => ['type' => 'str', 'allow_empty' => true, 'default' => '', 'regex' => ''], + 'width' => ['type' => 'int', 'allow_empty' => false, 'default' => 0, 'regex' => '^[0-9]+$'] ]; $params_new = []; @@ -626,7 +631,9 @@ function aggregate_validate_graph_params(array $posted, bool $has_override = fal } // validate value - if ($defs['type'] == 'bool') { + if (in_array($field, ['vertical_label_angle', 'right_axis_label_angle', 'right_axis_range'], true)) { + $params_new[$field] = CactiValidator::validateRrdGraphAxisInput($posted[$field] ?? '', $field); + } elseif ($defs['type'] == 'bool') { $params_new[$field] = (isset($posted[$field])) ? 'on' : ''; } else { $params_new[$field] = (isset($posted[$field]) ? form_input_validate(htmle($posted[$field]), $field, $defs['regex'], $defs['allow_empty'], 3) : $defs['default']); diff --git a/lib/rrd.php b/lib/rrd.php index 7843cb322..34ecd91b5 100644 --- a/lib/rrd.php +++ b/lib/rrd.php @@ -23,6 +23,7 @@ */ require_once(__DIR__ . '/rrd_graph_item.php'); +require_once(__DIR__ . '/rrd_graph_axis.php'); use phpseclib4\Crypt\Rijndael; use phpseclib4\Crypt\RSA; @@ -2174,6 +2175,8 @@ function rrd_function_process_graph_options(int $graph_start, int $graph_end, ar } } + $graph_opts .= rrd_graph_axis_options($graph, $version); + foreach ($graph as $key => $value) { if (is_string($value)) { $value = rrdtool_resolve_graph_text($value, $graph); @@ -2557,12 +2560,13 @@ function rrdtool_function_graph(int $local_graph_id, mixed $rra_id, array $graph } $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, + gl.snmp_query_id, gl.snmp_index, gtg.title_cache, gtg.vertical_label, gtg.vertical_label_angle, gtg.slope_mode, gtg.auto_scale, gtg.auto_scale_opts, gtg.auto_scale_log, gtg.scale_log_units, gtg.auto_scale_rigid, gtg.auto_padding, gtg.base_value, gtg.upper_limit, gtg.lower_limit, gtg.height, gtg.width, gtg.image_format_id, gtg.unit_value, gtg.unit_exponent_value, gtg.alt_y_grid, - gtg.right_axis, gtg.right_axis_label, gtg.right_axis_format, gtg.no_gridfit, + gtg.right_axis, gtg.right_axis_label, gtg.right_axis_label_angle, + gtg.right_axis_range, gtg.right_axis_format, gtg.no_gridfit, gtg.unit_length, gtg.tab_width, gtg.dynamic_labels, gtg.force_rules_legend, gtg.legend_position, gtg.legend_direction, gtg.right_axis_formatter, gtg.left_axis_format, gtg.left_axis_formatter, diff --git a/lib/rrd_graph_axis.php b/lib/rrd_graph_axis.php new file mode 100644 index 000000000..dddf9eca1 --- /dev/null +++ b/lib/rrd_graph_axis.php @@ -0,0 +1,81 @@ + 64) { + return false; + } + + $number = '-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+\-]?[0-9]+)?'; + + if (!preg_match('/\A(' . $number . ')?:(' . $number . ')?\z/', $value)) { + return false; + } + + [$minimum, $maximum] = explode(':', $value); + + if ($minimum === '' && $maximum === '') { + return false; + } + + foreach ([$minimum, $maximum] as $bound) { + if ($bound !== '' && !is_finite((float) $bound)) { + return false; + } + } + + return $minimum === '' || $maximum === '' || (float) $minimum <= (float) $maximum; +} + +/** Blank defaults preserve RRDtool's layout; older releases omit these flags. */ +function rrd_graph_axis_options(array $graph, string $version) : string { + if (version_compare($version, '1.11.0', '<')) { + return ''; + } + + $options = ''; + + foreach (['vertical_label_angle', 'right_axis_label_angle', 'right_axis_range'] as $field) { + $value = $graph[$field] ?? ''; + + if ($value !== '' && rrd_graph_axis_option_is_valid($field, $value)) { + $options .= '--' . str_replace('_', '-', $field) . ' ' . cacti_escapeshellarg($value) . RRD_NL; + } + } + + return $options; +} diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 9549b63cd..35c27df51 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -25463,3 +25463,35 @@ msgstr "" #: lib/Auth/LocalAuthLoginProvider.php:164 msgid "Access Denied! Login Failed." msgstr "" + +#: include/global_form.php +msgid "Vertical Label Angle (--vertical-label-angle)" +msgstr "" + +#: include/global_form.php +msgid "RRDtool Default" +msgstr "" + +#: include/global_form.php +msgid "Label rotation in degrees counter clockwise. Requires RRDtool 1.11 or newer. Blank uses the RRDtool default. Angles other than 90 and 270 may overlap nearby content." +msgstr "" + +#: include/global_form.php +msgid "Right Axis Label Angle (--right-axis-label-angle)" +msgstr "" + +#: include/global_form.php +msgid "Right label rotation in degrees counter clockwise. Requires RRDtool 1.11 or newer. Blank follows the vertical label angle. Angles other than 90 and 270 may overlap nearby content." +msgstr "" + +#: include/global_form.php +msgid "Right Axis Label Range (--right-axis-range)" +msgstr "" + +#: include/global_form.php +msgid "Only show right axis labels within min:max, for example 0:100. Either bound may be blank. This does not change the axis scale. Requires RRDtool 1.11 or newer. Blank leaves labels unrestricted." +msgstr "" + +#: lib/CactiValidator.php +msgid "Invalid RRDtool axis option." +msgstr "" diff --git a/tests/Unit/Core/Rrd/RrdAxisOptionsTest.php b/tests/Unit/Core/Rrd/RrdAxisOptionsTest.php new file mode 100644 index 000000000..093a9d935 --- /dev/null +++ b/tests/Unit/Core/Rrd/RrdAxisOptionsTest.php @@ -0,0 +1,106 @@ + '90', 'right_axis_label_angle' => '270', 'right_axis_range' => '0:100']; + + foreach (['1.5.0', '1.9.0', '1.10.0'] as $version) { + expect(rrd_graph_axis_options($fields, $version))->toBe(''); + } + expect(rrd_graph_axis_options([], '1.11.0'))->toBe('') + ->and(rrd_graph_axis_options(array_fill_keys(array_keys($fields), ''), '1.11.0'))->toBe(''); +}); + +test('new and future versions emit independent angles including zero', function () { + foreach (['1.11.0', '1.12.0'] as $version) { + $options = rrd_graph_axis_options(['vertical_label_angle' => '0', 'right_axis_label_angle' => '270', 'right_axis_range' => '0:100'], $version); + expect($options)->toBe('--vertical-label-angle ' . cacti_escapeshellarg('0') . RRD_NL . + '--right-axis-label-angle ' . cacti_escapeshellarg('270') . RRD_NL . + '--right-axis-range ' . cacti_escapeshellarg('0:100') . RRD_NL); + } +}); + +test('right axis bounds accept open endpoints and finite numeric values', function (string $range) { + expect(rrd_graph_axis_option_is_valid('right_axis_range', $range))->toBeTrue(); +})->with(['', '0:100', '0:', ':100', '-2.5:3.25', '-1e2:1e2', '.5:1.', '0:0']); + +test('invalid imported axis values cannot reach the graph command', function (string $field, mixed $value) { + expect(rrd_graph_axis_option_is_valid($field, $value))->toBeFalse() + ->and(rrd_graph_axis_options([$field => $value], '1.11.0'))->toBe(''); +})->with([ + ['vertical_label_angle', '45'], ['right_axis_label_angle', '270;touch /tmp/axis'], + ['vertical_label_angle', ['90']], ['right_axis_range', '100:0'], + ['right_axis_range', ':'], ['right_axis_range', 'NaN:100'], + ['right_axis_range', '0:INF'], ['right_axis_range', '1e999:'], + ['right_axis_range', "0:100\n"], ['right_axis_range', '0:100 --title injected'], + ['right_axis_range', str_repeat('1', 65) . ':'], +]); + +test('graph rendering includes the new options only at the configured capability level', function () { + global $image_types; + $image_types = [1 => 'PNG']; + $graph = [ + 'auto_scale' => '', 'upper_limit' => '', 'lower_limit' => '', + 'auto_scale_log' => '', 'scale_log_units' => '', 'auto_scale_rigid' => '', + 'unit_value' => '', 'unit_exponent_value' => '', 'height' => '100', 'width' => '200', + 'image_format_id' => 1, 'vertical_label_angle' => '90', + 'right_axis_label_angle' => '270', 'right_axis_range' => ':100', + ]; + $data = []; + + foreach (['1.10.0', '1.11.0'] as $version) { + $GLOBALS['axis_fixture_version'] = $version; + $options = CactiAxisFixture\rrd_function_process_graph_options(1700000000, 1700003600, $graph, $data); + + foreach (['vertical-label-angle', 'right-axis-label-angle', 'right-axis-range'] as $flag) { + expect(substr_count($options, '--' . $flag . ' '))->toBe($version === '1.11.0' ? 1 : 0); + } + } +}); + +test('aggregate overrides retain zero and reject invalid axis values', function () { + $_SESSION[SESS_ERROR_FIELDS] = []; + $posted = ['t_vertical_label_angle' => 'on', 'vertical_label_angle' => '0', + 't_right_axis_range' => 'on', 'right_axis_range' => '100:0']; + $params = aggregate_validate_graph_params($posted, true); + expect($params['vertical_label_angle'])->toBe('0') + ->and($params['t_vertical_label_angle'])->toBe('on') + ->and($params['right_axis_label_angle'])->toBe('') + ->and($params['t_right_axis_label_angle'])->toBe('') + ->and($_SESSION[SESS_ERROR_FIELDS])->toHaveKey('right_axis_range'); + unset($_SESSION[SESS_ERROR_FIELDS], $_SESSION[SESS_FIELD_VALUES], $_SESSION['messages']); +}); + +test('new template fields propagate while per graph overrides remain intact', function () { + global $struct_graph; + $previous = $struct_graph ?? null; + require dirname(__DIR__, 4) . '/include/global_arrays.php'; + require dirname(__DIR__, 4) . '/include/global_form.php'; + $GLOBALS['axis_fixture_template'] = [ + 'id' => 42, 'graph_template_id' => 7, + 't_vertical_label_angle' => '', 'vertical_label_angle' => '90', + 't_right_axis_label_angle' => 'on', 'right_axis_label_angle' => '270', + 't_right_axis_range' => '', 'right_axis_range' => '0:100', + ]; + $GLOBALS['axis_fixture_updates'] = []; + + try { + expect(CactiAxisFixture\push_out_graph(42, false))->toBeTrue(); + $updates = $GLOBALS['axis_fixture_updates']; + expect($updates)->toHaveCount(2) + ->and($updates[0][0])->toContain('SET vertical_label_angle = ?') + ->and($updates[0][1])->toBe(['90', 42]) + ->and($updates[1][0])->toContain('SET right_axis_range = ?') + ->and($updates[1][1])->toBe(['0:100', 42]); + } finally { + $struct_graph = $previous; + unset($GLOBALS['axis_fixture_template'], $GLOBALS['axis_fixture_updates']); + } +}); diff --git a/tests/fixtures/Core/Rrd/AxisOptions.php b/tests/fixtures/Core/Rrd/AxisOptions.php new file mode 100644 index 000000000..5b828275f --- /dev/null +++ b/tests/fixtures/Core/Rrd/AxisOptions.php @@ -0,0 +1,34 @@ +