feat: expose RRDtool 1.11 graph axis controls (#8356)

* feat: expose RRDtool 1.11 graph axis controls

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix: constrain axis template inheritance checkboxes

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(rrd): exercise native aggregate validation and enforce coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: share changed-code coverage contracts and enforce shell policy

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(ci): run request coverage with portable path matching

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci: retain bootstrap coverage and validate request dispatch

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: synchronize native coverage fixture dispatch

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: measure coverage at the exact PR revision

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
Thomas Vincent authored and GitHub committed 2026-10-14 12:10:28 -04:00
1 parent 930b085703
commit 4d11070ced
12 files changed
+361 -34

No files matched your search

+12
View File
@@ -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 '',
+6
View File
@@ -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);
+3
View File
@@ -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);
+22
View File
@@ -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 &lt;format&gt;)'),
'method' => 'drop_sql',
+7
View File
@@ -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']);
+13
View File
@@ -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.
+39 -32
View File
@@ -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']);
+6 -2
View File
@@ -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,
+81
View File
@@ -0,0 +1,81 @@
<?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/ |
+-------------------------------------------------------------------------+
*/
/** Validate optional RRDtool 1.11 axis settings, including imported values. */
function rrd_graph_axis_option_is_valid(string $field, mixed $value) : bool {
if (!is_string($value)) {
return false;
}
if ($value === '') {
return true;
}
if ($field === 'vertical_label_angle' || $field === 'right_axis_label_angle') {
return in_array($value, ['0', '90', '180', '270'], true);
}
if ($field !== 'right_axis_range' || strlen($value) > 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;
}
+32
View File
@@ -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 ""
+106
View File
@@ -0,0 +1,106 @@
<?php
// Copyright (C) 2004-2026 The Cacti Group. GPL-2.0-or-later.
require_once dirname(__DIR__, 4) . '/lib/CactiValidator.php';
require_once dirname(__DIR__, 4) . '/lib/rrd.php';
require_once dirname(__DIR__, 4) . '/lib/api_aggregate.php';
require_once dirname(__DIR__, 3) . '/Helpers/SourceSlice.php';
require_once dirname(__DIR__, 3) . '/fixtures/Core/Rrd/AxisOptions.php';
cacti_fixture_source_function(dirname(__DIR__, 4) . '/lib/rrd.php', 'rrd_function_process_graph_options', 'CactiAxisFixture');
cacti_fixture_source_function(dirname(__DIR__, 4) . '/lib/template.php', 'push_out_graph', 'CactiAxisFixture');
test('axis defaults and older configured versions preserve graph commands', function () {
$fields = ['vertical_label_angle' => '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']);
}
});
+34
View File
@@ -0,0 +1,34 @@
<?php
// Copyright (C) 2004-2026 The Cacti Group. GPL-2.0-or-later.
namespace CactiAxisFixture;
// These boundaries are confined to this fixture namespace.
function get_rrdtool_version(): string {
return $GLOBALS['axis_fixture_version'];
}
function read_config_option(string $name): string {
return '';
}
function rrdtool_resolve_graph_text(string $value, array $graph): string {
return $value;
}
function rrdtool_function_format_graph_date(array $data): string {
return '';
}
function rrdtool_function_theme_font_options(array $data): string {
return '';
}
class CactiValidator extends \CactiValidator {
}
function db_fetch_row_prepared(string $sql, array $params): array {
return $GLOBALS['axis_fixture_template'];
}
function db_fetch_cell_prepared(string $sql, array $params): int {
return 1;
}
function db_execute_prepared(string $sql, array $params): bool {
$GLOBALS['axis_fixture_updates'][] = [$sql, $params];
return true;
}