Files
cacti/lib/api_tree.php
TheWitnessandTheWitness 3ab4c69fb7 PHPStan Level 8 - Phase 1 Conversion (#8135)
* PHPStan Level 8 - Phase 1 Conversion

Reduces the real (no-baseline) PHPStan level 8 error count from ~2,724 to 425
and regenerates phpstan-baseline.neon accordingly (8,941 -> 1,921 lines).

Core type-safety changes
- lib/database.php: narrow db_fetch_row()/db_fetch_row_prepared() to return
  array (was bool|array) and db_fetch_assoc()/db_fetch_assoc_prepared() to
  return array (was mixed); the _prepared workers coerce the connection-failure
  path to an empty array so the declared type is honest. empty()/cacti_sizeof()
  checks are unaffected.
- lib/database.php: narrow $db_conn guards from is_object() to instanceof PDO in
  the transaction/quote/insert-id helpers so the PDO calls resolve.
- lib/mib_cache.php: the fluent MibCache methods mib()/object()/table()/row()
  now return self instead of a bare object so chained calls type-check.

Type-safety casts and guards (mechanical, behaviour-preserving)
- Cast string|false/|null builtin results feeding string parameters
  (json_decode, base64_encode, trim, htmle, preg_replace, explode, etc.).
- Cast int|string / int|false values feeding int id/pid parameters.
- Guard resource|false fopen()/popen() handles before fwrite/fclose/fread, and
  wrap foreach over array|false/list|false results with `?: []`.

Genuine bugs found and fixed
- cli/add_tree.php / lib/api_tree.php: site tree nodes could not be created via
  the CLI - the --site-id option was never parsed, $nodeTypes had no 'site'
  entry (passing null to api_tree_item_save(int $type) -> TypeError), and
  api_tree_item_save() never persisted site_id. All three fixed.
- tree.php: leaves_exist() is typed : int but returned db_fetch_assoc_prepared()
  (an array of rows) for a SELECT COUNT(*); now uses db_fetch_cell_prepared()
  and returns the scalar count.
- lib/mib_cache.php: $entries_per_object was a float (division / ceil) used as
  an array key (PHP 8.1 implicit float-to-int key deprecation); now cast to int.

Config
- .phpstan.neon: add notIdentical.alwaysTrue and booleanAnd.alwaysFalse to the
  existing always-true/false ignore family (same "PHPStan gets confused"
  dynamic-constant cause already covered by identical.alwaysTrue,
  notEqual.alwaysTrue, booleanAnd.leftAlwaysTrue, etc.).
- phpstan-baseline.neon: regenerated; the remaining 425 entries are the
  untouched long tail for Phase 2.

* Remove redundant @phpstan-ignore comments

Follow-up cleanup for the level 8 Phase 1 work. Removes 14 inline
@phpstan-ignore comments that no longer suppress any reported error:

- cli/repair_database.php: `@phpstan-ignore booleanAnd.alwaysFalse` is now
  covered by the global booleanAnd.alwaysFalse ignore added to .phpstan.neon.
- 13 others (host.php, include/global.php, include/session.php,
  lib/api_automation.php x2, lib/functions.php x2, lib/html_utility.php x2,
  lib/installer.php, lib/package.php, lib/reports.php, lib/rrd.php) no longer
  match any reported error after the type-safety fixes (e.g. the
  db_fetch_row_prepared() return-type narrowing removed the offset-assign
  errors the api_automation ignores were suppressing).

Verified with reportUnmatchedIgnoredErrors enabled: 0 unmatched ignores
remain and the full level 8 run still reports 0 errors.

* ci: satisfy validator ratchet and refresh security sink baselines

- lib/api_tree.php: give the new tree-item site_id a real numeric
  constraint ('^[-0-9]+$', matching html_reports.php) instead of an
  empty regex, so the validator constraint ratchet passes without
  bumping its baseline.
- tests/security/baselines: refresh the three sink/hotspot signatures
  whose code text changed from the (string) casts added in this branch
  (lib/poller.php setsid, cli/rrdresize.php json_encode, lib/import.php
  simplexml_load_string).

* ci(js): allow DOMPurify 3.4.16 and update sync_js pin test

The dependabot bump (#8134) moved package.json/package-lock.json to
DOMPurify 3.4.16 on develop but left tests/js/sync_js.test.mjs pinned to
3.4.15, so the PR merge commit fails the 'DOMPurify is pinned' contract.
Bring package.json/package-lock.json to 3.4.16 (matching develop) and
update the pin test to expect 3.4.16.

* test: update sql.php null-handling assertion for the (string) cast

This branch changed scripts/sql.php from trim(\ ?? '') to
trim((string) \) (the phpstan-safe form that coerces both null and
false from shell_exec()). Update SqlScriptsTest's null-return assertion
to match the new cast instead of the removed '?? ''.

* fix: address Copilot review - ownership IDs, fopen guards, mib_cache bound

- chown()/chgrp(): pass integer UID/GID from fileowner()/filegroup()
  (guarding the false lookup) instead of string-casting them, which made
  them resolve account/group names like '1001' and fail. Covers
  lib/api_device.php, lib/boost.php and poller_maintenance.php (x2).
- fopen() failure now short-circuits instead of reporting success:
  lib/package.php returns false, lib/clog_webapi.php and utilities.php
  render an error, and snmpagent_mibcachechild.php skips the cache write
  and sentinel unlink so a concurrent writer's lock is preserved.
- lib/mib_cache.php: keep the float quotient as the loop bound and cast
  only the array offset, so a non-even entries/columns split no longer
  truncates the loop and drops a cached row.

* revert: keep db_fetch_* returning false on error; baseline Level 8 instead

Per review, restoring the intentional fail-safe: db_fetch_row[_prepared]()
return bool|array and db_fetch_assoc[_prepared]() return mixed again, so a
genuine SQL error (not just a missing connection) is signalled as false and
the === false checks in cdef_is_in_use(), boost_process_* and aggregate.php
work as before. Dropped the is_array() ? : [] coercion.

Level 8 stays green by regenerating phpstan-baseline.neon to capture the
re-introduced bool|array/mixed call-site errors (the ~1,850 the narrowing
had eliminated). Tightening those callers to drop them from the baseline is
Phase 2 work.

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
2026-10-02 10:08:56 -04:00

1293 lines
36 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/ |
+-------------------------------------------------------------------------+
*/
/**
* Locks a tree for editing
*
* @param int $tree_id The ID of the tree to lock.
* @param int $user_id The ID of the user performing the lock operation. Default is 0.
* @param bool $web Indicates if the operation is performed via the web interface. Default is true.
*
* @return void
*/
function api_tree_lock(int $tree_id, int $user_id = 0, bool $web = true) : void {
// ================= input validation =================
input_validate_input_number($tree_id, 'tree_id');
input_validate_input_number($user_id, 'user_id');
// ====================================================
db_execute_prepared('UPDATE graph_tree
SET `locked` = 1, locked_date = NOW(), last_modified = NOW(), modified_by = ?
WHERE id = ?',
[$user_id, $tree_id]);
}
/**
* Unlocks a locked tree that has been locked for editing
*
* @param int $tree_id The ID of the tree to unlock.
* @param int $user_id The ID of the user performing the unlock action. Default is 0.
* @param bool $web Indicates if the action is performed via the web interface. Default is true.
*
* @return void
*/
function api_tree_unlock(int $tree_id, int $user_id = 0, bool $web = true) : void {
// ================= input validation =================
input_validate_input_number($tree_id, 'tree_id');
input_validate_input_number($user_id, 'user_id');
// ====================================================
db_execute_prepared('UPDATE graph_tree
SET `locked` = 0, last_modified = NOW(), modified_by = ?
WHERE id = ?',
[$user_id, $tree_id]);
}
/**
* Copies a node within a tree to a new parent node at a specified position.
*
* @param int $tree_id The ID of the tree.
* @param int|string $node_id The ID of the node to copy.
* @param int|string $new_parent The ID of the new parent node.
* @param int $new_position The position under the new parent node.
*
* @return void
*/
function api_tree_copy_node(int $tree_id, int|string $node_id, int|string $new_parent, int $new_position) : void {
input_validate_input_number($tree_id, 'tree_id');
input_validate_input_number($new_position, 'new_position');
$data = api_tree_parse_node_data($node_id);
$pdata = api_tree_parse_node_data($new_parent);
if (!isset($data['host']) && !isset($data['graph'])) {
cacti_log('ERROR: Copy node requires either a host or a graph, Function copy_node', false);
return;
}
if (isset($data['host']) && ($data['host'] < 0 || !is_numeric($data['host']))) {
cacti_log('ERROR: Copy node host data invalid, Function copy_node', false);
return;
}
if (isset($data['site']) && ($data['site'] < 0 || !is_numeric($data['site']))) {
cacti_log('ERROR: Copy node site data invalid, Function copy_node', false);
return;
}
if (isset($data['graph']) && ($data['graph'] < 0 || !is_numeric($data['graph']))) {
cacti_log('ERROR: Copy node graph data invalid, Function copy_node', false);
return;
}
if (!isset($pdata['leaf_id']) || $pdata['leaf_id'] < 0 || !is_numeric($pdata['leaf_id'])) {
cacti_log('ERROR: Copy node parent data invalid, Function copy_node', false);
return;
}
// Check to see if the node already exists
$title = '';
if ($data['host'] > 0) {
$exists = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE parent = ?
AND graph_tree_id = ?
AND host_id = ?',
[$pdata['leaf_id'], $tree_id, $data['host']]);
if ($exists) {
print 'tbranch:' . $exists;
return;
}
} elseif ($data['graph'] > 0) {
$exists = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE parent = ?
AND graph_tree_id = ?
AND local_graph_id = ?',
[$pdata['leaf_id'], $tree_id, $data['graph']]);
if ($exists) {
print 'tbranch:' . $exists;
return;
}
} elseif ($data['site'] > 0) {
$exists = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE parent = ?
AND graph_tree_id = ?
AND site_id = ?',
[$pdata['leaf_id'], $tree_id, $data['site']]);
if ($exists) {
print 'tbranch:' . $exists;
return;
}
} else {
$title = db_fetch_cell_prepared('SELECT title
FROM graph_tree_items
WHERE id = ?', [$data['leaf_id']]);
}
$save = [];
$save['parent'] = $pdata['leaf_id'];
$save['position'] = $new_position;
$save['graph_tree_id'] = $tree_id;
$save['local_graph_id'] = $data['graph'];
$save['host_id'] = $data['host'];
$save['site_id'] = $data['site'];
$save['host_grouping_type'] = 1;
$save['sort_children_type'] = TREE_ORDERING_INHERIT;
$save['title'] = $title;
$id = sql_save($save, 'graph_tree_items');
api_tree_sort_branch($id, $tree_id);
/**
* Save the last time a tree branch was created/updated
* for Caching.
*/
set_config_option('time_last_change_branch', time());
header('Content-Type: application/json; charset=utf-8');
print json_encode(['id' => 'tbranch:' . $id]);
}
/**
* Given a lock name, placed a timed lock on the database.
* This function allows simulating transactions in an MyISAM database.
*
* @param string $lockname The name of the lock to acquire.
* @param int $timeout The timeout period in seconds for acquiring the lock. Default is 10 seconds.
*
* @return bool Returns true if the lock is acquired, false otherwise.
*/
function api_tree_get_lock(string $lockname, int $timeout = 10) : bool {
input_validate_input_number($timeout, 'timeout');
$lockname = sanitize_search_string($lockname);
$count = 0;
while ($count < 5) {
if (register_process_start('tree_lock', $lockname, 0, $timeout)) {
return true;
} else {
$count++;
sleep(1);
}
}
return false;
}
/**
* Releases a lock on a tree.
* This function allows simulating transactions in an MyISAM database.
*
* @param string $lockname The name of the lock to be released.
*
* @return void
*/
function api_tree_release_lock(string $lockname) : void {
unregister_process('tree_lock', $lockname, 0);
}
/**
* Given a tree, a destination leaf_id, order position, and title, create a branch/leaf
* Data is not returned, it is printed to the screen.
*
* @param int $tree_id The ID of the tree where the node will be created.
* @param mixed $node_id The ID of the node to be created.
* @param int $position The position of the node within the tree.
* @param string $title The title of the node. Defaults to 'New Branch' if not provided.
*
* @return bool
*/
function api_tree_create_node(int $tree_id, mixed $node_id, int $position, string $title = '') : bool {
input_validate_input_number($tree_id, 'tree_id');
input_validate_input_number($position, 'position');
// Reject an out-of-range TreeID before the permission check: is_tree_allowed()
// caches into $_SESSION and hits the database, so a 0/negative id should fail
// as bad input rather than as an authorization result.
if ($tree_id <= 0) {
cacti_log("ERROR: Invalid TreeID: '$tree_id', Function create_node", false);
return false;
}
if (!is_tree_allowed($tree_id)) {
cacti_log("SECURITY: User is not permitted to modify TreeID:'$tree_id', Function create_node", false, 'AUTH');
return false;
}
if ($title == '') {
$title = __('New Branch');
}
$data = api_tree_parse_node_data($node_id);
if ($data['leaf_id'] < 0) {
cacti_log("ERROR: Invalid BranchID: '" . $data['leaf_id'] . "', Function create_node", false);
return false;
}
$i = 0;
$found = false;
$orig = $title;
while (true) {
$title = $orig . ($found ? ' (' . $i . ')' : '');
$exists_id = api_tree_branch_exists($tree_id, $data['leaf_id'], $title);
if ($exists_id == false) {
break;
} else {
$found = true;
$i++;
}
}
// watch out for monkey business
input_validate_input_number($data['leaf_id'], 'leaf_id');
$save = [];
$save['parent'] = $data['leaf_id'];
$save['position'] = $position;
$save['graph_tree_id'] = $tree_id;
$save['local_graph_id'] = 0;
$save['host_id'] = 0;
$save['site_id'] = 0;
$save['host_grouping_type'] = 1;
$save['sort_children_type'] = TREE_ORDERING_INHERIT;
$save['title'] = $title;
$id = sql_save($save, 'graph_tree_items');
api_tree_sort_branch($id, $tree_id);
/**
* Save the last time a tree branch was created/updated
* for Caching.
*/
set_config_option('time_last_change_branch', time());
header('Content-Type: application/json; charset=utf-8');
print json_encode(['id' => 'tbranch:' . $id, 'text' => $title]);
return true;
}
/**
* Checks if a tree branch exists in the graph_tree_items table.
*
* @param int $tree_id The ID of the tree to check.
* @param int $parent The parent ID of the branch to check.
* @param string $title The title of the branch to check.
*
* @return mixed - The ID of the branch if it exists, or false if it does not.
*/
function api_tree_branch_exists(int $tree_id, int $parent, string $title) : mixed {
$id = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE graph_tree_id = ?
AND parent = ?
AND title = ?',
[$tree_id, $parent, $title]);
return $id > 0 ? $id : false;
}
/**
* Given a tree, parent branch, and a host_id, will check host on that branch
*
* @param int $tree_id The ID of the tree.
* @param int $parent The ID of the parent item.
* @param int $site_id The ID of the site.
*
* @return int|false The ID of the site if it exists, or false if it does not exist.
*/
function api_tree_site_exists(int $tree_id, int $parent, int $site_id) : int|false {
$id = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE graph_tree_id = ?
AND parent = ?
AND site_id = ?',
[$tree_id, $parent, $site_id]);
return $id > 0 ? $id : false;
}
/**
* Given a tree, parent branch, and a host_id, will check host on that branch
*
* @param int $tree_id The ID of the tree to check.
* @param int $parent The ID of the parent under which to check for the host.
* @param int $host_id The ID of the host to check for.
*
* @return int|false The ID of the host if it exists, or false if it does not.
*/
function api_tree_host_exists(int $tree_id, int $parent, int $host_id) : int|false {
$id = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE graph_tree_id = ?
AND parent = ?
AND host_id = ?',
[$tree_id, $parent, $host_id]);
return $id > 0 ? $id : false;
}
/**
* Given a tree, parent branch, and a local_graph_id, will check graph on that branch
*
* @param int $tree_id The ID of the tree to check.
* @param int $parent The parent ID within the tree.
* @param int $local_graph_id The local graph ID to check for.
*
* @return int|false The ID of the graph if it exists, or false if it does not.
*/
function api_tree_graph_exists(int $tree_id, int $parent, int $local_graph_id) : int|false {
$id = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE graph_tree_id = ?
AND parent = ?
AND local_graph_id = ?',
[$tree_id, $parent, $local_graph_id]);
return $id > 0 ? $id : false;
}
/**
* Given a tree and a branch/leaf, delete the node and it's content
*
* @param int $tree_id The ID of the tree from which the node will be deleted.
* @param int|string $node_id The ID of the node to be deleted.
*
* @return void
*/
function api_tree_delete_node(int $tree_id, int|string $node_id) : void {
input_validate_input_number($tree_id, 'tree_id');
// Basic Error Checking - reject an out-of-range TreeID before is_tree_allowed(),
// which caches into $_SESSION and hits the database.
if ($tree_id <= 0) {
cacti_log("ERROR: Invalid TreeID: '$tree_id', Function delete_node", false);
return;
}
if (!is_tree_allowed($tree_id)) {
cacti_log("SECURITY: User is not permitted to modify TreeID:'$tree_id', Function delete_node", false, 'AUTH');
return;
}
if (empty($node_id)) {
cacti_log("ERROR: Invalid NodeID: '$node_id', Function delete_node", false);
return;
}
$data = api_tree_parse_node_data($node_id);
if (isset($data['leaf_id']) && $data['leaf_id'] > 0) {
if ($data['host'] == 0 && $data['graph'] == 0 && $data['site'] == 0) {
api_tree_delete_node_content($tree_id, $data['leaf_id']);
}
db_execute_prepared('DELETE FROM graph_tree_items
WHERE graph_tree_id = ?
AND id = ?',
[$tree_id, $data['leaf_id']]);
/**
* Save the last time a tree branch was created/updated
* for Caching.
*/
set_config_option('time_last_change_branch', time());
}
}
/**
* Given a tree and a branch/leaf, recursively remove all elements
*
* @param int $tree_id The ID of the tree from which the node content is to be deleted.
* @param int $leaf_id The ID of the node whose content is to be deleted.
*
* @return void
*/
function api_tree_delete_node_content(int $tree_id, int $leaf_id) : void {
$children = db_fetch_assoc_prepared('SELECT *
FROM graph_tree_items
WHERE graph_tree_id = ? AND parent = ?', [$tree_id, $leaf_id]);
if (cacti_sizeof($children)) {
foreach ($children as $child) {
if ($child['host_id'] == 0 && $child['local_graph_id'] == 0) {
api_tree_delete_node_content($tree_id, $child['id']);
}
db_execute_prepared('DELETE
FROM graph_tree_items
WHERE graph_tree_id = ?
AND id = ?', [$tree_id, $child['id']]);
/**
* Save the last time a tree branch was created/updated
* for Caching.
*/
set_config_option('time_last_change_branch', time());
}
}
}
/**
* Moves a node to a new parent and position within a tree.
*
* @param int $tree_id The ID of the tree.
* @param int|string $node_id The ID of the node to move.
* @param int|string $new_parent The ID of the new parent node or '#' for the root.
* @param int $new_position The new position of the node within the new parent.
*
* @return void
*/
function api_tree_move_node(int $tree_id, int|string $node_id, int|string $new_parent, int $new_position) : void {
input_validate_input_number($tree_id, 'tree_id');
input_validate_input_number($new_position, 'new_position');
$new_position++;
// Basic Error Checking
if (empty($tree_id) || $tree_id < 0) {
cacti_log("ERROR: Invalid TreeID: '$tree_id', Function delete_node", false);
return;
}
if (empty($node_id)) {
cacti_log("ERROR: Invalid NodeID: '$node_id', Function move_node", false);
return;
}
if (empty($new_parent)) {
cacti_log("ERROR: Invalid Parent Node '$new_parent' for NodeID: '$node_id', Function move_node", false);
return;
}
if ($new_parent == '#') {
$pdata['leaf_id'] = 0;
} else {
$pdata = api_tree_parse_node_data($new_parent);
}
$data = api_tree_parse_node_data($node_id);
if ($data['parent'] != $pdata['leaf_id']) {
db_execute_prepared('UPDATE graph_tree_items
SET parent = ?, position = ?
WHERE id = ?
AND graph_tree_id = ?',
[$pdata['leaf_id'], $new_position, $data['leaf_id'], $tree_id]);
$others = db_fetch_assoc_prepared('SELECT id
FROM graph_tree_items
WHERE parent = ?
AND id != ?
AND position >= ?', [$pdata['leaf_id'], $data['leaf_id'], $new_position]);
$position = $new_position + 1;
if (cacti_sizeof($others)) {
foreach ($others as $other) {
db_execute_prepared('UPDATE graph_tree_items SET position = ? WHERE id = ?', [$position, $other['id']]);
$position++;
}
}
api_tree_sort_branch($data['leaf_id'], $tree_id);
} elseif (isset($data['leaf_id']) && $data['leaf_id'] > 0 && isset($pdata['leaf_id']) && $pdata['leaf_id'] >= 0) {
db_execute_prepared('UPDATE graph_tree_items
SET position = ?
WHERE graph_tree_id = ?
AND id = ?',
[$new_position, $tree_id, $data['leaf_id']]);
$others = db_fetch_assoc_prepared('SELECT id
FROM graph_tree_items
WHERE parent = ?
AND id != ? AND
position >= ?', [$pdata['leaf_id'], $data['leaf_id'], $new_position]);
$position = $new_position + 1;
if (cacti_sizeof($others)) {
foreach ($others as $other) {
db_execute_prepared('UPDATE graph_tree_items SET position = ? WHERE id = ?', [$position, $other['id']]);
$position++;
}
}
/**
* Save the last time a tree branch was created/updated
* for Caching.
*/
set_config_option('time_last_change_branch', time());
api_tree_sort_branch($data['leaf_id'], $tree_id);
} else {
cacti_log('Invalid Source Destination Branches, Function move_node', false);
}
return;
}
/**
* Given the node information parse into a branch, parent, host, graph array
*
* @param string $variable The variable containing the node data to be parsed. If the variable is '#', it is treated as a special case.
*
* @return array An associative array containing the parsed node data
*/
function api_tree_parse_node_data(string $variable) : array {
// Initialize some variables
$leaf_id = 0;
$graph_id = 0;
$host_id = 0;
$site_id = 0;
if ($variable != '#') {
// Process the 'id' variable
$ndata = explode('_', $variable);
if (cacti_sizeof($ndata)) {
foreach ($ndata as $data) {
[$type, $tid] = explode(':', $data);
// watch out for monkey business
input_validate_input_number($tid, 'tid');
switch ($type) {
case 'tbranch':
$leaf_id = $tid;
break;
case 'tgraph':
$graph_id = $tid;
break;
case 'thost':
$host_id = $tid;
break;
case 'tsite':
$site_id = $tid;
break;
}
}
}
}
if ($leaf_id > 0) {
$parent = db_fetch_cell_prepared('SELECT parent
FROM graph_tree_items
WHERE id = ?',
[$leaf_id]);
} else {
$parent = '0';
}
return ['leaf_id' => $leaf_id, 'graph' => $graph_id, 'host' => $host_id, 'site' => $site_id, 'parent' => $parent];
}
/**
* Renames a node in a tree.
*
* @param int $tree_id The ID of the tree.
* @param string|null $node_id The ID of the node to rename. Defaults to an empty string.
* @param string $title The new title for the node. Defaults to an empty string.
*
* @return void
*/
function api_tree_rename_node(int $tree_id, string|null $node_id = '', string $title = '') : void {
input_validate_input_number($tree_id, 'tree_id');
// Basic Error Checking - reject an out-of-range TreeID before is_tree_allowed(),
// which caches into $_SESSION and hits the database.
if ($tree_id <= 0) {
cacti_log("ERROR: Invalid TreeID: '" . $tree_id . "', Function rename_node", false);
header('Content-Type: application/json; charset=utf-8');
print json_encode(['id' => $node_id, 'result' => false]);
return;
}
if (!is_tree_allowed($tree_id)) {
cacti_log("SECURITY: User is not permitted to modify TreeID:'$tree_id', Function rename_node", false, 'AUTH');
return;
}
if (empty($node_id)) {
cacti_log("ERROR: Invalid NodeID: '" . $node_id . "', Function rename_node", false);
header('Content-Type: application/json; charset=utf-8');
print json_encode(['id' => $node_id, 'result' => 'false']);
return;
}
// Initialize some variables
$leaf_id = 0;
$graph_id = 0;
$host_id = 0;
$site_id = 0;
// Process the 'id' variable
$ndata = explode('_', $node_id);
if (cacti_sizeof($ndata)) {
foreach ($ndata as $data) {
if (!str_contains($data, ':')) {
cacti_log("ERROR: Invalid NodeID: '" . $node_id . "', Function rename_node", false);
header('Content-Type: application/json; charset=utf-8');
print json_encode(['id' => $node_id, 'result' => 'false']);
return;
}
[$type, $tid] = explode(':', $data);
// watch out for monkey business
input_validate_input_number($tid, 'tid');
switch ($type) {
case 'tbranch':
$leaf_id = $tid;
break;
case 'tsite':
$site_id = $tid;
break;
case 'tgraph':
$graph_id = $tid;
break;
case 'thost':
$host_id = $tid;
break;
}
}
}
if ($leaf_id > 0) {
if ($host_id > 0 || $graph_id > 0 || $site_id > 0) {
// Ignore. Need to customize context
} else {
db_execute_prepared('UPDATE graph_tree_items
SET title = ?
WHERE graph_tree_id = ?
AND id = ?', [$title, $tree_id, $leaf_id]);
}
}
header('Content-Type: application/json; charset=utf-8');
print json_encode(['id' => $node_id, 'result' => 'true']);
}
/**
* Given the tree and the parent node information return tree elements
*
* @param mixed $tree_id The ID of the tree to retrieve. Can be null.
* @param int $parent The parent node ID. Defaults to 0. If -1, it indicates the root node.
*
* @return void
*/
function api_tree_get_main(mixed $tree_id, int $parent = 0) : void {
$is_root = false;
if ($parent == -1) {
$parent = 0;
$is_root = true;
if ($tree_id > 0) {
$name = db_fetch_cell_prepared('SELECT name
FROM graph_tree
WHERE id = ?',
[$tree_id]);
print "<ul><li class='jstree-closed' id='tree_anchor-$tree_id' data-jstree='{ \"type\" : \"tree\" }'><a href='" . htmle('graph_view.php?action=tree&node=tree_anchor-' . $tree_id) . "'>" . htmle($name) . "</a>\n";
$hierarchy = draw_dhtml_tree_level_graphing($tree_id, $parent);
if (cacti_sizeof($hierarchy)) {
foreach ($hierarchy as $h) {
print $h;
}
}
} else {
$hierarchy = draw_dhtml_tree_level_graphing($tree_id, $parent);
if (cacti_sizeof($hierarchy)) {
foreach ($hierarchy as $h) {
print $h;
}
}
}
} else {
$hierarchy = draw_dhtml_tree_level_graphing($tree_id, $parent);
if (cacti_sizeof($hierarchy)) {
foreach ($hierarchy as $h) {
print $h;
}
}
}
if ($is_root) {
print "</li></ul>\n";
}
}
/**
* Given the tree and the node information return tree elements
*
* @param int $tree_id The ID of the tree.
* @param string $node_id The ID of the node. If the node ID is '#', the top-level hierarchy is fetched.
* @param bool $editing Whether the tree is being edited. Default is false.
*
* @return void
*/
function api_tree_get_node(int $tree_id, string $node_id, bool $editing = false) : void {
if ($node_id == '#') {
$hierarchy = draw_dhtml_tree_level($tree_id, 0, $editing);
} else {
$data = api_tree_parse_node_data($node_id);
$id = $data['leaf_id'];
input_validate_input_number($id, 'id');
$hierarchy = draw_dhtml_tree_level($tree_id, $id, $editing);
}
if (cacti_sizeof($hierarchy)) {
foreach ($hierarchy as $h) {
print $h;
}
}
}
/**
* Saves the tree object and then resorts the tree
*
* @param int $id The ID of the tree item.
* @param int $tree_id The ID of the tree.
* @param int $type The type of the tree item.
* @param int $parent_tree_item_id The ID of the parent tree item.
* @param string $title The title of the tree item.
* @param int $local_graph_id The ID of the local graph.
* @param int $host_id The ID of the host.
* @param int $site_id The ID of the site.
* @param int $host_grouping_type The type of host grouping.
* @param int $sort_children_type The type of sorting for children.
* @param bool $propagate_changes Whether to propagate changes.
*
* @return int|false The ID of the saved tree item, or false if the item already exists.
*/
function api_tree_item_save(int $id, int $tree_id, int $type, int $parent_tree_item_id, string $title, int $local_graph_id,
int $host_id, int $site_id, int $host_grouping_type, int $sort_children_type, bool $propagate_changes) : int|false {
input_validate_input_number($tree_id, 'tree_id');
input_validate_input_number($parent_tree_item_id, 'parent_tree_item_id');
// api_tree_get_lock('tree-lock', 10);
if ($local_graph_id > 0) {
$exists = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE local_graph_id = ?
AND parent = ?
AND graph_tree_id = ?',
[$local_graph_id, $parent_tree_item_id, $tree_id]);
if ($exists) {
return false;
}
} elseif ($site_id > 0) {
$exists = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE site_id = ?
AND parent = ?
AND graph_tree_id = ?',
[$site_id, $parent_tree_item_id, $tree_id]);
if ($exists) {
return false;
}
} elseif ($host_id > 0) {
$exists = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE host_id = ?
AND parent = ?
AND graph_tree_id = ?',
[$host_id, $parent_tree_item_id, $tree_id]);
if ($exists) {
return false;
}
}
$save['id'] = $id;
$save['graph_tree_id'] = $tree_id;
$save['title'] = form_input_validate($title, 'title', '', ($type == TREE_ITEM_TYPE_HEADER ? false : true), 3);
$save['parent'] = $parent_tree_item_id;
$save['local_graph_id'] = form_input_validate($local_graph_id, 'local_graph_id', '', true, 3);
$save['host_id'] = form_input_validate($host_id, 'host_id', '', true, 3);
$save['site_id'] = form_input_validate($site_id, 'site_id', '^[-0-9]+$', true, 3);
$save['host_grouping_type'] = form_input_validate($host_grouping_type, 'host_grouping_type', '', true, 3);
$save['sort_children_type'] = form_input_validate($sort_children_type, 'sort_children_type', '', true, 3);
$tree_item_id = 0;
if (!is_error_message()) {
$tree_item_id = sql_save($save, 'graph_tree_items');
if ($tree_item_id) {
raise_message(1);
api_tree_sort_branch($tree_item_id, $tree_id);
} else {
raise_message(2);
}
if (empty($save['id'])) {
/**
* Save the last time a tree branch was created/updated
* for Caching.
*/
set_config_option('time_last_change_branch', time());
}
}
// api_tree_release_lock('tree-lock');
return $tree_item_id;
}
/**
* Retrieves the type of a tree item based on its ID.
*
* @param int $tree_item_id The ID of the tree item to check.
*
* @return string The type of the tree item.
*/
function api_tree_get_item_type(int $tree_item_id) : string {
$tree_item = db_fetch_row_prepared('SELECT title, local_graph_id, site_id, host_id
FROM graph_tree_items
WHERE id = ?',
[$tree_item_id]);
if (!cacti_sizeof($tree_item)) {
return '';
}
if ($tree_item['local_graph_id'] > 0) {
return 'graph';
}
if ($tree_item['site_id'] > 0) {
return 'site';
}
if ($tree_item['host_id'] > 0) {
return 'host';
}
if ($tree_item['title'] != '') {
return 'header';
}
return '';
}
/**
* Callback to naturally sort an array
* This function is used to sort graphs and trees.
*
* @deprecated
*
* @param array $a The first graph data array to compare.
* @param array $b The second graph data array to compare.
*
* @return int Returns < 0 if $a is less than $b; > 0 if $a is greater than $b, and 0 if they are equal.
*/
function naturally_sort_graphs(array $a, array $b) : int {
return strnatcasecmp($a['title_cache'], $b['title_cache']);
}
/**
* Determine the ordering of any elements owning leaf
* This function is to assist with ordering tree items
*
* @param int $leaf_id The ID of the leaf node for which to retrieve the branch ordering type.
*
* @return int The branch ordering type. Returns 1 if the leaf node is not found.
*/
function api_tree_get_branch_ordering(int $leaf_id) : int {
$leaf = db_fetch_row_prepared('SELECT sort_children_type, parent, graph_tree_id FROM graph_tree_items WHERE id = ?', [$leaf_id]);
if (cacti_sizeof($leaf)) {
if ($leaf['sort_children_type'] == 0) {
$parent = $leaf['parent'];
if ($parent > 0) {
return api_tree_get_branch_ordering($parent);
} else {
return (int) db_fetch_cell_prepared('SELECT sort_type FROM graph_tree WHERE id = ?', [$leaf['graph_tree_id']]);
}
} else {
return $leaf['sort_children_type'];
}
} else {
return 1;
}
}
/**
* Determine the name of a branch leaf
* This function is to assist with editing trees
*
* @param int $tree_id The ID of the tree.
* @param int $leaf_id The ID of the leaf within the tree.
*
* @return string The title of the branch.
*/
function api_tree_get_branch_name(int $tree_id, int $leaf_id) : string {
return db_fetch_cell_prepared('SELECT title FROM graph_tree_items WHERE graph_tree_id = ? AND id = ?', [$tree_id, $leaf_id]);
}
/**
* Given a tree, parent, and title return the leaf_id
*
* @param int $tree_id The ID of the tree.
* @param int $parent The parent ID within the tree.
* @param string $title The title of the branch.
*
* @return int|null The ID of the branch if found, or null if not found.
*/
function api_tree_get_branch_id(int $tree_id, int $parent, string $title) : int|null {
return db_fetch_cell_prepared('SELECT id FROM graph_tree_items WHERE graph_tree_id = ? AND parent = ? AND title = ?', [$tree_id, $parent, $title]);
}
/**
* Sorts a branch based upon sorting rules.
* The sorting order is as follows: Tree Branches, Devices, Graphs.
*
* @param int|string $leaf_id The ID of the leaf or a string representing the node data.
* @param int $tree_id The ID of the tree. Default is 0.
* @param bool $lock Whether to lock the tree during sorting. Default is true.
*
* @return void
*/
function api_tree_sort_branch(int|string $leaf_id, int $tree_id = 0, bool $lock = true) : void {
static $level = 1;
if ($lock) {
// api_tree_get_lock('tree-lock', 10);
}
// Sorting will go in this order for anyone sorting:
// Tree Branches go first, then Devices, then Graphs
$sequence = 1;
if (!is_numeric($leaf_id)) {
$data = api_tree_parse_node_data($leaf_id);
$leaf_id = $data['leaf_id'];
}
if ($leaf_id > 0) {
$pdata = db_fetch_row_prepared('SELECT parent, graph_tree_id
FROM graph_tree_items
WHERE id = ?',
[$leaf_id]);
$parent = $pdata['parent'];
$tree_id = $pdata['graph_tree_id'];
} elseif ($tree_id > 0) {
$parent = 0;
} else {
cacti_log('Error Sorting Tree');
return;
}
if ($parent > 0) {
$sort_style = api_tree_get_branch_ordering($parent);
} else {
$sort_style = db_fetch_cell_prepared('SELECT sort_type
FROM graph_tree
WHERE id = ?',
[$tree_id]);
}
if ($sort_style == TREE_ORDERING_ALPHABETIC) {
$order_by = 'ORDER BY title';
} else {
$order_by = 'ORDER BY position';
}
$sort_array = array_rekey(
db_fetch_assoc_prepared('SELECT id, title
FROM graph_tree_items AS gti
WHERE parent = ?
AND graph_tree_id = ?
AND local_graph_id = 0
AND host_id = 0
AND site_id = 0 ' . $order_by,
[$parent, $tree_id]),
'id', 'title'
);
if (cacti_sizeof($sort_array)) {
if ($sort_style == TREE_ORDERING_NUMERIC) {
asort($sort_array, SORT_NUMERIC);
} elseif ($sort_style == TREE_ORDERING_ALPHABETIC) {
// Let's let the database do it!
} elseif ($sort_style == TREE_ORDERING_NATURAL) {
if (defined('SORT_FLAG_CASE')) {
asort($sort_array, SORT_NATURAL | SORT_FLAG_CASE);
} else {
natcasesort($sort_array);
}
}
foreach ($sort_array as $id => $element) {
$sort = db_fetch_cell_prepared('SELECT sort_children_type
FROM graph_tree_items
WHERE id = ?',
[$id]);
if ($sort == TREE_ORDERING_INHERIT) {
$first_child = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE parent = ?
ORDER BY position
LIMIT 1', [$id]);
if (!empty($first_child)) {
$level++;
api_tree_sort_branch($first_child, $tree_id, false);
}
}
db_execute_prepared('UPDATE graph_tree_items
SET position = ?
WHERE id = ?',
[$sequence, $id]);
$sequence++;
}
}
if ($sort_style == TREE_ORDERING_ALPHABETIC) {
$order_by = 'ORDER BY s.name';
} else {
$order_by = 'ORDER BY position';
}
$sort_array = array_rekey(
db_fetch_assoc_prepared('SELECT s.name, gti.id
FROM graph_tree_items AS gti
INNER JOIN sites AS s
ON s.id = gti.site_id
WHERE parent = ?
AND graph_tree_id = ?
AND local_graph_id = 0
AND site_id > 0 ' . $order_by,
[$parent, $tree_id]),
'id', 'name'
);
if (cacti_sizeof($sort_array)) {
if ($sort_style == TREE_ORDERING_NUMERIC) {
asort($sort_array, SORT_NUMERIC);
} elseif ($sort_style == TREE_ORDERING_ALPHABETIC) {
// Let's let the database do it!
} elseif ($sort_style == TREE_ORDERING_NATURAL) {
if (defined('SORT_FLAG_CASE')) {
asort($sort_array, SORT_NATURAL | SORT_FLAG_CASE);
} else {
natcasesort($sort_array);
}
}
foreach ($sort_array as $id => $element) {
$sort = db_fetch_cell_prepared('SELECT sort_children_type
FROM graph_tree_items
WHERE id = ?',
[$id]);
if ($sort == TREE_ORDERING_INHERIT) {
$first_child = db_fetch_cell_prepared('SELECT id
FROM graph_tree_items
WHERE parent = ?
ORDER BY position
LIMIT 1',
[$id]);
if (!empty($first_child)) {
$level++;
api_tree_sort_branch($first_child, $tree_id, false);
}
}
db_execute_prepared('UPDATE graph_tree_items
SET position = ?
WHERE id = ?',
[$sequence, $id]);
$sequence++;
}
}
if ($sort_style == TREE_ORDERING_ALPHABETIC) {
$order_by = 'ORDER BY h.description';
} else {
$order_by = 'ORDER BY position';
}
$sort_array = array_rekey(
db_fetch_assoc_prepared('SELECT h.description, gti.id
FROM graph_tree_items AS gti
INNER JOIN host AS h
ON h.id=gti.host_id
WHERE parent = ?
AND graph_tree_id = ?
AND host_id > 0 ' . $order_by,
[$parent, $tree_id]),
'id', 'description'
);
if (cacti_sizeof($sort_array)) {
if ($sort_style == TREE_ORDERING_NUMERIC) {
asort($sort_array, SORT_NUMERIC);
} elseif ($sort_style == TREE_ORDERING_ALPHABETIC) {
// Let's let the database do it!
} elseif ($sort_style == TREE_ORDERING_NATURAL) {
if (defined('SORT_FLAG_CASE')) {
asort($sort_array, SORT_NATURAL | SORT_FLAG_CASE);
} else {
natcasesort($sort_array);
}
}
foreach ($sort_array as $id => $element) {
db_execute_prepared('UPDATE graph_tree_items
SET position = ?
WHERE id = ?',
[$sequence, $id]);
$sequence++;
}
}
if ($sort_style == TREE_ORDERING_ALPHABETIC) {
$order_by = 'ORDER BY gtg.title_cache';
} else {
$order_by = 'ORDER BY position';
}
$sort_array = array_rekey(
db_fetch_assoc_prepared('SELECT gtg.title_cache, gti.id
FROM graph_tree_items AS gti
INNER JOIN graph_templates_graph AS gtg
ON gtg.local_graph_id=gti.local_graph_id
WHERE parent = ?
AND graph_tree_id = ?
AND gti.local_graph_id > 0 ' . $order_by,
[$parent, $tree_id]),
'id', 'title_cache'
);
if (cacti_sizeof($sort_array)) {
if ($sort_style == TREE_ORDERING_NUMERIC) {
asort($sort_array, SORT_NUMERIC);
} elseif ($sort_style == TREE_ORDERING_ALPHABETIC) {
// Let's let the database do it!
} elseif ($sort_style == TREE_ORDERING_NATURAL) {
if (defined('SORT_FLAG_CASE')) {
asort($sort_array, SORT_NATURAL | SORT_FLAG_CASE);
} else {
natcasesort($sort_array);
}
}
foreach ($sort_array as $id => $element) {
db_execute_prepared('UPDATE graph_tree_items
SET position = ?
WHERE id = ?',
[$sequence, $id]);
$sequence++;
}
}
if ($lock) {
// api_tree_release_lock('tree-lock');
}
}
/**
* Retrieves the maximum sequence number from the graph_tree table.
*
* @return int The maximum sequence number, or 0 if no sequences are found.
*/
function api_tree_get_max_sequence() : int {
$max_seq = db_fetch_cell('SELECT MAX(sequence) FROM graph_tree');
if ($max_seq == null) {
return 0;
}
return $max_seq;
}