Files
cacti/lib/data_query.php
T
1e5329dd89 security: consolidate verified GHSA-rjvj win32 cmd.exe hardening for develop (#8056)
* fix(win32): harden the cmd.exe shell sinks on develop (GHSA-rjvj-r52f-8v5q)

Forward-port of the 1.2.x win32 command-injection remediation. On Windows the
SNMP/ping/data-query/trap commands run through cmd.exe, which ignores the \"
escape and toggles quoting on every ", so cacti_escapeshellarg cannot stop
& | ^ < > ( ).

- add cacti_escapeshellarg_cmd(), stripping those operators on win32 before
  quoting (a Unix pass-through)
- snmp_escape_string(): strip the operators in the win32 branch
- route the SNMP binary hostname, the data-query device values
  (get_script_query_path), the data-input arg builders, the Windows ping
  hostname, and the SNMP trap notification arguments through the strip

get_script_query_path is device-reachable (a rogue monitored device supplies an
SNMP index/system field that executes on the Windows poller); the rest are
post-auth.

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

* fix(win32): strip %VAR% from the hostname on Windows (GHSA-rjvj-r52f-8v5q)

cmd.exe expands %VAR% even inside quotes, so a device hostname of %USERNAME%
would expand into the SNMP/ping command line. Add an opt-in $strip_env flag to
cacti_escapeshellarg_cmd() and set it for the SNMP binary and ping hostnames,
which never legitimately contain a percent. Community and credential values are
left untouched because they can legitimately contain %.

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

* docs(changelog): note win32 cmd.exe hardening for develop

* security: require Package Management permission to trust new package signers

package_accept_key() (reachable via the repository-based package import flow)
inserted new keys into package_public_keys, the global trust store consulted
by import_validate_signature() for every future import by every user,
without checking for the Package Management (realm 29) permission that
gates package_keys.php/package.php/package_repos.php for the same table.
An Import Templates (realm 17) user could therefore add themselves as a
trusted signer for the whole install. Also add the 'accept' action to the
CSRF-protected bad_actions list so the acceptance step requires POST plus a
valid token like every other state-changing action.

GHSA-gxgm-8rhv-r7jv

* docs(changelog): note package-signer trust authorization fix

* security: close tab-based open redirect bypass in CSRF timeout handler

sanitize_uri()'s guard (20298c6cd) models the WHATWG rule that browsers
strip leading C0 controls/whitespace before resolving a redirect, but
Chromium separately strips an embedded tab from anywhere in the target.
A request target of /%09/evil.example passes the guard (no leading control
char, not two leading slashes) but Chromium reduces the resulting
'Location: /<TAB>/evil.example' header to a network-path reference and
navigates off-site. 1.2.x already uses validate_redirect_url() for this
same sink (3338a63e1), which resolves the URL via parse_url() and checks
host/port against the server-configured values, so it isn't bypassable this
way. Switch develop's CSRF timeout handler to match.

GHSA-c725-4jm5-9fx5

* security: authorize single-item Report actions against ownership

reports.php sets \ = true, so include/auth.php grants an
implicit guest session (when the Guest User feature is enabled) without
ever evaluating the realm 22 gate declared for this page. The single-item
action handlers reports_item_remove()/_moveup()/_movedown() relied solely
on that page-level gate and performed no independent ownership check, so an
unauthenticated guest-bound request could delete or reorder reports_items
rows belonging to any report, including ones owned by an administrator.
reports_form_actions() already gained an equivalent \
check; add the same authorization (Reports admin realm 21, or the
requesting user owns the item's report) to the three single-item handlers.

Same underlying chain as the 1.2.31 fix for reports_user.php (guest bind
removed + cacti_authorize_resource() added to reports_item_remove()); the
rewritten develop reports.php/lib/html_reports.php did not carry either
half of that fix forward.

GHSA-j5jh-fhwg-r56x

* security: authorize tree node CRUD actions against per-tree access

tree.php dispatches create_node/delete_node/move_node/rename_node/copy_node
directly to api_tree_*() with no is_tree_allowed() check, even though the
normal tree-view path already enforces that boundary. A non-admin user with
the Trees realm but no explicit grant on a given tree_id could still create,
delete, move, rename or copy nodes on that tree by calling these actions
directly, bypassing the authorization boundary enforced elsewhere.

GHSA-prxj-84qq-v5jq

* security: remove shell from Realtime RRD create and validate DS bounds

poller_realtime.php built the Realtime-graph RRD 'create' command line
(including the SNMP-sourced ifSpeed/ifHighSpeed interface speed, substituted
in verbatim as the DS maximum) and ran it through shell_exec(). A hostile or
spoofed monitored device could return an ifSpeed value such as
'1000000000\uid=197608(ladams) gid=197121 groups=197121' and get arbitrary OS command execution as the web/poller
user the next time any user with the Realtime Graphs permission (or an
unauthenticated guest, where configured) viewed a realtime graph for an
affected data source.

- lib/rrd.php: rrdtool_function_interface_speed() now casts ifHighSpeed/
  ifSpeed to int before they become the DS maximum.
- lib/rrd.php: rrdtool_function_create() now rejects any non-numeric
  rrd_minimum/rrd_maximum right before they are concatenated onto the
  rrdtool command line, forcing them to 'U' and logging instead.
- poller_realtime.php: the RRD create is now routed through the same
  shell-free rrdtool_execute()/proc_open(bypass_shell) path every other
  RRDtool invocation already uses, matching the pattern 1.2.x already uses
  for this exact call site, instead of shell_exec().

GHSA-f88m-q789-g22j

* security: authorize Report save/edit against report ownership

reports_form_save()'s update path fetched the existing report's owner from
the database but never compared it to the session user before calling
sql_save(), so any user holding only the Reports realm could overwrite
another user's report by POSTing its id. reports_edit() had the same gap on
the read side: it fetched a report by id with no ownership check, so the
same user could view any other user's report configuration (recipients,
schedule, etc.). 1.2.x already guards both call sites with
cacti_authorize_resource(..., 'reports'); develop had lost that check.

GHSA-8f5c-3h59-q7gc

* security: reject ORDER BY sort_column subquery bypass

sanitize_sql_column()'s allowlist regex kept parentheses to support the
INET_ATON(col)/NATURAL_SORT_KEY(col) sort wrappers, but that also let
MySQL subquery syntax through unchanged, since a subquery like
(SELECT(0)FROM(SELECT(SLEEP(5)))a) needs only alphanumerics and
parentheses. get_order_string() then detects the '(' and disables its
backtick quoting, so the payload reaches ORDER BY verbatim, giving any
user with realm 2-5 a blind, time-based SQL injection via sort_column.

sanitize_sql_column() now only allows '(' through when the whole value is
exactly one of the two known safe wrapper forms around a plain identifier;
anything else containing '(' -- including subquery syntax -- now falls
back to the default. Added regression tests covering the reported
subquery and REGEXP-exfiltration payloads.

GHSA-8j73-cvf6-jrvx

* security: confine Reports format_file to a bare filename

reports_form_save() wrote the POST-supplied format_file value verbatim
into reports.format_file, and reports_load_format_file() concatenated it
onto CACTI_PATH_FORMATS with no directory-component check, so a value
such as ../../../../etc/passwd escaped the formats directory and any
Reports-realm user could read arbitrary files readable by the web
server. 1.2.x already fixed the same sink (GHSA-g37j-39f4-6r4j /
CVE-2026-41884, GHSA-mjvw-mhj5-9jcj / CVE-2026-40084); develop had lost
the guard. format_file is now confined to basename() at both the save
site and the load site, with the load site additionally rejecting a
bare '.'/'..'/empty result before any file IO. Added regression tests.

GHSA-5x27-wqpm-m24v

* security: authorize Data Source enable/disable against owning device

ds_enable()/ds_disable() and the bulk drp_action enable/disable loops in
form_actions() passed a user-controlled local_data_id straight into
api_data_source_enable()/api_data_source_disable() with no object-level
authorization check, so any user holding the Sites/Devices/Data realm
could toggle the enabled state of a data source belonging to a device
outside their assigned Device Permissions.

Added data_source_authorized(\), which allows
host-independent data sources (data_local.host_id = 0) and otherwise
defers to is_device_allowed(\), and applied it to both the
single-item and bulk action paths.

GHSA-2fhv-c794-v433

* security: authorize bulk Change Data Source Profile against owning device

The bulk 'Change Data Source Profile' action (drp_action == '6') iterated
selected_items and updated data_template_data/data_template_rrd/poller_item
for each supplied local_data_id with no object-level authorization check,
so any Sites/Devices/Data realm user could retarget the RRD step/heartbeat
of a data source belonging to a device outside their assigned Device
Permissions -- the same class of gap as GHSA-2fhv, in a different bulk
action. Guarded with the data_source_authorized() helper added for that
fix. This action does not exist on 1.2.x (develop-only feature).

GHSA-fc82-w6xq-4jg7

* security: grant AllowOverride All in docker/apache.conf

The packaged docker/apache.conf granted only 'AllowOverride FileInfo
Options=FollowSymLinks' for the Cacti document root. Cacti's shipped
.htaccess deny rules (log/, cache/*, scripts/, cli/, bin/, mibs/, rra/,
contrib/, tests/) rely on 'Require all denied' (Apache 2.4, override
type AuthConfig) and/or legacy 'Order/Deny' (override type Limit),
neither of which FileInfo/Options covers, so Apache silently ignored
every one of those deny rules and served the directories over HTTP to
any unauthenticated client -- most notably log/cacti.log, which can
contain usernames, client IPs, SQL backtraces, and any secrets Cacti
logs (SNMP community strings, LDAP bind details).

AllowOverride All lets every shipped .htaccess apply as intended. Added
a regression test. docker/ does not exist on 1.2.x (develop-only).

GHSA-fxfg-j995-j69j

* docs: add security advisory / CVE policy to Copilot instructions

Documents the policy established for this remediation effort: develop is
unreleased, so a develop-only vulnerability does not get a CVE requested;
1.2.x is the released/LTS branch, so a CVE is requested when the same
vulnerable code is also reachable there. Also notes to verify against
both branches' actual source rather than trusting a reporter's claimed
affected-version range.

* fix(security): update stale test harnesses and address review findings

CI fixes:
- CsrfCheckContractProbe.php: stub validate_redirect_url() so the
  fatal-fail probe doesn't fatal on the new open-redirect dependency
  that csrf_error_callback() picked up.
- InstallerCsrfRecoveryTest.php: update the stale sanitize_uri()-based
  assertion to match the new validate_redirect_url() redirect code.
- psalm-baseline.xml: drop the TaintedHeader suppression for
  include/csrf.php now that validate_redirect_url() resolves it.

Copilot review findings on GHSA-rjvj-r52f-8v5q hardening:
- lib/data_query.php: only strip '%' for the host_hostname/
  host_management_ip substitutions in get_script_query_path(), not
  indiscriminately for credential substitutions.
- lib/functions.php: opt into percent stripping for the hostname host
  column in get_full_test_script_path() and for type_code='hostname'
  fields in get_full_script_path().
- lib/snmpagent.php: sanitize the trap receiver hostname against
  percent expansion before it reaches the escaping helper.
- tests/security/baselines/*.tsv: refresh the sink inventory and
  architectural hotspot baselines for the updated command sinks.
- tests/Unit/DataCollection/Snmp/SnmpCoverageTest.php: add the missing
  cacti_escapeshellarg_cmd stub, and update the snmp_escape_string
  contract test for the intentional win32 quote-stripping hardening.
- tests/Unit/Security/Shell/CmdMetacharHelperTest.php +
  tests/fixtures/CmdMetacharHelperWin32Probe.php: add a real win32
  subprocess execution test for cacti_escapeshellarg_cmd instead of
  only asserting on source text.
- CHANGELOG: add the pull-request reference to the new entry.

* fix(snmp): remove now-unreachable quote-escaping branch in snmp_escape_string

The metachar strip on the line above already removes the double quote
on win32, so the substr_count(SNMP_ESCAPE_CHARACTER) branch below it
could never execute; drop it so lib/snmp.php reaches 100% coverage
again.

* style: fix pre-existing php-cs-fixer blank-line drift blocking CI

data_sources.php and lib/html_reports.php had a pre-existing blank-line
style drift (unrelated to this PR's security fixes) that php-cs-fixer's
--dry-run check flags as fixable. Since "PHP 8.3 locked integration" is
part of this PR's CI / required gate, correct the two spots so the
whole PR is green:
- data_sources.php: collapse a double blank line before form_actions().
- lib/html_reports.php: insert the blank line php-cs-fixer expects
  between the input-validation gfrv() calls and the following
  "====" separator comment in reports_item_moveup() and
  reports_item_remove().

No behavior change; purely CI/style formatting.

* style: refresh phpstan baseline for pre-existing, unrelated findings

Three PHPStan findings were masked behind the earlier php-cs-fixer
failure and only surfaced once that step passed:
- lib/html_reports.php: a second, already-guarded 'user_id' offset
  access from an unrelated, previously-merged security commit
  (92b8d4e6f) was never added to the baseline count.
- cli/splice_rrd.php, poller_realtime.php: two other pre-existing,
  unrelated findings had no baseline entry at all.

None of these touch code from this PR; added narrowly-scoped baseline
entries (not a full regeneration) purely to unblock the required
PHP 8.3 locked integration check.

* fix(security): address follow-up review findings on consolidated PR

Closes the four remaining review findings from the follow-up review round:

- package_import.php: trustSigner() now POSTs with a CSRF token instead of
  $.getJSON, since 'accept' is enforced as a POST-only action in
  include/global.php's bad_actions list (GHSA-gxgm-8rhv-r7jv).

- lib/html_reports.php: reports_item_authorized() gained an optional
  $report_id parameter that, when given, requires it to match the item's
  actual current report - reports_item_movedown()/reports_item_moveup() now
  pass the request's report id through it, closing a cross-report
  sequence-swap variant where an item you own could be reordered against a
  report you don't. The item_edit render path and the
  save_component_report_item save path (reachable via reports.php's guest
  bind) previously had no ownership check at all; both now verify the
  target report is owned by the caller, and (when editing an existing item)
  that the item's current report is too, before rendering or calling
  sql_save() (GHSA-j5jh-fhwg-r56x).

- tree.php + include/global.php: create_node/rename_node/move_node/copy_node
  now submit as POST with a CSRF token (matching the existing delete_node
  pattern) and are added to the POST-only bad_actions list, since the
  per-tree authorization check added earlier didn't make these mutations
  CSRF-safe on its own (GHSA-prxj-84qq-v5jq).

Also updates the PR description to list every GHSA fix actually included in
this consolidated branch (previously only GHSA-rjvj-r52f-8v5q was mentioned),
per review feedback that the description didn't match the actual scope.

* fix: resolve CI failures on PR #8056 (docblock alignment, sink baseline)

- lib/html_reports.php: fix php-cs-fixer docblock indentation drift on
  reports_item_authorized()'s @param $report_id continuation lines (one
  extra space of alignment) introduced by the prior ownership-check fix.
- tests/security/baselines/sink_inventory.baseline.tsv: refresh to track
  the 4 new header('Location: ' . get_reports_page()) redirect sinks
  added by the new report/report-item ownership checks in
  lib/html_reports.php. Verified via
  tests/security/verify_sink_inventory.sh and confirmed
  tests/security/verify_architectural_hotspots.sh still reports no new
  hotspots.

* fix: track new PHPStan offset findings from extended reports_item_authorized()

phpstan-baseline.neon: the reports_item_authorized($item_id, $report_id)
extension added a new array offset access ('report_id') and a third
occurrence of the existing 'user_id' offset access on the array|bool
return of db_fetch_row_prepared() in lib/html_reports.php. Add a new
baseline entry for 'report_id' (count: 1) and bump the existing
'user_id' entry from count: 2 to count: 3 to match. Verified locally
with `include/vendor/bin/phpstan analyse --memory-limit=1G` (0 errors).

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <copilot@users.noreply.github.com>
2026-09-23 09:45:42 -04:00

3175 lines
112 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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/ |
+-------------------------------------------------------------------------+
*/
/**
* Executes a data query for a given host and SNMP query ID. This function handles
* various scenarios such as host status, poller assignment, and data query type.
* It also manages re-indexing, orphaned data, and updates related caches.
*
* @param int $host_id The ID of the host for which the data query is being run.
* @param int $snmp_query_id The ID of the SNMP query to execute.
* @param bool $automation Whether the query is being run as part of an automation process. Default is false.
* @param bool $force Whether to force the re-indexing and cache updates. Default is false.
*
* @global array $config Global configuration array.
* @global array $input_types Global array of input types.
*
* @return mixed Returns false if the query fails or the host is down/disabled.
* Returns true or the result of the query execution otherwise.
*
* @throws Exception If there are issues with database queries or remote calls.
*/
function run_data_query(int $host_id, int $snmp_query_id, bool $automation = false, bool $force = false) : mixed {
global $config, $input_types;
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Running Re-Index for Device[$host_id], DQ[$snmp_query_id]", false, 'DSTRACE');
}
// required for upgrading old versions of cacti
if (!db_column_exists('host', 'poller_id')) {
return false;
}
// don't run/rerun the query if the host is down, or disabled
if (db_column_exists('sites', 'disabled')) {
$sdisabled = ',s.disabled AS site_disabled';
} else {
$sdisabled = ',"" AS site_disabled';
}
$status = db_fetch_row_prepared("SELECT h.status, h.poller_id,
h.disabled $sdisabled
FROM host h
LEFT JOIN sites s
ON h.site_id = s.id
WHERE h.id = ?",
[$host_id]);
if (!cacti_sizeof($status)) {
return false;
}
if ($status['status'] == HOST_DOWN || $status['disabled'] == 'on' || $status['site_disabled'] == 'on') {
return true;
}
$poller_id = $status['poller_id'];
if ($poller_id != POLLER_ID) {
$hostname = db_fetch_cell_prepared('SELECT hostname
FROM poller
WHERE id = ?',
[$poller_id]);
$url = CACTI_PATH_URL . 'remote_agent.php?action=runquery&host_id=' . $host_id . '&data_query_id=' . $snmp_query_id;
$response = call_remote_data_collector($poller_id, $url);
if ($response != '') {
$response = json_decode($response, true);
$_SESSION['debug_log']['data_query'] = $response['data_query'];
if (isset($_SESSION['debug_log']['response'])) {
$result = $_SESSION['debug_log']['response'];
unset($_SESSION['debug_log']['response']);
automation_execute_data_query($host_id, $snmp_query_id);
query_debug_timer_offset('data_query', __esc('Automation Execution for Data Query complete'));
api_plugin_hook_function('run_data_query', ['host_id' => $host_id, 'snmp_query_id' => $snmp_query_id]);
query_debug_timer_offset('data_query', __esc('Plugin Hooks complete'));
return $result;
}
}
return false;
}
include_once(CACTI_PATH_LIBRARY . '/poller.php');
include_once(CACTI_PATH_LIBRARY . '/api_data_source.php');
include_once(CACTI_PATH_LIBRARY . '/utility.php');
query_debug_timer_start();
$reindex_start = microtime(true);
// Load the XML structure for custom settings detection
$query_array = get_data_query_array($snmp_query_id);
query_debug_timer_offset('data_query', __esc('Running Data Query [%s].', $snmp_query_id));
$type_id = db_fetch_cell_prepared('SELECT data_input.type_id
FROM snmp_query
INNER JOIN data_input
ON snmp_query.data_input_id = data_input.id
WHERE snmp_query.id = ?',
[$snmp_query_id]);
if (isset($input_types[$type_id])) {
query_debug_timer_offset('data_query', __esc('Found Type = \'%s\' [%s].', $type_id, $input_types[$type_id]));
}
if ($type_id == DATA_INPUT_TYPE_SNMP_QUERY) {
$result = query_snmp_host($host_id, $snmp_query_id);
} elseif ($type_id == DATA_INPUT_TYPE_SCRIPT_QUERY) {
$result = query_script_host($host_id, $snmp_query_id);
} elseif ($type_id == DATA_INPUT_TYPE_QUERY_SCRIPT_SERVER) {
$result = query_script_host($host_id, $snmp_query_id);
} else {
$result = false;
$arguments = [
'result' => $result,
'host_id' => $host_id,
'snmp_query_id' => $snmp_query_id
];
$arguments = api_plugin_hook_function('run_data_query', $arguments);
if (isset($arguments['result']) && $arguments['result'] !== false) {
$result = $arguments['result'];
} else {
query_debug_timer_offset('data_query', __esc('Unknown Type = \'%s\'.', $type_id));
}
}
// If re-indexing fails, don't continue
if (!$result) {
$cur_time = microtime(true);
$total = $cur_time - $reindex_start;
db_execute_prepared('UPDATE host_snmp_query
SET reindex_last_runtime = CURRENT_TIMESTAMP(),
reindex_last_duration = ?
WHERE host_id = ?
AND snmp_query_id = ?',
[$total, $host_id, $snmp_query_id]);
return false;
}
// SNMP Queries can specify the index as a 'source' column
if (!isset($query_array['index_order'])) {
$index_is_source = true;
} else {
$index_is_source = false;
}
$old_sort_field = get_best_data_query_index_type($host_id, $snmp_query_id);
// update the sort cache if there is a sort field
if ($index_is_source == false) {
$new_sort_field = update_data_query_sort_cache($host_id, $snmp_query_id);
} else {
$new_sort_field = $old_sort_field;
}
if ($new_sort_field === false) {
$cur_time = microtime(true);
$total = $cur_time - $reindex_start;
db_execute_prepared('UPDATE host_snmp_query
SET reindex_last_runtime = CURRENT_TIMESTAMP(),
reindex_last_duration = ?
WHERE host_id = ?
AND snmp_query_id = ?',
[$total, $host_id, $snmp_query_id]);
cacti_log('ERROR: Re-Indexing failed due to a NULL sort field for Device[' . $host_id . '] and DQ[' . $snmp_query_id . ']. Can not continue with Re-Index.', false, 'REINDEX');
return false;
}
$remap = false;
if ($index_is_source == false) {
if (query_check_suitable($new_sort_field, $old_sort_field, $host_id, $snmp_query_id)) {
if ($old_sort_field != $new_sort_field) {
if ($old_sort_field != '') {
query_debug_timer_offset('data_query', __esc('WARNING: Sort Field Association has Changed. Re-mapping issues may occur!'));
cacti_log('WARNING: Sort Field has Changed for Device[' . $host_id . '] and DQ[' . $snmp_query_id . ']. Old Sort:' . $old_sort_field . ', New Sort:' . $new_sort_field . '. Re-mapping issues may occur!', false, 'REINDEX');
}
$remap = true;
}
} else {
$new_sort_field = $old_sort_field;
}
}
query_debug_timer_offset('data_query', __esc('Update Data Query Sort Cache complete'));
// recalculate/change sort order
$local_data = db_fetch_assoc_prepared('SELECT dl.id AS local_data_id, dl.host_id,
dl.snmp_query_id, dl.snmp_index, dl.orphan,
did.data_input_field_id, did.data_template_data_id,
did.value AS query_index, "' . $old_sort_field . '" AS sort_field
FROM data_local AS dl
INNER JOIN data_template_data AS dtd
ON dl.id = dtd.local_data_id
INNER JOIN data_input_fields AS dif
ON dtd.data_input_id = dif.data_input_id
LEFT JOIN data_input_data AS did
ON dtd.id = did.data_template_data_id
AND dif.id = did.data_input_field_id
WHERE dif.data_name="index_value"
AND dl.snmp_query_id = ?
AND dl.host_id = ?',
[$snmp_query_id, $host_id]);
$changed_ids = [];
$orphaned_ids = [];
$push_out = [];
if (cacti_sizeof($local_data)) {
query_debug_timer_offset('data_query', __esc('Found %s Local Data ID\'s to Verify', cacti_sizeof($local_data)));
foreach ($local_data as $data_source) {
// Just in case there is a forced type from the data source page
$forced_type = false;
// Check to see if the user had manually set the index type
$previous_sort_field = $old_sort_field;
$previous_did = db_fetch_row_prepared('SELECT did.*
FROM data_local AS dl
INNER JOIN data_template_data AS dtd
ON dl.id = dtd.local_data_id
INNER JOIN data_input_fields AS dif
ON dtd.data_input_id = dif.data_input_id
LEFT JOIN data_input_data AS did
ON dtd.id = did.data_template_data_id
AND dif.id = did.data_input_field_id
WHERE dif.data_name="index_type"
AND dl.id = ?',
[$data_source['local_data_id']]);
if (cacti_sizeof($previous_did)) {
if ($previous_did['value'] != $previous_sort_field) {
$forced_type = true;
$previous_sort_field = $previous_did['value'];
}
}
if (!$forced_type) {
// See if the index is in the host cache
$current_index = db_fetch_cell_prepared('SELECT snmp_index
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?
AND field_value = ?',
[$host_id, $snmp_query_id, $data_source['sort_field'], $data_source['query_index']]);
} else {
$current_value = db_fetch_cell_prepared('SELECT value
FROM data_input_data AS did
INNER JOIN data_input_fields AS dif
ON did.data_input_field_id=dif.id
INNER JOIN data_template_data AS dtd
ON dtd.id=did.data_template_data_id
INNER JOIN data_local AS dl
ON dl.id=dtd.local_data_id
WHERE dl.id = ?
AND dl.snmp_query_id = ?
AND data_name="index_value"',
[$data_source['local_data_id'], $snmp_query_id]);
$current_index = db_fetch_cell_prepared('SELECT snmp_index
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?
AND field_value = ?',
[$host_id, $snmp_query_id, $previous_sort_field, $current_value]);
}
$new_field_value = '';
if ($remap) {
if ($new_sort_field != $previous_sort_field) {
$new_field_value = db_fetch_cell_prepared('SELECT field_value
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?
AND snmp_index = ?',
[$host_id, $snmp_query_id, $new_sort_field, $current_index]);
$did_map_data = db_fetch_row_prepared('SELECT value, data_input_field_id, data_template_data_id,
host_id, snmp_query_id
FROM data_input_data AS did
INNER JOIN data_input_fields AS dif
ON did.data_input_field_id=dif.id
INNER JOIN data_template_data AS dtd
ON dtd.id=did.data_template_data_id
INNER JOIN data_local AS dl
ON dl.id=dtd.local_data_id
WHERE value = ?
AND dl.id = ?
AND dl.snmp_query_id = ?
AND data_name="index_type"',
[$previous_sort_field, $data_source['local_data_id'], $snmp_query_id]);
} else {
$remap = false;
$new_field_value = '';
}
}
if ($current_index != '') {
// Non blank index found
// Check to see if the index changed
if ($current_index != $data_source['snmp_index']) {
query_debug_timer_offset('data_query', __esc('Index Change Detected! CurrentIndex: %s, PreviousIndex: %s', $current_index, $data_source['query_index']));
db_execute_prepared('UPDATE data_local
SET snmp_index = ?,
orphan = 0
WHERE id = ?',
[$current_index, $data_source['local_data_id']]);
$changed_ids[] = $data_source['local_data_id'];
} elseif ($data_source['orphan'] == 1) {
db_execute_prepared('UPDATE data_local
SET orphan = 0
WHERE id = ?',
[$data_source['local_data_id']]);
$push_out[$data_source['local_data_id']] = $data_source['local_data_id'];
}
} elseif ($data_source['snmp_index'] != '' && !$forced_type) {
if (isset($query_array['index_transient']) && $query_array['index_transient'] == 'true') {
// Found removed index, but this is expected, so no action taken
query_debug_timer_offset('data_query', __esc('Transient Index Removal Detected! PreviousIndex: %s. No action taken.', $data_source['query_index']));
} else {
// Found a deleted index, masking off to prevent issues
query_debug_timer_offset('data_query', __esc('Index Removal Detected! PreviousIndex: %s', $data_source['query_index']));
// Set the index to Null, note that the Data Source still has the value
db_execute_prepared('UPDATE data_local
SET orphan = 1
WHERE id = ?',
[$data_source['local_data_id']]);
$orphaned_ids[] = $data_source['local_data_id'];
}
}
if ($remap && trim($new_field_value) != '' && $new_sort_field != '') {
db_execute_prepared('UPDATE data_input_data
SET value = ?
WHERE data_input_field_id = ?
AND data_template_data_id = ?',
[$new_field_value, $data_source['data_input_field_id'], $data_source['data_template_data_id']]);
db_execute_prepared('UPDATE data_input_data
SET value = ?
WHERE data_input_field_id = ?
AND data_template_data_id = ?',
[$new_sort_field, $previous_did['data_input_field_id'], $previous_did['data_template_data_id']]);
}
}
// if we've resolved orphan issues, push out the data source
if (cacti_sizeof($push_out)) {
foreach ($push_out as $po_local_data_id) {
push_out_host($host_id, $po_local_data_id);
}
}
query_debug_timer_offset('data_query', __esc('Verification of %s Local Data ID\'s Complete', cacti_sizeof($local_data)));
if (cacti_sizeof($changed_ids) || cacti_sizeof($orphaned_ids)) {
query_debug_timer_offset('data_query', __esc('Found Changed %s and %s Orphaned Local Data ID\'s to Re-map.', cacti_sizeof($changed_ids), cacti_sizeof($orphaned_ids)));
data_query_remap_indexes($changed_ids);
data_query_remap_indexes($orphaned_ids);
query_debug_timer_offset('data_query', __esc('Done remapping Graphs to their new Indexes'));
}
if ((cacti_sizeof($changed_ids) || cacti_sizeof($orphaned_ids)) && !$force) {
// update title cache for graph and data source
update_data_source_title_cache_from_host($host_id, $snmp_query_id, array_merge($changed_ids, $orphaned_ids));
query_debug_timer_offset('data_query', __esc('Done updating Data Source Title Cache'));
update_graph_title_cache_from_host($host_id, $snmp_query_id, array_merge($changed_ids, $orphaned_ids));
query_debug_timer_offset('data_query', __esc('Done updating Graph Title Cache'));
} elseif ($force) {
// update title cache for graph and data source
update_data_source_title_cache_from_host($host_id, $snmp_query_id);
query_debug_timer_offset('data_query', __esc('Done updating Data Source Title Cache'));
update_graph_title_cache_from_host($host_id, $snmp_query_id);
query_debug_timer_offset('data_query', __esc('Done updating Graph Title Cache'));
}
}
query_debug_timer_offset('data_query', __esc('Index Association with Local Data complete'));
update_reindex_cache($host_id, $snmp_query_id);
// update the auto reindex cache
if (cacti_sizeof($changed_ids)) {
query_debug_timer_offset('data_query', __esc('Update Re-Index Cache complete. There were %s index changes, and %s orphaned indexes.', cacti_sizeof($changed_ids), cacti_sizeof($orphaned_ids)));
// update the poller cache
update_poller_cache_from_query($host_id, $snmp_query_id, $changed_ids);
query_debug_timer_offset('data_query', __esc('Update Poller Cache for Query complete'));
} else {
query_debug_timer_offset('data_query', __esc('No Index Changes Detected, Skipping Re-Index and Poller Cache Re-population'));
$existing_orphans = db_fetch_cell_prepared('SELECT COUNT(*)
FROM data_local
WHERE snmp_query_id = ?
AND host_id = ?
AND orphan = 1',
[$snmp_query_id, $host_id]);
if ($existing_orphans) {
$data_template_id = db_fetch_cell_prepared('SELECT data_template_id
FROM data_local
WHERE snmp_query_id = ?
LIMIT 1',
[$snmp_query_id]);
db_execute_prepared('UPDATE data_local
SET orphan = 0
WHERE snmp_query_id = ?
AND host_id = ?',
[$snmp_query_id, $host_id]);
if (isset($query_array['index_orphan_removal']) && $query_array['index_orphan_removal'] == 'true') {
push_out_host($host_id, 0, $data_template_id);
}
}
}
if (cacti_sizeof($orphaned_ids) &&
(isset($query_array['index_orphan_removal']) && $query_array['index_orphan_removal'] == 'true')) {
$poller_ids = array_rekey(
db_fetch_assoc_prepared('SELECT DISTINCT poller_id
FROM poller_item
WHERE local_data_id IN (' . implode(', ', $orphaned_ids) . ')'),
'poller_id', 'poller_id'
);
if (cacti_sizeof($poller_ids)) {
foreach ($poller_ids as $poller_id) {
api_data_source_cache_crc_update($poller_id);
}
}
}
if (POLLER_ID == 1) {
// perform any automation on reindex
automation_execute_data_query($host_id, $snmp_query_id);
query_debug_timer_offset('data_query', __esc('Automation Executing for Data Query complete'));
api_plugin_hook_function('run_data_query', ['host_id' => $host_id, 'snmp_query_id' => $snmp_query_id]);
query_debug_timer_offset('data_query', __esc('Plugin hooks complete'));
} elseif (CACTI_CONNECTION == 'online') {
poller_push_reindex_data_to_poller($host_id, $snmp_query_id);
automation_execute_data_query($host_id, $snmp_query_id);
query_debug_timer_offset('data_query', __esc('Automation Execution for Data Query complete'));
api_plugin_hook_function('run_data_query', ['host_id' => $host_id, 'snmp_query_id' => $snmp_query_id]);
query_debug_timer_offset('data_query', __esc('Plugin Hooks complete'));
if (!isset($_SESSION)) {
$config['debug_log']['result'] = $result;
print json_encode($config['debug_log']);
} else {
$_SESSION['debug_log']['result'] = $result;
print json_encode($_SESSION['debug_log']);
kill_session_var('debug_log');
}
}
$cur_time = microtime(true);
$total = $cur_time - $reindex_start;
db_execute_prepared('UPDATE host_snmp_query
SET reindex_last_runtime = CURRENT_TIMESTAMP(),
reindex_last_duration = ?
WHERE host_id = ?
AND snmp_query_id = ?',
[$total, $host_id, $snmp_query_id]);
// Remove orphans only if the data query expressly calls for it
if (cacti_sizeof($orphaned_ids) &&
(isset($query_array['index_orphan_removal']) && $query_array['index_orphan_removal'] == 'true')) {
data_query_remove_disabled_items($orphaned_ids);
}
return $result;
}
/**
* Removes disabled items from the database based on the provided orphaned IDs.
*
* @param array $orphaned_ids An array of orphaned `local_data_id` values to be removed.
*
* @return void
*/
function data_query_remove_disabled_items(array $orphaned_ids) : void {
if (cacti_sizeof($orphaned_ids)) {
// resolve which pollers currently hold these items before the local
// delete removes the rows we'd need to look that up from
$poller_ids = array_rekey(
db_fetch_assoc_prepared('SELECT DISTINCT poller_id
FROM poller_item
WHERE local_data_id IN (' . implode(', ', $orphaned_ids) . ')'),
'poller_id', 'poller_id'
);
poller_item_delete_for_data_source($orphaned_ids);
if (cacti_sizeof($poller_ids)) {
foreach ($poller_ids as $poller_id) {
if ($poller_id > 1) {
if (remote_poller_up($poller_id)) {
if (($rcnn_id = poller_push_to_remote_db_connect($poller_id, true)) !== false) {
poller_item_delete_for_data_source($orphaned_ids, $rcnn_id, false);
} else {
raise_message('poller_down_' . $poller_id, __('Remote Poller %s is Down, you will need to perform a FullSync once it is up again', $poller_id), MESSAGE_LEVEL_WARN);
}
} else {
raise_message('poller_down_' . $poller_id, __('Remote Poller %s is Down, you will need to perform a FullSync once it is up again', $poller_id), MESSAGE_LEVEL_WARN);
}
}
}
}
db_execute_prepared('DELETE FROM poller_output_boost
WHERE local_data_id IN (' . implode(', ', $orphaned_ids) . ')');
$archive_tables = array_rekey(
db_fetch_assoc('SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_NAME LIKE "poller_output_boost_arch%"'),
'TABLE_NAME', 'TABLE_NAME'
);
if (cacti_sizeof($archive_tables)) {
foreach ($archive_tables as $table) {
db_execute("DELETE FROM $table
WHERE local_data_id IN (" . implode(', ', $orphaned_ids) . ')', false);
}
}
}
}
/**
* Checks if a new sort field is suitable for updating the sort field of a host's SNMP query.
*
* @param string $new_sort_field The new sort field to validate.
* @param string $old_sort_field The current sort field to compare against.
* @param int $host_id The ID of the host associated with the SNMP query.
* @param int $snmp_query_id The ID of the SNMP query being validated.
*
* @return bool Returns true if the new sort field is suitable and the sort field can be updated;
* otherwise, returns false if the new sort field is not suitable.
*/
function query_check_suitable(string $new_sort_field, string $old_sort_field, int $host_id, int $snmp_query_id) : bool {
if ($new_sort_field == $old_sort_field) {
query_debug_timer_offset('data_query', __esc('Checking for Sort Field change. No changes detected.'));
return true;
}
query_debug_timer_offset('data_query', __esc('Detected New Sort Field: \'%s\' Old Sort Field \'%s\'', $new_sort_field, $old_sort_field));
$new_sort_count = db_fetch_cell_prepared('SELECT COUNT(*)
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?',
[$host_id, $snmp_query_id, $new_sort_field]);
$old_sort_count = db_fetch_cell_prepared('SELECT COUNT(*)
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?',
[$host_id, $snmp_query_id, $old_sort_field]);
if ($new_sort_count < $old_sort_count) {
query_debug_timer_offset('data_query', __esc('ERROR: New Sort Field not suitable. Sort Field will not change.'));
// update the cache
db_execute_prepared('UPDATE host_snmp_query
SET sort_field = ?
WHERE host_id = ?
AND snmp_query_id = ?',
[$old_sort_field, $host_id, $snmp_query_id]);
return false;
}
query_debug_timer_offset('data_query', __esc('New Sort Field validated. Sort Field be updated.'));
return true;
}
/**
* Remaps SNMP indexes for local data entries and updates associated graph templates.
*
* @param array $local_data An array of local data IDs to process.
*
* @return void
*/
function data_query_remap_indexes(array $local_data) : void {
if (cacti_sizeof($local_data)) {
foreach ($local_data as $id) {
$index = db_fetch_cell_prepared('SELECT snmp_index
FROM data_local
WHERE id = ?',
[$id]);
$local_graph_ids = array_rekey(
db_fetch_assoc_prepared('SELECT DISTINCT gti.local_graph_id
FROM graph_templates_item AS gti
INNER JOIN data_template_rrd AS dtr
ON gti.task_item_id=dtr.id
WHERE dtr.local_data_id = ?',
[$id]),
'local_graph_id', 'local_graph_id'
);
if (cacti_sizeof($local_graph_ids)) {
db_execute_prepared('UPDATE graph_local
SET snmp_index = ?
WHERE id IN(' . implode(', ', $local_graph_ids) . ')',
[$index]);
}
}
}
}
/**
* Updates the input method for a given SNMP query ID by changing the associated
* data input ID in the database. This function ensures that the data templates
* and their associated data inputs are updated correctly when the input method
* changes.
*
* @param int $snmp_query_id - The ID of the SNMP query to update.
* @param int $previous_input_id - The previous data input ID associated with the SNMP query.
* @param int $new_input_id - The new data input ID to associate with the SNMP query (optional).
*
* @return void
*/
function data_query_update_input_method(int $snmp_query_id, int $previous_input_id, int $new_input_id = 0) : void {
$change_data_input = false;
if ($previous_input_id != $new_input_id) {
$change_data_input = true;
}
$data_templates = array_rekey(
db_fetch_assoc_prepared('SELECT DISTINCT data_template_id
FROM data_local
WHERE snmp_query_id = ?',
[$snmp_query_id]),
'data_template_id', 'data_template_id'
);
if (cacti_sizeof($data_templates)) {
// Look for messed up templates
$data_inputs = db_fetch_assoc('SELECT DISTINCT data_input_id
FROM data_template_data
WHERE data_template_id IN (' . implode(', ', array_keys($data_templates)) . ')');
if (cacti_sizeof($data_inputs) > 1) {
$change_data_input = true;
} elseif (cacti_sizeof($data_inputs) && $new_input_id != '' && $data_inputs[0]['data_input_id'] != $new_input_id) {
$change_data_input = true;
}
if ($change_data_input) {
if (cacti_sizeof($data_templates)) {
db_execute_prepared('UPDATE data_template_data
SET data_input_id = ?
WHERE data_input_id = ?
AND data_template_id IN (' . implode(', ', array_keys($data_templates)) . ')',
[$new_input_id, $previous_input_id]);
push_out_data_input_method($new_input_id);
}
}
}
}
/**
* Retrieves the data query array for a given SNMP query ID.
*
* @param int $snmp_query_id The ID of the SNMP query to retrieve the data query array for.
*
* @return array The parsed data query array. Returns an empty array if the XML file cannot be found.
*
* @throws Exception If there is an error during the XML file processing.
*/
function get_data_query_array(int $snmp_query_id) : array {
global $data_query_xml_arrays;
include_once(CACTI_PATH_LIBRARY . '/xml.php');
// load the array into memory if it hasn't been done yet
if (!isset($data_query_xml_arrays[$snmp_query_id])) {
$xml_file_path = db_fetch_cell_prepared('SELECT xml_path FROM snmp_query WHERE id = ?', [$snmp_query_id]);
$search = ['<path_cacti>', '<path_snmpget>', '<path_php_binary>'];
$replace = [CACTI_PATH_BASE, read_config_option('path_snmpget'), read_config_option('path_php_binary')];
$xml_file_path = str_replace($search, $replace, $xml_file_path);
// Sanitise for log calls — encodes CR/LF and strips the full ASCII control
// range (0x00-0x1F plus DEL 0x7F) to prevent log injection via crafted
// xml_path values. LF/CR are already encoded by str_replace above, so
// preg_replace never sees raw newlines; the wider range closes the TAB gap.
$safe_path = preg_replace('/[\x00-\x1F\x7F]/', '', str_replace(["\r", "\n"], ['\\r', '\\n'], $xml_file_path));
$allowed_base = realpath(CACTI_PATH_BASE);
// Reject root or unresolvable base. rtrim('/') returns '' which makes the
// boundary check trivially true for every absolute path (fails open). Also
// reject Windows drive-letter roots (C:\, D:\, ...) which are too broad
// for a safe containment check.
if ($allowed_base === false
|| $allowed_base === DIRECTORY_SEPARATOR
|| preg_match('#^[A-Za-z]:[\\\\/]?$#', $allowed_base) === 1) {
cacti_log('SECURITY: CACTI_PATH_BASE is invalid or resolves to filesystem root', false, 'SECURITY');
return [];
}
// Reject null bytes before any filesystem call. PHP 8.0+ throws ValueError
// when passing a string containing \0 to realpath()/file_exists(), so catch
// it explicitly as a traversal attempt.
if (str_contains($xml_file_path, "\0")) {
cacti_log('SECURITY: Null byte in data query XML path rejected', false, 'SECURITY');
return [];
}
// Check existence before resolving — realpath() returns false for missing
// files, which would otherwise trigger a misleading SECURITY log entry.
if (!file_exists($xml_file_path)) {
query_debug_timer_offset('data_query', __esc('Could not find data query XML file at \'%s\'', $safe_path));
return [];
}
$resolved = realpath($xml_file_path);
if ($resolved === false
|| !str_starts_with($resolved . DIRECTORY_SEPARATOR, rtrim($allowed_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR)) {
cacti_log('SECURITY: data query XML path outside Cacti base: ' . $safe_path, false, 'SECURITY');
query_debug_timer_offset('data_query', __esc('SECURITY: data query XML path outside Cacti base: \'%s\'', $safe_path));
return [];
}
query_debug_timer_offset('data_query', __esc('Found data query XML file at \'%s\'', $safe_path));
// Reject directories — realpath() resolves both files and dirs, so the
// boundary check alone does not prevent file() being called on a directory.
if (!is_file($resolved)) {
cacti_log('SECURITY: data query XML path is not a file: ' . $safe_path, false, 'SECURITY');
return [];
}
$data = implode('', file($resolved));
$xml_data = xml2array($data);
/* snmp queries do not require index_order
* get it from the fields if not set
*/
calculate_or_set_index_order($xml_data);
// store the array value to the global array for future reference
$data_query_xml_arrays[$snmp_query_id] = $xml_data;
}
return $data_query_xml_arrays[$snmp_query_id];
}
/**
* Executes a data query script for a specified host and SNMP query ID.
*
* @param int $host_id The ID of the host for which the data query is executed.
* @param int $snmp_query_id The ID of the SNMP query associated with the data query.
*
* @return bool Returns true on successful execution and data retrieval, or false on failure.
*/
function query_script_host(int $host_id, int $snmp_query_id) : bool {
$script_queries = get_data_query_array($snmp_query_id);
// invalid xml check
if (cacti_sizeof($script_queries) == 0) {
query_debug_timer_offset('data_query', __esc('Error parsing XML file into an array.'));
return false;
}
query_debug_timer_offset('data_query', __esc('XML file parsed ok.'));
// are we talking to script server?
if (isset($script_queries['script_server'])) {
$script_queries['script_path'] = '"|path_php_binary|" -q ' . $script_queries['script_path'];
}
if (!verify_index_order($script_queries)) {
query_debug_timer_offset('data_query', __('Invalid field &lt;index_order&gt;%s&lt;/index_order&gt;', $script_queries['index_order']));
query_debug_timer_offset('data_query', __('Must contain &lt;direction&gt;input&lt;/direction&gt; or &lt;direction&gt;input-output&lt;/direction&gt; fields only'));
return false;
}
// We get the current sort field. If for some reason the query
// fails for this field, or the snmp indexes we will give up.
$sort_field = get_best_data_query_index_type($host_id, $snmp_query_id);
// provide data for arg_num_indexes, if given
if (isset($script_queries['arg_num_indexes'])) {
$script_path = get_script_query_path((isset($script_queries['arg_prepend']) ? $script_queries['arg_prepend'] . ' ' : '') . $script_queries['arg_num_indexes'], $script_queries['script_path'], $host_id);
// fetch specified index at specified OID
$script_num_index_array = exec_into_array($script_path);
// if the number of indexes does not exist use emulation
if (!cacti_sizeof($script_num_index_array)) {
query_debug_timer_offset('data_query', __esc('Data Query returned no indexes.'));
query_debug_timer_offset('data_query', __('&lt;arg_num_indexes&gt; exists in XML file but no data returned., \'Index Count Changed\' not supported'));
}
query_debug_timer_offset('data_query', __esc('Executing script for num of indexes \'%s\'', $script_path));
foreach ($script_num_index_array as $element) {
query_debug_timer_offset('data_query', __esc('Found number of indexes: %s' , $element));
}
} else {
if (isset($script_queries['script_server'])) {
query_debug_timer_offset('data_query', __('&lt;arg_num_indexes&gt; missing in XML file, \'Index Count Changed\' not supported'));
} else {
query_debug_timer_offset('data_query', __('&lt;arg_num_indexes&gt; missing in XML file, \'Index Count Changed\' emulated by counting arg_index entries'));
}
}
// provide data for index, mandatory
$script_path = get_script_query_path((isset($script_queries['arg_prepend']) ? $script_queries['arg_prepend'] . ' ' : '') . $script_queries['arg_index'], $script_queries['script_path'], $host_id);
// fetch specified index
$script_index_array = exec_into_array($script_path);
if (!cacti_sizeof($script_index_array)) {
query_debug_timer_offset('data_query', __esc('ERROR: Data Query returned no indexes.'));
return false;
}
query_debug_timer_offset('data_query', __esc('Executing script for list of indexes \'%s\', Index Count: %s', $script_path, cacti_sizeof($script_index_array)));
debug_log_insert_section_start('data_query', __esc('Click to show Data Query output for \'index\''), true);
foreach ($script_index_array as $element) {
debug_log_insert('data_query', __esc('Found index: %s', $element));
}
debug_log_insert_section_end('data_query');
$empty_types = [];
// set an array to host all updates
$output_array = [];
foreach ($script_queries['fields'] as $field_name => $field_array) {
if ($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output') {
$rewrite_value = $field_array['rewrite_value'] ?? null;
$script_path = get_script_query_path((isset($script_queries['arg_prepend']) ? $script_queries['arg_prepend'] . ' ' : '') . $script_queries['arg_query'] . ' ' . $field_array['query_name'], $script_queries['script_path'], $host_id);
debug_log_insert_section_start('data_query', __esc('Click to show Data Query output for field \'%s\'', $field_name), true);
$script_data_array = exec_into_array($script_path);
if (!cacti_sizeof($script_data_array) && $field_name == $sort_field) {
$empty_types[] = $field_name;
query_debug_timer_offset('data_query', __esc('Sort field returned no data for field name %s, skipping', $field_name));
} else {
debug_log_insert('data_query', __esc('Executing script query \'%s\'', $script_path));
if (cacti_sizeof($script_data_array)) {
foreach ($script_data_array as $element) {
// We have two spellings for 'delimiter' so keep both for backward compatibility
if (isset($script_queries['output_delimeter']) && preg_match('/(.*?)' . preg_quote($script_queries['output_delimeter'], '/') . '(.*)/', $element, $matches)) {
$script_index = $matches[1];
$field_value = $matches[2];
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $field_value, $script_index, '');
debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s', $field_name, $field_value, $script_index));
} elseif (isset($script_queries['output_delimiter']) && preg_match('/(.*?)' . preg_quote($script_queries['output_delimiter'], '/') . '(.*)/', $element, $matches)) {
$script_index = $matches[1];
$field_value = $matches[2];
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $field_value, $script_index, '');
debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s', $field_name, $field_value, $script_index));
}
}
}
}
debug_log_insert_section_end('data_query');
}
}
if (cacti_sizeof($output_array)) {
data_query_update_host_cache_from_buffer($host_id, $snmp_query_id, $output_array, $empty_types);
}
return true;
}
/**
* Starts the query debug timer by recording the current time.
*
* @return void
*/
function query_debug_timer_start() : void {
global $query_debug_timer, $query_debug_start;
// record the start time
$query_debug_timer = microtime(true);
$query_debug_start = $query_debug_timer;
}
/**
* Logs the time offset for debugging purposes.
*
* @param string $section The section or context of the debug log entry.
* @param string $message A descriptive message to include in the debug log.
*
* @return float The time delta (in seconds) since the last recorded timestamp.
*/
function query_debug_timer_offset(string $section, string $message) : float {
global $query_debug_timer, $query_debug_start;
if (empty($query_debug_timer)) {
query_debug_timer_start();
}
// record the start time
$cur_time = microtime(true);
$delta = $cur_time - $query_debug_timer;
$total = $cur_time - $query_debug_start;
$query_debug_timer = $cur_time;
debug_log_insert($section, __('Total: %f, Delta: %f, %s', round($total, 2), round($delta, 2), $message));
return $delta;
}
/**
* Stops the query debug timer, calculates the elapsed time, and logs the result.
*
* @param string $section The section or context in which the debug timer is being stopped.
* @param string $message A custom message to include in the debug log entry.
*
* @return float The delta time (time elapsed since the last checkpoint) in seconds.
*/
function query_debug_timer_stop(string $section, string $message) : float {
global $query_debug_timer, $query_debug_start;
// record the start time
$cur_time = microtime(true);
$delta = $cur_time - $query_debug_timer;
$total = $cur_time - $query_debug_start;
unset($query_debug_timer);
unset($query_debug_start);
debug_log_insert($section, __esc('Total: %f, Delta: %f, %s', round($total, 2), round($delta, 2), $message));
return $delta;
}
/**
* Queries SNMP data for a specific host and SNMP query ID.
*
* @param int $host_id The ID of the host to query.
* @param int $snmp_query_id The ID of the SNMP query to execute.
*
* @return bool Returns true on success, or false on failure.
*
* @throws Exception If there are issues with the SNMP session or query execution.
*/
function query_snmp_host(int $host_id, int $snmp_query_id) : bool {
global $data_query_rewrite_indexes_cache;
include_once(CACTI_PATH_LIBRARY . '/snmp.php');
$host = db_fetch_row_prepared('SELECT hostname, snmp_community, snmp_version,
snmp_username, snmp_password, snmp_auth_protocol, snmp_priv_passphrase,
snmp_priv_protocol, snmp_context, snmp_engine_id, snmp_port, snmp_timeout,
ping_retries, max_oids, bulk_walk_size
FROM host
WHERE id = ?',
[$host_id]);
$snmp_queries = get_data_query_array($snmp_query_id);
// We get the current sort field. If for some reason the query
// fails for this field, or the snmp indexes we will give up.
$sort_field = get_best_data_query_index_type($host_id, $snmp_query_id);
if (!cacti_sizeof($host) || $host['hostname'] == '') {
query_debug_timer_offset('data_query', __esc('Invalid host_id: %s', $host_id));
return false;
}
// invalid xml check
if (cacti_sizeof($snmp_queries) == 0) {
query_debug_timer_offset('data_query', __esc('Error parsing XML file into an array.'));
return false;
}
query_debug_timer_offset('data_query', __esc('XML file parsed ok.'));
if (!verify_index_order($snmp_queries)) {
query_debug_timer_offset('data_query', __('Invalid field &lt;index_order&gt;%s&lt;/index_order&gt;', $snmp_queries['index_order']));
query_debug_timer_offset('data_query', __('Must contain &lt;direction&gt;input&lt;/direction&gt; or &lt;direction&gt;input-output&lt;/direction&gt; fields only'));
return false;
}
$snmp_indexes = [];
$snmp_index = '';
if ($host['bulk_walk_size'] <= 0) {
$walk_size = 5;
$walk_sizes = [1, 5, 10, 15, 20, 25, 30, 40, 50, 60];
$low_total = 999;
$selected = -1;
query_debug_timer_offset('data_query', __('Auto Bulk Walk Size Selected.'));
foreach ($walk_sizes as $size) {
$session = cacti_snmp_session_from_host($host, $size);
if ($session === false) {
debug_log_insert('data_query', __esc('Failed to load SNMP session.'));
return false;
}
// fetch specified index at specified OID, measuring time
$start = microtime(true);
$snmp_indexes = cacti_snmp_session_walk($session, $snmp_queries['oid_index']);
$end = microtime(true);
query_debug_timer_offset('data_query', __esc('Tested Bulk Walk Size %d with a response of %2.4f.', $size, $end - $start));
$total = $end - $start;
if ($total > $low_total) {
break;
} else {
$walk_size = $size;
$low_total = $total;
}
}
query_debug_timer_offset('data_query', __esc('Bulk Walk Size selected was %d.', $walk_size));
if ($host['bulk_walk_size'] == 0) {
query_debug_timer_offset('data_query', __esc('Saving Bulk Walk Size to Device.'));
$host['bulk_walk_size'] = $walk_size;
db_execute_prepared('UPDATE host
SET bulk_walk_size = ?
WHERE id = ?',
[$walk_size, $host_id]);
}
} else {
$walk_size = $host['bulk_walk_size'];
query_debug_timer_offset('data_query', __esc('Bulk Walk Size is fixed at %d.', $walk_size));
$session = cacti_snmp_session_from_host($host, $walk_size);
if ($session === false) {
debug_log_insert('data_query', __esc('Failed to load SNMP session.'));
return false;
}
// fetch specified index at specified OID
$snmp_indexes = cacti_snmp_session_walk($session, $snmp_queries['oid_index']);
}
// provide data for oid_num_indexes, if given
if (isset($snmp_queries['oid_num_indexes'])) {
$snmp_num_indexes = cacti_snmp_session_get($session, $snmp_queries['oid_num_indexes']);
query_debug_timer_offset('data_query', __esc('Executing SNMP get for num of indexes @ \'%s\' Index Count: %s' , $snmp_queries['oid_num_indexes'] , $snmp_num_indexes));
} else {
query_debug_timer_offset('data_query', __('&lt;oid_num_indexes&gt; missing in XML file, \'Index Count Changed\' emulated by counting oid_index entries'));
}
query_debug_timer_offset('data_query', __esc('Executing SNMP walk for list of indexes @ \'%s\' Index Count: %s', $snmp_queries['oid_index'] , cacti_sizeof($snmp_indexes)));
// no data found; get out
if (!cacti_sizeof($snmp_indexes)) {
query_debug_timer_offset('data_query', __esc('No SNMP data returned'));
return false;
} else {
// show list of indices found
foreach ($snmp_indexes as $oid => $value) {
query_debug_timer_offset('data_query', __esc('Index found at OID: \'%s\' value: \'%s\'', $oid , $value));
}
}
// the last octet of the oid is the index by default
$index_parse_regexp = '/.*\.([0-9]+)$/';
/* Some devices (e.g. Juniper Netscreen) pad OID indexes with trailing
.0 octets. When the default regex extracts the last octet, every
index resolves to 0. Detect this and strip the padding before
downstream parsing. This intentionally fixes the all-zero collapse
without changing the existing last-octet parsing model. See GitHub
issue #6108. */
if (!isset($snmp_queries['oid_index_parse']) && oid_index_should_strip_trailing_zero_padding($snmp_indexes, $index_parse_regexp)) {
query_debug_timer_offset('data_query', __('All indexes resolved to 0; stripping trailing .0 padding from OIDs'));
$result = oid_index_strip_trailing_zero_padding($snmp_indexes);
if ($result !== $snmp_indexes) {
$snmp_indexes = $result;
} else {
query_debug_timer_offset('data_query', __('Skipping trailing-zero stripping: collision detected after stripping'));
}
}
// Filtered index by value
if (isset($snmp_queries['value_index_parse'])) {
$value_parse_regexp = '/' . str_replace('VALUE/REGEXP:', '', $snmp_queries['value_index_parse']) . '/';
foreach ($snmp_indexes as $oid => $value) {
if (!preg_match($value_parse_regexp, $value)) {
unset($snmp_indexes[$oid]);
}
}
query_debug_timer_offset('data_query', __esc('List of indexes filtered by value @ \'%s\' Index Count: %s', $snmp_queries['oid_index'] , cacti_sizeof($snmp_indexes)));
// show list of indices found
foreach ($snmp_indexes as $oid => $value) {
query_debug_timer_offset('data_query', __esc('Filtered Index by value found at OID: \'%s\' value: \'%s\'', $oid , $value));
}
}
// parse the index if required
if (isset($snmp_queries['oid_index_parse'])) {
$index_parse_regexp = '/' . str_replace('OID/REGEXP:', '', $snmp_queries['oid_index_parse']) . '/';
$parsed_indexes = [];
foreach ($snmp_indexes as $oid => $value) {
if (preg_match($index_parse_regexp, $oid)) {
$parsed_indexes[$oid] = preg_replace($index_parse_regexp, '\\1', $oid);
}
}
$snmp_indexes = $parsed_indexes;
query_debug_timer_offset('data_query', __esc('Filtering list of indexes @ \'%s\' Index Count: %s', $snmp_queries['oid_index'] , cacti_sizeof($snmp_indexes)));
// show list of indices found
foreach ($snmp_indexes as $oid => $value) {
query_debug_timer_offset('data_query', __esc('Filtered Index found at OID: \'%s\' value: \'%s\'', $oid , $value));
}
}
// set an array to host all updates
$output_array = [];
// invalidate rewrite_index cache
$data_query_rewrite_indexes_cache = [];
$fields_processed = [];
$empty_types = [];
rewrite_snmp_enum_value(null);
foreach ($snmp_queries['fields'] as $field_name => $field_array) {
if (($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output') && $field_array['source'] != 'index' && $field_array['method'] != 'get' &&
(isset($field_array['rewrite_index']) || isset($field_array['oid_suffix']))) {
$field_array['method'] = 'get';
debug_log_insert('data_query', __esc('Fixing wrong \'method\' field for \'%s\' since \'rewrite_index\' or \'oid_suffix\' is defined', $field_name));
}
$rewrite_value = $field_array['rewrite_value'] ?? null;
if ((!isset($field_array['oid'])) && ($field_array['source'] == 'index')) {
foreach ($snmp_indexes as $value) {
query_debug_timer_offset('data_query', __esc('Inserting index data for field \'%s\' [value=\'%s\']' , $field_name, $value));
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $value, $value, '');
}
} elseif (($field_array['method'] == 'get') && ($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output')) {
query_debug_timer_offset('data_query', __esc('Located input field \'%s\' [get]',$field_name));
if ($field_array['source'] == 'value' && !isset($field_array['rewrite_index'])) {
$oid_rewrite_pattern = null;
$oid_rewrite_replacement = null;
if (isset($field_array['oid_rewrite_pattern']) && isset($field_array['oid_rewrite_replacement'])) {
$oid_rewrite_pattern = '/' . str_replace('OID/REGEXP:', '', $field_array['oid_rewrite_pattern']) . '/';
$oid_rewrite_replacement = $field_array['oid_rewrite_replacement'];
query_debug_timer_offset('data_query', __esc('Found OID rewrite rule: \'s/%s/%s/\'', $oid_rewrite_pattern, $oid_rewrite_replacement));
}
foreach ($snmp_indexes as $oid => $index) {
$oid = $field_array['oid'] . '.' . $index;
$oid .= isset($field_array['oid_suffix']) ? ('.' . $field_array['oid_suffix']) : '';
// rewrite the oid if required
if (isset($oid_rewrite_pattern)) {
$orig_oid = $oid;
$oid = preg_replace($oid_rewrite_pattern, $oid_rewrite_replacement, $oid);
query_debug_timer_offset('data_query', __esc('oid_rewrite at OID: \'%s\' new OID: \'%s\'', $orig_oid , $oid));
}
if (isset($field_array['output_format'])) {
if ($field_array['output_format'] == 'hex') {
$value_output_format = SNMP_STRING_OUTPUT_HEX;
} elseif ($field_array['output_format'] == 'ascii') {
$value_output_format = SNMP_STRING_OUTPUT_ASCII;
} else {
$value_output_format = SNMP_STRING_OUTPUT_GUESS;
}
$value = cacti_snmp_get($host['hostname'], $host['snmp_community'], $oid, $host['snmp_version'],
$host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'],
$host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'],
$host['snmp_port'], $host['snmp_timeout'], SNMP_POLLER, $host['snmp_engine_id'],
$value_output_format);
} else {
$value = cacti_snmp_session_get($session, $oid);
}
query_debug_timer_offset('data_query', __esc('Executing SNMP get for data @ \'%s\' [value=\'%s\']', $oid, $value));
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $value, $index, $oid);
}
} elseif (isset($field_array['rewrite_index'])) {
$rewritten_indexes = [];
if (isset($field_array['rewrite_index'])) {
$errmsg = [];
$rewritten_indexes = data_query_rewrite_indexes($errmsg, $host_id, $snmp_query_id, $field_array['rewrite_index'], $snmp_indexes, $fields_processed);
if (cacti_sizeof($errmsg)) {
foreach ($errmsg as $message) {
debug_log_insert('data_query', __esc('Field \'%s\' %s', $field_name,$message));
}
}
}
$values = [];
$oid = '';
foreach ($snmp_indexes as $index) {
$oid = $field_array['oid'];
if (isset($field_array['rewrite_index'])) {
if (isset($rewritten_indexes[$index])) {
$oid_suffix = $rewritten_indexes[$index];
} else {
// we failed to build rewritten index. warnings are sent already, just skip this index silently
continue;
}
$oid .= '.' . $oid_suffix;
} else {
$oid .= '.' . $index;
}
$oid .= isset($field_array['oid_suffix']) ? ('.' . $field_array['oid_suffix']) : '';
$value = null;
if (str_starts_with($field_array['source'], 'OID/REGEXP:')) {
$value = preg_replace('/' . str_replace('OID/REGEXP:', '', $field_array['source']) . '/', '\\1', $oid);
} elseif (str_starts_with($field_array['source'], 'OID2HEX/REGEXP:')) {
$value = preg_replace('/' . str_replace('OID2HEX/REGEXP:', '', $field_array['source']) . '/', '\\1', $oid);
$parts = explode('.', $value);
$ip_value = '';
foreach ($parts as $idx => $part) {
if (is_numeric($part)) {
$parts[$idx] = substr(cacti_strtoupper('00' . dechex(intval($part))), -2);
} else {
$parts[$idx] = substr(cacti_strtoupper('00' . $part), -2);
}
if ($idx % 2 == 0 && $idx > 0) {
$ip_value .= ':' . $parts[$idx];
} else {
$ip_value .= $parts[$idx];
}
}
$value = implode(':', $parts);
// Check for ip address and shorten
if (is_ipaddress($ip_value)) {
$value = inet_ntop(inet_pton($ip_value));
}
}
$values[] = ['value' => $value, 'index' => $index, 'oid' => $oid];
}
if (cacti_sizeof($values) > 0 && (str_starts_with($field_array['source'], 'VALUE/REGEXP:') || $field_array['source'] == 'value')) {
$oids = [];
foreach ($values as $key => $value) {
$oids[] = $value['oid'];
}
debug_log_insert('data_query', __esc('Executing SNMP get for %s oids (%s)' , cacti_count($oids), implode(', ', $oids)));
$value_output_format = SNMP_STRING_OUTPUT_GUESS;
if (isset($field_array['output_format'])) {
if ($field_array['output_format'] == 'hex') {
$value_output_format = SNMP_STRING_OUTPUT_HEX;
} elseif ($field_array['output_format'] == 'ascii') {
$value_output_format = SNMP_STRING_OUTPUT_ASCII;
} else {
$value_output_format = SNMP_STRING_OUTPUT_GUESS;
}
}
$results = [];
foreach ($oids as $oid) {
$results[$oid] = cacti_snmp_get($host['hostname'], $host['snmp_community'], $oid, $host['snmp_version'],
$host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'],
$host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'],
$host['snmp_port'], $host['snmp_timeout'], SNMP_POLLER, $host['snmp_engine_id'],
$value_output_format);
}
if (!cacti_sizeof($results) && $field_name == $sort_field) {
query_debug_timer_offset('data_query', __esc('Sort field returned no data for Host[%s] DQ[%s], skipping.', $host_id, $snmp_query_id));
} elseif (cacti_sizeof($results)) {
foreach ($results as $key => $value) {
debug_log_insert('data_query',
__esc('Found result for data @ \'%s\' [value=\'%s\']',
$key, $value));
}
foreach (array_keys($values) as $key) {
if (isset($results[$values[$key]['oid']])) {
$values[$key]['value'] = $results[$values[$key]['oid']];
debug_log_insert('data_query',
__esc('Setting result for data @ \'%s\' [key=\'%s\', value=\'%s\']',
$values[$key]['oid'], $key, $values[$key]['value']));
} else {
debug_log_insert('data_query',
__esc('Skipped result for data @ \'%s\' [key=\'%s\', value=\'%s\']',
$values[$key]['oid'], $key, $values[$key]['value']));
}
}
foreach ($values as $key => $value) {
if (str_starts_with($field_array['source'], 'VALUE/REGEXP:')) {
$values[$key]['value'] = preg_replace('/' . str_replace('VALUE/REGEXP:', '', $field_array['source']) . '/', '\\1', $values[$key]['value']);
}
}
}
}
foreach ($values as $item) {
debug_log_insert('data_query', __esc('Got SNMP get result for data @ \'%s\' [value=\'%s\'] (index: %s)', $item['oid'], $item['value'], $item['index']));
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $item['value'], $item['index'], $item['oid']);
}
$values = null;
} elseif (str_starts_with($field_array['source'], 'VALUE/REGEXP:')) {
foreach ($snmp_indexes as $oid => $index) {
$oid = $field_array['oid'] . '.' . $index;
$oid .= isset($field_array['oid_suffix']) ? ('.' . $field_array['oid_suffix']) : '';
if (isset($field_array['output_format'])) {
if ($field_array['output_format'] == 'hex') {
$value_output_format = SNMP_STRING_OUTPUT_HEX;
} elseif ($field_array['output_format'] == 'ascii') {
$value_output_format = SNMP_STRING_OUTPUT_ASCII;
} else {
$value_output_format = SNMP_STRING_OUTPUT_GUESS;
}
$value = cacti_snmp_get($host['hostname'], $host['snmp_community'], $oid, $host['snmp_version'],
$host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'],
$host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'],
$host['snmp_port'], $host['snmp_timeout'], SNMP_POLLER, $host['snmp_engine_id'],
$value_output_format);
} else {
$value = cacti_snmp_session_get($session, $oid);
}
$value = preg_replace('/' . str_replace('VALUE/REGEXP:', '', $field_array['source']) . '/', '\\1', $value);
query_debug_timer_offset('data_query', __esc('Executing SNMP get for data @ \'%s\' [value=\'$value\']', $oid, $value));
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $value, $index, $oid);
}
}
} elseif ($field_array['method'] == 'walk' && ($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output')) {
debug_log_insert_section_start('data_query', __esc('Click to show Data Query output for field \'%s\'', $field_name), true);
query_debug_timer_offset('data_query', __esc('Located input field \'%s\' [walk]', $field_name));
if (isset($field_array['output_format'])) {
if ($field_array['output_format'] == 'hex') {
$value_output_format = SNMP_STRING_OUTPUT_HEX;
} elseif ($field_array['output_format'] == 'ascii') {
$value_output_format = SNMP_STRING_OUTPUT_ASCII;
} else {
$value_output_format = SNMP_STRING_OUTPUT_GUESS;
}
$data = cacti_snmp_walk($host['hostname'], $host['snmp_community'], $field_array['oid'], $host['snmp_version'],
$host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'],
$host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'],
$host['snmp_port'], $host['snmp_timeout'], $host['ping_retries'], $walk_size, 'SNMP',
$host['snmp_engine_id'], $value_output_format);
$snmp_data = [];
if (cacti_sizeof($data)) {
foreach ($data as $d) {
$snmp_data[$d['oid']] = $d['value'];
}
} elseif ($field_name == $sort_field) {
$empty_types[] = $field_name;
query_debug_timer_offset('data_query', __esc('Sort field returned no data for OID[%s], skipping.', $field_array['oid']));
continue;
}
} else {
$snmp_data = cacti_snmp_session_walk($session, $field_array['oid']);
if (!cacti_sizeof($snmp_data) && $field_name == $sort_field) {
$empty_types[] = $field_name;
query_debug_timer_offset('data_query', __esc('Sort field returned no data for OID[%s], skipping.', $field_array['oid']));
continue;
}
}
query_debug_timer_offset('data_query', __esc('Executing SNMP walk for data @ \'%s\'', $field_array['oid']));
$const_table = [];
if (preg_match('/^VALUE\/TABLE:(.*)/',$field_array['source'],$matches)) {
preg_match_all('/([^:]+):([^:]+)/',$matches[1],$match_temp);
$const_table = array_combine($match_temp[1],$match_temp[2]);
}
if (preg_match('/^value/i', $field_array['source'])) {
if (cacti_sizeof($snmp_data)) {
foreach ($snmp_data as $oid => $value) {
$index_regex = ($field_array['oid_index_parse'] ?? $index_parse_regexp) . (isset($field_array['oid_suffix']) ? ('.' . $field_array['oid_suffix']) : '');
if (!preg_match($index_regex, $oid)) {
continue;
}
$snmp_index = preg_replace($index_regex,'\\1', $oid);
if (isset($snmp_queries['value_index_parse'])) {
if (!in_array($snmp_index, $snmp_indexes, true)) {
debug_log_insert('data_query', __esc('No index[%s] in value_index_parse, skipping.', $snmp_index));
unset($snmp_data[$oid]);
continue;
}
}
$oid = $field_array['oid'] . ".$snmp_index" . (isset($field_array['oid_suffix']) ? ('.' . $field_array['oid_suffix']) : '');
if ($field_name == 'ifOperStatus' || $field_name == 'ifAdminStatus') {
$value = match (true) {
preg_match('/^(down|2)/i', $value) > 0 => 'Down',
preg_match('/^(up|1)/i', $value) > 0 => 'Up',
preg_match('/^(not|6)/i', $value) > 0 => 'notPresent',
default => 'Testing',
};
$mode = 'value';
} elseif (preg_match('/VALUE\/REGEXP:(.*)/', $field_array['source'], $matches)) {
$modified_value = preg_replace('/' . $matches[1] . '/', '\\1', $value);
$mode = 'regexp value parse';
} elseif (preg_match('/^VALUE\/TEST:(.*):(.*):(.*)/', $field_array['source'], $matches)) {
$modified_value = (strcmp($matches[1], $value) !== 0) ? $matches[3] : $matches[2];
$mode = 'test value parse';
} elseif (preg_match('/^VALUE\/TABLE:(.*)/',$field_array['source'])) {
$modified_value = (array_key_exists($value, $const_table)) ? $const_table[$value] : 'N/A';
$mode = 'table value parse';
} elseif (preg_match('/^VALUE\/HEX2IP:(\d+):(\d+)/', $field_array['source'], $matches)) {
// $modified_value = substr(str_replace(':','',$value),$matches[1],$matches[2]);
$modified_value = substr(preg_replace('/(:|HEX-)/','', $value), intval($matches[1]), intval($matches[2]));
$modified_value = implode('.', unpack('C*', pack('H*', $modified_value)));
$mode = 'hex2ip value parse';
} else {
$mode = 'value';
}
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $modified_value ?? $value, $snmp_index, $oid);
debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s [from %s]',$field_name,isset($modified_value) ? "$modified_value ($value)" : $value,$snmp_index,$mode));
unset($modified_value);
}
}
} elseif (str_starts_with($field_array['source'], 'OID/REGEXP:')) {
if (cacti_sizeof($snmp_data)) {
foreach ($snmp_data as $oid => $value) {
// Re-initialize the index
$snmp_index = '';
$parse_value = preg_replace('/' . str_replace('OID/REGEXP:', '', $field_array['source']) . '/', '\\1', $oid);
if (isset($snmp_queries['oid_index_parse'])) {
$snmp_index = preg_replace($index_parse_regexp, '\\1', $oid);
} elseif ((isset($value)) && ($value != '')) {
$snmp_index = $value;
}
if (isset($snmp_queries['value_index_parse'])) {
if (!in_array($snmp_index, $snmp_indexes, true)) {
debug_log_insert('data_query', __esc('No index[%s] in value_index_parse, skipping.', $snmp_index));
unset($snmp_data[$oid]);
continue;
}
}
$oid = $field_array['oid'] . '.' . $parse_value;
// rewrite octet strings
if (preg_match('/^\d{1,3}(\.\d{1,3}){2,}$/', $parse_value)) {
$octets = explode('.', $parse_value);
$size = array_shift($octets);
if (cacti_count($octets) == $size) {
$decoded = '';
$isascii = true;
foreach ($octets as $octet) {
$octet = intval($octet);
if (($octet <= 31) || ($octet >= 127)) {
$isascii = false;
} else {
$decoded .= chr($octet);
}
}
if ($isascii) {
query_debug_timer_offset('data_query', __esc('Found OCTET STRING \'%s\' decoded value: \'%s\'', $parse_value, $decoded));
$parse_value = $decoded;
}
}
}
debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s [from regexp oid parse]', $field_name, $parse_value, $snmp_index));
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $parse_value, $snmp_index, $oid);
}
}
} elseif (str_starts_with($field_array['source'], 'OID2HEX/REGEXP:')) {
if (cacti_sizeof($snmp_data)) {
foreach ($snmp_data as $oid => $value) {
// Re-initialize the index
$snmp_index = '';
$parse_value = preg_replace('/' . str_replace('OID2HEX/REGEXP:', '', $field_array['source']) . '/', '\\1', $oid);
if (isset($snmp_queries['oid_index_parse'])) {
$snmp_index = preg_replace($index_parse_regexp, '\\1', $oid);
} elseif ((isset($value)) && ($value != '')) {
$snmp_index = $value;
}
if (isset($snmp_queries['value_index_parse'])) {
if (!in_array($snmp_index, $snmp_indexes, true)) {
debug_log_insert('data_query', __esc('No index[%s] in value_index_parse, skipping.', $snmp_index));
unset($snmp_data[$oid]);
continue;
}
}
$oid = $field_array['oid'] . '.' . $parse_value;
$ip_value = '';
// Check for an IPv6 Address or Hex String
$parts = explode('.', $parse_value);
foreach ($parts as $idx => $part) {
if (is_numeric($part)) {
$parts[$idx] = substr(cacti_strtoupper('00' . dechex(intval($part))), -2);
} else {
$parts[$idx] = substr(cacti_strtoupper('00' . $part), -2);
}
if ($idx % 2 == 0 && $idx > 0) {
$ip_value .= ':' . $parts[$idx];
} else {
$ip_value .= $parts[$idx];
}
}
$parse_value = implode(':', $parts);
// Check for ip address and shorten
if (is_ipaddress($ip_value)) {
$parse_value = inet_ntop(inet_pton($ip_value));
}
debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s [from regexp oid parse]', $field_name, $parse_value, $snmp_index));
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $parse_value, $snmp_index, $oid);
}
}
} elseif (str_starts_with($field_array['source'], 'OIDVALUE/REGEXP:')) {
$regex_array = explode(':', str_replace('OIDVALUE/REGEXP:', '', $field_array['source']));
if (cacti_sizeof($snmp_data)) {
foreach ($snmp_data as $oid => $value) {
// Re-initialize the index
$snmp_index = '';
$parse_value = preg_replace('/' . $regex_array[0] . '/', $regex_array[1], $oid);
if (isset($snmp_queries['oid_index_parse'])) {
$snmp_index = preg_replace($index_parse_regexp, '\\1', $oid);
} elseif ((isset($value)) && ($value != '')) {
$snmp_index = $value;
}
if (isset($snmp_queries['value_index_parse'])) {
if (!in_array($snmp_index, $snmp_indexes, true)) {
debug_log_insert('data_query', __esc('No index[%s] in value_index_parse, skipping.', $snmp_index));
unset($snmp_data[$oid]);
continue;
}
}
$oid = $field_array['oid'];
debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s [from regexp oid value parse]', $field_name, $parse_value, $snmp_index));
$output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $parse_value, $snmp_index, $oid);
}
}
}
debug_log_insert_section_end('data_query');
}
$fields_processed[] = $field_name;
}
$session->close();
if (cacti_sizeof($output_array)) {
data_query_update_host_cache_from_buffer($host_id, $snmp_query_id, $output_array, $empty_types);
}
return true;
}
/**
* Formats a data query record for insertion into the database.
*
* @param int $host_id The ID of the host associated with the data query.
* @param int $snmp_query_id The ID of the SNMP query being processed.
* @param string $field_name The name of the field being queried.
* @param mixed $rewrite_value Optional value used to rewrite the SNMP field value.
* @param string $value The value retrieved from the SNMP query.
* @param string $snmp_index The SNMP index associated with the value.
* @param string $oid The Object Identifier (OID) for the SNMP query.
*
* @return string A formatted string representing the data query record, ready for database insertion.
*/
function data_query_format_record(int $host_id, int $snmp_query_id, string $field_name, $rewrite_value, string $value, string $snmp_index, string $oid) : string {
global $data_query_rewrite_indexes_cache;
if ($rewrite_value !== null) {
$value = rewrite_snmp_enum_value($field_name, $value, $rewrite_value);
}
$hash = "$host_id@$snmp_query_id@$field_name";
if (!isset($data_query_rewrite_indexes_cache[$hash])) {
$data_query_rewrite_indexes_cache[$hash] = [];
}
$data_query_rewrite_indexes_cache[$hash][$snmp_index] = $value;
if (mb_detect_encoding($value, mb_detect_order(), true) === false) {
$value = bin2hex($value);
}
return "($host_id, $snmp_query_id, " . db_qstr($field_name) . ', ' . db_qstr($value) . ', ' . db_qstr($snmp_index) . ', ' . db_qstr($oid) . ', 1)';
}
/**
* Validates if a given string contains only Unicode letters, numbers,
* whitespace, and a specific set of special characters.
*
* @param string $value The input string to validate.
*
* @return int Returns 1 if the string matches the pattern, 0 if it does not.
*/
function data_query_ctype_print_unicode(string $value) : int {
$pattern = "~^[\pL\pN\s\"\~" . preg_quote("!#$%&'()*+,-./:;<=>?@[\]^_`{|}´") . ']+$~u';
return (int) preg_match($pattern, $value);
}
/**
* Updates the SNMP host cache for a specific host and SNMP query using the provided data buffer.
*
* @param int $host_id The ID of the host whose SNMP cache is being updated.
* @param int $snmp_query_id The ID of the SNMP query associated with the cache entries.
* @param array $output_array An array of records to be inserted or updated in the cache.
* Each record should be a string formatted as:
* "(host_id, snmp_query_id, field_name, field_value, snmp_index, oid, present)".
* @param array $empty_types An array of field names to be marked as present in the cache.
*
* @return void
*/
function data_query_update_host_cache_from_buffer(int $host_id, int $snmp_query_id, array &$output_array, array &$empty_types) : void {
// set all fields present value to 0, to mark the outliers when we are all done
db_execute_prepared('UPDATE host_snmp_cache
SET present = 0
WHERE host_id = ?
AND snmp_query_id = ?',
[$host_id, $snmp_query_id]);
// setup the database call
$sql_prefix = 'INSERT INTO host_snmp_cache (host_id, snmp_query_id, field_name, field_value, snmp_index, oid, present) VALUES';
$sql_suffix = ' ON DUPLICATE KEY UPDATE field_value=VALUES(field_value), oid=VALUES(oid), present=VALUES(present)';
// use a reasonable insert buffer, the default is 1MByte
$max_packet = 256000;
// setup some defaults
$overhead = strlen($sql_prefix) + strlen($sql_suffix);
$buf_len = 0;
$buf_count = 0;
$buffer = '';
foreach ($output_array as $record) {
if ($buf_count == 0) {
$delim = ' ';
} else {
$delim = ', ';
}
$buffer .= $delim . $record;
$buf_len += strlen($record);
if (($overhead + $buf_len) > ($max_packet - 1024)) {
db_execute($sql_prefix . $buffer . $sql_suffix);
$buffer = '';
$buf_len = 0;
$buf_count = 0;
} else {
$buf_count++;
}
}
if ($buf_count > 0) {
db_execute($sql_prefix . $buffer . $sql_suffix);
}
if (cacti_sizeof($empty_types)) {
foreach ($empty_types as $field) {
db_execute_prepared('UPDATE host_snmp_cache
SET present = 1
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?',
[$host_id, $snmp_query_id, $field]);
}
}
// remove stale records from the host cache
db_execute_prepared("DELETE FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND present='0'",
[$host_id, $snmp_query_id]);
}
/**
* Rewrites SNMP indexes based on a specified rewrite index pattern.
*
* @param array &$errmsg Reference to an array that will store error messages.
* @param int $host_id The ID of the host for which the SNMP query is being processed.
* @param int $snmp_query_id The ID of the SNMP query being processed.
* @param string $rewrite_index The rewrite index pattern, which may include placeholders for dynamic substitution.
* @param array|string $snmp_indexes The SNMP indexes to be processed. Can be a single index or an array of indexes.
* @param array|bool $fields_processed An array of fields that have already been processed. Defaults to false.
*
* @return array|string|null Returns the rewritten indexes as an array if multiple indexes are provided,
* a string if a single index is provided, or null if no valid output is generated.
*
* @throws Exception This function does not explicitly throw exceptions, but database or other errors
* may occur during execution.
*/
function data_query_rewrite_indexes(array &$errmsg, int $host_id, int $snmp_query_id, string $rewrite_index,
array|string $snmp_indexes, array|bool $fields_processed = false) : array|string|null {
global $data_query_rewrite_indexes_cache;
$errmsg = [];
$oid_items = explode('.', $rewrite_index);
$chain_indexes = [];
if (!is_array($data_query_rewrite_indexes_cache)) {
$data_query_rewrite_indexes_cache = [];
}
if (cacti_sizeof($oid_items)) {
foreach ($oid_items as $item) {
$matches = [];
if (preg_match('/^\|query_([^|]+)\|$/', $item, $matches)) {
$iv = $matches[1];
if (is_array($fields_processed) && !in_array($iv, $fields_processed, true)) {
$errmsg[] = "rewrite_index='$rewrite_index': '$iv' is not processed yet, could not use it as index source";
continue;
}
if (!isset($chain_indexes[$iv])) {
$hash = "$host_id@$snmp_query_id@$iv";
if (!isset($data_query_rewrite_indexes_cache[$hash])) {
$data_query_rewrite_indexes_cache[$hash] = [];
$iv = db_qstr($iv);
$field_values = db_fetch_assoc_prepared('SELECT snmp_index, field_value
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?',
[$host_id, $snmp_query_id, $iv]);
if (cacti_sizeof($field_values)) {
foreach ($field_values as $item) {
$data_query_rewrite_indexes_cache[$hash][$item['snmp_index']] = $item['field_value'];
}
} else {
$errmsg[] = " field '$iv' was not found either in cache nor in DB";
}
}
$chain_indexes[$iv] = $data_query_rewrite_indexes_cache[$hash];
}
}
}
}
$out = [];
$numeric_output = false;
if (!is_array($snmp_indexes)) {
$snmp_indexes = [$snmp_indexes];
$numeric_output = true;
}
foreach ($snmp_indexes as $num_index) {
$index = $rewrite_index;
foreach ($chain_indexes as $key => $values) {
if (isset($values[$num_index]) && preg_match('/^[0-9.]+$/', $values[$num_index])) {
$index = str_replace("|query_$key|", trim($values[$num_index]), $index);
} else {
$errmsg[] = "@'" . $num_index . "': could not load value of '$key'";
}
}
$index = str_replace('|index|', trim($num_index), $index);
if (!preg_match('/^[0-9.]*$/', $index)) {
$errmsg[] = "@'" . $num_index . "': some portions of rewrite_index field were not parsed: '$index'";
continue;
}
if ($numeric_output) {
return $index;
}
$out[$num_index] = $index;
}
if ($numeric_output) {
return null;
}
return $out;
}
/**
* Rewrites an SNMP enumeration value based on a translation map.
*
* @param string|null $field_name The name of the field to which the translation map applies.
* If null, the internal map cache is cleared.
* @param string|null $value The value to be rewritten. Defaults to null.
* @param mixed $map The translation map, either as an array or a serialized string.
* Defaults to null.
*
* @return string|null The rewritten value, or the original value if no match is found.
* Returns null if the field name is null.
*
* @throws Exception If the translation map cannot be parsed.
*/
function rewrite_snmp_enum_value(string|null $field_name, string|null $value = null, mixed $map = null) : string|null {
static $mapcache = [];
if ($field_name === null) {
$mapcache = [];
return null;
}
if (is_array($map)) { // produced from XML, needs to be reformatted
$newmap = [];
foreach ($map as $index => $item) {
if (!isset($item['match']) || !isset($item['replace'])) {
debug_log_insert('data_query', __esc('Bogus rewrite_value item found, index=\'%s\'', $index));
continue;
}
$newmap[$item['match']] = $item['replace'];
}
$map = $newmap;
} else {
$map = cacti_unserialize($map);
}
if ($map === false || !is_array($map)) {
debug_log_insert('data_query', __esc('Could not parse translation map (rewrite_value)'));
return $value;
}
if (!isset($mapcache[$field_name])) {
$mapcache[$field_name] = [];
foreach ($map as $src => $dst) {
if (preg_match('/^REGEXP(NC)?:(.*)$/', $src, $matches)) {
if ($matches[1] == 'NC') {
$src = '/' . str_replace('/', '\/', $matches[2]) . '/i';
} else {
$src = '/' . str_replace('/', '\/', $matches[1]) . '/';
}
} else {
$src = '/^' . str_replace('/^', '\/', $src) . '$/';
}
$mapcache[$field_name][$src] = $dst;
}
}
foreach ($mapcache[$field_name] as $src => $dst) {
if (preg_match($src, $value)) {
$nvalue = preg_replace($src, $dst, $value);
debug_log_insert('data_query', __esc("rewrite_value: '%s' => '%s'", $value, $nvalue));
$value = $nvalue;
break;
}
}
return $value;
}
/**
* Returns an array containing the data query ID and index value given
* a data query index type/value combination and a host ID
*
* @param string $index_type The type of the SNMP index field (e.g., field name).
* @param string $index_value The value of the SNMP index field.
* @param int $host_id The ID of the host to query.
* @param int $data_query_id The ID of the SNMP query.
*
* @return string|null The SNMP index if found, or null if no matching record exists.
*/
function data_query_index(string $index_type, string $index_value, int $host_id, int $data_query_id) : string|null {
return db_fetch_cell_prepared('SELECT snmp_index
FROM host_snmp_cache
WHERE field_name = ?
AND field_value = ?
AND host_id = ?
AND snmp_query_id = ?',
[$index_type, $index_value, $host_id, $data_query_id]);
}
/**
* Returns an array containing data query information for a given data source
*
* @param int $data_template_data_id the ID of the data source to retrieve information for
*
* @return array an array that looks like:
*
* [
* index_type => ifIndex,
* index_value => 3,
* output_type => 13
* ]
*/
function data_query_field_list(int $data_template_data_id) : array|int {
$field = db_fetch_assoc_prepared("SELECT dif.type_code, did.value
FROM data_input_fields AS dif
INNER JOIN data_input_data AS did
ON dif.id=did.data_input_field_id
WHERE did.data_template_data_id = ?
AND dif.type_code IN ('index_type','index_value','output_type')",
[$data_template_data_id]);
$field = array_rekey($field, 'type_code', 'value');
if ((!isset($field['index_type'])) || (!isset($field['index_value'])) || (!isset($field['output_type']))) {
return 0;
} else {
return $field;
}
}
/**
* Encodes a data query index value so that it can be included inside of a form
*
* @param string $index The index name to encode
*
* @return string The encoded data query index
*/
function encode_data_query_index(string $index) : string {
return md5($index);
}
/**
* Decodes a data query index value so that it can be read from a form
*
* @param string $encoded_index The encoded SNMP index to decode.
* @param int $data_query_id The ID of the data query associated with the SNMP index.
* @param int $host_id The ID of the host associated with the SNMP index.
*
* @return mixed The decoded SNMP index if a match is found, or null if no match is found.
*/
function decode_data_query_index(string $encoded_index, int $data_query_id, int $host_id) : mixed {
/* yes, i know MySQL has a MD5() function that would make this a bit quicker. however i would like to
keep things abstracted for now so Cacti works with ADODB fully when i get around to porting my db calls */
$indexes = db_fetch_assoc_prepared('SELECT snmp_index
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
GROUP BY snmp_index', [$host_id, $data_query_id]);
if (cacti_sizeof($indexes)) {
foreach ($indexes as $index) {
if (encode_data_query_index($index['snmp_index']) == $encoded_index) {
return $index['snmp_index'];
}
}
}
return null;
}
/**
* Uupdates the local data query cache for each graph AND data source tied to this host/data query
*
* @param int $host_id The ID of the host for which the data query cache is being updated.
* @param int $data_query_id The ID of the data query to update the cache for.
*
* @return void
*/
function update_data_query_cache(int $host_id, int $data_query_id) : void {
$graphs = db_fetch_assoc_prepared('SELECT *
FROM graph_local
WHERE host_id = ?
AND snmp_query_id = ?',
[$host_id, $data_query_id]);
if (cacti_sizeof($graphs)) {
foreach ($graphs as $graph) {
update_graph_data_query_cache($graph['id'], $host_id, $data_query_id, $graph['snmp_index']);
}
}
query_debug_timer_offset('data_query', __esc('Update graph data query cache complete'));
$data_sources = db_fetch_assoc_prepared('SELECT *
FROM data_local
WHERE host_id = ?
AND snmp_query_id = ?',
[$host_id, $data_query_id]);
if (cacti_sizeof($data_sources) > 0) {
// every row here shares $host_id, so there's a single poller_id boundary
// for the whole loop; accumulate and flush once instead of committing
// the poller cache write (present/insert/delete) per changed row
$poller_id = db_fetch_cell_prepared('SELECT poller_id FROM host WHERE id = ?', [$host_id]);
$poller_items = [];
$changed_data_ids = [];
foreach ($data_sources as $data_source) {
$changed = update_data_source_data_query_cache($data_source['id'], $host_id, $data_query_id, $data_source['snmp_index']);
if ($changed) {
$changed_data_ids[] = $data_source['id'];
$poller_items = array_merge($poller_items, update_poller_cache($data_source, false, $poller_id));
}
}
if (cacti_sizeof($changed_data_ids)) {
poller_update_poller_cache_from_buffer($changed_data_ids, $poller_items, $poller_id);
}
}
query_debug_timer_offset('data_query', __esc('Re-Indexing Data Query complete'));
}
/**
* Updates the local data query cache for a particular graph
*
* @param int $local_graph_id The ID of the local graph to update.
* @param int $host_id The ID of the host associated with the graph.
* If not provided, it will be fetched from the database.
* @param int $data_query_id The ID of the data query. If not provided,
* it will be fetched based on the graph's output type.
* @param string $previous_index The previous index value. If the current
* index differs from this value, the graph's index will
* be updated.
*
* @return void
*/
function update_graph_data_query_cache(int $local_graph_id, int $host_id = 0, int $data_query_id = 0, string $previous_index = '') : void {
global $data_query_id_cache;
if ((!is_array($data_query_id_cache)) || cacti_sizeof($data_query_id_cache) == 0) {
$data_query_id_cache = array_rekey(
db_fetch_assoc('SELECT id, snmp_query_id
FROM snmp_query_graph'),
'id', 'snmp_query_id'
);
}
if (empty($host_id)) {
$host_id = db_fetch_cell_prepared('SELECT host_id
FROM graph_local
WHERE id = ?',
[$local_graph_id]);
}
$field = data_query_field_list(db_fetch_cell_prepared('SELECT dtd.id
FROM graph_templates_item AS gti
INNER JOIN data_template_rrd AS dtr
ON gti.task_item_id=dtr.id
INNER JOIN data_template_data AS dtd
ON dtr.local_data_id=dtd.local_data_id
WHERE gti.local_graph_id = ?
LIMIT 1',
[$local_graph_id]));
if (empty($field)) {
return;
}
if (empty($data_query_id)) {
$data_query_id = db_fetch_cell_prepared('SELECT snmp_query_id
FROM snmp_query_graph
WHERE id = ?',
[$field['output_type']]);
}
$current_index = data_query_index($field['index_type'], $field['index_value'], $host_id, $data_query_id);
if ($data_query_id != 0 && $current_index != '' && $current_index != $previous_index) {
// set the index to the new index
db_execute_prepared('UPDATE graph_local
SET snmp_query_id = ?,
snmp_index = ?
WHERE id = ?',
[$data_query_id, $current_index, $local_graph_id]);
// update graph title cache
update_graph_title_cache($local_graph_id);
}
}
/**
* Updates the local data query cache for a particular data source
*
* This function ensures that the data query cache is updated with the latest
* SNMP query ID and index for a specific data source. It also updates the
* data source title cache if changes are made.
*
* @param int $local_data_id The ID of the local data source to update.
* @param int $host_id The ID of the host associated with the data source.
* If not provided, it will be fetched from the database.
* @param int $data_query_id The ID of the SNMP query. If not provided, it will
* be determined based on the data source's output type.
* @param string $previous_index The previous index value. If the current index
* matches the previous index, no update will occur.
*
* @return bool - Returns true if the data query cache was updated successfully, false otherwise.
*/
function update_data_source_data_query_cache(int $local_data_id, int $host_id = 0, int $data_query_id = 0, string $previous_index = '') : bool {
global $data_query_id_cache;
if ((!is_array($data_query_id_cache)) || cacti_sizeof($data_query_id_cache) == 0) {
$data_query_id_cache = array_rekey(
db_fetch_assoc('SELECT id, snmp_query_id
FROM snmp_query_graph'),
'id', 'snmp_query_id'
);
}
if (empty($host_id)) {
$host_id = db_fetch_cell_prepared('SELECT host_id
FROM data_local
WHERE id = ?',
[$local_data_id]);
}
$field = data_query_field_list(db_fetch_cell_prepared('SELECT
data_template_data.id
FROM data_template_data
WHERE data_template_data.local_data_id = ?',
[$local_data_id]));
if (empty($field)) {
return false;
}
if (empty($data_query_id)) {
$data_query_id = db_fetch_cell_prepared('SELECT snmp_query_id
FROM snmp_query_graph
WHERE id = ?',
[$field['output_type']]);
}
$current_index = data_query_index($field['index_type'], $field['index_value'], (int) $host_id, (int) $data_query_id);
if ($data_query_id != 0 && $current_index != '' && $current_index != $previous_index) {
// set the index to the new index
db_execute_prepared('UPDATE data_local
SET snmp_query_id = ?,
snmp_index = ?,
orphan = 0
WHERE id = ?',
[$data_query_id, $current_index, $local_data_id]);
// update data source title cache
update_data_source_title_cache($local_data_id);
return true;
}
return false;
}
/**
* Obtains a list of indexes for a host/data query that is sorted by the
* chosen index field and formatted using the data query index title format
*
* @param int $host_id The ID of the host. If 0, the function will
* fetch data without considering a specific host.
* @param int $data_query_id The ID of the data query. Must not be empty.
*
* @return array an array formatted like the following: $arr[snmp_index] = 'formatted data query index string'
*/
function get_formatted_data_query_indexes(int $host_id, int $data_query_id) : array {
include_once(CACTI_PATH_LIBRARY . '/sort.php');
if (empty($data_query_id)) {
return ['' => __('Unknown Index')];
}
// from the xml; cached in 'host_snmp_query'
if ($host_id > 0) {
$sort_cache = db_fetch_row_prepared('SELECT sort_field, title_format
FROM host_snmp_query
WHERE host_id = ?
AND snmp_query_id = ?',
[$host_id, $data_query_id]);
} else {
$sort_cache = db_fetch_row_prepared('SELECT sort_field, title_format
FROM host_snmp_query
WHERE snmp_query_id = ?
LIMIT 1',
[$data_query_id]);
}
// in case no unique index is available, fallback to first field in XML
if (empty($sort_cache['sort_field'])) {
$snmp_queries = get_data_query_array($data_query_id);
$sort_cache = ['sort_field' => '', 'title_format' => ''];
if (cacti_sizeof($snmp_queries) && isset($snmp_queries['index_order'])) {
$i = explode(':', $snmp_queries['index_order']);
if (cacti_sizeof($i) > 0) {
$sort_cache['sort_field'] = array_shift($i);
}
}
}
/* get a list of data query indexes AND the field value that we are supposed
to sort */
if ($host_id > 0) {
$sort_field_data = array_rekey(db_fetch_assoc_prepared('SELECT gl.snmp_index, hsc.field_value
FROM graph_local AS gl
INNER JOIN host_snmp_cache AS hsc
ON gl.host_id=hsc.host_id
AND gl.snmp_query_id=hsc.snmp_query_id
AND gl.snmp_index=hsc.snmp_index
WHERE gl.snmp_query_id = ?
AND gl.host_id = ?
AND hsc.field_name = ?
GROUP BY gl.snmp_index',
[$data_query_id, $host_id, $sort_cache['sort_field']]), 'snmp_index', 'field_value');
} else {
$sort_field_data = array_rekey(db_fetch_assoc_prepared('SELECT DISTINCT gl.snmp_index, hsc.field_value
FROM graph_local AS gl
INNER JOIN host_snmp_cache AS hsc
ON gl.host_id=hsc.host_id
AND gl.snmp_query_id=hsc.snmp_query_id
AND gl.snmp_index=hsc.snmp_index
WHERE gl.snmp_query_id = ?
AND hsc.field_name = ?
GROUP BY gl.snmp_index',
[$data_query_id, $sort_cache['sort_field']]), 'snmp_index', 'field_value');
}
// sort the data using the 'data query index' sort algorithm
uasort($sort_field_data, 'usort_data_query_index');
$sorted_results = [];
foreach ($sort_field_data as $snmp_index => $sort_field_value) {
$sorted_results[$snmp_index] = substitute_snmp_query_data($sort_cache['title_format'], $host_id, $data_query_id, $snmp_index);
}
return $sorted_results;
}
/**
* Obtains a single index for a host/data query/data query
* index that is formatted using the data query index title format
*
* @param int $host_id The ID of the host for which the data query index is being retrieved.
* @param int $data_query_id The ID of the SNMP query associated with the host.
* @param string $data_query_index The index of the data query to be formatted.
*
* @return string The formatted data query index after substituting the SNMP query data.
*/
function get_formatted_data_query_index(int $host_id, int $data_query_id, string $data_query_index) : string {
// from the xml; cached in 'host_snmp_query'
$sort_cache = db_fetch_row_prepared('SELECT sort_field, title_format
FROM host_snmp_query
WHERE host_id = ?
AND snmp_query_id = ?',
[$host_id, $data_query_id]);
return substitute_snmp_query_data($sort_cache['title_format'], $host_id, $data_query_id, $data_query_index);
}
/**
* Calculates or sets the 'index_order' key in the provided raw XML array.
*
* @param array $raw_xml The raw XML data array passed by reference. It may
* be modified to include the 'index_order' key if it
* is not already set.
*
* @return void
*/
function calculate_or_set_index_order(array &$raw_xml) : void {
if (!isset($raw_xml['index_order']) && isset($raw_xml['fields']) && is_array($raw_xml['fields'])) {
foreach ($raw_xml['fields'] as $name => $attribs) {
if (isset($attribs['source']) && $attribs['source'] == 'index') {
$raw_xml['index_order'] = $name;
break;
}
}
}
}
/**
* Builds an ordered list of data query index types that are valid given a list of data query
* indexes that will be checked against the data query cache
*
* @param int $host_id The ID of the host for which the index types are being retrieved.
* @param int $data_query_id The ID of the data query associated with the host.
*
* @return array An ordered list of index types suitable for the given host and data query.
* Returns an empty array if no valid index types are found.
*
* @throws Exception If database queries fail or unexpected conditions occur.
*/
function get_ordered_index_type_list(int $host_id, int $data_query_id) : array {
$raw_xml = get_data_query_array($data_query_id);
// invalid xml check
if (cacti_sizeof($raw_xml) == 0) {
return [];
}
$must_be_numeric = false;
$must_be_alpha = false;
$order_found = true;
$order_unknown = false;
$nonunique = false;
$oid_index = false;
$avail_indexes = [];
if (isset($raw_xml['oid_index'])) {
$oid_index = $raw_xml['oid_index'];
}
if (isset($raw_xml['index_order_type'])) {
switch ($raw_xml['index_order_type']) {
case 'numeric':
$mustbenumeric = true;
break;
case 'alpha':
case 'alphabetic':
$mustbealpha = true;
break;
case 'alphanumeric':
break;
default:
cacti_log('WARNING: Unknown index_order_type of ' . $raw_xml['index_order_type'] . " found for Device[$host_id], DQ[$data_query_id]. Permitted types are [alpha:numeric:alphanumeric]. Data collection can be impacted.", false, 'REINDEX');
$order_unknown = true;
}
if (isset($raw_xml['index_order'])) {
$avail_indexes = explode(':', $raw_xml['index_order']);
}
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Available Sort Fields for Re-Index for Device[$host_id], DQ[$data_query_id] are [" . $raw_xml['index_order'] . ']', false, 'DSTRACE');
}
} elseif (!isset($raw_xml['arg_index'])) {
cacti_log("WARNING: Missing index_order_type XML tag for Device[$host_id], DQ[$data_query_id]. Permitted types [alpha:numeric:alphanumeric]. If a monitored object changes its index, those changes will not be detected.", false, 'REINDEX');
$order_found = false;
}
if (cacti_sizeof($avail_indexes) == 1) {
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Only One possible Sort Field found during Re-Index for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
return $avail_indexes;
}
$xml_outputs = [];
// check for nonunique query parameter, set value
if (isset($raw_xml['index_type']) && $raw_xml['index_type'] == 'nonunique') {
$nonunique = true;
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Non-Uniqueue Specified during Re-Index for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
}
$num_indexes = db_fetch_cell_prepared('SELECT COUNT(DISTINCT snmp_index)
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?',
[$host_id, $data_query_id]);
// list each of the input fields for this snmp query
foreach ($raw_xml['fields'] as $field_name => $field_array) {
if ($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output') {
if (isset($field_array['oid']) && $oid_index == $field_array['oid']) {
// this is a suitable sort_field
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Field Name '$field_name' is an SNMP index and suitable Re-Index for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
array_push($xml_outputs, $field_name);
continue;
}
if ($order_found && array_search($field_name, $avail_indexes, true) === false) {
// This is not a suitable index field
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Field Name '$field_name' found not suitable during Re-Index for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
continue;
}
// Check for a non-unique sort field
$unique = db_fetch_cell_prepared('SELECT COUNT(*)
FROM (
SELECT field_value, COUNT(*) AS totals
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?
GROUP BY field_value
HAVING totals > 1
) AS totals',
[$host_id, $data_query_id, $field_name]);
$total_unique = db_fetch_cell_prepared('SELECT COUNT(DISTINCT field_value)
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?',
[$host_id, $data_query_id, $field_name]);
/* If the entries are not unique and non-unique is
* not specified, skip this sort field
*/
if ($unique > 0 && !$nonunique) {
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Field Name '$field_name' found not suitable. Non-unique and nonunique not specified during Re-Index for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
continue;
}
if ($total_unique != $num_indexes) {
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Field Name '$field_name' found not suitable. The Sort field does not have sufficient values for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
continue;
}
// TODO: I think there was a plan here, not sure what it was.
if ($must_be_alpha || $must_be_numeric) { // @phpstan-ignore-line
// create a list of all values for this index
$field_values = db_fetch_assoc_prepared('SELECT field_value
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?',
[$host_id, $data_query_id, $field_name]);
if (cacti_sizeof($field_values)) {
foreach ($field_values as $value) {
if ($must_be_numeric && !is_numeric($value)) {
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Field Name '$field_name' found not suitable. Must be numeric and non-numeric data found during Re-Index for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
continue;
}
if ($must_be_alpha && is_numeric($value)) { // @phpstan-ignore-line
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Field Name '$field_name' found not suitable. Must be alphabetic and alphabetic data found during Re-Index for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
continue;
}
}
}
}
if (read_config_option('data_source_trace') == 'on') {
cacti_log("Field Name '$field_name' found suitable during Re-Index for Device[$host_id], DQ[$data_query_id]", false, 'DSTRACE');
}
array_push($xml_outputs, $field_name);
}
}
$return_array = [];
// Reorder the sort fields by desired order
if (cacti_sizeof($avail_indexes)) {
foreach ($avail_indexes as $index) {
if (array_search($index, $xml_outputs, true) !== false) {
$return_array[] = $index;
}
}
} else {
$return_array = $xml_outputs;
}
return $return_array;
}
/**
* Updates the sort cache for a particular host/data query combination. This works
* by fetching a list of valid data query index types and choosing the first one in the
* list. The user can optionally override how the cache is updated in the data query xml file.
*
* @param int $host_id The ID of the host for which the data query sort cache is being updated.
* @param int $data_query_id The ID of the data query to update.
*
* @return string|false Returns the sort field if the update is successful, or `false` if no valid index types are found.
*/
function update_data_query_sort_cache(int $host_id, int $data_query_id) : string|false {
$raw_xml = get_data_query_array($data_query_id);
// get a list of valid data query types
$valid_index_types = get_ordered_index_type_list($host_id, $data_query_id);
// something is probably wrong with the data query
if (cacti_sizeof($valid_index_types) == 0) {
$sort_field = '';
return false;
} else {
// grab the first field off the list
$sort_field = $valid_index_types[0];
}
// substitute variables
if (cacti_sizeof($raw_xml) && isset($raw_xml['index_title_format'])) {
$title_format = str_replace('|chosen_order_field|', "|query_$sort_field|", $raw_xml['index_title_format']);
} else {
$title_format = "|query_$sort_field|";
}
// update the cache
// TODO: if both $sort field and $title_format are empty, this yields funny results
db_execute_prepared('UPDATE host_snmp_query
SET sort_field = ?,
title_format = ?
WHERE host_id = ?
AND snmp_query_id = ?',
[$sort_field, $title_format, $host_id, $data_query_id]);
return (string) $sort_field;
}
/**
* Updates the sort cache for all data queries associated with a particular host.
* * See update_data_query_sort_cache() for details about updating the cache.
*
* @param int $host_id The ID of the host for which the data query sort cache
* needs to be updated.
*
* @return void
*/
function update_data_query_sort_cache_by_host(int $host_id) : void {
$data_queries = db_fetch_assoc_prepared('SELECT snmp_query_id
FROM host_snmp_query
WHERE host_id = ?',
[$host_id]);
if (cacti_sizeof($data_queries) > 0) {
foreach ($data_queries as $data_query) {
update_data_query_sort_cache($host_id, $data_query['snmp_query_id']);
}
}
}
/**
* Returns the best available data query index type using the sort cache
*
* @param int $host_id The ID of the host for which the data query is being evaluated.
* @param int $data_query_id The ID of the data query to evaluate.
*
* @return mixed Returns the determined index type as a string, or `false` if the index type
* cannot be determined or if there is an error (e.g., missing XML file).
*
* @throws Exception If there is an issue with database operations or XML parsing.
*/
function get_best_data_query_index_type(int $host_id, int $data_query_id) : mixed {
$index_type = db_fetch_cell_prepared('SELECT sort_field
FROM host_snmp_query
WHERE host_id = ?
AND snmp_query_id = ?',
[$host_id, $data_query_id]);
if ($index_type == '') {
$raw_xml = get_data_query_array($data_query_id);
if (cacti_sizeof($raw_xml)) {
if (isset($raw_xml['index_order']) && $raw_xml['index_order'] != '') {
$order = explode(':', $raw_xml['index_order']);
db_execute_prepared('UPDATE host_snmp_query
SET sort_field = ?
WHERE host_id = ?
AND snmp_query_id = ?',
[$order[0], $host_id, $data_query_id]);
return (string) $order[0];
}
if (isset($raw_xml['fields']) && cacti_sizeof($raw_xml['fields'])) {
$key = '';
foreach ($raw_xml['fields'] as $key => $attribs) {
break;
}
db_execute_prepared('UPDATE host_snmp_query
SET sort_field = ?
WHERE host_id = ?
AND snmp_query_id = ?',
[$key, $host_id, $data_query_id]);
return $key;
} else {
return false;
}
} else {
cacti_log('ERROR: Cacti Data Query DQ[' . $data_query_id . '] XML file may be missing or not readable.');
return false;
}
}
return (string) $index_type;
}
/**
* Builds the complete script query executable path
*
* @param string $args A string of arguments to be passed to the script.
* Arguments can be enclosed in single or double quotes
* and are split into individual components.
* @param string $script_path The path to the script that will be executed.
* @param int $host_id The ID of the host for which the script is being executed.
*
* @return string The complete script path with substituted arguments, ready for execution.
*/
function get_script_query_path(string $args, string $script_path, int $host_id) : string {
include_once(CACTI_PATH_LIBRARY . '/variables.php');
// get any extra arguments that need to be passed to the script
if ($args != '') {
$parts = preg_split("/[\s,]*\\\"([^\\\"]+)\\\"[\s,]*|" . "[\s,]*'([^']+)'[\s,]*|" . "[\s,]+/", $args, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$extra_arguments = '';
foreach ($parts as $index => $part) {
/* only the hostname/IP placeholders are stripped of '%'; SNMP
* community and credential substitutions may legitimately
* contain one, so they keep their value unmodified. */
$strip_env = (strpos($part, '|host_hostname|') !== false || strpos($part, '|host_management_ip|') !== false);
$part = substitute_host_data($part, '|', '|', $host_id);
/* GHSA-rjvj-r52f-8v5q: a rogue device's SNMP index or system field
* reaches here; on Windows cmd.exe would interpret & | ^ < > ( ) it
* carries, so strip them before quoting. */
if (CACTI_SERVER_OS == 'win32') {
$part = str_replace(['"', '&', '|', '^', '<', '>', '(', ')'], '', $part);
if ($strip_env) {
$part = str_replace('%', '', $part);
}
}
$extra_arguments .= ($index > 0 ? ' ' : '') . cacti_escapeshellarg($part);
}
} else {
$extra_arguments = '';
}
// get a complete path for out target script
return substitute_script_query_path($script_path) . ' ' . $extra_arguments;
}
/**
* Verify a given index_order against the list of input fields in the XML file
*
* @param array $raw_xml The raw XML data array to be verified.
* @return bool Returns true if all fields in 'index_order' are found in the list
*/
function verify_index_order(array $raw_xml) : bool {
// invalid xml check
if (cacti_sizeof($raw_xml) == 0) {
query_debug_timer_offset('data_query', __esc('Error parsing XML file into an array.'));
return false;
}
$xml_inputs = [];
// list each of the input fields for this snmp query
foreach ($raw_xml['fields'] as $field_name => $field_array) {
if (is_array($field_array) && isset($field_array['direction'])) {
if ($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output') {
// create a list of all values for this index
array_push($xml_inputs, $field_name);
}
}
}
$all_index_order_fields_found = true;
// the xml file contains an ordered list of 'indexable' fields
if (isset($raw_xml['index_order'])) {
$index_order_array = explode(':', $raw_xml['index_order']);
foreach ($index_order_array as $element) {
$all_index_order_fields_found = $all_index_order_fields_found && (in_array($element, $xml_inputs, true));
}
} else {
// the xml file does not contain an index order
}
return $all_index_order_fields_found;
}
/**
* Perform sql updates for all required tables for new index_sort_order.
* This code stems from lib/template.php, function create_complete_graph_from_template.
*
* @param array $local_data An associative array containing the local data information
*
* @return void
*/
function update_snmp_index_order(array $local_data) : void {
if (cacti_sizeof($local_data)) {
$data_input_field = array_rekey(db_fetch_assoc_prepared("SELECT
data_input_fields.id,
data_input_fields.type_code
FROM snmp_query
INNER JOIN data_input
ON snmp_query.data_input_id = data_input.id
INNER JOIN data_input_fields
ON data_input.id = data_input_fields.data_input_id
WHERE data_input_fields.type_code IN('index_type', 'index_value', 'output_type')
AND snmp_query.id = ?",
[$local_data['snmp_query_id']]), 'type_code', 'id');
$snmp_cache_value = db_fetch_cell_prepared('SELECT field_value
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND field_name = ?
AND snmp_index = ?',
[$local_data['host_id'], $local_data['snmp_query_id'], $local_data['snmp_index_on'], $local_data['snmp_index']]);
// only update data source index if there actually *is* an index returned from host_snmp_cache
if (!empty($snmp_cache_value)) {
db_execute_prepared('REPLACE INTO data_input_data
(data_input_field_id, data_template_data_id, data_template_id, local_data_id, host_id, t_value, value)
VALUES
(?, ?, ?, ?, ?, "", ?),
(?, ?, ?, ?, ?, "", ?),
(?, ?, ?, ?, ?, "", ?)',
[
// save the value to index on (ie. ifindex, ifip, etc)
$data_input_field['index_type'], $local_data['data_template_data_id'], $local_data['data_template_id'], $local_data['local_data_id'], $local_data['host_id'], $local_data['snmp_index_on'],
// save the actual value (ie. 3, 192.168.1.101, etc)
$data_input_field['index_value'], $local_data['data_template_data_id'], $local_data['data_template_id'], $local_data['local_data_id'], $local_data['host_id'], $snmp_cache_value,
// set the expected output type (ie. bytes, errors, packets)
$data_input_field['output_type'], $local_data['data_template_data_id'], $local_data['data_template_id'], $local_data['local_data_id'], $local_data['host_id'], $local_data['snmp_query_graph_id']
]
);
}
// now that we have put data into the 'data_input_data' table, update the snmp cache for ds's
// update_data_source_data_query_cache($local_data['local_data_id']);
}
}
/**
* Verify that a Data Query Graph Template is properly mapped
*
* @param int $snmp_query_graph_id The ID of the SNMP query graph to validate.
* @param array $post The POST data containing user input for the data source mappings.
*
* @return bool Returns true if there are validation errors, false otherwise.
*/
function api_data_query_errors(int $snmp_query_graph_id, array $post) : bool {
$graph_template_id = db_fetch_cell_prepared('SELECT gt.id
FROM graph_templates AS gt
INNER JOIN snmp_query_graph AS sqg
ON gt.id = sqg.graph_template_id
WHERE sqg.id = ?',
[$snmp_query_graph_id]);
$data_sources = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.data_template_id
FROM data_template_rrd AS dtr
INNER JOIN graph_templates_item AS gti
ON dtr.id = gti.task_item_id
WHERE dtr.local_data_id = 0
AND gti.graph_template_id = ?',
[$graph_template_id]);
$errors = false;
if (cacti_sizeof($data_sources)) {
foreach ($data_sources as $ds) {
if (!isset($post['dsdt_' . $ds['data_template_id'] . '_' . $ds['id'] . '_check'])) {
raise_message('mapping_error', __('You must select an XML output column for Data Source \'%s\' and toggle the checkbox to its right', $ds['data_source_name']), MESSAGE_LEVEL_ERROR);
$errors = true;
}
}
} else {
raise_message('assign_error', __('Your Graph Template has not Data Templates in use. Please correct your Graph Template'), MESSAGE_LEVEL_ERROR);
$errors = true;
}
return $errors;
}
/**
* Duplicates an SNMP data query and its associated graphs and sub-graphs.
*
* @param int $_data_query_id The ID of the data query to duplicate.
* @param string $data_query_name The name for the new duplicated data query.
*
* @return int|false The ID of the newly created data query on success, or false on failure.
*/
function data_query_duplicate(int $_data_query_id, string $data_query_name) : int|false {
$data_query = db_fetch_row_prepared('SELECT *
FROM snmp_query
WHERE id = ?',
[$_data_query_id]);
if (!cacti_sizeof($data_query)) {
return false;
}
$data_query_name = str_replace('<dataquery_name>', $data_query['name'], $data_query_name);
$save = $data_query;
$save['id'] = 0;
$save['hash'] = generate_hash();
$save['name'] = $data_query_name;
$data_query_id = sql_save($save, 'snmp_query');
$_snmp_query_graph = db_fetch_assoc_prepared('SELECT *
FROM snmp_query_graph
WHERE snmp_query_id = ?',
[$_data_query_id]);
if (cacti_sizeof($_snmp_query_graph)) {
foreach ($_snmp_query_graph as $_sqg) {
$save = $_sqg;
$save['id'] = 0;
$save['hash'] = generate_hash();
$save['snmp_query_id'] = $data_query_id;
$sqg_id = sql_save($save, 'snmp_query_graph');
$_snmp_query_graph_rrd = db_fetch_assoc_prepared('SELECT *
FROM snmp_query_graph_rrd
WHERE snmp_query_graph_id = ?',
[$_sqg['id']]);
if (cacti_sizeof($_snmp_query_graph_rrd)) {
foreach ($_snmp_query_graph_rrd as $_sqgr) {
$save = $_sqgr;
$save['snmp_query_graph_id'] = $sqg_id;
sql_save($save, 'snmp_query_graph_rrd');
}
}
$_snmp_query_graph_rrd_sv = db_fetch_assoc_prepared('SELECT *
FROM snmp_query_graph_rrd_sv
WHERE snmp_query_graph_id = ?',
[$_sqg['id']]);
if (cacti_sizeof($_snmp_query_graph_rrd_sv)) {
foreach ($_snmp_query_graph_rrd_sv as $_sqgrs) {
$save = $_sqgrs;
$save['id'] = 0;
$save['hash'] = generate_hash();
$save['snmp_query_graph_id'] = $sqg_id;
sql_save($save, 'snmp_query_graph_rrd_sv');
}
}
$_snmp_query_graph_sv = db_fetch_assoc_prepared('SELECT *
FROM snmp_query_graph_sv
WHERE snmp_query_graph_id = ?',
[$_sqg['id']]);
if (cacti_sizeof($_snmp_query_graph_sv)) {
foreach ($_snmp_query_graph_sv as $_sqgs) {
$save = $_sqgs;
$save['id'] = 0;
$save['hash'] = generate_hash();
$save['snmp_query_graph_id'] = $sqg_id;
sql_save($save, 'snmp_query_graph_sv');
}
}
}
return $data_query_id;
}
return false;
}
/**
* Detect when a multi-row OID walk appears padded with trailing .0 octets and
* can safely be considered for stripping. The final safety check parses the
* candidate stripped OIDs and requires unique indexes, so single-.0 padding
* can be repaired without accepting collisions.
*
* @param array $indexes Associative array of OID => value.
* @param string $index_parse_regexp Default last-octet index regexp.
*/
function oid_index_should_strip_trailing_zero_padding(array $indexes, string $index_parse_regexp): bool {
if (cacti_sizeof($indexes) <= 1) {
return false;
}
$test_indexes = [];
$all_end_with_zero = true;
foreach ($indexes as $oid => $value) {
if (preg_match($index_parse_regexp, $oid, $matches)) {
$test_indexes[$oid] = $matches[1];
} else {
return false;
}
if (!preg_match('/(?:\.0)+$/', $oid)) {
$all_end_with_zero = false;
}
}
$unique = array_unique(array_values($test_indexes));
if (!$all_end_with_zero || cacti_sizeof($unique) !== 1 || $unique[0] !== '0') {
return false;
}
$stripped = oid_index_strip_trailing_zero_padding($indexes);
if ($stripped === $indexes) {
return false;
}
$stripped_indexes = [];
foreach (array_keys($stripped) as $oid) {
if (!preg_match($index_parse_regexp, $oid, $matches)) {
return false;
}
$stripped_indexes[] = $matches[1];
}
return cacti_sizeof(array_unique($stripped_indexes)) === cacti_sizeof($stripped_indexes);
}
/**
* Strips trailing .0 octets from SNMP OID index keys when all indexes in a
* multi-row walk resolve to 0 under the default last-octet regex.
*
* Only called when cacti_sizeof($indexes) > 1 and oid_index_parse is absent,
* so scalar OIDs (single-row walks and custom-regex walks) are never touched.
*
* Detection is intentionally conservative: stripping is only attempted when all
* OIDs currently resolve to index 0, every OID ends in .0, and the stripped
* OIDs parse to a unique index set.
*
* Residual risk: a multi-row walk where every legitimate index ends in .0 and
* stripping still yields unique indexes can match the heuristic. Operators can
* avoid auto-detection for those walks by configuring oid_index_parse.
*
* @param array $indexes Associative array of OID => value from an SNMP walk.
*
* @return array Stripped array if safe (no key collisions); original array otherwise.
*/
function oid_index_strip_trailing_zero_padding(array $indexes): array {
if (cacti_sizeof($indexes) <= 1) {
return $indexes;
}
/* Partial padding would silently reshape a legitimate walk: only strip
when every OID in the set ends with a .0 segment. */
foreach ($indexes as $oid => $value) {
if (!preg_match('/(?:\.0)+$/', (string) $oid)) {
return $indexes;
}
}
$stripped = [];
foreach ($indexes as $oid => $value) {
$stripped_oid = preg_replace('/(?:\.0)+$/', '', $oid);
if (isset($stripped[$stripped_oid])) {
/* Stripping would merge two distinct OIDs into one key,
silently dropping a row. Bail out and keep originals. */
return $indexes;
}
$stripped[$stripped_oid] = $value;
}
/* Defence-in-depth: if any row was lost between the input and the
stripped map (collisions the isset() guard above did not catch,
e.g. via type juggling on numeric string keys), return originals
rather than silently dropping rows. */
if (cacti_sizeof($stripped) !== cacti_sizeof($indexes)) {
return $indexes;
}
return $stripped;
}