mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
* test(cdef): enforce resolver coverage Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(cdef): pin cache action correctly Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: isolate CDEF coverage bootstrap * fix(cdef): fail closed on invalid recursive definitions Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): log corrupt resolver records Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): bound and isolate recursive resolution Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): preserve resolver failure state Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): isolate invalid graph expressions Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): abort unsafe consumer operations Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): close consumer failure paths Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): make aggregate updates atomic Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): keep repair queries auditable Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): bind bulk deletes and escape item types Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cdef): guard corrupt types and repair reporting Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(aggregate): propagate push-out failures Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(aggregate): document mutable message checks Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
277 lines
8.8 KiB
PHP
277 lines
8.8 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/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
/**
|
|
* Retrieves the name of a CDEF item based on its ID.
|
|
*
|
|
* This function fetches the type and value of a CDEF item from the database and returns
|
|
* the corresponding name or value based on the item's type.
|
|
*
|
|
* @param int $cdef_item_id The ID of the CDEF item.
|
|
*
|
|
* @return string|null The name/value, or null when the stored item is invalid.
|
|
*/
|
|
function get_cdef_item_name(int $cdef_item_id) : ?string {
|
|
global $cdef_functions, $cdef_operators;
|
|
|
|
$cdef_item = db_fetch_row_prepared('SELECT type, value FROM cdef_items WHERE id = ?', [$cdef_item_id]);
|
|
|
|
if (!is_array($cdef_item) || !array_key_exists('type', $cdef_item) || !array_key_exists('value', $cdef_item)) {
|
|
cacti_log(sprintf('ERROR: CDEF item %d is missing or corrupt.', $cdef_item_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
$current_cdef_value = $cdef_item['value'];
|
|
|
|
switch ($cdef_item['type']) {
|
|
case '1':
|
|
if (!isset($cdef_functions[$current_cdef_value])) {
|
|
cacti_log(sprintf('ERROR: CDEF item %d references an unknown function.', $cdef_item_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
return (string) $cdef_functions[$current_cdef_value];
|
|
case '2':
|
|
if (!isset($cdef_operators[$current_cdef_value])) {
|
|
cacti_log(sprintf('ERROR: CDEF item %d references an unknown operator.', $cdef_item_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
return (string) $cdef_operators[$current_cdef_value];
|
|
case '4':
|
|
return (string) $current_cdef_value;
|
|
case '5':
|
|
$cdef_name = db_fetch_cell_prepared('SELECT name FROM cdef WHERE id = ?', [$current_cdef_value]);
|
|
|
|
if ($cdef_name === false || $cdef_name === null || $cdef_name === '') {
|
|
cacti_log(sprintf('ERROR: CDEF item %d references a missing definition.', $cdef_item_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
return (string) $cdef_name;
|
|
case '6':
|
|
return (string) $current_cdef_value;
|
|
}
|
|
|
|
cacti_log(sprintf('ERROR: CDEF item %d has an unknown type.', $cdef_item_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
/**
|
|
* Resolves an entire CDEF into its text-based representation for use in the RRDtool 'graph'
|
|
* string. this name will be resolved recursively if necessary
|
|
*
|
|
* This function fetches the CDEF items associated with the provided CDEF ID from the database,
|
|
* constructs the CDEF string by iterating through the items, and handles nested CDEFs recursively.
|
|
*
|
|
* @param int $cdef_id The ID of the CDEF to retrieve.
|
|
*
|
|
* @return string|null The constructed CDEF string, or null on invalid data.
|
|
*/
|
|
function get_cdef(int $cdef_id) : ?string {
|
|
$visited = [];
|
|
$expansion = 0;
|
|
$cache = [];
|
|
$cache_bytes = 0;
|
|
|
|
return get_cdef_recursive($cdef_id, $visited, $expansion, $cache, $cache_bytes);
|
|
}
|
|
|
|
/**
|
|
* Resolves nested CDEFs while rejecting cycles and unreasonable depth.
|
|
*
|
|
* @param int $cdef_id The ID of the CDEF to retrieve.
|
|
* @param array<int,true> $visited IDs active in the current recursion path.
|
|
* @param int $expansion Number of definitions expanded by this call.
|
|
* @param array<int,string> $cache Successfully resolved definitions.
|
|
* @param int $cache_bytes Total bytes retained by the resolver cache.
|
|
*
|
|
* @return string|null The CDEF string, or null when recursion is unsafe.
|
|
*/
|
|
function get_cdef_recursive(int $cdef_id, array &$visited, int &$expansion, array &$cache, int &$cache_bytes) : ?string {
|
|
if (isset($visited[$cdef_id])) {
|
|
cacti_log(sprintf('ERROR: CDEF %d contains a recursive cycle.', $cdef_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
if (array_key_exists($cdef_id, $cache)) {
|
|
return $cache[$cdef_id];
|
|
}
|
|
|
|
if (count($visited) >= 64) {
|
|
cacti_log(sprintf('ERROR: CDEF %d exceeds the resolver nesting depth.', $cdef_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
if (++$expansion > 4096) {
|
|
cacti_log(sprintf('ERROR: CDEF %d exceeds the resolver expansion budget.', $cdef_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
$visited[$cdef_id] = true;
|
|
$cdef_items = db_fetch_assoc_prepared('SELECT id, type, value FROM cdef_items WHERE cdef_id = ? ORDER BY sequence', [$cdef_id]);
|
|
|
|
if ($cdef_items === false) {
|
|
unset($visited[$cdef_id]);
|
|
cacti_log(sprintf('ERROR: Unable to load CDEF %d.', $cdef_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
if (cacti_sizeof($cdef_items) === 0) {
|
|
$cdef_exists = db_fetch_assoc_prepared('SELECT id FROM cdef WHERE id = ?', [$cdef_id]);
|
|
|
|
unset($visited[$cdef_id]);
|
|
|
|
if ($cdef_exists === false) {
|
|
cacti_log(sprintf('ERROR: Unable to verify CDEF %d.', $cdef_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
if (cacti_sizeof($cdef_exists) === 0) {
|
|
cacti_log(sprintf('ERROR: CDEF %d does not exist.', $cdef_id), false, 'CDEF');
|
|
|
|
return null;
|
|
}
|
|
|
|
$cache[$cdef_id] = '';
|
|
|
|
return $cache[$cdef_id];
|
|
}
|
|
|
|
$parts = [];
|
|
$length = 0;
|
|
|
|
foreach ($cdef_items as $cdef_item) {
|
|
if ($cdef_item['type'] == 5) {
|
|
$current_cdef_id = $cdef_item['value'];
|
|
$nested = get_cdef_recursive((int) $current_cdef_id, $visited, $expansion, $cache, $cache_bytes);
|
|
|
|
if ($nested === null) {
|
|
unset($visited[$cdef_id]);
|
|
|
|
return null;
|
|
}
|
|
|
|
if ($nested === '') {
|
|
cacti_log(sprintf('ERROR: CDEF %d references an empty definition.', $cdef_id), false, 'CDEF');
|
|
unset($visited[$cdef_id]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$length += strlen($nested) + ($parts === [] ? 0 : 1);
|
|
|
|
if ($length > 1048576) {
|
|
cacti_log(sprintf('ERROR: CDEF %d exceeds the resolver output budget.', $cdef_id), false, 'CDEF');
|
|
unset($visited[$cdef_id]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$parts[] = $nested;
|
|
} else {
|
|
$item_name = get_cdef_item_name($cdef_item['id']);
|
|
|
|
if ($item_name === null) {
|
|
unset($visited[$cdef_id]);
|
|
|
|
return null;
|
|
}
|
|
|
|
if ($item_name === '') {
|
|
cacti_log(sprintf('ERROR: CDEF %d contains an empty item.', $cdef_id), false, 'CDEF');
|
|
unset($visited[$cdef_id]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$length += strlen($item_name) + ($parts === [] ? 0 : 1);
|
|
|
|
if ($length > 1048576) {
|
|
cacti_log(sprintf('ERROR: CDEF %d exceeds the resolver output budget.', $cdef_id), false, 'CDEF');
|
|
unset($visited[$cdef_id]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$parts[] = $item_name;
|
|
}
|
|
}
|
|
|
|
unset($visited[$cdef_id]);
|
|
|
|
$resolved = implode(',', $parts);
|
|
|
|
if ($cache_bytes + strlen($resolved) <= 8388608) {
|
|
$cache_bytes += strlen($resolved);
|
|
$cache[$cdef_id] = $resolved;
|
|
}
|
|
|
|
return $resolved;
|
|
}
|
|
|
|
/**
|
|
* Determines whether deleting a CDEF would leave a live reference behind.
|
|
*
|
|
* References owned by another CDEF in the same delete request are ignored.
|
|
* Database errors fail closed so an uncertain dependency cannot be deleted.
|
|
*
|
|
* @param int $cdef_id CDEF being considered for deletion.
|
|
* @param array<int> $deleting_ids All CDEF IDs in the delete request.
|
|
*/
|
|
function cdef_is_in_use(int $cdef_id, array $deleting_ids = []) : bool {
|
|
foreach (['graph_templates_item', 'aggregate_graph_templates_item', 'aggregate_graphs_graph_item'] as $table) {
|
|
$references = db_fetch_assoc_prepared("SELECT cdef_id FROM $table WHERE cdef_id = ? LIMIT 1", [$cdef_id]);
|
|
|
|
if ($references === false || cacti_sizeof($references) > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$parents = db_fetch_assoc_prepared('SELECT DISTINCT cdef_id FROM cdef_items WHERE type = 5 AND value = ?', [$cdef_id]);
|
|
|
|
if ($parents === false) {
|
|
return true;
|
|
}
|
|
|
|
$deleting = array_fill_keys(array_map('intval', $deleting_ids), true);
|
|
|
|
foreach ($parents as $parent) {
|
|
if (!isset($deleting[(int) $parent['cdef_id']])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|