Files
Cactiplugin_thold/setup.php
T
10c93541dd issue: shorten thold VRULE graph drilldown button title (#855)
* issue: shorten thold VRULE graph drilldown button title

The graph action menu now renders each drilldown button's title as a visible label, so 'Toggle Threshold VRULES %s' reads as an overlong menu entry. Shorten it to 'Toggle VRULES %s', add a unit test exercising thold_graph_button()'s VRULE branch (with the two missing request-var stubs it needs) so the changed line stays covered, and regenerate locales/po/cacti.pot.

* Update copyright notice in TholdGraphButtonTest.php

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix Last-Translator entry in cacti.pot

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* test: cover the VRULEs-enabled 'Off' title branch for patch coverage

---------

Co-authored-by: TheWitness <1439914+TheWitness@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-13 12:38:48 -04:00

2429 lines
82 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDTool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/**
* Registers all of this plugin's hooks (settings, breadcrumbs, poller
* output/bottom, device/data-source/graph management actions, graph
* buttons, SNMP agent cache, clog regex, and more) and permission
* realms with Cacti, then creates/updates its database schema and SNMP
* agent cache. Cacti plugin API entry point invoked when the plugin is
* installed or re-registered during an upgrade. A no-op returning false
* on Cacti versions older than 1.2.
*
* @param bool $upgrade Whether this call is for an in-place upgrade of
* an already-installed plugin (true) rather than
* a fresh install (false, default); affects
* whether the database is upgraded vs. freshly
* created.
*
* @return bool|void False when the Cacti version is too old; otherwise
* no explicit return value.
*
* @global array $config Cacti global configuration array; used to
* check the Cacti version and locate the database
* include file.
*/
function plugin_thold_install($upgrade = false) {
global $config;
if (version_compare($config['cacti_version'], '1.2') < 0) {
return false;
}
// The reinstall below registers the plugin's hooks/realms and provisions
// its schema against a live Cacti database (include_once database.php plus
// thold_setup_database()/thold_upgrade_database()), so it cannot run in the
// isolated unit process.
// @codeCoverageIgnoreStart
$plugin = 'thold';
// Insert CSS and JavaScript
api_plugin_register_hook($plugin, 'page_head', 'thold_page_head', 'setup.php');
// Add the Thold tab
api_plugin_register_hook($plugin, 'top_header_tabs', 'thold_show_tab', 'includes/tab.php');
api_plugin_register_hook($plugin, 'top_graph_header_tabs', 'thold_show_tab', 'includes/tab.php');
// Settings forms and arrays
api_plugin_register_hook($plugin, 'config_insert', 'thold_config_insert', 'includes/settings.php');
api_plugin_register_hook($plugin, 'config_arrays', 'thold_config_arrays', 'includes/settings.php');
api_plugin_register_hook($plugin, 'config_form', 'thold_config_form', 'includes/settings.php');
api_plugin_register_hook($plugin, 'config_settings', 'thold_config_settings', 'includes/settings.php');
// Breadcrumbs
api_plugin_register_hook($plugin, 'draw_navigation_text', 'thold_draw_navigation_text', 'includes/settings.php');
// Inline thold checks
api_plugin_register_hook($plugin, 'poller_output', 'thold_poller_output', 'includes/polling.php');
// Device Hooks
api_plugin_register_hook($plugin, 'device_action_array', 'thold_device_action_array', 'setup.php');
api_plugin_register_hook($plugin, 'device_action_execute', 'thold_device_action_execute', 'setup.php');
api_plugin_register_hook($plugin, 'device_action_prepare', 'thold_device_action_prepare', 'setup.php');
api_plugin_register_hook($plugin, 'api_device_save', 'thold_api_device_save', 'setup.php');
api_plugin_register_hook($plugin, 'host_edit_bottom', 'thold_host_edit_bottom', 'setup.php');
api_plugin_register_hook($plugin, 'device_threshold_autocreate', 'thold_device_autocreate', 'setup.php');
api_plugin_register_hook($plugin, 'device_template_change', 'thold_device_template_change', 'setup.php');
api_plugin_register_hook($plugin, 'device_change_javascript', 'thold_host_edit_bottom', 'setup.php');
// Automation Hooks
api_plugin_register_hook($plugin, 'create_complete_graph_from_template', 'thold_create_graph_thold', 'setup.php');
// Hooks to enable thold maintenance
api_plugin_register_hook($plugin, 'poller_bottom', 'thold_poller_bottom', 'includes/polling.php');
// Setup buttons on Graph Pages
api_plugin_register_hook($plugin, 'graph_buttons', 'thold_graph_button', 'setup.php');
api_plugin_register_hook($plugin, 'graph_buttons_thumbnails', 'thold_graph_button', 'setup.php');
// Data Source Hooks
api_plugin_register_hook($plugin, 'data_source_action_array', 'thold_data_source_action_array', 'setup.php');
api_plugin_register_hook($plugin, 'data_source_action_prepare', 'thold_data_source_action_prepare', 'setup.php');
api_plugin_register_hook($plugin, 'data_source_action_execute', 'thold_data_source_action_execute', 'setup.php');
api_plugin_register_hook($plugin, 'data_source_remove', 'thold_data_source_remove', 'setup.php');
// Create Threshold from Data Source table
api_plugin_register_hook($plugin, 'data_sources_table', 'thold_data_sources_table', 'setup.php');
// Follow Graph Actions
api_plugin_register_hook($plugin, 'graphs_action_array', 'thold_graphs_action_array', 'setup.php');
api_plugin_register_hook($plugin, 'graphs_action_prepare', 'thold_graphs_action_prepare', 'setup.php');
api_plugin_register_hook($plugin, 'graphs_action_execute', 'thold_graphs_action_execute', 'setup.php');
// Follow Device Template Actions
api_plugin_register_hook($plugin, 'device_template_edit', 'thold_device_template_edit', 'setup.php');
api_plugin_register_hook($plugin, 'device_template_top', 'thold_device_template_top', 'setup.php');
// Display Threshold Templates in Devices
api_plugin_register_hook($plugin, 'device_edit_pre_bottom', 'thold_device_edit_pre_bottom', 'setup.php');
api_plugin_register_hook($plugin, 'device_top', 'thold_device_top', 'setup.php');
// Follow New Graph Actions
api_plugin_register_hook($plugin, 'api_device_new', 'thold_api_device_new', 'setup.php');
// Allow settings javascript hooks
api_plugin_register_hook('thold', 'settings_bottom', 'thold_settings_bottom', 'setup.php', true);
// Miscellaneous hooks
api_plugin_register_hook($plugin, 'graphs_new_top_links', 'thold_graphs_new', 'setup.php');
api_plugin_register_hook($plugin, 'update_host_status', 'thold_update_host_status', 'includes/polling.php');
api_plugin_register_hook($plugin, 'user_admin_setup_sql_save', 'thold_user_admin_setup_sql_save', 'setup.php');
api_plugin_register_hook($plugin, 'rrd_graph_graph_options', 'thold_rrd_graph_graph_options', 'setup.php');
api_plugin_register_hook($plugin, 'snmpagent_cache_install', 'thold_snmpagent_cache_install', 'setup.php');
api_plugin_register_hook($plugin, 'clog_regex_array', 'thold_clog_regex_array', 'setup.php');
// Setup permissions
api_plugin_register_realm($plugin, 'thold.php', 'Configure Thresholds', 1);
api_plugin_register_realm($plugin, 'thold_templates.php', 'Configure Threshold Templates', 1);
api_plugin_register_realm($plugin, 'notify_lists.php,notify_queue.php', 'Manage Notification', 1);
api_plugin_register_realm($plugin, 'thold_graph.php,graph_thold.php,thold_view_failures.php,thold_view_normal.php,thold_view_recover.php,thold_view_recent.php,thold_view_host.php', 'View Thresholds', 1);
include_once($config['base_path'] . '/plugins/thold/includes/database.php');
if ($upgrade) {
thold_upgrade_database();
if (api_plugin_is_enabled($plugin)) {
api_plugin_enable_hooks($plugin);
}
} else {
thold_setup_database();
thold_snmpagent_cache_install();
}
// @codeCoverageIgnoreEnd
}
/**
* Removes this plugin's SNMP agent cache entries and all of its
* settings table rows. Cacti plugin API entry point invoked when the
* plugin is uninstalled.
*
* @return void
*/
function plugin_thold_uninstall() {
// Do any extra Uninstall stuff here
thold_snmpagent_cache_uninstall();
// Remove items from the settings table
db_execute('DELETE FROM settings
WHERE name LIKE "%thold%"');
}
/**
* Ensures the plugin's configuration/schema is up to date by delegating
* to plugin_thold_upgrade(). Cacti plugin API entry point invoked on
* relevant page loads to catch pending upgrades.
*
* @return bool Always true.
*/
function plugin_thold_check_config() {
// Here we will check to ensure everything is configured
plugin_thold_upgrade();
return true;
}
/**
* Detects a version mismatch between the running plugin code and the
* version recorded in plugin_config, and when found (and only on a
* small set of pages, to limit overhead), re-runs
* plugin_thold_install() in upgrade mode to re-register hooks and
* upgrade the database schema.
*
* @return bool True (whether or not an upgrade was actually needed);
* false only when called from a page that doesn't need
* this check.
*
* @global array $config Cacti global configuration array (declared but
* not used directly here).
*/
function plugin_thold_upgrade() {
// Here we will upgrade to the newest version
global $config;
// Let's only run this check if we are on a page that actually needs the data
$files = ['thold.php', 'thold_graph.php', 'thold_templates.php', 'poller.php', 'plugins.php', 'clog.php'];
if (!in_array(get_current_page(), $files, true)) {
return false;
}
$info = plugin_thold_version();
$current = $info['version'];
$old = db_fetch_cell('SELECT version
FROM plugin_config
WHERE directory="thold"');
if ($current != $old) {
plugin_thold_install(true);
thold_prune_files();
}
return true;
}
/**
* Reads this plugin's version and metadata from its INFO file. Cacti
* plugin API entry point used throughout the plugin to display version
* information.
*
* @return array The parsed INFO file's 'info' section (name, author,
* homepage, version, etc.).
*
* @global array $config Cacti global configuration array; used to
* locate the plugin's INFO file.
*/
function plugin_thold_version() {
global $config;
$info = parse_ini_file($config['base_path'] . '/plugins/thold/INFO', true);
return $info['info'];
}
/**
* Checks whether this plugin's dependencies are satisfied. Cacti
* plugin API hook point; currently always reports dependencies as
* satisfied.
*
* @return bool Always true.
*/
function thold_check_dependencies() {
return true;
}
/**
* Checks whether the database session is running in MySQL/MariaDB
* STRICT SQL mode, which this plugin (or its schema) is incompatible
* with.
*
* @return bool False if STRICT mode is active, true otherwise.
*/
function plugin_thold_check_strict() {
$mode = db_fetch_cell('select @@global.sql_mode', false);
if (stristr($mode, 'strict') !== false) {
return false;
}
return true;
}
/**
* Renders this plugin's graph-page action buttons (toggle threshold
* VRULE display, create-threshold link) for a graph, shown only to
* users with the relevant view/edit permissions. Registered as the
* 'graph_buttons' and 'graph_buttons_thumbnails' Cacti hooks.
*
* @param array $data The graph button hook payload; $data[1] contains
* the graph's local_graph_id and rra selection.
*
* @return void
*
* @global array $config Cacti global configuration array; used to
* build the Create Threshold link's URL.
*/
function thold_graph_button($data) {
global $config;
$local_graph_id = $data[1]['local_graph_id'];
$thold_id = db_fetch_cell_prepared('SELECT id
FROM thold_data
WHERE local_graph_id = ?',
[$local_graph_id]);
$rra_id = $data[1]['rra'];
if (isset_request_var('view_type') && !isempty_request_var('view_type')) {
$view_type = get_request_var('view_type');
} else {
set_request_var('view_type', 'tree');
$view_type = 'preview';
}
if (isset_request_var('graph_start') && !isempty_request_var('graph_start')) {
$start = get_filter_request_var('graph_start');
} else {
set_request_var('graph_start', '');
$start = time() - 3600;
}
if (isset_request_var('graph_end') && !isempty_request_var('graph_end')) {
$end = get_filter_request_var('graph_end');
} else {
set_request_var('graph_end', '');
$end = time();
}
if (isset_request_var('thold_vrule') || isset($_SESSION['sess_config_array']['thold_draw_vrules'])) {
if (isset_request_var('thold_vrule')) {
if (get_nfilter_request_var('thold_vrule') == 'on') {
$vrules = 'off';
} else {
$vrules = 'on';
}
$_SESSION['sess_config_array']['thold_draw_vrules'] = $vrules;
} else {
$vrules = $_SESSION['sess_config_array']['thold_draw_vrules'];
}
} else {
$vrules = 'off';
$_SESSION['sess_config_array']['thold_draw_vrules'] = $vrules;
}
$url = $_SERVER['REQUEST_URI'];
$url = str_replace('&thold_vrule=on', '', $url);
$url = str_replace('&thold_vrule=off', '', $url);
if (!substr_count($url, '?')) {
$separator = '?';
} else {
$separator = '&';
}
if (api_user_realm_auth('thold_graph.php') && !empty($thold_id)) {
// Two complete strings rather than a trailing On/Off fragment so the
// label stays grammatical when a translation reorders the words.
if ($vrules == 'on') {
$vrule_title = __esc('Toggle Threshold VRULES Off', 'thold');
} else {
$vrule_title = __esc('Toggle Threshold VRULES On', 'thold');
}
print '<a class="iconLink tholdVRule" href="' . html_escape($url . $separator . 'thold_vrule=' . $vrules) . '" title="' . $vrule_title . '"><i class="tholdVRules far fa-chart-bar"></i></a><br>';
}
// Add Threshold Creation button
if (api_user_realm_auth('thold.php')) {
if (isset_request_var('tree_id')) {
get_filter_request_var('tree_id');
}
if (isset_request_var('leaf_id')) {
get_filter_request_var('leaf_id');
}
$is_aggregate = db_fetch_cell_prepared('SELECT id
FROM aggregate_graphs
WHERE local_graph_id = ?',
[$local_graph_id]);
if (empty($is_aggregate)) {
print '<a class="iconLink" href="' . html_escape($config['url_path'] . 'plugins/thold/thold.php?action=add' . '&usetemplate=1&local_graph_id=' . $local_graph_id) . '" title="' . __esc('Create Threshold', 'thold') . '"><i class="tholdEdit fas fa-wrench"></i></a><br>';
}
}
}
/**
* Splits a string on any of several delimiter characters at once, by
* first collapsing all given delimiters down to the first one and then
* exploding on it.
*
* @param array $delimiters The list of delimiter characters/strings
* to split on (the first entry is used as
* the unified delimiter).
* @param string $string The string to split.
*
* @return array|false The resulting array of substrings, or false on
* failure.
*/
function thold_multiexplode($delimiters, $string) {
$ready = str_replace($delimiters, $delimiters[0], $string);
return @explode($delimiters[0], $ready);
}
/**
* Substitutes this plugin's `|thold:hi:<ds>|` / `|thold:low:<ds>|`
* graph text-item placeholder variables with each referenced data
* source's current threshold hi/low values, by mapping the graph's RRD
* definitions' data source names to their underlying local_data_id and
* looking up the matching threshold. Registered as the
* 'rrd_graph_graph_options' Cacti hook.
*
* @param array $g The graph rendering options array, including
* 'graph_defs' and 'txt_graph_items'.
*
* @return array The graph rendering options array with thold
* placeholder variables substituted in
* 'txt_graph_items'.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include.
*/
function thold_rrd_graph_graph_options($g) {
global $config;
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
// handle thold replacement variables
$needles = [];
$replacements = [];
// map the data_template_rrd_id's to the datasource names
$defs = explode("\\\n", $g['graph_defs'], -1);
if (is_array($defs)) {
foreach ($defs as $def) {
if (!substr_count($def, 'CDEF') && !substr_count($def, 'VDEF')) {
$ddef = thold_multiexplode(['"', "'"], $def);
$kdef = explode(':', $def);
$dsname = $kdef[2];
$temp1 = str_replace('.rrd', '', basename($ddef[1]));
if (substr_count(basename($ddef[1]), '_') == 0) {
$local_data_id = $temp1;
} else {
$temp2 = explode('_', $temp1);
$local_data_id = $temp2[sizeof($temp2) - 1];
}
$dsname = trim($dsname, "'\" ");
$data_template_rrd[$dsname] = $local_data_id;
// Map the dsnames to def id's for percentile
$ndef = explode('=', $kdef[1]);
$data_defs[$dsname] = $ndef[0];
}
}
}
// look for any variables to replace
$txt_items = explode("\\\n", $g['txt_graph_items']);
foreach ($txt_items as $item) {
if (substr_count($item, '|thold')) {
preg_match("/\|thold\\\:(hi|low)\\\:(.+)\|/", $item, $matches);
if (count($matches) == 3) {
$needles[] = $matches[0];
$data_source = explode('|', $matches[2]);
// look up the data_id from the data source name and data_template_rrd
$data_template_rrd_id = db_fetch_cell_prepared('SELECT id
FROM data_template_rrd
WHERE local_data_id = ?
AND data_source_name = ?',
[$data_template_rrd[$data_source[0]], $data_source[0]]);
$thold_type = db_fetch_cell_prepared('SELECT thold_type
FROM thold_data
WHERE thold_enabled="on"
AND data_template_rrd_id = ?',
[$data_template_rrd_id]);
// fetch the value from thold
if ($thold_type == '') {
$value = '';
} elseif ($thold_type == 0 || $thold_type == 1) { // Hi/Low & Baseline
$value = db_fetch_cell_prepared('SELECT thold_' . $matches[1] . '
FROM thold_data
WHERE data_template_rrd_id = ?',
[$data_template_rrd_id]);
} elseif ($thold_type == 1) { // Time Based
$value = db_fetch_cell_prepared('SELECT time_' . $matches[1] . '
FROM thold_data
WHERE data_template_rrd_id = ?',
[$data_template_rrd_id]);
}
if ($value == '' || !is_numeric($value)) {
$replacements['|thold\:' . $matches[1] . '\:' . $data_source[0] . '|'] = 'strip';
} else {
$replacements['|thold\:' . $matches[1] . '\:' . $data_source[0] . '|'] = $value;
}
}
preg_match("/\|thold\\\:(warning_hi|warning_low)\\\:(.+)\|/", $item, $matches);
if (count($matches) == 3) {
$needles[] = $matches[0];
$data_source = explode('|', $matches[2]);
// look up the data_id from the data source name and data_template_rrd_id
$data_template_rrd_id = db_fetch_cell_prepared('SELECT id
FROM data_template_rrd
WHERE local_data_id = ?
AND data_source_name = ?',
[$data_template_rrd[$data_source[0]], $data_source[0]]);
$thold_type = db_fetch_cell_prepared('SELECT thold_type
FROM thold_data
WHERE thold_enabled="on"
AND data_template_rrd_id = ?',
[$data_template_rrd_id]);
// fetch the value from thold
if ($thold_type == '') {
$value = '';
} elseif ($thold_type == 0 || $thold_type == 1) { // Hi/Low & Baseline
$value = db_fetch_cell_prepared('SELECT thold_' . $matches[1] . '
FROM thold_data
WHERE data_template_rrd_id = ?',
[$data_template_rrd_id]);
} elseif ($thold_type == 1) { // Time Based
$value = db_fetch_cell_prepared('SELECT time_' . $matches[1] . '
FROM thold_data
WHERE data_template_rrd_id = ?',
[$data_template_rrd_id]);
}
if ($value == '' || !is_numeric($value)) {
$replacements['|thold\:' . $matches[1] . '\:' . $data_source[0] . '|'] = 'strip';
} else {
$replacements['|thold\:' . $matches[1] . '\:' . $data_source[0] . '|'] = $value;
}
}
}
}
// do we have any needles to replace?
$i = 0;
$unsets = [];
if (is_array($replacements)) {
foreach ($txt_items as $item) {
foreach ($replacements as $key => $replace) {
// cacti_log('Key:' . $key . ', Replace:' . $replace, false);
if (substr_count($item, $key)) {
if ($replace == 'strip') {
$unsets[] = $i;
} else {
$txt_items[$i] = str_replace($key, $replace, $item);
}
}
}
$i++;
}
if (cacti_sizeof($unsets)) {
foreach ($unsets as $i) {
unset($txt_items[$i]);
}
}
$g['txt_graph_items'] = implode("\\\n", $txt_items);
}
$id = $g['graph_id'];
// print "<pre>"; print_r($g);print "</pre>";
if (isset($_SESSION['sess_config_array']['thold_draw_vrules']) && $_SESSION['sess_config_array']['thold_draw_vrules'] == 'on') {
$end = $g['end'];
if ($end < 0) {
$end = time() + $end;
}
$end++;
$start = $g['start'];
if ($start < 0) {
$start = $end + $start;
}
$start--;
if ($id) {
$rows = db_fetch_assoc_prepared('SELECT time, status
FROM plugin_thold_log
WHERE local_graph_id = ?
AND type = 0
AND time > ?
AND time < ?',
[$id, $start, $end]);
if (cacti_sizeof($rows)) {
$color = '';
foreach ($rows as $row) {
switch($row['status']) {
case '3':
$color = '#CC6600';
break;
case '4':
$color = '#FF0000';
break;
case '5':
$color = '#00FF00';
break;
default:
$color = '';
break;
}
if ($color != '') {
$g['graph_defs'] .= 'VRULE:' . $row['time'] . $color . ' \\' . "\n";
}
}
}
}
}
$tholds_w_hrule = db_fetch_assoc_prepared('SELECT *
FROM thold_data
WHERE thold_enabled = 1
AND data_type IN (0, 1, 2)
AND (thold_hrule_alert > 0 || thold_hrule_warning > 0)
&& local_graph_id = ?',
[$id]);
$thold_id = 0;
$txt_graph_items = '';
if (cacti_sizeof($tholds_w_hrule)) {
foreach ($tholds_w_hrule as $t) {
// Adjust number for graph
thold_modify_values_by_cdef($t);
$baseu = db_fetch_cell_prepared('SELECT base_value
FROM graph_templates_graph
WHERE local_graph_id = ?',
[$t['local_graph_id']]);
if ($t['data_type'] == 2) {
$suffix = false;
} else {
$suffix = true;
}
$show_units = ($t['show_units'] ? true : false);
$skipscale = ($t['skipscale'] == 'on' ? ':skipscale' : '');
switch($t['data_type']) {
case '0': // Exact value
case '1': // CDEF
case '3': // Upper+Lower
if ($t['thold_hrule_alert'] > 0) {
$color = db_fetch_cell_prepared('SELECT hex
FROM colors
WHERE id = ?',
[$t['thold_hrule_alert']]);
switch($t['thold_type']) {
case '0': // Hi / Low
if ($t['thold_hi'] != '') {
$txt_graph_items .= 'LINE1:' . $t['thold_hi'] . '#' . $color . ':' . thold_prep_rrd_string(__esc('Alert Hi for %s (%s)', $t['name_cache'], thold_format_number($t['thold_hi'], 2, $baseu, $suffix, $show_units), 'thold')) . $skipscale . ' \\' . "\n";
}
if ($t['thold_low'] != '') {
$txt_graph_items .= 'LINE1:' . $t['thold_low'] . '#' . $color . ':' . thold_prep_rrd_string(__esc('Alert Low for %s (%s)', $t['name_cache'], thold_format_number($t['thold_low'], 2, $baseu, $suffix, $show_units), 'thold')) . $skipscale . ' \\' . "\n";
}
break;
case '2': // Time Based
if ($t['time_hi'] != '') {
$txt_graph_items .= 'LINE1:' . $t['time_hi'] . '#' . $color . ':' . thold_prep_rrd_string(__esc('Alert Hi for %s (%s)', $t['name_cache'], thold_format_number($t['time_hi'], 2, $baseu, $suffix, $show_units), 'thold')) . $skipscale . ' \\' . "\n";
}
if ($t['time_low'] != '') {
$txt_graph_items .= 'LINE1:' . $t['time_low'] . '#' . $color . ':' . thold_prep_rrd_string(__esc('Alert Low for %s (%s)', $t['name_cache'], thold_format_number($t['time_low'], 2, $baseu, $suffix, $show_units), 'thold')) . $skipscale . ' \\' . "\n";
}
break;
}
}
if ($t['thold_hrule_warning'] > 0) {
$color = db_fetch_cell_prepared('SELECT hex
FROM colors
WHERE id = ?',
[$t['thold_hrule_warning']]);
switch($t['thold_type']) {
case '0': // Hi / Low
if ($t['thold_warning_hi'] != '') {
$txt_graph_items .= 'LINE1:' . $t['thold_warning_hi'] . '#' . $color . ':' . thold_prep_rrd_string(__esc('Warning Hi for %s (%s)', $t['name_cache'], thold_format_number($t['thold_warning_hi'], 2, $baseu, $suffix, $show_units), 'thold')) . $skipscale . ' \\' . "\n";
}
if ($t['thold_warning_low'] != '') {
$txt_graph_items .= 'LINE1:' . $t['thold_warning_low'] . '#' . $color . ':' . thold_prep_rrd_string(__esc('Warning Low for %s (%s)', $t['name_cache'], thold_format_number($t['thold_warning_low'], 2, $baseu, $suffix, $show_units), 'thold')) . $skipscale . ' \\' . "\n";
}
break;
case '2': // Time Based
if ($t['time_warning_hi'] != '') {
$txt_graph_items .= 'LINE1:' . $t['time_warning_hi'] . '#' . $color . ':' . thold_prep_rrd_string(__esc('Warning Hi for %s (%s)', $t['name_cache'], thold_format_number($t['time_warning_hi'], 2, $baseu, $suffix, $show_units), 'thold')) . $skipscale . ' \\' . "\n";
}
if ($t['time_warning_low'] != '') {
$txt_graph_items .= 'LINE1:' . $t['time_warning_low'] . '#' . $color . ':' . thold_prep_rrd_string(__esc('Warning Low for %s (%s)', $t['name_cache'], thold_format_number($t['time_warning_low'], 2, $baseu, $suffix, $show_units), 'thold')) . $skipscale . ' \\' . "\n";
}
break;
}
}
break;
case '2': // Percentage
if (isset($data_defs[$t['percent_ds']])) {
if ($t['thold_hrule_alert'] > 0) {
$color = db_fetch_cell_prepared('SELECT hex
FROM colors
WHERE id = ?',
[$t['thold_hrule_alert']]);
switch($t['thold_type']) {
case '0': // Hi / Low
if ($t['thold_hi'] != '') {
$g['graph_defs'] .= 'CDEF:th' . $thold_id . 'ahi=' . $data_defs[$t['percent_ds']] . ',' . $t['thold_hi'] . ',100,/,* \\' . "\n";
$txt_graph_items .= 'LINE1:th' . $thold_id . 'ahi#' . $color . ':' . thold_prep_rrd_string(__esc('Alert Hi for %s (%s %%%)', $t['name_cache'], number_format_i18n($t['thold_hi']), 'thold')) . $skipscale . ' \\' . "\n";
$thold_id++;
}
if ($t['thold_low'] != '') {
$g['graph_defs'] .= 'CDEF:th' . $thold_id . 'alow=' . $data_defs[$t['percent_ds']] . ',' . $t['thold_low'] . ',100,/,* \\' . "\n";
$txt_graph_items .= 'LINE1:th' . $thold_id . 'alow#' . $color . ':' . thold_prep_rrd_string(__esc('Alert Low for %s (%s %%%)', $t['name_cache'], number_format_i18n($t['thold_low']), 'thold')) . $skipscale . ' \\' . "\n";
$thold_id++;
}
break;
case '2': // Time Based
if ($t['time_hi'] != '') {
$g['graph_defs'] .= 'CDEF:th' . $thold_id . 'ahi=' . $data_defs[$t['percent_ds']] . ',' . $t['time_hi'] . ',100,/,* \\' . "\n";
$txt_graph_items .= 'LINE1:th' . $thold_id . 'ahi#' . $color . ':' . thold_prep_rrd_string(__esc('Alert Hi for %s (%s %%%)', $t['name_cache'], number_format_i18n($t['time_hi']), 'thold')) . $skipscale . ' \\' . "\n";
$thold_id++;
}
if ($t['time_low'] != '') {
$g['graph_defs'] .= 'CDEF:th' . $thold_id . 'alow=' . $data_defs[$t['percent_ds']] . ',' . $t['time_low'] . ',100,/,* \\' . "\n";
$txt_graph_items .= 'LINE1:th' . $thold_id . 'alow#' . $color . ':' . thold_prep_rrd_string(__esc('Alert Low for %s (%s %%%)', $t['name_cache'], number_format_i18n($t['time_low']), 'thold')) . $skipscale . ' \\' . "\n";
$thold_id++;
}
break;
}
}
if ($t['thold_hrule_warning'] > 0) {
$color = db_fetch_cell_prepared('SELECT hex
FROM colors
WHERE id = ?',
[$t['thold_hrule_warning']]);
switch($t['thold_type']) {
case '0': // Hi / Low
if ($t['thold_warning_hi'] != '') {
$g['graph_defs'] .= 'CDEF:th' . $thold_id . 'whi=' . $data_defs[$t['percent_ds']] . ',' . $t['thold_warning_hi'] . ',100,/,* \\' . "\n";
$txt_graph_items .= 'LINE1:th' . $thold_id . 'whi#' . $color . ':' . thold_prep_rrd_string(__esc('Warning Hi for %s (%s %%%)', $t['name_cache'], number_format_i18n($t['thold_warning_hi']), 'thold')) . $skipscale . ' \\' . "\n";
$thold_id++;
}
if ($t['thold_warning_low'] != '') {
$g['graph_defs'] .= 'CDEF:th' . $thold_id . 'wlow=' . $data_defs[$t['percent_ds']] . ',' . $t['thold_warning_low'] . ',100,/,* \\' . "\n";
$txt_graph_items .= 'LINE1:th' . $thold_id . 'wlow#' . $color . ':' . thold_prep_rrd_string(__esc('Warning Low for %s (%s %%%)', $t['name_cache'], number_format_i18n($t['thold_warning_low']), 'thold')) . $skipscale . ' \\' . "\n";
$thold_id++;
}
break;
case '2': // Time Based
if ($t['time_warning_hi'] != '') {
$g['graph_defs'] .= 'CDEF:th' . $thold_id . 'whi=' . $data_defs[$t['percent_ds']] . ',' . $t['time_warning_hi'] . ',100,/,* \\' . "\n";
$txt_graph_items .= 'LINE1:th' . $thold_id . 'whi#' . $color . ':' . thold_prep_rrd_string(__esc('Warning Hi for %s (%s %%%)', $t['name_cache'], number_format_i18n($t['time_warning_hi']), 'thold')) . $skipscale . ' \\' . "\n";
$thold_id++;
}
if ($t['time_warning_low'] != '') {
$g['graph_defs'] .= 'CDEF:th' . $thold_id . 'wlow=' . $data_defs[$t['percent_ds']] . ',' . $t['time_warning_low'] . ',100,/,* \\' . "\n";
$txt_graph_items .= 'LINE1:th' . $thold_id . 'wlow#' . $color . ':' . thold_prep_rrd_string(__esc('Warning Low for %s (%s %%%)', $t['name_cache'], number_format_i18n($t['time_warning_low']), 'thold')) . $skipscale . ' \\' . "\n";
$thold_id++;
}
break;
}
}
}
}
}
}
if ($txt_graph_items) {
$g['txt_graph_items'] .= ' \\' . "\n" . 'COMMENT:\' ' . '\\n' . '\' \\' . "\n" . 'COMMENT:\'<u><b>' . __('Threshold Alert/Warning Values', 'thold') . '</b></u>' . '\\n' . '\' \\' . "\n" . $txt_graph_items;
}
return $g;
}
/**
* Prepares a string value for safe embedding in an RRDtool command
* line argument by shell-escaping it, then RRDtool-escaping it, then
* wrapping it in single quotes.
*
* @param string $string The raw string value to prepare.
*
* @return string The quoted, escaped string ready for RRDtool command
* construction.
*/
function thold_prep_rrd_string($string) {
return '\'' . trim(cacti_escapeshellarg(rrdtool_escape_string($string)), "'") . '\'';
}
/**
* Executes the "Apply Thresholds" bulk device action: auto-creates
* thresholds for each selected device using its device template's
* associated threshold templates. Registered as the
* 'device_action_execute' Cacti hook; a no-op passthrough for any
* other action.
*
* @param string $action The bulk action identifier being executed.
*
* @return string The unmodified $action, for hook chaining.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include.
*/
function thold_device_action_execute($action) {
global $config;
if ($action != 'thold') {
return $action;
}
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
$selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items'));
if ($selected_items != false) {
for ($i = 0; ($i < cacti_sizeof($selected_items)); $i++) {
autocreate($selected_items[$i]);
}
}
return $action;
}
/**
* Auto-creates thresholds for a newly created device when the global
* thold_autocreate setting is enabled. Registered as the
* 'api_device_new' Cacti hook.
*
* @param array $save The newly created device's saved field data,
* including its 'id'.
*
* @return array The unmodified $save array, for hook chaining.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include.
*/
function thold_api_device_new($save) {
global $config;
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if (read_config_option('thold_autocreate') == 'on') {
if (!empty($save['id'])) {
autocreate($save['id']);
}
}
return $save;
}
/**
* Renders the confirmation box listing the devices selected for the
* "Apply Thresholds" bulk device action. Registered as the
* 'device_action_prepare' Cacti hook; a no-op passthrough for any
* other action.
*
* @param array $save The bulk-action form submission data, including
* 'drp_action' and a pre-built 'host_list' HTML
* fragment.
*
* @return array The unmodified $save array, for hook chaining.
*
* @global array $host_list Reserved/declared for parity with other
* functions in this file; not used directly
* here.
*/
function thold_device_action_prepare($save) {
global $host_list;
if ($save['drp_action'] != 'thold') {
return $save;
}
print "<tr>
<td colspan='2' class='textArea'>
<p>" . __('Click \'Continue\' to apply all appropriate Thresholds to these Device(s).', 'thold') . "</p>
<div class='itemlist'><ul>" . $save['host_list'] . '</ul></div>
</td>
</tr>';
return $save;
}
/**
* Adds the "Apply Thresholds" entry to the device management bulk-
* actions dropdown. Registered as the 'device_action_array' Cacti
* hook.
*
* @param array $device_action_array The existing bulk-action label
* map, keyed by action id.
*
* @return array The action map with this plugin's entry added.
*/
function thold_device_action_array($device_action_array) {
$device_action_array['thold'] = 'Apply Thresholds';
return $device_action_array;
}
/**
* Reacts to a device being saved: when the device's disabled state
* changed, triggers this plugin's device up/down/enable/disable
* notification handling; and (elsewhere in this function, per the
* broader save flow) keeps this plugin's per-device data in sync with
* the saved device fields. Registered as the 'api_device_save' Cacti
* hook.
*
* @param array $save The device's saved field data, including 'id' and
* 'disabled'.
*
* @return array The unmodified $save array, for hook chaining.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include.
*/
function thold_api_device_save($save) {
global $config;
$result = db_fetch_row_prepared('SELECT disabled
FROM host
WHERE id = ?',
[$save['id']]);
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if ($save['id'] > 0) {
if ($save['disabled'] != $result['disabled']) {
if ($save['disabled'] == '') {
plugin_thold_log_changes($save['id'], 'enabled_host');
db_execute_prepared('UPDATE thold_data
SET thold_enabled = "on"
WHERE host_id = ?',
[$save['id']]);
} else {
plugin_thold_log_changes($save['id'], 'disabled_host');
db_execute_prepared('UPDATE thold_data
SET thold_enabled = "off"
WHERE host_id = ?',
[$save['id']]);
}
}
}
if (isset_request_var('thold_send_email')) {
$save['thold_send_email'] = form_input_validate(get_nfilter_request_var('thold_send_email'), 'thold_send_email', '', true, 3);
} else {
$save['thold_send_email'] = form_input_validate('', 'thold_send_email', '', true, 3);
}
if (isset_request_var('thold_host_email')) {
$save['thold_host_email'] = form_input_validate(get_nfilter_request_var('thold_host_email'), 'thold_host_email', '', true, 3);
} else {
$save['thold_host_email'] = form_input_validate('', 'thold_host_email', '', true, 3);
}
if (isset_request_var('thold_failure_count')) {
$save['thold_failure_count'] = form_input_validate(get_nfilter_request_var('thold_failure_count'), 'thold_failure_count', '', true, 3);
} else {
$save['thold_failure_count'] = form_input_validate('', 'thold_failure_count', '', true, 3);
}
return $save;
}
/**
* Turns a data source's name cell in the Console data sources table
* into a link to either edit its existing threshold, or (when no graph
* exists yet for it) create a new one, when applicable. Registered as
* the 'data_sources_table' Cacti hook.
*
* @param array $ds The data source row being rendered.
*
* @return array The $ds row, with 'data_template_name' replaced by a
* linked HTML fragment when applicable.
*
* @global array $config Cacti global configuration array (declared but
* not used directly here).
*/
function thold_data_sources_table($ds) {
global $config;
if (isset($ds['local_data_id'])) {
$exists = db_fetch_cell_prepared('SELECT id
FROM thold_data
WHERE local_data_id = ?
LIMIT 1',
[$ds['local_data_id']]);
if ($exists) {
$ds['data_template_name'] = "<a title='" . __esc('Create Threshold from Data Source', 'thold') . "' class='linkEditMain' href='" . html_escape('plugins/thold/thold.php?action=edit&id=' . $exists) . "'>" . ((empty($ds['data_template_name'])) ? '<em>' . __('None', 'thold') . '</em>' : html_escape($ds['data_template_name'])) . '</a>';
} else {
$graph_exists = db_fetch_cell_prepared('SELECT DISTINCT gl.id
FROM graph_local AS gl
INNER JOIN graph_templates_item AS gti
ON gl.id = gti.local_graph_id
INNER JOIN data_template_rrd AS dtr
ON gti.task_item_id = dtr.id
WHERE dtr.local_data_id = ?
LIMIT 1',
[$ds['local_data_id']]);
$data_template_id = db_fetch_cell_prepared('SELECT data_template_id
FROM data_local
WHERE id = ?',
[$ds['local_data_id']]);
if ($graph_exists) {
$ds['data_template_name'] = "<a title='" . __esc('Create Threshold from Data Source', 'thold') . "' class='linkEditMain' href='" . html_escape('plugins/thold/thold.php?action=edit&local_data_id=' . $ds['local_data_id'] . '&host_id=' . $ds['host_id'] . '&data_template_id=' . $data_template_id . '&data_template_rrd_id=&local_graph_id=' . $graph_exists . '&thold_template_id=0') . "'>" . ((empty($ds['data_template_name'])) ? '<em>' . __('None', 'thold') . '</em>' : html_escape($ds['data_template_name'])) . '</a>';
}
}
}
return $ds;
}
/**
* Prints an "Auto-create Thresholds" link on the "new graphs" page for
* the current host. Registered as the 'graphs_new_top_links' Cacti
* hook.
*
* @return void
*
* @global array $config Cacti global configuration array; used to
* build the link URL.
*/
function thold_graphs_new() {
global $config;
print '<span class="linkMarker">*</span><a class="autocreate linkEditMain" href="' . html_escape($config['url_path'] . 'plugins/thold/thold.php?action=autocreate&host_id=' . get_filter_request_var('host_id')) . '">' . __('Auto-create Thresholds', 'thold') . '</a><br>';
}
/**
* Saves (or clears) a user's notification email address into this
* plugin's contacts table whenever a Cacti user account is saved with
* an email/email_address field. Registered as the
* 'user_admin_setup_sql_save' Cacti hook; a no-op passthrough when the
* user save already failed validation.
*
* @param array $save The user account's saved field data, including
* 'id'.
*
* @return array The unmodified (or id-populated) $save array, for hook
* chaining.
*/
function thold_user_admin_setup_sql_save($save) {
if (is_error_message()) {
return $save;
}
if (isset_request_var('email') || isset_request_var('email_address')) {
$email = form_input_validate(get_nfilter_request_var('email_address'), 'email_address', '', true, 3);
if ($save['id'] == 0) {
$save['id'] = sql_save($save, 'user_auth');
}
$cid = db_fetch_cell_prepared('SELECT id
FROM plugin_thold_contacts
WHERE type = "email"
AND user_id = ?',
[$save['id']]);
if ($cid) {
db_execute_prepared('REPLACE INTO plugin_thold_contacts
(id, user_id, type, data) VALUES
(?, ?, "email", ?)',
[$cid, $save['id'], $email]);
} else {
db_execute_prepared('REPLACE INTO plugin_thold_contacts
(user_id, type, data) VALUES
(?, "email", ?)',
[$save['id'], $email]);
}
}
return $save;
}
/**
* Executes the "Create Threshold from Data Source" bulk data source
* action: creates thresholds for each selected data source using the
* threshold template chosen on the confirmation page. Registered as
* the 'data_source_action_execute' Cacti hook; a no-op passthrough for
* any other action.
*
* @param string $action The bulk action identifier being executed.
*
* @return string The unmodified $action, for hook chaining.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include.
* @global array $form_array Reserved/declared for parity with other
* functions in this file; not used directly
* here.
*/
function thold_data_source_action_execute($action) {
global $config, $form_array;
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if ($action == 'plugin_thold_create') {
$selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items'));
if ($selected_items != false) {
$message = '';
$created = 0;
get_filter_request_var('thold_template_id');
$template = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[get_request_var('thold_template_id')]);
if (cacti_sizeof($template)) {
foreach ($selected_items as $local_data_id) {
$data_sources = db_fetch_assoc_prepared('SELECT DISTINCT
dtr.id, gl.id AS local_graph_id, dtr.local_data_id
FROM data_template_rrd AS dtr
INNER JOIN graph_templates_item AS gti
ON gti.task_item_id=dtr.id
INNER JOIN graph_local AS gl
ON gl.id=gti.local_graph_id
WHERE dtr.local_data_id = ?
AND dtr.data_source_name = ?',
[$local_data_id, $template['data_source_name']]);
if (cacti_sizeof($data_sources)) {
foreach ($data_sources as $data_source) {
$local_data_id = $data_source['local_data_id'];
$local_graph_id = $data_source['local_graph_id'];
$data_template_rrd_id = $data_source['id'];
if (thold_create_from_template($local_data_id, $local_graph_id, $data_template_rrd_id, $template, $message)) {
$created++;
}
}
}
}
if (strlen($message)) {
thold_raise_message(__('Created %s thresholds', $created) . '<br>' . $message, MESSAGE_LEVEL_INFO);
} else {
thold_raise_message(__('No Threshold(s) Created. Either they already exist, or no suitable matches found.', 'thold'), MESSAGE_LEVEL_INFO);
}
} else {
thold_raise_message(__('No Threshold(s) Created. Threshold(s) Template not found.', 'thold'), MESSAGE_LEVEL_ERROR);
}
}
}
return $action;
}
/**
* Renders the confirmation page for the "Create Threshold from
* Template" bulk data source action, listing which selected data
* sources have at least one applicable threshold template (and
* offering a template selection), while filtering out any that don't.
* Registered as the 'data_source_action_prepare' Cacti hook; a no-op
* passthrough for any other action.
*
* @param array $save The bulk-action form submission data, including
* 'drp_action' and 'ds_array' (selected data source
* ids).
*
* @return array|void The unmodified $save array (for hook chaining)
* when the action isn't 'plugin_thold_create';
* otherwise this renders the confirmation UI
* directly and returns nothing.
*
* @global array $config Cacti global configuration array (declared but
* not used directly here).
*/
function thold_data_source_action_prepare($save) {
global $config;
if ($save['drp_action'] == 'plugin_thold_create') {
/* get the valid thold templates
* remove those hosts that do not have any valid templates
*/
$templates = '';
$found_list = '';
$not_found = '';
if (cacti_sizeof($save['ds_array'])) {
foreach ($save['ds_array'] as $item) {
$data_template_id = db_fetch_cell_prepared('SELECT data_template_id
FROM data_local
WHERE id = ?',
[$item]);
if ($data_template_id != '') {
$templates_ids = db_fetch_assoc_prepared('SELECT id
FROM thold_template
WHERE data_template_id = ?',
[$data_template_id]);
if (cacti_sizeof($templates_ids)) {
$found_list .= '<li>' . html_escape(get_data_source_title($item)) . '</li>';
if (strlen($templates)) {
$templates .= ", $data_template_id";
} else {
$templates = "$data_template_id";
}
} else {
$not_found .= '<li>' . html_escape(get_data_source_title($item)) . '</li>';
}
} else {
$not_found .= '<li>' . html_escape(get_data_source_title($item)) . '</li>';
}
}
}
if (strlen($templates)) {
$sql = 'SELECT id, name FROM thold_template WHERE data_template_id IN (' . $templates . ') ORDER BY name';
} else {
$sql = 'SELECT id, name FROM thold_template ORDER BY name';
}
print "<tr><td colspan='2' class='textArea'>";
if (strlen($found_list)) {
if (strlen($not_found)) {
print '<p>' . __('The following Data Sources have no Threshold Templates associated with them', 'thold') . '</p>';
print '<div class="itemlist"><ul>' . $not_found . '</ul></div>';
}
print '<p>' . __('Click \'Continue\' to create Thresholds for these Data Sources?', 'thold') . '</p>
<div class="itemlist"><ul>' . $found_list . "</ul></div>
</td>
</tr></table><table class='cactiTable'><tr><td>";
$form_array = [
'general_header' => [
'friendly_name' => __('Available Threshold Templates', 'thold'),
'method' => 'spacer',
],
'thold_template_id' => [
'method' => 'drop_sql',
'friendly_name' => __('Select a Threshold Template', 'thold'),
'description' => '',
'none_value' => __('None', 'thold'),
'value' => __('None', 'thold'),
'sql' => $sql
]
];
draw_edit_form(
[
'config' => ['no_form_tag' => true],
'fields' => $form_array
]
);
} else {
if (strlen($not_found)) {
print '<p>' . __('There are no Threshold Templates associated with the following Data Sources', 'thold') . '</p>';
print '<div class="itemlist"><ul>' . $not_found . '</ul></div>';
}
}
print '</td></tr></table><table class="cactiTable"><tr><td class="saveRow">';
} else {
return $save;
}
}
/**
* Adds the "Create Threshold from Template" entry to the data source
* management bulk-actions dropdown. Registered as the
* 'data_source_action_array' Cacti hook.
*
* @param array $action The existing bulk-action label map, keyed by
* action id.
*
* @return array The action map with this plugin's entry added.
*/
function thold_data_source_action_array($action) {
$action['plugin_thold_create'] = __('Create Threshold from Template', 'thold');
return $action;
}
/**
* Executes the "Create Threshold from Template" bulk graph action:
* applies the selected threshold template to each selected graph's
* matching data source(s), creating a new threshold for each one that
* doesn't already have one from that template. Registered as the
* 'graphs_action_execute' Cacti hook; a no-op passthrough for any
* other action.
*
* @param string $action The bulk action identifier being executed.
*
* @return string The unmodified $action, for hook chaining.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include.
* @global array $form_array Reserved/declared for parity with other
* functions in this file; not used directly
* here.
*/
function thold_graphs_action_execute($action) {
global $config, $form_array;
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if ($action == 'plugin_thold_create') {
$selected_items = sanitize_unserialize_selected_items(get_nfilter_request_var('selected_items'));
if ($selected_items != false) {
$message = '';
$created = 0;
get_filter_request_var('thold_template_id');
$template = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[get_request_var('thold_template_id')]);
if (cacti_sizeof($template)) {
foreach ($selected_items as $local_graph_id) {
$data_sources = db_fetch_assoc_prepared('SELECT DISTINCT
dtr.id, gl.id AS local_graph_id, dtr.local_data_id
FROM data_template_rrd AS dtr
INNER JOIN graph_templates_item AS gti
ON gti.task_item_id=dtr.id
INNER JOIN graph_local AS gl
ON gl.id=gti.local_graph_id
WHERE gl.id = ?
AND dtr.data_source_name = ?',
[$local_graph_id, $template['data_source_name']]);
if (cacti_sizeof($data_sources)) {
foreach ($data_sources as $data_source) {
$local_data_id = $data_source['local_data_id'];
$local_graph_id = $data_source['local_graph_id'];
$data_template_rrd_id = $data_source['id'];
if (thold_create_from_template($local_data_id, $local_graph_id, $data_template_rrd_id, $template, $message)) {
$created++;
}
}
}
}
if (strlen($message)) {
thold_raise_message(__('Created %s thresholds', $created) . '<br>' . $message, MESSAGE_LEVEL_INFO);
} else {
thold_raise_message(__('No Threshold(s) Created. Either they already exist, or no suitable matches found.', 'thold'), MESSAGE_LEVEL_INFO);
}
} else {
thold_raise_message(__('No Threshold(s) Created. Threshold(s) Template not found.', 'thold'), MESSAGE_LEVEL_ERROR);
}
}
}
return $action;
}
/**
* Renders the confirmation page for the "Create Threshold from
* Template" bulk graph action, listing which selected graphs have at
* least one applicable threshold template (and offering a template
* selection), while filtering out any that don't. Registered as the
* 'graphs_action_prepare' Cacti hook; a no-op passthrough for any
* other action.
*
* @param array $save The bulk-action form submission data, including
* 'drp_action'.
*
* @return array|void The unmodified $save array (for hook chaining)
* when the action isn't 'plugin_thold_create';
* otherwise this renders the confirmation UI
* directly and returns nothing.
*
* @global array $config Cacti global configuration array (declared but
* not used directly here).
*/
function thold_graphs_action_prepare($save) {
global $config;
if ($save['drp_action'] == 'plugin_thold_create') {
/* get the valid thold templates
* remove those hosts that do not have any valid templates
*/
$found_list = '';
$not_found = '';
$template_ids = [];
if (cacti_sizeof($save['graph_array'])) {
foreach ($save['graph_array'] as $item) {
$item_found = false;
$data_template_ids = db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_template_id
FROM data_template_rrd AS dtr
LEFT JOIN graph_templates_item AS gti
ON gti.task_item_id=dtr.id
LEFT JOIN graph_local AS gl
ON gl.id=gti.local_graph_id
WHERE gl.id = ?',
[$item]);
if (cacti_sizeof($data_template_ids)) {
foreach ($data_template_ids as $i => $data_template_rec) {
$data_template_id = $data_template_rec['data_template_id'];
$templates = db_fetch_assoc_prepared('SELECT id
FROM thold_template
WHERE data_template_id = ?',
[$data_template_id]);
if (cacti_sizeof($templates)) {
$item_found = true;
$template_ids[] = $data_template_id;
}
}
}
if (!$item_found) {
$not_found .= '<li>' . html_escape(get_graph_title($item)) . '</li>';
} else {
$found_list .= '<li>' . html_escape(get_graph_title($item)) . '</li>';
}
}
}
if (cacti_sizeof($template_ids)) {
$sql = 'SELECT id, name FROM thold_template WHERE data_template_id IN (' . implode(', ', $template_ids) . ') ORDER BY name';
} else {
$sql = 'SELECT id, name FROM thold_template ORDER BY name';
}
print "<tr><td colspan='2' class='textArea'>";
if (strlen($found_list)) {
if (strlen($not_found)) {
print '<p>' . __('The following Graphs have no Threshold Templates associated with them', 'thold') . '</p>';
print '<div class="itemlist"><ul>' . $not_found . '</ul></div>';
}
print '<p>' . __('Press \'Continue\' if you wish to create Threshold(s) for these Graph(s)', 'thold') . '</p>
<div class="itemlist"><ul>' . $found_list . "</ul></div>
</td>
</tr></table><table class='cactiTable'><tr><td>";
$form_array = [
'general_header' => [
'friendly_name' => __('Available Threshold Templates', 'thold'),
'method' => 'spacer',
],
'thold_template_id' => [
'method' => 'drop_sql',
'friendly_name' => __('Select a Threshold Template', 'thold'),
'description' => '',
'value' => __('None', 'thold'),
'sql' => $sql
]
];
draw_edit_form(
[
'config' => ['no_form_tag' => true],
'fields' => $form_array
]
);
} else {
if (strlen($not_found)) {
print '<p>' . __('There are no Threshold Templates associated with the following Graphs', 'thold') . '</p>';
print '<div class="itemlist"><ul>' . $not_found . '</ul></div>';
}
}
print '</td></tr></table><table class="cactiTable"><tr><td class="saveRow">';
} else {
return $save;
}
}
/**
* Adds the "Create Threshold from Template" entry to the graph
* management bulk-actions dropdown. Registered as the
* 'graphs_action_array' Cacti hook.
*
* @param array $action The existing bulk-action label map, keyed by
* action id.
*
* @return array The action map with this plugin's entry added.
*/
function thold_graphs_action_array($action) {
$action['plugin_thold_create'] = __('Create Threshold from Template', 'thold');
return $action;
}
/**
* Emits the JavaScript that shows/hides the notification-list dropdown
* on the device edit form based on the selected "Threshold Up/Down
* Email Notification" mode. Registered as the 'host_edit_bottom' and
* 'device_change_javascript' Cacti hooks.
*
* @return void
*/
function thold_host_edit_bottom() {
// This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/includes/functions.php');
?>
<script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>>
changeNotify();
function changeNotify() {
if ($('#thold_send_email').val() < 2) {
$('#row_thold_host_email').hide();
} else {
$('#row_thold_host_email').show();
}
}
</script>
<?php
}
/**
* Installs this plugin's CACTI-THOLD-MIB definitions into Cacti's SNMP
* agent cache (SNMP Notification Receiver / SNMP Agent support),
* when the SNMP agent's MibCache class is available. Registered as the
* 'snmpagent_cache_install' Cacti hook, and invoked during plugin
* installation.
*
* @return void
*
* @global array $config Cacti global configuration array; used to
* locate the MIB definition file.
*/
function thold_snmpagent_cache_install() {
global $config;
if (class_exists('MibCache')) {
$mc = new MibCache('CACTI-THOLD-MIB');
$mc->install($config['base_path'] . '/plugins/thold/CACTI-THOLD-MIB', true);
}
}
/**
* Removes this plugin's CACTI-THOLD-MIB definitions from Cacti's SNMP
* agent cache, when the SNMP agent's MibCache class is available.
* Invoked during plugin uninstallation.
*
* @return void
*
* @global array $config Cacti global configuration array; used to
* locate the MIB definition file.
*/
function thold_snmpagent_cache_uninstall() {
global $config;
if (class_exists('MibCache')) {
$mc = new MibCache('CACTI-THOLD-MIB');
$mc->uninstall();
}
}
/**
* Emits this plugin's theme-specific stylesheet link (when one exists)
* and a JavaScript handler that rebinds the threshold-VRULE toggle
* link's click behavior to an AJAX partial-page refresh after each AJAX
* completion. Registered as the 'page_head' Cacti hook.
*
* @return void
*
* @global array $config Cacti global configuration array; used to
* check for a theme-specific stylesheet and build
* asset URLs.
*/
function thold_page_head() {
global $config;
// This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/includes/functions.php');
if (file_exists($config['base_path'] . '/plugins/thold/css/' . get_selected_theme() . '.css')) {
print get_md5_include_css('plugins/thold/css/' . get_selected_theme() . '.css');
}
print get_md5_include_css('plugins/thold/css/pills.css');
?>
<script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>>
$(function() {
$(document).ajaxComplete(function() {
$('.tholdVRule').unbind().click(function(event) {
event.preventDefault();
href = $(this).attr('href');
href += '&header=false';
$.get(href, function(data) {
$('#main').empty().hide();
$('div[class^="ui-"]').remove();
$('#main').html(data);
applySkin();
});
});
});
});
</script>
<?php
}
/**
* Renders the "Associated Threshold Templates" box on the device edit
* page, listing threshold templates already associated with the
* device (with their per-device threshold existence status and a
* remove link) and offering a dropdown to associate an additional
* template. Registered as the 'device_edit_pre_bottom' Cacti hook.
*
* @return void
*/
function thold_device_edit_pre_bottom() {
// This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/includes/functions.php');
html_start_box(__('Associated Threshold Templates', 'thold'), '100%', false, '3', 'center', '');
$host_id = get_request_var('id');
$host_template_id = db_fetch_cell_prepared('SELECT host_template_id
FROM host
WHERE id = ?',
[$host_id]);
$threshold_templates = db_fetch_assoc_prepared('SELECT DISTINCT pth.thold_template_id, tt.name
FROM plugin_thold_host AS pth
INNER JOIN thold_template AS tt
ON tt.id = pth.thold_template_id
WHERE pth.host_id = ?
ORDER BY name',
[$host_id]);
html_header([__('Name', 'thold'), __('Status', 'thold')], 2);
$i = 1;
if (cacti_sizeof($threshold_templates)) {
foreach ($threshold_templates as $item) {
$exists = db_fetch_cell_prepared('SELECT id
FROM thold_data
WHERE host_id = ?
AND thold_template_id = ?',
[get_request_var('id'), $item['thold_template_id']]);
if ($exists) {
$exists = __('Threshold Exists', 'thold');
} else {
$exists = __('Threshold Does Not Exist', 'thold');
}
form_alternate_row("tt$i", true);
?>
<td class='left'>
<strong><?php print $i; ?>)</strong> <?php print html_escape($item['name']); ?>
</td>
<td>
<?php print $exists; ?>
</td>
<td class='nowrap right'>
<a class='delete deleteMarker fa fa-times' title='<?php print __esc('Delete Threshold Template Association', 'thold'); ?>' href='<?php print html_escape('host.php?action=item_remove_tt_confirm&id=' . $item['thold_template_id'] . '&host_id=' . get_request_var('id')); ?>'></a>
</td>
<?php
form_end_row();
$i++;
}
} else {
print '<tr><td class="templateAdd" colspan="2"><em>' . __('No Associated Threshold Templates.', 'thold') . '</em></td></tr>';
}
$available_thold_templates = db_fetch_assoc_prepared('SELECT DISTINCT tt.id, tt.name
FROM thold_template AS tt
LEFT JOIN plugin_thold_host AS pth
ON pth.thold_template_id = tt.id
AND pth.host_id = ?
WHERE tt.id NOT IN (SELECT thold_template_id FROM plugin_thold_host WHERE host_id = ?)',
[$host_id, $host_id]);
if (cacti_sizeof($available_thold_templates)) {
?>
<tr class='odd'>
<td class='saveRow' colspan='3'>
<table>
<tr style='line-height:10px;'>
<td class='nowrap templateAdd' style='padding-right:15px;'>
<?php print __('Add Threshold Template'); ?>
</td>
<td class='noHide'>
<?php form_dropdown('thold_template_id', $available_thold_templates, 'name', 'id', '', '', ''); ?>
</td>
<td class='noHide'>
<input id='add_tt' type='button' class='ui-button ui-corner-all ui-widget' value='<?php print __esc('Add'); ?>' title='<?php print __esc('Add Threshold Template to Device'); ?>' onClick='addThresholdTemplate()'>
</td>
</tr>
</table>
<script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>>
function addThresholdTemplate() {
scrollTop = $(window).scrollTop();
$.post(urlPath+'host.php?header=false&action=item_add_tt', {
host_id: $('#id').val(),
thold_template_id: $('#thold_template_id').val(),
__csrf_magic: csrfMagicToken})
.done(function() {
loadPageNoHeader(urlPath+'host.php?header=false&action=edit&id='+$('#id').val());
});
}
</script>
</td>
</tr>
<?php
}
html_end_box();
}
/**
* Renders the confirmation page for removing a threshold template's
* association with a device (action=item_remove_tt_confirm), or
* otherwise defers to the standard device edit page (this hook is
* registered on host.php, whose own action-dispatch relies on this
* function to intercept this plugin-specific action). Registered as
* the 'device_top' Cacti hook.
*
* @return void
*/
function thold_device_top() {
if (get_request_var('action') == 'item_remove_tt_confirm') {
// ================= input validation =================
get_filter_request_var('id');
get_filter_request_var('host_id');
// ====================================================
form_start('host.php?action=edit&id' . get_request_var('host_id'));
html_start_box('', '100%', false, '3', 'center', '');
$template = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[get_request_var('id')]);
?>
<tr>
<td class='topBoxAlt'>
<p><?php print __('Click \'Continue\' to Delete the following Threshold Template will be disassociated from the Device.', 'thold'); ?></p>
<p><?php print __esc('Threshold Template Name: %s', $template['name'], 'thold'); ?>'<br>
</td>
</tr>
<tr>
<td align='right'>
<input id='cancel' type='button' value='<?php print __esc('Cancel', 'thold'); ?>' onClick='$("#cdialog").dialog("close")' name='cancel'>
<input id='continue' type='button' value='<?php print __esc('Continue', 'thold'); ?>' name='continue' title='<?php print __esc('Remove Threshold Template', 'thold'); ?>'>
</td>
</tr>
<?php
html_end_box();
form_end();
?>
<script type='text/javascript'>
$(function() {
$('#cdialog').dialog();
});
$('#continue').click(function(data) {
$.post('host.php?action=item_remove_tt', {
__csrf_magic: csrfMagicToken,
host_id: <?php print get_request_var('host_id'); ?>,
id: <?php print get_request_var('id'); ?>
}).done(function(data) {
$('#cdialog').dialog('close');
loadPageNoHeader('host.php?action=edit&header=false&id=<?php print get_request_var('host_id'); ?>');
});
});
</script>
<?php
exit;
}
if (get_request_var('action') == 'item_remove_tt') {
// ================= input validation =================
get_filter_request_var('id');
get_filter_request_var('host_id');
// ====================================================
db_execute_prepared('DELETE
FROM plugin_thold_host
WHERE thold_template_id = ?
AND host_id = ?',
[get_request_var('id'), get_request_var('host_id')]);
header('Location: host.php?header=false&action=edit&id=' . get_request_var('host_id'));
exit;
}
if (get_request_var('action') == 'item_add_tt') {
// ================= input validation =================
get_filter_request_var('host_id');
get_filter_request_var('thold_template_id');
// ====================================================
db_execute_prepared('REPLACE INTO plugin_thold_host
(host_id, thold_template_id) VALUES (?, ?)',
[get_request_var('host_id'), get_request_var('thold_template_id')]);
header('Location: host.php?header=false&action=edit&id=' . get_request_var('host_id'));
exit;
}
}
/**
* Renders the "Associated Threshold Templates" box on the device
* template edit page, listing threshold templates already associated
* with the device template (with a remove link) and offering a
* dropdown to associate an additional template. Registered as the
* 'device_template_edit' Cacti hook.
*
* @return void
*/
function thold_device_template_edit() {
// This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/includes/functions.php');
html_start_box(__('Associated Threshold Templates', 'thold'), '100%', false, '3', 'center', '');
$threshold_templates = db_fetch_assoc_prepared('SELECT ptdt.thold_template_id, tt.name
FROM plugin_thold_host_template AS ptdt
INNER JOIN thold_template AS tt
ON tt.id=ptdt.thold_template_id
WHERE ptdt.host_template_id = ?
ORDER BY name',
[get_request_var('id')]);
$i = 0;
if (cacti_sizeof($threshold_templates)) {
foreach ($threshold_templates as $item) {
form_alternate_row("tt$i", true);
?>
<td class='left'>
<strong><?php print $i; ?>)</strong> <?php print html_escape($item['name']); ?>
</td>
<td class='right'>
<a class='delete deleteMarker fa fa-times' title='<?php print __esc('Delete', 'thold'); ?>' href='<?php print html_escape('host_templates.php?action=item_remove_tt_confirm&id=' . $item['thold_template_id'] . '&host_template_id=' . get_request_var('id')); ?>'></a>
</td>
<?php
form_end_row();
$i++;
}
} else {
print '<tr><td class="templateAdd" colspan="2"><em>' . __('No Associated Threshold Templates.', 'thold') . '</em></td></tr>';
}
$unmapped = db_fetch_assoc_prepared('SELECT DISTINCT tt.id, tt.name
FROM thold_template AS tt
LEFT JOIN plugin_thold_host_template AS ptdt
ON tt.id = ptdt.thold_template_id
AND ptdt.host_template_id = ?
WHERE ptdt.host_template_id IS NULL OR ptdt.host_template_id != ?
ORDER BY tt.name',
[get_request_var('id'), get_request_var('id')]);
if (cacti_sizeof($unmapped)) {
?>
<tr class='odd'>
<td colspan='2'>
<table>
<tr style='line-height:10px;'>
<td style='padding-right: 15px;'>
<?php print __('Add Threshold Template', 'thold'); ?>
</td>
<td>
<?php form_dropdown('thold_template_id', $unmapped, 'name', 'id', '', '', ''); ?>
</td>
<td>
<input type='button' value='<?php print __esc('Add', 'thold'); ?>' id='add_tt' title='<?php print __esc('Add Threshold Template to Device Template', 'thold'); ?>' onClick='addThresholdTemplate()'>
</td>
</tr>
</table>
<script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>>
function addThresholdTemplate() {
$('#add_tt').click(function() {
scrollTop = $(window).scrollTop();
$.post('host_templates.php?header=false&action=item_add_tt', {
host_template_id: $('#id').val(),
thold_template_id: $('#thold_template_id').val(),
__csrf_magic: csrfMagicToken})
.done(function(data) {
loadPageNoHeader(urlPath+'host_templates.php?header=false&action=edit&id='+$('#id').val());
});
});
}
</script>
</td>
</tr>
<?php
}
html_end_box();
}
/**
* Renders the confirmation page for removing a threshold template's
* association with a device template (action=item_remove_tt_confirm).
* Registered as the 'device_template_top' Cacti hook.
*
* @return void
*/
function thold_device_template_top() {
if (get_request_var('action') == 'item_remove_tt_confirm') {
// ================= input validation =================
get_filter_request_var('id');
get_filter_request_var('host_template_id');
// ====================================================
form_start('host_templates.php?action=edit&id' . get_request_var('host_template_id'));
html_start_box('', '100%', false, '3', 'center', '');
$template = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[get_request_var('id')]);
?>
<tr>
<td class='topBoxAlt'>
<p><?php print __('Click \'Continue\' to Delete the following Threshold Template will be disassociated from the Device Template.', 'thold'); ?></p>
<p><?php print __esc('Threshold Template Name: %s', $template['name'], 'thold'); ?>'<br>
</td>
</tr>
<tr>
<td align='right'>
<input id='cancel' type='button' value='<?php print __esc('Cancel', 'thold'); ?>' onClick='$("#cdialog").dialog("close")' name='cancel'>
<input id='continue' type='button' value='<?php print __esc('Continue', 'thold'); ?>' name='continue' title='<?php print __esc('Remove Threshold Template', 'thold'); ?>'>
</td>
</tr>
<?php
html_end_box();
form_end();
?>
<script type='text/javascript'>
$(function() {
$('#cdialog').dialog();
});
$('#continue').click(function(data) {
$.post('host_templates.php?action=item_remove_tt', {
__csrf_magic: csrfMagicToken,
host_template_id: <?php print get_request_var('host_template_id'); ?>,
id: <?php print get_request_var('id'); ?>
}).done(function(data) {
$('#cdialog').dialog('close');
loadPageNoHeader('host_templates.php?action=edit&header=false&id=<?php print get_request_var('host_template_id'); ?>');
});
});
</script>
<?php
exit;
}
if (get_request_var('action') == 'item_remove_tt') {
// ================= input validation =================
get_filter_request_var('id');
get_filter_request_var('host_template_id');
// ====================================================
db_execute_prepared('DELETE
FROM plugin_thold_host_template
WHERE thold_template_id = ?
AND host_template_id = ?',
[get_request_var('id'), get_request_var('host_template_id')]);
header('Location: host_templates.php?header=false&action=edit&id=' . get_request_var('host_template_id'));
exit;
}
if (get_request_var('action') == 'item_add_tt') {
// ================= input validation =================
get_filter_request_var('host_template_id');
get_filter_request_var('thold_template_id');
// ====================================================
db_execute_prepared('REPLACE INTO plugin_thold_host_template
(host_template_id, thold_template_id) VALUES (?, ?)',
[get_request_var('host_template_id'), get_request_var('thold_template_id')]);
header('Location: host_templates.php?header=false&action=edit&id=' . get_request_var('host_template_id'));
exit;
}
}
/**
* Propagates a device template's associated threshold templates to a
* device when its device template is changed, associating the device
* with every threshold template linked to the newly selected device
* template. Registered as the 'device_template_change' Cacti hook.
*
* @param array $data The device template change payload, including
* 'device_id' and 'device_template_id'.
*
* @return array The unmodified $data array, for hook chaining.
*/
function thold_device_template_change($data) {
$device_id = $data['device_id'];
$device_template_id = $data['device_template_id'];
$thold_templates = db_fetch_assoc_prepared('SELECT *
FROM plugin_thold_host_template
WHERE host_template_id = ?',
[$device_template_id]);
if (cacti_sizeof($thold_templates)) {
foreach ($thold_templates as $tt) {
db_execute_prepared('REPLACE INTO plugin_thold_host (host_id, thold_template_id) VALUES (?, ?)',
[$device_id, $tt['thold_template_id']]);
}
}
return $data;
}
/**
* Auto-creates thresholds for a device using its device template's
* associated threshold templates. Registered as the
* 'device_threshold_autocreate' Cacti hook.
*
* @param int $host_id The device id to auto-create thresholds for.
*
* @return int The unmodified $host_id, for hook chaining.
*/
function thold_device_autocreate($host_id) {
autocreate($host_id);
return $host_id;
}
/**
* Auto-creates a threshold for a newly created graph using its host's
* associated threshold templates, when the global thold_autocreate
* setting is enabled. Registered as the
* 'create_complete_graph_from_template' Cacti hook.
*
* @param array $save The newly created graph's saved field data,
* including its 'id'.
*
* @return array The unmodified $save array, for hook chaining.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include.
*/
function thold_create_graph_thold($save) {
global $config;
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
if (read_config_option('thold_autocreate') == 'on') {
$graph = db_fetch_row_prepared('SELECT *
FROM graph_local
WHERE id = ?',
[$save['id']]);
if (cacti_sizeof($graph)) {
autocreate($graph['host_id'], [$graph['id']]);
}
}
return $save;
}
/**
* Deletes any thresholds associated with data sources that are being
* removed, logging each removal for audit purposes. Registered as the
* 'data_source_remove' Cacti hook.
*
* @param array $data_ids The local_data_id values of the data sources
* being removed.
*
* @return array The unmodified $data_ids array, for hook chaining.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include.
*/
function thold_data_source_remove($data_ids) {
global $config;
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
$tholds = array_rekey(
db_fetch_assoc('SELECT id
FROM thold_data
WHERE local_data_id IN (' . implode(', ', $data_ids) . ')'),
'id', 'id'
);
if (cacti_sizeof($tholds)) {
foreach ($tholds as $thold) {
plugin_thold_log_changes($thold, 'deleted', ['message' => 'Deleted due to Data Source removal']);
thold_api_thold_remove($thold);
}
}
return $data_ids;
}
/**
* Registers a "TH[id, id, ...]" pattern with Cacti's log viewer (clog)
* regex hyperlinking system, so that threshold id references embedded
* in log messages are rendered as links via
* thold_clog_regex_threshold(). Registered as the 'clog_regex_array'
* Cacti hook.
*
* @param array $regex_array The existing list of clog hyperlink regex
* definitions.
*
* @return array The regex definitions list with this plugin's pattern
* added.
*/
function thold_clog_regex_array($regex_array) {
$regex_array[] = ['name' => 'TH', 'regex' => '( TH\[)([, \d]+)(\])', 'func' => 'thold_clog_regex_threshold'];
return $regex_array;
}
/**
* Replaces a "TH[id, id, ...]" log-message match with linked threshold
* descriptions (falling back to the raw threshold id when no matching
* threshold is found), used as the clog regex callback registered by
* thold_clog_regex_array().
*
* @param array $matches The regex match groups: [0] the full match,
* [1] the opening " TH[" text, [2] the
* comma-separated threshold ids, [3] the closing
* "]" text.
*
* @return string The rendered HTML with each threshold id replaced by a
* link to its edit page.
*
* @global array $config Cacti global configuration array; used to
* locate the library file to include and build
* edit link URLs.
*/
function thold_clog_regex_threshold($matches) {
global $config;
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
$result = $matches[0];
$threshold_ids = explode(',', str_replace(' ', '', $matches[2]));
if (cacti_sizeof($threshold_ids)) {
$result = '';
$thresholds = db_fetch_assoc('SELECT id, name, name_cache, local_data_id
FROM thold_data
WHERE id IN (' . implode(',',$threshold_ids) . ')');
$thresholdDescriptions = [];
if (cacti_sizeof($thresholds)) {
foreach ($thresholds as $threshold) {
$thresholdDescriptions[$threshold['id']] = html_escape(
!empty($threshold['name_cache']) ?
$threshold['name_cache'] :
thold_substitute_data_source_description($threshold['name'], $threshold['local_data_id'])
);
}
}
foreach ($threshold_ids as $threshold_id) {
$result .= $matches[1] . '<a href=\'' . $config['url_path'] .
'plugins/thold/thold.php?action=edit&id=' . $threshold_id . '\'>' .
html_escape(isset($thresholdDescriptions[$threshold_id]) ? $thresholdDescriptions[$threshold_id] : $threshold_id) .
'</a>' . $matches[3];
}
}
return $result;
}
/**
* Emits the JavaScript that initializes the multiselect widget for the
* "Pause Notifications" settings field on the Console settings page.
* Registered as the 'settings_bottom' Cacti hook.
*
* @return void
*/
function thold_settings_bottom() {
// This hook can fire on core pages that have not loaded includes/functions.php, where plugin_thold_csp_nonce() lives.
require_once(__DIR__ . '/includes/functions.php');
?>
<script type='text/javascript' <?php print plugin_thold_csp_nonce(); ?>>
$(function() {
$('#alert_notification_pause').multiselect();
});
</script>
<?php
}
/**
* Removes files and directories that a previous version of this plugin
* shipped but that have since moved or been deleted, using the tombstone
* and whitelist lists in manifest.json. Whitelisted (user-data) paths and
* any VCS metadata (.git*) are never touched; the dev-only tests/ tree is
* removed. Any path that resolves outside the plugin directory (a tampered
* manifest.json) is refused, and any file/directory that cannot be removed
* (e.g. read-only) is reported to the Cacti log. Any top-level entry that is
* neither expected nor a tombstone nor whitelisted is logged to the Cacti
* log and left in place. Called on a plugin version change.
*
* @return void
*
* @global array $config Cacti global configuration array; used to resolve
* the plugin directory.
*/
function thold_prune_files(): void {
global $config;
$plugin_dir = $config['base_path'] . '/plugins/thold';
$manifest_path = $plugin_dir . '/manifest.json';
if (!is_readable($manifest_path)) {
return;
}
$manifest = json_decode((string) file_get_contents($manifest_path), true);
if (!is_array($manifest)) {
cacti_log('WARNING: thold manifest.json could not be parsed; skipping file prune', false, 'THOLD');
return;
}
$tombstones = isset($manifest['tombstones']) && is_array($manifest['tombstones']) ? $manifest['tombstones'] : [];
$expected = isset($manifest['expected']) && is_array($manifest['expected']) ? $manifest['expected'] : [];
$whitelist = isset($manifest['whitelist']) && is_array($manifest['whitelist']) ? $manifest['whitelist'] : [];
$protected = function (string $rel) use ($whitelist): bool {
if (strncmp($rel, '.git', 4) === 0 || strncmp($rel, '.md', 3) === 0) {
return true;
}
foreach ($whitelist as $entry) {
$entry = trim((string) $entry, '/');
if ($entry !== '' && ($rel === $entry
|| strncmp($rel, $entry . '/', strlen($entry) + 1) === 0
|| strncmp($entry, $rel . '/', strlen($rel) + 1) === 0)) {
return true;
}
}
return false;
};
// Security: resolve the plugin directory so a tampered manifest.json
// cannot steer the prune outside of it.
$plugin_real = realpath($plugin_dir);
// Remove tombstoned (moved/deleted) paths plus the dev-only tests/
// tree and the phpunit.xml test configuration.
$remove = $tombstones;
$remove[] = 'tests/';
$remove[] = 'phpunit.xml';
foreach ($remove as $rel) {
$rel = trim((string) $rel, '/');
if ($rel === '' || $protected($rel)) {
continue;
}
// A tombstone must never contain '.'/'..' segments; a tampered manifest
// could use them to escape the plugin directory or target its root.
$segments = explode('/', $rel);
if (in_array('.', $segments, true) || in_array('..', $segments, true)) {
cacti_log(sprintf('WARNING: thold prune refused to remove %s: path contains a traversal segment (tampered manifest.json?)', $rel), false, 'THOLD');
continue;
}
$path = $plugin_dir . '/' . $rel;
if (!is_link($path) && !file_exists($path)) {
continue;
}
// Refuse any path that, after resolving symlinks and ../ segments,
// escapes the plugin directory (protects user data from a tampered
// manifest.json).
$anchor = is_link($path) ? dirname($path) : $path;
$real = realpath($anchor);
if ($real === false || ($real !== $plugin_real && strncmp($real, $plugin_real . DIRECTORY_SEPARATOR, strlen((string) $plugin_real) + 1) !== 0)) {
cacti_log(sprintf('WARNING: thold prune refused to remove %s: path resolves outside the plugin directory (tampered manifest.json?)', $rel), false, 'THOLD');
continue;
}
if (is_dir($path) && !is_link($path)) {
$removed = thold_rmtree($path);
} else {
$removed = @unlink($path);
}
if (!$removed) {
cacti_log(sprintf('WARNING: thold upgrade could not remove %s (check file/directory permissions)', $rel), false, 'THOLD');
}
}
// Surface any top-level entry the manifest does not account for.
$known = [];
foreach (array_merge($expected, $tombstones) as $entry) {
$top = explode('/', trim((string) $entry, '/'))[0];
if ($top !== '') {
$known[$top] = true;
}
}
$entries = scandir($plugin_dir);
foreach (($entries !== false ? $entries : []) as $entry) {
if ($entry === '.' || $entry === '..' || $entry === 'tests' || $entry === 'phpunit.xml' || $protected($entry) || isset($known[$entry])) {
continue;
}
cacti_log(sprintf('WARNING: thold upgrade found a file/directory not described in manifest.json: %s (left in place)', $entry), false, 'THOLD');
}
}
/**
* Recursively deletes a directory and its contents. Symlinks are removed
* without being followed. Helper for thold_prune_files().
*
* @param string $dir Absolute path to the directory to remove.
*
* @return bool True if the directory and everything under it was removed;
* false if any entry could not be deleted.
*/
function thold_rmtree(string $dir): bool {
$entries = scandir($dir);
$ok = true;
foreach (($entries !== false ? $entries : []) as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$path = $dir . '/' . $entry;
if (is_dir($path) && !is_link($path)) {
if (!thold_rmtree($path)) {
$ok = false;
}
} elseif (!@unlink($path)) {
$ok = false;
}
}
if (!@rmdir($dir)) {
$ok = false;
}
return $ok;
}