Files
cacti/lib/html.php
T
TheWitnessandTheWitness 2a44c7a791 feature: modern filter bar polish (Rows/Refresh on bar, dialog button labels, separators) (#8191)
* fix: label modern dialog apply/clear with the page button text; show Rows on the bar

The dialog apply button was hardcoded 'Search', which mismatched the page's submit label (e.g. 'Go') and collided with pages that have a filter field named Search (clog). It now uses the configured go/clear button display text. Also route the 'rows' field to the modern bar alongside timespan/refresh so the per-page Rows selector stays visible.

* feature: always label the row-count selector 'Rows'

emit_field() forces the 'rows' field friendly_name to Rows regardless of page-specific naming (Devices, Trees, ...), in both legacy and modern filters. Refresh already renders on the modern bar via field_is_bar.

* feature: order modern bar selectors (Rows, Refresh) and separate them from the buttons

Rows renders first and Refresh second on the modern filter bar, and a vertical separator now divides the selectors (Layouts, Rows, Refresh) from the layout action buttons.

* changelog: 8191.feature fragment for modern filter bar polish

* jQuery 4: replace removed $.isFunction in bundled plugins

jQuery 4.0 removed $.isFunction. contextMenu, colorpicker and timepicker are hand-vendored orphans fixed in place; tablesorter is npm-managed, so it is also rewritten at sync time in build/sync-js.mjs so a rebuild reproduces the fix (no-ops once the upstream fix is released and the pin is bumped).

* Render the Thumbnails and Business Hours graph filter checkboxes as Cacti toggles

* fix: address review — update modern JS docblock and strengthen bar/label tests

Rewrite the create_modern_javascript docblock to describe the page-configured apply/clear labels (and document the change/clear callback params). Extend FilterLayoutsTest to assert the selector|button separator, the applied Rows-then-Refresh ordering, and both go/clear label lookups, and that the hardcoded Search dialog label is gone.

* Filter bar polish: toggle-style filter checkboxes and flicker-free layout buttons

Render the shared filter_checkbox field as a Cacti toggle switch (.checkboxSwitch/.checkboxSlider) so every page's filter checkboxes match the rest of the UI, and start the layout Rename/Delete/Save As buttons hidden so layoutUpdateButtons() reveals them only when applicable instead of flashing visible on load.

* Modern filter: move the timespan selector onto its own second filter line

Timespan controls were rendered inline on the single modern filter bar. Route method=timespan fields into a separate group and emit them as a second filterTable row so the bar isn't overcrowded, matching the legacy multi-row layout. Applies to every modern filter with a timespan (the graph view pages).

* Modern filter: keep toggle checkboxes on the bar with a visible label and tooltip

filter_checkbox fields now count as bar fields so the Thumbnails/Business Hours toggles sit on the single bar instead of the Edit dialog. Their label used .checkboxLabel (display:none by design for forms), which hid the text; switch to .checkboxLabelWanted and always set a title tooltip from the field's title/friendly_name so each toggle is self-describing.

* Modern filter: toggles back in the dialog, restrict auto-apply to bar fields, drop dialog Clear

- Move filter_checkbox toggles back into the Edit dialog (field_is_bar no longer claims them) and render them as a form row: friendly name in the label column, toggle in the value column.
- In modern mode only bar selectors (Rows, Refresh) and the Layout selector auto-trigger a filter change; dialog field changes/clicks no longer call applyFilter (gate changeChain/clickChain on !use_modern_filter() || field_is_bar). Legacy keeps auto-applying on change (text inputs still excluded).
- Remove the Clear button from the modern Edit dialog and the now-unused clearLabel; update the dialog-labels test accordingly.

* Fix filter toggle vertical alignment across themes

The generic .filterTable .filterColumn label { vertical-align: -10% } nudge (meant for text labels) out-specified .checkboxSwitch and dragged the toggle switch down, which also pulled the adjacent label baseline low in the Edit dialog and misaligned the toggle on the legacy filter bar. Add a higher-specificity label.checkboxSwitch { vertical-align: middle } in every theme carrying the rule (midwinter has no such rule).

* Hide the filter search glyph in the modern Edit dialog

Theme main.js appends a ti-search glyph after #rfilter, positioned for a table cell; in the modern Edit dialog #rfilter sits in a wide div (no td), so the glyph wraps below the field. Skip the injection when #rfilter is inside .cactiFilterEditTable (modern dialog); legacy, which has no such ancestor, keeps the glyph.

* jQuery 4: full removed-API sweep of bundled plugins

Replace every jQuery-4-removed helper across the vendored plugins: \$.trim -> String.prototype.trim, \$.isArray -> Array.isArray, \$.proxy -> Function.prototype.bind, \$.parseJSON -> JSON.parse, \$.type -> typeof/\$.isPlainObject, \$.isWindow -> native window check (plus the earlier \$.isFunction). Orphans (colorpicker, timepicker, multiselect, multiselect.filter, tableDnD) are fixed in place; the npm-managed tablesorter core+widgets are rewritten at sync time in build/sync-js.mjs so a rebuild reproduces the fix.

* Modern filter dialog: non-modal, Apply gated on save, Save keeps dialog open

- Rename the dialog apply action to Apply and disable it until the (new or changed) filter is saved (layoutDirty tracking); Close just closes without applying.
- Save no longer reloads the page: it reflects the saved layout in the selector and re-enables Apply, keeping the dialog open.
- Dialogs are now modal: false.
- Switch the layout selector change from a full document.location reload to an AJAX loadUrl().
- Vertically center the modern bar cells so the barSep separator lines up between the buttons.

* Modern filter: add layout to the filter's session variable structure

Register layout (FILTER_VALIDATE_INT, default 0) in sanitize_filter_variables() alongside page so the selected layout is validated and stored in the filter's session object and persists across page views.

* Modern filter: Graph Template amalgam on line 2, timespan on line 3

Tag the Graph Template + Source/Order/CF/Measure group ('bar' => true on graph_template_id and the html_graph_order_filter_array() metrics) so create_modern_filter renders it as its own second filter line on Graph View and Tree View, pushing the timespan to the third line. These bar fields also auto-trigger the filter so the Template->Source->Order callback chain works on the main bar.

* Fix graph_template_id multi-select: allow comma lists, type get_host_graph_list correctly

get_host_graph_list() immediately transforms graph_template_id via html_transform_graph_template_ids() (handles cg_/dq_ prefixes and comma lists), so its parameter is a string, not an int -- correct the type hint and docblock. Also widen the preview and listview graph_template_id validation regexp to accept a comma-separated list like 'cg_118,cg_106,cg_119', matching the tree filter.

* Modern filter: hide the whole cell for hidden layout buttons to even out bar separator spacing

The Rename/Delete/Save As buttons hid only the <button>, leaving their table-cell (with padding) in the bar, so the separator after them was pushed away from Edit. Put the display:none on the .filterColumnButton cell and toggle the cell in layoutUpdateButtons so hidden buttons take no space and the separators sit evenly between the visible buttons.

* Remove legacy .drop-callback/makeCallbacks() autocomplete widget

The Device dropdown and other form_callback() lookups now always use the
ajax-backed select2 selectmenu. Drop the old makeCallbacks() jQuery-UI
autocomplete entirely and have form_callback() render the select2-callback
<select> by default, keeping the plain full-list <select> only when the
'autocomplete' config option is enabled.

* Carry selected layout through the modern dialog Apply action

The layout_save callback already returns the new layout id (and url), and
layoutDialogSave() selects it in #filter_layout. The dialog Apply button,
however, ran the plain filter apply (applyFilter()) which rebuilds the URL
from the field values and omits filter_layout, so the page reloaded as
(New Filter). Apply now navigates to the selected layout's data-url (which
includes filter_layout=<id>) when a layout is selected, falling back to the
normal filter apply when none is.

* Fix Filter Layouts manage page table and use jQuery UI dialogs for delete/rename

The management table was opened with html_start_box(\ = true), emitting a
<div> instead of a <table>, so the header and rows rendered unstyled and
run-together. Switch it to table mode.

Replace the generic confirm()-based delete with a CDEF-style jQuery UI
confirmation dialog loaded over ajax, and add a Rename action that opens a
matching jQuery UI dialog with a name field. Both post to the existing
layout_delete/layout_rename handlers on confirm.

* Use jQuery UI dialogs for filter-bar layout rename/delete; drop rename from manage page

The filter bar (modern and legacy) was still using native prompt()/confirm()
for the layout Rename and Delete actions - convert both to jQuery UI dialogs
(a name-entry dialog for rename, a confirm dialog for delete).

On the Filter Layouts management page, remove the rename action entirely and
instead link the layout name to its page (with filter_layout set) as an edit
link; keep the Delete confirmation dialog and Publish action.

* Rework Layouts manage page: preset filter bar + actions dropdown; rename to Layouts

Model layouts.php on the preset pages (CDEFs, etc.): add the standard
CactiTableFilter search bar in the legacy look, a sortable list with a
checkbox column, a pager, and a draw_actions_dropdown with Delete, Make
Global (publish), and Make Local (reassign to the current user). The
unimpressive per-row action icons and ajax dialogs are replaced by the
checkbox/confirmation flow. Make Global/Make Local are admin-only.

Treat layouts.php as a filter_layouts preset page so its own filter bar
does not offer the layout dropdown. Rename the page title and Console
Presets menu entry from 'Filters'/'Filter Layouts' to 'Layouts'.

* Show friendly page name in the Layouts list Page column

Map the stored page basename to the Console navigation title (e.g. host.php
=> Devices) via a layouts_page_name() lookup against \, falling
back to the raw basename. The Page column is now nosort since the displayed
label no longer matches the sortable database value.

* Fix filter search TypeError: use filter_options key in default search field

sanitize_filter_variables() reads a field's callback options from the
'filter_options' key, but the default search field in create_default() stored
them under 'options', so the key never reached validate_store_request_vars().
That left FILTER_CALLBACK with no callback and raised 'filter_var(): Option
must be a valid callback' whenever a table filter was searched.

* Address PR scan findings: tablesorter useless conditional + layouts SQL

- jquery.tablesorter.js: drop the redundant 'str &&' guard inside the
  already-truthy 'str ?' branch of the digit and currency parsers (CodeQL
  useless-conditional). Add a matching sync-js transform rule so a rebuild
  from upstream keeps the simplification.
- layouts.php: fully parameterize the management list queries with bound
  '?' parameters for the search term and user id, and build the LIMIT from
  explicit (int) page/rows, so no request variable reaches the SQL string
  (Semgrep cacti-request-var-in-sql).

* Address PR review: colorpicker trim, tablesorter JSON guard, bar separator, i18n

- jquery.colorpicker.js: _setAltField() iterated index <= properties.length, so
  the last pass read properties[undefined]; \$.trim(undefined) was '' but native
  .trim() throws. Iterate with < so no TypeError.
- jquery.tablesorter.widgets.js: the retrieveValue feature-check still tested the
  removed \$.parseJSON; under jQuery 4 it was always falsy so saved widget state
  never loaded. Test native window.JSON.parse instead, and add a matching
  build/sync-js.mjs transform rule so a rebuild keeps the guard.
- html_filter.php: the Layouts selector is always rendered, so emit the
  selector-to-button separator unconditionally (graph preview routes fields to
  the amalgam/timespan lines, leaving the bar line without it before).
- changelog/i18n: reword 8191.feature to describe the Apply (save-gated) dialog
  action, and add the new layouts.php/html_filter.php msgids to cacti.pot.

* Add tablesorter transform coverage to the sync-js test

Feed a fixture containing every removed jQuery 4 utility (\$.isFunction,
\$.trim, \$.type ===string/object, \$.isWindow, \$.isArray, \$.parseJSON calls
and the bare if (\$.parseJSON) guard) plus the CodeQL str && simplification
through syncAssets, and assert the synced tablesorter output contains the
native equivalents and none of the removed APIs. Previously the transform ran
against a generic stub, so an incomplete rewrite could pass unnoticed.

* Apply php-cs-fixer: comment style, alignment, and single-quote strings

Run through the project php-cs-fixer config (the mandatory pre-PR check):
line comments for the input-validation banners, aligned assignment, and
single-quoted strings where no interpolation is used. No behaviour change.

* Render the filter Go action as a refresh glyph button (modern + legacy)

Replace the 'Go' text submit with a compact 'ti ti-refresh' glyph button
titled 'Refresh page' for every page filter:
- emit_field() forces the glyph/title on any 'go' submit, so both the shared
  default and pages that define their own 'go' get it on the legacy filter.
- The modern bar (which has no form) gets a JS-wired #layout_refresh glyph
  button right of Edit that loadUrl()s the selected layout url (carrying
  filter_layout) or the page change callback.
Add the 'Refresh page' msgid to cacti.pot and note it in 8191.feature.

* Add a Tabler .icon-rotate spin class and use it for refreshing icons

Tabler ships only the icon webfont, with no fa-spin equivalent, so spinning
reload icons stopped animating (e.g. the Troubleshooting > Data Sources
auto-refresh indicator). Define .icon-rotate in every theme (reusing the
globally loaded fa-spin keyframes) and apply it:
- the modern filter refresh button spins while it loads,
- the realtime graph drill-down loader switches from fa-spin to icon-rotate,
- the data_debug auto-refresh indicators spin again.

* tests: sync FilterLayouts assertions with the reworded separator comment and dw.addClass

The source scan expected the old 'Separate the selectors' comment and a chained
.dialog('widget').addClass() form; update to the current reworded comment and
the 'var dw' + dw.addClass('cactiFilterDialog') the dialog now uses.

* Glyph-ify the timespan Refresh/Clear and the generic Clear action

Replace the timespan 'Refresh' text button with a spinning ti ti-refresh glyph
and 'Clear' with a ti ti-eraser glyph (also fixing a stray double </span>), and
make the generic filter Clear a ti ti-eraser glyph button across legacy and
modern via emit_field (button case now honours a 'glyph' key). The timespan
refresh icon spins (icon-rotate) while it reloads.

* Glyph-ify filter Import/Export, layout, and modern-bar action buttons

Force ti-table-import / ti-table-export glyphs for any filter button named import/export (centralized in emit_field like go/clear), so page-defined Import/Export buttons and the modern bar's always-visible action strip render consistent Tabler glyphs. Add a glyph parameter to layout_button() and glyph the Edit/Rename/Delete/Save As/Save/New/Publish buttons across the modern bar and legacy layouts row.

* Use table-import/table-export glyphs for cactiTableButton Import/Export icons

The filter header Export/Import icons injected into cactiTableButton used generic chevron-down/chevron-up glyphs; switch them to ti ti-table-export / ti ti-table-import to match the rest of the glyph theme.

* Glyph-ify the Save Filter Defaults button on the filter bar

The has_save 'Save' action rendered as a text button on the modern bar (and legacy filter); force a ti ti-device-floppy glyph via emit_field like go/clear/import/export so it matches the rest of the glyph theme.

* Regenerate tree-view graphs when applying a saved filter layout

The modern layout selector loaded a layout's saved data-url (action=tree) directly, so on the Graph Tree view it pulled the tree shell instead of regenerating graphs. Route the layout-url loads (selector change, refresh button, and dialog Apply) through correctUrlParameters() so action=tree becomes action=tree_content and graphs reload into #main, matching applyGraphFilter(). No-op on non-tree pages.

* Keep timespan controls working after ajax reloads and tip custom date fields

On the Graph Preview page the predefined_timespan change and tsrefresh/tsclear/shift click handlers were bound directly inside the filter's inline ready script, so an ajax content reload that replaced the timespan controls dropped the bindings (a full page refresh restored them). Bind them with namespaced event delegation on document so they survive content replacement and never double-bind. Add tooltips to the custom start/end date inputs telling the user to click Refresh to apply the range, plus POT entries and a changelog note.

* Record layouts.php static redirect in the sink inventory baseline

The sink and architectural hotspot guard flagged inventory drift: layouts.php's static header('Location: layouts.php') redirect (no request input) was never added to the tracked baseline. Refresh the baseline via tests/security/build_sink_inventory.sh to record it.

* Fix layout Save-As dialog state and use cacti_redirect in layouts.php

Address review findings: (1) use cacti_redirect('layouts.php') instead of a raw header()+exit so the standard redirect checks run (and drop the now-unneeded sink-inventory entry); (2) reset the dialog's force-new flag after the first successful save so later saves update the created layout instead of creating duplicates; (3) preserve an existing dirty state when reopening the Edit dialog so pending edits are not silently discarded by Apply.

* Drop stale layout msgids from the translation template

Remove the Filter Layouts, Published to all users, Publish to all users, and No Filter Layouts Found msgids that this PR replaced (now Layouts, Make Global, Make Local, No Layouts Found); all four are absent from PHP. Keeps the surgically-maintained POT in sync with the shipped strings.

---------

Co-authored-by: TheWitness <TheWitness@users.noreply.github.com>
2026-10-06 11:35:13 -04:00

3672 lines
125 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/ |
+-------------------------------------------------------------------------+
*/
/**
* html_start_box - draws the start of an HTML box with an optional title
*
* @param string $title The title of this box ("" for no title)
* @param string $width The width of the box in pixels or percent
* @param bool $div End with a starting div
* @param int $cell_padding The amount of cell padding to use inside of the box
* @param string $align The HTML alignment to use for the box (center, left, or right)
* @param mixed $url_or_buttons The url to use when the user clicks 'Add'
* in the upper-right corner of the box ("" for no 'Add' link)
* This function has two method. This first is for legacy behavior where you
* you pass in a href to the function, and an optional label as $add_label
* The new format accepts an array of hrefs to add to the start box. The format
* of the array is as follows:
*
* $url_or_buttons = array(
* array(
* 'id' => 'uniqueid',
* 'href' => 'value',
* 'title' => 'title',
* 'callback' => true|false,
* 'class' => 'fa fa-icon'
* ),
* ...
* );
*
* If the callback is true, the Cacti attribute will be added to the href
* to present only the contents and not to include both the headers. If
* the link must go off page, simply make sure $callback is false. There
* is a requirement to use fontawesome icon sets for this class, but it
* can include other classes. In addition, the href can be a hash '#' if
* your page has a ready function that has it's own javascript.
*
* @param mixed $add_label Used with legacy behavior to add specific text to the link.
* This parameter is only used in the legacy behavior.
* @param bool $showcols Show the column selector icon
*
* @return void
*/
function html_start_box(string $title, string $width, bool $div, int $cell_padding, string $align,
mixed $url_or_buttons, mixed $add_label = false, bool $showcols = false) : void {
static $table_suffix = 1;
static $help_count = 0;
static $mode_count = 0;
static $beta_count = 0;
if ($add_label === false) {
$add_label = __('Add');
}
if (!is_cacti_release() && $title != '' && $beta_count == 0) {
$title .= ' [ ' . CACTI_VERSION_BRIEF_FULL . ' ]';
$beta_count = 1;
}
if (POLLER_ID > 1 && $title != '' && $mode_count == 0) { // @phpstan-ignore-line
$title .= ' [ ' . __('Remote Server') . ': ';
if (CACTI_CONNECTION == 'offline') {
$title .= '<span class="deviceDown">' . __('Offline') . '</span>';
} elseif (CACTI_CONNECTION == 'recovery') {
$title .= '<span class="deviceRecovering">' . __('Recovering') . '</span>';
} else {
$title .= __('Online');
}
$title .= ' ]';
$mode_count = 1;
}
$table_prefix = basename(get_current_page(), '.php');
if (!ierv('action')) {
$table_prefix .= '_' . clean_up_name(gnrv('action'));
} elseif (!ierv('report')) {
$table_prefix .= '_' . clean_up_name(gnrv('report'));
} elseif (!ierv('tab')) {
$table_prefix .= '_' . clean_up_name(gnrv('tab'));
}
$table_id = $table_prefix . $table_suffix;
if ($title != '') {
print "<div id='$table_id' class='cactiTable' style='width:$width;text-align:$align;'>";
print '<div class="cactiTableTitleRow">';
print "<div class='cactiTableTitle'>" . ($title != '' ? '<span>' . $title . '</span>' : '') . '</div>';
print "<div class='cactiTableButton'>";
$page = get_current_page();
$help_file = html_help_page($page);
if ($help_file === false) {
if (isrv('tab')) {
$tpage = $page . ':' . gnrv('tab');
$help_file = html_help_page($tpage);
}
}
if ($help_file === false) {
if (isrv('action')) {
$tpage = $page . ':' . gnrv('action');
$help_file = html_help_page($tpage);
}
}
if ($help_file !== false && $help_count == 0 && is_realm_allowed(28)) {
print "<span class='cactiHelp' title='" . __esc('Get Page Help') . "'><a class='linkOverDark helpPage' data-page='" . htmle(basename($help_file)) . "' href='#'><i class='ti ti-help actionHelp'></i></a></span>";
$help_count = 1;
}
if ($showcols) {
print "<span class='cactiFilterColumns' title='" . __esc('Select Columns for Display') . "'><a id='showColumns' href='#'><i class='ti ti-table threeBars'></i></a></span>";
}
if ($url_or_buttons != '' && !is_array($url_or_buttons)) {
print "<span class='cactiFilterAdd' title='$add_label'><a class='linkOverDark' href='" . htmle($url_or_buttons) . "'><i class='ti ti-plus plusAdd'></i></a></span>";
} else {
if (is_array($url_or_buttons)) {
if (cacti_sizeof($url_or_buttons)) {
foreach ($url_or_buttons as $icon) {
if (isset($icon['callback']) && $icon['callback'] === true) {
$classo = 'pic';
} else {
$classo = '';
}
if (isset($icon['class']) && $icon['class'] !== '') {
$classi = $icon['class'];
} else {
$classi = 'ti ti-plus plusAdd';
}
if (isset($icon['href'])) {
$href = htmle($icon['href']);
} else {
$href = '#';
}
if (isset($icon['data_url'])) {
$href = '#';
$data_url = " data-url='" . html_escape_url($icon['data_url']) . "'";
$classo .= ' linkOverDark cactiPostAction';
} else {
$data_url = '';
}
if (isset($icon['title'])) {
$title = $icon['title'];
} else {
$title = $add_label;
}
print "<span class='cactiFilterAdd' title='$title'><a" . (isset($icon['id']) ? " id='" . $icon['id'] . "'" : '') . " class='" . trim($classo) . "' href='$href'$data_url><i class='$classi'></i></a></span>";
}
}
} else {
print '';
}
}
print '</div><div class="cactiTableAction"></div></div>';
if ($div === true) {
print "<div id='$table_id" . "_child' class='cactiTable'>";
} else {
print "<table id='$table_id" . "_child' class='cactiTable' style='padding:" . $cell_padding . "px;'>";
}
} else {
print "<div id='$table_id' class='cactiTable' style='width:$width;text-align:$align;'>";
if ($div === true) {
print "<div id='$table_id" . "_child' class='cactiTable'>";
} else {
print "<table id='$table_id" . "_child' class='cactiTable' style='padding:" . $cell_padding . "px;'>";
}
}
$table_suffix++;
}
/**
* Wrapper function for the html_start_box to control filters which presently are displayed
* as tables in Cacti. This function will show the three bar show/hide column setting
*
* @param string $title
* @param mixed $url_or_buttons
* @param bool $div
* @param bool $showcols
* @param mixed $add_label
* @param string $width
*
* @return void
*/
function html_filter_start_box(string $title, mixed $url_or_buttons = '', bool $div = false, bool $showcols = true, mixed $add_label = false, string $width = '100%') : void {
html_start_box($title, $width, $div, 3, 'center', $url_or_buttons, $add_label, $showcols);
}
/**
* html_sub_tabs - Creates a memory persistent sub-tab interface
* for a page or pages using a simple method to lay those tabs
* out.
*
* @param array $tabs An associative array of tab variables and names
* Alternatively an array of names that can be converted
* using the cacti_strtoupper() function to titles.
* @param string $uri A string of URL parameters like 'action=edit&id=x'
* @param string $session_var An option session variable to use to store
* the current tab status. Defaults to the page
* name and the suffix of current_tab
*
* @return void Output is printed to standard output
*/
function html_sub_tabs(array $tabs, string $uri = '', string $session_var = '') : void {
// determine the session variables if not set
if ($session_var == '') {
$session_var = basename(get_current_page(), '.php') . '_current_tab';
}
$page_name = get_current_page() . '?' . $uri . ($uri != '' ? '&' : '');
// set the default settings category
if (!isrv('tab')) {
// there is no selected tab; select the first one
if (isset($_SESSION[$session_var])) {
$current_tab = $_SESSION[$session_var];
} else {
$current_tab = array_keys($tabs)[0];
}
} else {
$current_tab = grv('tab');
}
// Check to see if the tab exists, and if not, use the default
if (!isset($tabs[$current_tab])) {
$current_tab = array_keys($tabs)[0];
}
$_SESSION[$session_var] = $current_tab;
srv('tab', $current_tab);
// draw the categories tabs on the top of the page
print '<div>';
print "<div class='tabs' style='float:left;'>
<nav>
<ul role='tablist'>";
if (cacti_sizeof($tabs)) {
foreach ($tabs as $id => $name) {
if (is_numeric($id)) {
print "<li class='subTab'>
<a class='pic" . ($name == $current_tab ? ' selected' : '') . "'
href='" . htmle($page_name . 'tab=' . $name) . "'>" . cacti_strtoupper($name) . '</a>
</li>';
} else {
print "<li class='subTab'>
<a class='pic" . ($id == $current_tab ? ' selected' : '') . "'
href='" . htmle($page_name . 'tab=' . $id) . "'>" . $name . '</a>
</li>';
}
}
}
print '</ul></nav></div>';
print '</div>';
}
/**
* html_end_box - draws the end of an HTML box
*
* @param bool $trailing_br Whether to draw a trailing <br> tag after ending
* @param bool $div Whether type of box is div or table
*
* @return void
*/
function html_end_box(bool $trailing_br = true, bool $div = false) : void {
if ($div) {
print '</div></div>';
} else {
print '</table></div>';
}
if ($trailing_br == true) {
print "<div class='break'></div>";
}
}
function html_filter_end_box() : void {
html_end_box(true, true);
}
/**
* html_graph_area - draws an area the contains full sized graphs
*
* @param array $graph_array The array to contains graph information. for each graph in the
* array, the following two keys must exist
* $arr[0]["local_graph_id"] // graph id
* $arr[0]["title_cache"] // graph title
* @param string $no_graphs_message Display this message if no graphs are found in $graph_array
* @param string $extra_url_args Extra arguments to append to the url
* @param string $header Html to use as a header
* @param int $columns The number of columns to present
* @param int $tree_id The tree id if this is a tree thumbnail
* @param int $branch_id The branch id if this is a tree thumbnail
*
* @return void
*/
function html_graph_area(array &$graph_array, string $no_graphs_message = '', string $extra_url_args = '',
string $header = '', int $columns = 0, int $tree_id = 0, int $branch_id = 0) : void {
$i = 0;
$k = 0;
$j = 0;
$num_graphs = cacti_sizeof($graph_array);
if ($columns == 0) {
$columns = read_user_setting('num_columns');
}
?>
<script type='text/javascript' <?php print CactiSecureHeaders::getNonceAttribute(); ?>>
if ($('#predefined_timespan').val() == 0) {
refreshMSeconds = 999999;
} else {
refreshMSeconds = <?php print read_user_setting('page_refresh') * 1000; ?>;
}
refreshPage = '<?php print get_current_page(); ?>?action=preview';
refreshFunction = 'refreshGraphs()';
var graph_start = <?php print get_current_graph_start(); ?>;
var graph_end = <?php print get_current_graph_end(); ?>;
</script>
<?php
print '<div class=\'graphPage\'>';
if ($num_graphs > 0) {
if ($header != '') {
print $header;
}
foreach ($graph_array as $graph) {
if (!isset($graph['host_id'])) {
[$graph['host_id'], $graph['disabled']] = db_fetch_row_prepared('SELECT host_id, disabled
FROM graph_local AS gl
LEFT JOIN host AS h
ON gl.host_id = h.id
WHERE gl.id = ?',
[$graph['local_graph_id']]);
}
?>
<div class='graphWrapperOuter cols<?php print $columns; ?>' data-disabled='<?php print($graph['disabled'] == 'on' ? 'true' : 'false'); ?>'>
<div>
<div class='graphWrapper' id='wrapper_<?php print $graph['local_graph_id']?>' graph_width='<?php print $graph['width']; ?>' graph_height='<?php print $graph['height']; ?>' title_font_size='<?php print((read_user_setting('custom_fonts') == 'on') ? read_user_setting('title_size') : read_config_option('title_size')); ?>'></div>
<?php if (is_realm_allowed(27)) { ?>
<div id='dd<?php print $graph['local_graph_id']; ?>' class='noprint graphDrillDown'>
<?php graph_drilldown_icons($graph['local_graph_id'], 'graph_buttons', $tree_id, $branch_id); ?>
</div>
<?php } ?>
</div>
<?php print(read_user_setting('show_graph_title') == 'on' ? '<div>' . htmle($graph['title_cache']) . '</div>' : ''); ?>
</div>
<?php
}
} else {
if ($no_graphs_message != '') {
print "<div class='tableHeaderGraph'><span><em>$no_graphs_message</em></span></div>";
}
}
print '</div>';
}
/**
* html_graph_thumbnail_area - draws an area the contains thumbnail sized graphs
*
* @param array $graph_array The array to contains graph information. for each graph in the
* array, the following two keys must exist
* $arr[0]["local_graph_id"] // graph id
* $arr[0]["title_cache"] // graph title
* @param string $no_graphs_message Display this message if no graphs are found in $graph_array
* @param string $extra_url_args Extra arguments to append to the url
* @param string $header Html to use as a header
* @param int $columns The number of columns to present
* @param int $tree_id The tree id if this is a tree thumbnail
* @param int $branch_id The branch id if this is a tree thumbnail
*
* @return void
*/
function html_graph_thumbnail_area(array &$graph_array, string $no_graphs_message = '', string $extra_url_args = '', string $header = '',
int $columns = 0, int $tree_id = 0, int $branch_id = 0) : void {
$i = 0;
$k = 0;
$j = 0;
$num_graphs = cacti_sizeof($graph_array);
if ($columns == 0) {
$columns = read_user_setting('num_columns');
}
?>
<script type='text/javascript' <?php print CactiSecureHeaders::getNonceAttribute(); ?>>
if ($('#predefined_timespan').val() == 0) {
refreshMSeconds = 999999;
} else {
refreshMSeconds = <?php print read_user_setting('page_refresh') * 1000; ?>;
}
refreshPage = '<?php print get_current_page(); ?>?action=tree';
refreshFunction = 'refreshGraphs()';
var graph_start = <?php print get_current_graph_start(); ?>;
var graph_end = <?php print get_current_graph_end(); ?>;
</script>
<?php
print '<div class=\'graphPage\'>';
if ($num_graphs > 0) {
if ($header != '') {
print $header;
}
$start = true;
foreach ($graph_array as $graph) {
if (!isset($graph['host_id'])) {
$gdata = db_fetch_row_prepared('SELECT host_id, disabled
FROM graph_local AS gl
LEFT JOIN host AS h
ON gl.host_id = h.id
WHERE gl.id = ?',
[$graph['local_graph_id']]);
if (cacti_sizeof($gdata)) {
$graph['host_id'] = $gdata['host_id'];
$graph['disabled'] = $gdata['disabled'];
} else {
continue;
}
}
if (isset($graph['graph_template_name'])) {
if (isset($prev_graph_template_name)) {
if ($prev_graph_template_name != $graph['graph_template_name']) {
$prev_graph_template_name = $graph['graph_template_name'];
}
} else {
$prev_graph_template_name = $graph['graph_template_name'];
}
} elseif (isset($graph['data_query_name'])) {
if (isset($prev_data_query_name)) {
if ($prev_data_query_name != $graph['data_query_name']) {
$print = true;
$prev_data_query_name = $graph['data_query_name'];
} else {
$print = false;
}
} else {
$print = true;
$prev_data_query_name = $graph['data_query_name'];
}
if ($print) {
print "<div class='tableHeaderGraph graphSubHeaderColumn textHeaderDark'><span>" . __('Data Query:') . ' ' . htmle($graph['data_query_name']) . '</span></div>';
}
}
?>
<div class='graphWrapperOuter cols<?php print $columns; ?>' data-disabled='<?php print($graph['disabled'] == 'on' ? 'true' : 'false'); ?>'>
<div>
<div class='graphWrapper' id='wrapper_<?php print $graph['local_graph_id']?>' graph_width='<?php print read_user_setting('default_width'); ?>' graph_height='<?php print read_user_setting('default_height'); ?>'></div>
<?php if (is_realm_allowed(27)) { ?>
<div id='dd<?php print $graph['local_graph_id']; ?>' class='noprint graphDrillDown'>
<?php graph_drilldown_icons($graph['local_graph_id'], 'graph_buttons_thumbnails', $tree_id, $branch_id); ?>
</div>
<?php } ?>
</div>
<?php print(read_user_setting('show_graph_title') == 'on' ? "<div class='center'>" . htmle($graph['title_cache']) . '</div>' : ''); ?>
</div>
<?php
}
} else {
if ($no_graphs_message != '') {
print "<div class='tableHeaderGraph'><span><em>$no_graphs_message</em></span></div>";
}
}
print '</div>';
}
/**
* Generates and prints HTML for graph drilldown icons.
*
* This function creates a set of icons for various graph-related actions such as
* viewing graph details, exporting CSV data, viewing time graphs, editing devices,
* editing graph templates, viewing graphs in real-time, and killing spikes in graphs.
* It also allows for plugin hooks to add additional icons.
*
* @param int $local_graph_id The ID of the local graph.
* @param string $type The type of icons to generate, default is 'graph_buttons'.
* @param int $tree_id The ID of the tree, default is 0.
* @param int $branch_id The ID of the branch, default is 0.
*
* @return void
*/
function graph_drilldown_icons(int $local_graph_id, string $type = 'graph_buttons', int $tree_id = 0, int $branch_id = 0) : void {
static $rand = 0;
$aggregate_url = aggregate_build_children_url($local_graph_id);
$graph_template_id = db_fetch_cell_prepared('SELECT graph_template_id
FROM graph_local
WHERE id = ?',
[$local_graph_id]);
print "<div class='iconWrapper'>";
print "<a class='iconLink utils' href='#' role='link' id='graph_" . $local_graph_id . "_util'><i class='drillDown ti ti-settings-filled actionCog' title='" . __esc('Graph Details, Zooming and Debugging Utilities') . "'></i></a><br>";
print "<a class='iconLink csvexport' href='#' role='link' id='graph_" . $local_graph_id . "_csv'><i class='drillDown ti ti-file-type-csv fileCSV' title='" . __esc('CSV Export of Graph Data') . "'></i></a><br>";
print "<a class='iconLink mrtg' href='#' role='link' id='graph_" . $local_graph_id . "_mrtg'><i class='drillDown ti ti-table threeBars' title='" . __esc('Time Graph View') . "'></i></a><br>";
if (is_realm_allowed(3)) {
$host_id = db_fetch_cell_prepared('SELECT host_id
FROM graph_local
WHERE id = ?',
[$local_graph_id]);
if ($host_id > 0) {
print "<a class='iconLink' href='" . htmle(CACTI_PATH_URL . "host.php?action=edit&id=$host_id") . "' data-graph='" . $local_graph_id . "' id='graph_" . $local_graph_id . "_de'><i id='de" . $host_id . '_' . $rand . "' class='drillDown ti ti-server editDevice' title='" . __esc('Edit Device') . "'></i></a>";
print '<br>';
$rand++;
}
}
if (is_realm_allowed(10) && $graph_template_id > 0) {
print "<a class='iconLink' role='link' href='" . htmle(CACTI_PATH_URL . 'graph_templates.php?action=template_edit&id=' . $graph_template_id) . "'><i class='drillDown ti ti-edit editTemplate' title='" . __esc('Edit Graph Template') . "'></i></a>";
print '<br>';
}
if (read_config_option('realtime_enabled') == 'on' && is_realm_allowed(25)) {
if (read_user_setting('realtime_mode') == '' || read_user_setting('realtime_mode') == '1') {
print "<a class='iconLink realtime' href='#' role='link' id='graph_" . $local_graph_id . "_realtime'><i class='drillDown ti ti-chart-area realTime' title='" . __esc('Click to view just this Graph in Real-time') . "'></i></a><br>";
} else {
print "<a class='iconLink' href='#' onclick=\"window.open('" . CACTI_PATH_URL . 'graph_realtime.php?top=0&left=0&local_graph_id=' . $local_graph_id . "', 'popup_" . $local_graph_id . "', 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=yes,width=650,height=300');return false\"><i class='drillDown ti ti-chart-area realTime' title='" . __esc('Click to view just this Graph in Real-time') . "'></i></a><br>";
}
}
if (is_realm_allowed(1043)) {
print "<a href='#' class='iconLink spikekill' data-graph='" . $local_graph_id . "' id='graph_" . $local_graph_id . "_sk'><i id='sk" . $local_graph_id . "' class='drillDown ti ti-paint-filled spikeKill' title='" . __esc('Kill Spikes in Graphs') . "'></i></a>";
print '<br>';
}
if ($aggregate_url != '') {
print $aggregate_url;
}
api_plugin_hook($type, [
'hook' => $type,
'local_graph_id' => $local_graph_id,
'rra' => 0,
'view_type' => $tree_id > 0 ? 'tree' : 'preview',
'tree_id' => $tree_id,
'branch_id' => $branch_id]
);
print '</div>';
}
/**
* Generates an HTML navigation bar for paginated content.
*
* @param string $base_url The base URL for the navigation links.
* @param int $max_pages The maximum number of pages to display in the navigation.
* @param int $current_page The current page number.
* @param int $rows_per_page The number of rows to display per page.
* @param int $total_rows The total number of rows available.
* @param int $colspan The number of columns to span for the navigation bar (default is 30).
* @param string $object The name of the object being paginated (default is 'Rows').
* @param string $page_var The query parameter name for the page number (default is 'page').
* @param string $return_to The ID of the HTML element to update with the new page content (default is '').
* @param bool $page_count Whether to display the page count (default is true).
*
* @return string The generated HTML for the navigation bar.
*/
function html_nav_bar(string $base_url, int $max_pages, int $current_page, int $rows_per_page, int $total_rows,
int $colspan = 30, string $object = '', string $page_var = 'page', string $return_to = '', bool $page_count = true) : string {
if (!preg_match('/^[A-Za-z_$][A-Za-z0-9_$]*$/', $page_var)) {
$page_var = 'page';
}
if ($object == '') {
$object = __('Rows');
}
$object = htmle($object);
if ($total_rows >= $rows_per_page && $page_count) {
if (substr_count($base_url, '?') == 0) {
$base_url = trim($base_url) . '?';
} else {
$base_url = trim($base_url) . '&';
}
$url_page_select = get_page_list($current_page, $max_pages, $rows_per_page, $total_rows, $base_url, $page_var, $return_to);
$nav = "<div class='navBarNavigation'>
<div class='navBarNavigationPrevious'>
" . (($current_page > 1) ? "<a href='#' onClick='goto$page_var(" . ($current_page - 1) . ");return false;'><i class='ti ti-chevrons-left previous'></i>" . __('Previous') . '</a>' : '') . "
</div>
<div class='navBarNavigationCenter'>
" . __('%d to %d of %s [ %s ]', (($rows_per_page * ($current_page - 1)) + 1), (($total_rows < $rows_per_page) || ($total_rows < ($rows_per_page * $current_page)) ? $total_rows : $rows_per_page * $current_page), $total_rows, $url_page_select) . "
</div>
<div class='navBarNavigationNext'>
" . (($current_page * $rows_per_page) < $total_rows ? "<a href='#' onClick='goto$page_var(" . ($current_page + 1) . ");return false;'>" . __('Next') . "<i class='ti ti-chevrons-right next'></i></a>" : '') . '
</div>
</div>';
} elseif ($total_rows > 0) {
if ($page_count || ($total_rows < $rows_per_page && $current_page == 1)) {
$nav = "<div class='navBarNavigation'>
<div class='navBarNavigationNone'>
" . __('All %d %s', $total_rows, $object) . "
</div>
</div>\n";
} else {
if (substr_count($base_url, '?') == 0) {
$base_url = trim($base_url) . '?';
} else {
$base_url = trim($base_url) . '&';
}
$url_page_select = "<ul class='pagination'>"; // for the same height as write in get_page_list()
$url_page_select .= "<li>$current_page</a></li>";
$url_page_select .= '</ul>';
$nav = "<div class='navBarNavigation'>
<div class='navBarNavigationPrevious'>
" . (($current_page > 1) ? "<a href='#' onClick='goto$page_var(" . ($current_page - 1) . ");return false;'><i class='ti ti-chevrons-left previous'></i>" . __('Previous') . '</a>' : '') . "
</div>
<div class='navBarNavigationCenter'>
" . __('Current Page: %s', $url_page_select) . "
</div>
<div class='navBarNavigationNext'>
" . ($total_rows >= $rows_per_page ? "<a href='#' onClick='goto$page_var(" . ($current_page + 1) . ");return false;'>" . __('Next') . "<i class='ti ti-chevrons-right next'></i></a>" : '') . "
</div>
</div>\n";
if ($return_to == '') {
$return_to = 'main';
}
$url = cacti_js_encode($base_url . $page_var);
$return_to_json = cacti_js_encode($return_to);
$nav .= "<script type='text/javascript' " . CactiSecureHeaders::getNonceAttribute() . ">
function goto$page_var(pageNo) {
if (typeof url_graph === 'function') {
var url_add=url_graph('')
} else {
var url_add='';
};
strURL = $url + '=' + pageNo + url_add;
loadUrl({
url: strURL,
elementId: $return_to_json,
});
}</script>";
}
} else {
$nav = "<div class='navBarNavigation'>
<div class='navBarNavigationNone'>
" . __('No %s Found', $object) . '
</div>
</div>';
}
return $nav;
}
/**
* html_header_sort - draws a header row suitable for display inside of a box element. When
* a user selects a column header, the callback function "filename" will be called to handle
* the sort the column and display the altered results.
*
* @param array $header_items An array containing a list of column items to display. The
* format is similar to the html_header, with the exception that it has three
* dimensions associated with each element (db_column => display_text, default_sort_order)
* alternatively (db_column => array('display' = 'blah', 'align' = 'blah', 'sort' = 'blah'))
* @param string $sort_column The value of current sort column.
* @param string $sort_direction The value the current sort direction. The actual sort direction
* will be opposite this direction if the user selects the same named column.
* @param int $last_item_colspan The TD 'colspan' to apply to the last cell in the row
* @param string $url A base url to redirect sort actions to
* @param string $return_to The id of the object to inject output into as a result of the sort action
*
* @return void
*/
function html_header_sort(array $header_items, string $sort_column, string $sort_direction, int $last_item_colspan = 1, string $url = '', string $return_to = '') : void {
static $page_count = 0;
$table_id = form_get_table_id();
$header_items = form_process_visible_display_text($table_id, $header_items);
// reverse the sort direction
if ($sort_direction == 'ASC') {
$new_sort_direction = 'DESC';
} else {
$new_sort_direction = 'ASC';
}
$page = $page_count . '_' . str_replace('.php', '', get_current_script_name());
if (isrv('action')) {
$page .= '_' . grv('action');
}
if (isrv('tab')) {
$page .= '_' . grv('tab');
}
if (isset($_SESSION['sort_data'][$page])) {
$order_data = $_SESSION['sort_data'][$page];
} else {
$order_data = [grv('sort_column') => grv('sort_direction')];
}
$primarySort = '';
foreach ($order_data as $key => $direction) {
$primarySort = $key;
break;
}
$table_visibility = [
'table_id' => $table_id,
'columns' => $header_items
];
print "<thead><tr class='tableHeader' data-columns='" . base64_encode((string) json_encode($table_visibility)) . "'>";
$i = 1;
foreach ($header_items as $db_column => $display_array) {
// if the column is not visible, don't display it
if (isset($display_array['visible']) && $display_array['visible'] === false) {
continue;
}
$isSort = '';
if (isset($display_array['nohide'])) {
$nohide = 'nohide';
} else {
$nohide = '';
}
if (array_key_exists('display', $display_array)) {
$display_text = $display_array['display'];
if ($sort_column == $db_column) {
$icon = $sort_direction;
$direction = $new_sort_direction;
if ($db_column == $primarySort) {
$isSort = 'primarySort';
} else {
$isSort = 'secondarySort';
}
} else {
if (isset($order_data[$db_column])) {
$icon = $order_data[$db_column];
if ($order_data[$db_column] == 'DESC') {
$direction = 'ASC';
} else {
$direction = 'DESC';
}
if ($db_column == $primarySort) {
$isSort = 'primarySort';
} else {
$isSort = 'secondarySort';
}
} else {
$icon = '';
if (isset($display_array['sort'])) {
$direction = $display_array['sort'];
} else {
$direction = 'ASC';
}
}
}
if (isset($display_array['align'])) {
$align = $display_array['align'];
} else {
$align = 'left';
}
if (isset($display_array['tip'])) {
$tip = $display_array['tip'];
} else {
$tip = '';
}
} else {
// by default, you will always sort ascending, with the exception of an already sorted column
if ($sort_column == $db_column) {
$icon = $sort_direction;
$direction = $new_sort_direction;
$display_text = $display_array[0];
if ($db_column == $primarySort) {
$isSort = 'primarySort';
} else {
$isSort = 'secondarySort';
}
} else {
if (isset($order_data[$db_column])) {
$icon = $order_data[$db_column];
if ($order_data[$db_column] == 'DESC') {
$direction = 'ASC';
} else {
$direction = 'DESC';
}
if ($db_column == $primarySort) {
$isSort = 'primarySort';
} else {
$isSort = 'secondarySort';
}
} else {
$icon = '';
$direction = $display_array[1];
}
$display_text = $display_array[0];
}
$align = 'left';
$tip = '';
}
if (cacti_strtolower($icon) == 'asc') {
$icon = 'ti ti-caret-up-filled';
} elseif (cacti_strtolower($icon) == 'desc') {
$icon = 'ti ti-caret-down-filled';
} else {
$icon = 'ti ti-caret-up-down-filled';
}
if (($db_column == '') || (substr_count($db_column, 'nosort'))) {
print '<th ' . ($tip != '' ? "title='" . htmle($tip) . "'" : '') . " class='$nohide $align' " . ((($i + 1) == cacti_count($header_items)) ? "colspan='$last_item_colspan' " : '') . '>' . $display_text . '</th>';
} else {
print '<th ' . ($tip != '' ? "title='" . htmle($tip) . "'" : '') . " class='sortable $align $nohide $isSort'>";
print "<div class='sortinfo' sort-return='" . ($return_to == '' ? 'main' : $return_to) . "' sort-page='" . ($url == '' ? htmle(get_current_page(false)) : $url) . "' sort-column='$db_column' sort-direction='$direction'><div class='textSubHeaderDark'>" . $display_text . "<i class='$icon'></i></div></div></th>";
}
$i++;
}
print '</tr></thead>';
$page_count++;
}
/**
* html_header_sort_checkbox - draws a header row with a 'select all' checkbox in the last cell
* suitable for display inside of a box element. When a user selects a column header,
* the callback function "filename" will be called to handle the sort the column and display
* the altered results.
*
* @param array $header_items An array containing a list of column items to display. The
* fonrmat is similar to the html_header, with the exception that it has three
* dimensions associated with each element (db_column => display_text, default_sort_order)
* alternatively (db_column => array('display' = 'blah', 'align' = 'blah', 'sort' = 'blah'))
* @param string $sort_column The value of current sort column.
* @param string $sort_direction The value the current sort direction. The actual sort direction
* will be opposite this direction if the user selects the same named column.
* @param bool $include_form Whether to include the 'select all' form
* @param string $form_action The url to post the 'select all' form to
* @param string $return_to The id of the object to inject output into as a result of the sort action
* @param string $prefix The prefix to use for the checkbox names
*
* @return void
*/
function html_header_sort_checkbox(array $header_items, string $sort_column, string $sort_direction,
bool $include_form = true, string $form_action = '', string $return_to = '', string $prefix = 'chk') : void {
static $page_count = 0;
$table_id = form_get_table_id();
$header_items = form_process_visible_display_text($table_id, $header_items);
// reverse the sort direction
if ($sort_direction == 'ASC') {
$new_sort_direction = 'DESC';
} else {
$new_sort_direction = 'ASC';
}
$page = $page_count . '_' . str_replace('.php', '', get_current_script_name());
if (isrv('action')) {
$page .= '_' . grv('action');
}
if (isrv('tab')) {
$page .= '_' . grv('tab');
}
if (isset($_SESSION['sort_data'][$page])) {
$order_data = $_SESSION['sort_data'][$page];
} else {
$order_data = [grv('sort_column') => grv('sort_direction')];
}
$primarySort = '';
foreach ($order_data as $key => $direction) {
$primarySort = $key;
break;
}
// default to the 'current' file
if ($form_action == '') {
$form_action = get_current_page();
}
$table_visibility = [
'table_id' => $table_id,
'columns' => $header_items
];
print "<thead><tr class='tableHeader' data-columns='" . base64_encode((string) json_encode($table_visibility)) . "'>";
foreach ($header_items as $db_column => $display_array) {
// if the column is not visible, don't display it
if (isset($display_array['visible']) && $display_array['visible'] === false) {
continue;
}
$isSort = '';
if (isset($display_array['nohide'])) {
$nohide = 'nohide';
} else {
$nohide = '';
}
$icon = '';
if (array_key_exists('display', $display_array)) {
$display_text = $display_array['display'];
if ($sort_column == $db_column) {
$icon = $sort_direction;
$direction = $new_sort_direction;
if ($db_column == $primarySort) {
$isSort = 'primarySort';
} else {
$isSort = 'secondarySort';
}
} else {
if (isset($order_data[$db_column])) {
$icon = $order_data[$db_column];
if ($order_data[$db_column] == 'DESC') {
$direction = 'ASC';
} else {
$direction = 'DESC';
}
if ($db_column == $primarySort) {
$isSort = 'primarySort';
} else {
$isSort = 'secondarySort';
}
} else {
$icon = '';
if (isset($display_array['sort'])) {
$direction = $display_array['sort'];
} else {
$direction = 'ASC';
}
}
}
if (isset($display_array['align'])) {
$align = $display_array['align'];
} else {
$align = 'left';
}
if (isset($display_array['tip'])) {
$tip = $display_array['tip'];
} else {
$tip = '';
}
} else {
// by default, you will always sort ascending, with the exception of an already sorted column
if ($sort_column == $db_column) {
$icon = $sort_direction;
$direction = $new_sort_direction;
$display_text = $display_array[0];
if ($db_column == $primarySort) {
$isSort = 'primarySort';
} else {
$isSort = 'secondarySort';
}
} else {
if (isset($order_data[$db_column])) {
$icon = $order_data[$db_column];
if ($order_data[$db_column] == 'DESC') {
$direction = 'ASC';
} else {
$direction = 'DESC';
}
if ($db_column == $primarySort) {
$isSort = 'primarySort';
} else {
$isSort = 'secondarySort';
}
} else {
$icon = '';
$direction = $display_array[1];
}
$display_text = $display_array[0];
}
$align = 'left';
$tip = '';
}
if (cacti_strtolower($icon) == 'asc') {
$icon = 'ti ti-caret-up-filled';
} elseif (cacti_strtolower($icon) == 'desc') {
$icon = 'ti ti-caret-down-filled';
} else {
$icon = 'ti ti-caret-down-filled';
}
if (($db_column == '') || (substr_count($db_column, 'nosort'))) {
print '<th ' . ($tip != '' ? "title='" . htmle($tip) . "'" : '') . " class='$align $nohide'>" . $display_text . '</th>';
} else {
print '<th ' . ($tip != '' ? "title='" . htmle($tip) . "'" : '') . " class='sortable $align $nohide $isSort'>";
print "<div class='sortinfo' sort-return='" . ($return_to == '' ? 'main' : $return_to) . "' sort-page='" . htmle($form_action) . "' sort-column='$db_column' sort-direction='$direction'><div class='textSubHeaderDark'>" . $display_text . "<i class='$icon'></i></div></div></th>";
}
}
print "<th class='tableSubHeaderCheckbox'><input id='selectall' class='checkbox' type='checkbox' title='" . __esc('Select All Rows') . "' onClick='selectAll(\"$prefix\",this.checked)'><label class='formCheckboxLabel' title='" . __esc('Select All Rows') . "' for='selectall'></label></th>" . ($include_form ? "<th style='display:none;'><form id='$prefix' name='$prefix' method='post' action='$form_action'></th>" : '');
print '</tr></thead>';
$page_count++;
}
/**
* html_header - draws a header row suitable for display inside a box element
*
* @param array $header_items An array containing a list of items to be included in the header
* alternatively and array of header names and alignment
* array('display' = 'blah', 'align' = 'blah')
* @param int $last_item_colspan The TD 'colspan' to apply to the last cell in the row
* @param bool $resizable Is the header resizable
*
* @return void
*/
function html_header(array $header_items, int $last_item_colspan = 1, bool $resizable = true) : void {
$table_id = form_get_table_id();
$header_items = form_process_visible_display_text($table_id, $header_items);
$table_visibility = [
'table_id' => $table_id,
'columns' => $header_items
];
print "<thead><tr class='tableHeader " . ($last_item_colspan > 1 || !$resizable ? 'tableFixed' : '') . "' data-columns='" . base64_encode((string) json_encode($table_visibility)) . "'>";
$i = 0;
foreach ($header_items as $item) {
if (is_array($item)) {
// if the column is not visible, don't display it
if (isset($header_items['visible']) && $header_items['visible'] === false) {
continue;
}
if (isset($item['nohide'])) {
$nohide = 'nohide';
} else {
$nohide = '';
}
if (isset($item['align'])) {
$align = $item['align'];
} else {
$align = 'left';
}
if (isset($item['tip'])) {
$tip = $item['tip'];
} else {
$tip = '';
}
print '<th ' . ($tip != '' ? "title='" . htmle($tip) . "' " : '') . "class='$nohide $align' " . ((($i + 1) == cacti_count($header_items)) ? "colspan='$last_item_colspan' " : '') . '>' . htmle($item['display']) . '</th>';
} else {
print '<th ' . ((($i + 1) == cacti_count($header_items)) ? "colspan='$last_item_colspan' " : '') . '>' . htmle($item) . '</th>';
}
$i++;
}
print '</tr></thead>';
}
/**
* html_section_header - draws a header row suitable for display inside a box element
* but for display as a section title and not as a series of table header columns
*
* @param mixed $header_item An array of the display name of the header for the section and
* optional alignment.
* @param int $last_item_colspan The TD 'colspan' to apply to the last cell in the row
* @param bool $resizable Will this header be resizable
*
* @return void
*/
function html_section_header(mixed $header_item, int $last_item_colspan = 1, bool $resizable = true) : void {
print "<tr class='tableHeader " . ($last_item_colspan > 1 || !$resizable ? 'tableFixed' : '') . "'>";
if (is_array($header_item) && isset($header_item['display'])) {
print '<th ' . (isset($header_item['align']) ? "style='text-align:" . $header_item['align'] . ";'" : '') . " colspan='$last_item_colspan'>" . $header_item['display'] . '</th>';
} else {
print "<th colspan='$last_item_colspan'>" . $header_item . '</th>';
}
print '</tr>';
}
/**
* html_header_checkbox - draws a header row with a 'select all' checkbox in the last cell
* suitable for display inside a box element
*
* @param array $header_items An array containing a list of items to be included in the header
* alternatively and array of header names and alignment
* array('display' = 'blah', 'align' = 'blah')
* @param bool $include_form Whether to include the 'select all' form
* @param string $form_action The url to post the 'select all' form to
* @param bool $resizable Whether the table is resizable
* @param string $prefix The prefix to use for the checkbox names
*
* @return void
*/
function html_header_checkbox(array $header_items, bool $include_form = true, string $form_action = '', bool $resizable = true, string $prefix = 'chk') : void {
$table_id = form_get_table_id();
$header_items = form_process_visible_display_text($table_id, $header_items);
// default to the 'current' file
if ($form_action == '') {
$form_action = get_current_page();
}
$table_visibility = [
'table_id' => $table_id,
'columns' => $header_items
];
print "<thead><tr class='tableHeader " . (!$resizable ? 'tableFixed' : '') . "' data-columns='" . base64_encode((string) json_encode($table_visibility)) . "'>";
foreach ($header_items as $item) {
if (is_array($item)) {
// if the column is not visible, don't display it
if (isset($header_items['visible']) && $header_items['visible'] === false) {
continue;
}
if (isset($item['nohide'])) {
$nohide = 'nohide';
} else {
$nohide = '';
}
if (isset($item['align'])) {
$align = $item['align'];
} else {
$align = 'left';
}
if (isset($item['tip'])) {
$tip = $item['tip'];
} else {
$tip = '';
}
print '<th ' . ($tip != '' ? " title='" . htmle($tip) . "' " : '') . "class='$align $nohide'>" . htmle($item['display']) . '</th>';
} else {
print "<th class='left'>" . htmle($item) . '</th>';
}
}
print "<th class='tableSubHeaderCheckbox'><input id='selectall' class='checkbox' type='checkbox' title='" . __esc('Select All Rows') . "' onClick='selectAll(\"$prefix\",this.checked)'><label class='formCheckboxLabel' title='" . __esc('Select All') . "' for='selectall'></label></th>" . ($include_form ? "<th style='display:none;'><form id='$prefix' name='$prefix' method='post' action='$form_action'></th>" : '');
print '</tr></thead>';
}
/**
* html_create_list - draws the items for a html dropdown given an array of data
*
* @param array $form_data An array containing data for this dropdown. it can be
* formatted in one of three ways:
*
* $dropdown_array = array(
* 'id' => 'name1'
* 'id2' => 'name2',
* ...
* );
*
* -- or --
*
* $dropdown_array = array(
* array(
* 'id' => 'id1',
* 'name' => 'name1',
* ),
* array(
* 'id' => 'id2',
* 'name' => 'name2'
* ),
* ...
* );
*
* -- or for custom rendering of icons --
*
* $dropdown_array = array(
* 'server' => array(
* 'display' => __('Some Value'),
* 'class' => 'ti ti-server',
* 'style' => 'width:30px;...'
* ),
* ...
* );
*
* @param string $column_display Used to identify the key to be used for display data. this
* is only applicable if the array is formatted using the second method above
* @param string $column_id Used to identify the key to be used for id data. this
* is only applicable if the array is formatted using the second method above
* @param string $prev_val The current value of this form element
*
* @return void
*/
function html_create_list(array $form_data, string $column_display, string $column_id, string $prev_val) : void {
if (cacti_sizeof($form_data)) {
foreach ($form_data as $key => $row) {
if (is_array($row)) {
if ($column_id != '') {
print "<option value='" . htmle($row[$column_id]) . "'";
} else {
print "<option value='" . htmle($key) . "'";
}
} else {
print "<option value='" . htmle($key) . "'";
}
if ($column_id != '' && isset($row[$column_id]) && $prev_val == $row[$column_id]) {
print ' selected';
} elseif ($column_id == '' && $key == $prev_val) {
print ' selected';
}
if (isset($row['class'])) {
print " data-class='" . $row['class'] . "'";
}
if (isset($row['style'])) {
print " data-style='" . $row['style'] . "'";
}
if (!is_array($row)) {
print '>' . htmle($row) . '</option>';
} elseif (isset($row['host_id'])) {
print '>' . htmle($row[$column_display]) . '</option>';
} elseif (isset($row['display'])) {
print '>' . htmle($row['display']) . '</option>';
} elseif (isset($row[$column_display])) {
print '>' . htmle(null_out_substitutions($row[$column_display])) . '</option>';
}
}
}
}
/**
* alias of html_escape_request_var()
*
* @param string $string String the request variable to escape
*
* @return string The escaped request variable to be returned.
*/
function htmlerv(string $string) : string {
return html_escape(gnrv($string));
}
/**
* html_escape_request_var - sanitizes a request variable for display
*
* @param string $string String the request variable to escape
*
* @return string The escaped request variable to be returned.
*/
function html_escape_request_var(mixed $string) : string {
return html_escape(gnrv($string));
}
/**
* alias of html_escape()
*
* @param string $string String the string to escape
*
* @return string The escaped string to be returned.
*/
function htmle(mixed $string) : string {
return html_escape($string);
}
/**
* html_escape_attr - sanitizes a string for display in an HTML attribute
*
* @param mixed $string String the attribute value to escape
*
* @return string The escaped attribute value to be returned.
*/
function html_escape_attr(mixed $string = '') : string {
static $charset;
if ($string === null || $string === '') {
return '';
}
if ($charset == '') {
$charset = ini_get('default_charset');
}
if ($charset == '') {
$charset = 'UTF-8';
}
// double_encode is enabled here (unlike html_escape) so that an
// already-encoded entity in the input cannot survive into an attribute
// value and be decoded back into a quote by the HTML parser.
$string = htmlspecialchars((string) $string, ENT_QUOTES | ENT_HTML5, $charset, true);
// Grave accent can lead to xss; htmlspecialchars leaves it untouched, so
// replace it after escaping to avoid double-encoding the entity itself.
return str_replace('`', '&#96;', $string);
}
/**
* html_escape_url - escapes a URL for display in an HTML attribute. This
* function does not validate or restrict URL schemes; callers accepting
* untrusted navigation targets must validate them separately.
*
* @param mixed $url URL the attribute value to escape
*
* @return string The escaped URL value to be returned.
*/
function html_escape_url(mixed $url = '') : string {
return html_escape_attr($url);
}
/**
* cacti_script_data - renders JSON data for scripts to read from the DOM
*
* @param string $id HTML id for the script element
* @param mixed $data Data to encode as a JavaScript-safe JSON literal
*
* @return string The script tag containing JSON data.
*/
function cacti_script_data(string $id, mixed $data) : string {
return "<script type='application/json' id='" . html_escape_attr($id) . "'>" . cacti_js_encode($data) . '</script>';
}
/**
* html_escape - sanitizes a string for display
*
* @param mixed $string String the string to escape
*
* @return string The escaped string to be returned.
*/
function html_escape(mixed $string = '') : string {
static $charset;
if ($charset == '') {
$charset = ini_get('default_charset');
}
if ($charset == '') {
$charset = 'UTF-8';
}
// Grave Accent character can lead to xss
if ($string !== null && $string !== '') {
$string = str_replace('`', '&#96;', $string);
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, $charset, false);
} else {
return '';
}
}
/**
* Renders HTML attributes with escaped values.
*
* @param array $attributes Attribute name/value pairs
*
* @return string Rendered attributes with a leading space when not empty
*/
function html_attributes(array $attributes) : string {
$output = '';
foreach ($attributes as $name => $value) {
if ($value === null || $value === false) {
continue;
}
$name = (string) $name;
if (preg_match('/^[A-Za-z][A-Za-z0-9_:-]*$/', $name) !== 1) {
continue;
}
if ($value === true) {
$output .= ' ' . $name;
} else {
$output .= ' ' . $name . "='" . html_escape_attr($value) . "'";
}
}
return $output;
}
/**
* Renders a hidden input with escaped attributes.
*
* @param string $name Input name
* @param mixed $value Input value
* @param array $attributes Additional attributes
*
* @return string The rendered hidden input
*/
function html_hidden_input(string $name, mixed $value, array $attributes = []) : string {
return '<input' . html_attributes(['type' => 'hidden', 'name' => $name, 'value' => $value] + $attributes) . '>';
}
/**
* Renders a text input with escaped attributes.
*
* @param string $name Input name
* @param mixed $value Input value
* @param array $attributes Additional attributes
*
* @return string The rendered text input
*/
function html_text_input(string $name, mixed $value, array $attributes = []) : string {
return '<input' . html_attributes(['type' => 'text', 'name' => $name, 'value' => $value] + $attributes) . '>';
}
/**
* html_split_string - takes a string and breaks it into a number of <br> separated segments
*
* @param string $string - string to be modified and returned
* @param int $length - the maximal string length to split to
* @param int $forgiveness - the maximum number of characters to walk back from to determine
* the correct break location.
*
* @return string $new_string - the modified string to be returned.
*/
function html_split_string(string $string, int $length = 90, int $forgiveness = 10) : string {
$new_string = '';
$j = 0;
$done = false;
while (!$done) {
if (mb_strlen($string, 'UTF-8') > $length) {
for ($i = 0; $i < $forgiveness; $i++) {
if (substr($string, $length - $i, 1) == ' ') {
$new_string .= mb_substr($string, 0, $length - $i, 'UTF-8') . '<br>';
break;
}
}
$string = mb_substr($string, $length - $i, null, 'UTF-8');
} else {
$new_string .= $string;
$done = true;
}
$j++;
if ($j > 4) {
break;
}
}
return $new_string;
}
/**
* draw_graph_items_list - draws a nicely formatted list of graph items for display
* on an edit form
*
* @param array $item_list - an array representing the list of graph items. this array should
* come directly from the output of db_fetch_assoc()
* @param string $filename - the filename to use when referencing any external url
* @param string $url_data - any extra GET url information to pass on when referencing any
* external url
* @param bool $disable_controls - whether to hide all edit/delete functionality on this form
*
* @return void
*/
function draw_graph_items_list(array $item_list, string $filename, string $url_data, bool $disable_controls) : void {
global $consolidation_functions, $graph_item_types;
include(CACTI_PATH_INCLUDE . '/global_arrays.php');
$display_text = [
[
'display' => __('Data Source'),
'align' => 'left'
],
[
'display' => __('Seq#'),
'align' => 'center'
],
[
'display' => __('Type'),
'align' => 'left'
],
[
'display' => __('Consolidation'),
'align' => 'left'
],
[
'display' => __('Legend'),
'tip' => __('When exporting or showing the values while hovering over the Graph, what legend to you want displayed? If empty, it will default to data_source_name (consolidation function). This does not work for some Cacti items such as TOTAL_ALL_DATA_SOURCES for example.'),
'align' => 'left'
],
[
'display' => __('GPrint'),
'align' => 'left'
],
[
'display' => __('CDEF'),
'align' => 'left'
],
[
'display' => __('VDEF'),
'align' => 'left'
],
[
'display' => __('Primary Color'),
'align' => 'left'
],
[
'display' => __('Gradient Color'),
'align' => 'left'
],
[
'display' => __('Actions'),
'align' => 'right'
]
];
html_header($display_text);
$group_counter = 0;
$_graph_type_name = '';
$i = 0;
$matrix_title = '';
$customClass = '';
if (cacti_sizeof($item_list)) {
foreach ($item_list as $item) {
$_graph_type_name = $graph_item_types[$item['graph_type_id']];
// graph grouping display logic
$this_row_style = '';
$use_custom_class = false;
$hard_return = '';
if (!preg_match('/(GPRINT|TEXTALIGN|HRULE|VRULE|TICK)/', $_graph_type_name)) {
$this_row_style = 'font-weight: bold;';
$use_custom_class = true;
$item['gprint_name'] = '-';
if ($group_counter % 2 == 0) {
$customClass = 'graphItem';
} else {
$customClass = 'graphItemAlternate';
}
$group_counter++;
}
// alternating row color
if ($use_custom_class == false) {
print "<tr id='{$item['id']}' class='tableRow selectable'>";
} else {
print "<tr id='{$item['id']}' class='tableRow selectable $customClass'>";
}
if (empty($item['data_source_name'])) {
$item['data_source_name'] = __('No Source');
}
switch (true) {
case preg_match('/(TEXTALIGN)/', $_graph_type_name):
$matrix_title = 'TEXTALIGN: ' . ucfirst($item['textalign']);
break;
case preg_match('/(TICK)/', $_graph_type_name):
$matrix_title = $item['data_source_name'] . ': ' . $item['text_format'];
break;
case preg_match('/(AREA|STACK|GPRINT|LINE[123])/', $_graph_type_name):
$matrix_title = $item['data_source_name'] . ': ' . $item['text_format'];
break;
case preg_match('/(HRULE)/', $_graph_type_name):
$matrix_title = 'HRULE: ' . $item['value'];
break;
case preg_match('/(VRULE)/', $_graph_type_name):
$matrix_title = 'VRULE: ' . $item['value'];
break;
case preg_match('/(COMMENT)/', $_graph_type_name):
$matrix_title = 'COMMENT: ' . $item['text_format'];
break;
}
if (preg_match('/(TEXTALIGN)/', $_graph_type_name)) {
$hard_return = '';
} elseif ($item['hard_return'] == 'on') {
$hard_return = "<span style='font-weight:bold;color:#FF0000;'>&lt;HR&gt;</span>";
}
if ($disable_controls == false) {
$display = "<a class='linkEditMain' href='" . htmle("$filename?action=item_edit&id=" . $item['id'] . "&$url_data") . "'>" . htmle($matrix_title) . '</a>';
} else {
$display = htmle($matrix_title);
}
// data source display
print "<td style='$this_row_style'>" . $display . $hard_return . '</td>';
// sequence number
print "<td class='center' style='$this_row_style'>" . $item['sequence'] . '</td>';
// graph item type display
print "<td style='$this_row_style'>" . $graph_item_types[$item['graph_type_id']] . '</td>';
// consolidation function display
if (!preg_match('/(TICK|TEXTALIGN|HRULE|VRULE)/', $_graph_type_name)) {
print "<td style='$this_row_style'>" . $consolidation_functions[$item['consolidation_function_id']] . '</td>';
} else {
print '<td>-</td>';
}
// export/hover legend
if ($item['legend'] != '') {
print "<td style='$this_row_style'>" . htmle($item['legend']) . '</td>';
} else {
print '<td>-</td>';
}
// grpint display
print "<td class='prewrap' style='$this_row_style'>";
if ($item['gprint_name'] != '') {
print htmle($item['gprint_name']);
} else {
print '-';
}
print '</td>';
// cdef display
print "<td class='prewrap' style='$this_row_style'>";
if ($item['cdef_name'] != '') {
print $item['cdef_name'];
} else {
print '-';
}
print '</td>';
// vdef display
print "<td class='prewrap' style='$this_row_style'>";
if ($item['vdef_name'] != '') {
print $item['vdef_name'];
} else {
print '-';
}
print '</td>';
// color display
$blank = '-';
if (preg_match('/(AREA|STACK|TICK|LINE[123])/', $_graph_type_name)) {
if (preg_match('/(AREA|STACK)/', $_graph_type_name)) {
if ($item['hex'] != '') {
$color1 = $item['hex'] . $item['alpha'];
if ($item['hex2'] != '') {
$color2 = $item['hex2'] . ($item['hex2'] != '' ? $item['alpha2'] : '');
} else {
$color2 = $blank;
}
} else {
$color1 = $color2 = $blank;
}
} else {
if ($item['hex'] != '') {
$color1 = $item['hex'] . $item['alpha'];
} else {
$color1 = $blank;
}
$color2 = $blank;
}
} else {
$color1 = $color2 = $blank;
}
if (!preg_match('/(TEXTALIGN)/', $_graph_type_name)) {
if (preg_match('/(AREA|STACK)/', $_graph_type_name)) {
// color1
print "<td class='nowrap'>";
print "<div style='display:table-cell;min-width:16px;background-color:#{$color1}'></div>";
print "<div style='display:table-cell;padding-left:5px;'>{$color1}</div>";
print '</td>';
// color2
print "<td class='nowrap'>";
if ($color2 != $blank) {
print "<div style='display:table-cell;min-width:16px;background-color:#{$color2}'></div>";
print "<div style='display:table-cell;padding-left:5px;'>{$color2}</div>";
} else {
print $color2;
}
print '</td>';
} else {
// color 1
print "<td class='nowrap'>";
if ($color1 != $blank) {
print "<div style='display:table-cell;min-width:16px;background-color:#{$color1}'></div>";
print "<div style='display:table-cell;padding-left:5px;'>{$color1}</div>";
} else {
print $color1;
}
print '</td>';
// color2
print "<td>{$color2}</td>";
}
} else {
print '<td></td><td></td>';
}
if ($disable_controls == false) {
print "<td class='right nowrap'>";
if ($i != cacti_sizeof($item_list) - 1) {
print "<span><a class='moveArrow ti ti-caret-down-filled cactiPostAction' title='" . __esc('Move Down') . "' href='#' data-url='" . html_escape_url("$filename?action=item_movedown&id=" . $item['id'] . "&$url_data") . "'></a></span>";
} else {
print "<span class='moveArrowNone'></span>";
}
if ($i > 0) {
print "<span><a class='moveArrow ti ti-caret-up-filled cactiPostAction' title='" . __esc('Move Up') . "' href='#' data-url='" . html_escape_url("$filename?action=item_moveup&id=" . $item['id'] . "&$url_data") . "'></a></span>";
} else {
print "<span class='moveArrowNone'></span>";
}
print "<a class='deleteMarker ti ti-x cactiPostAction' title='" . __esc('Delete') . "' href='#' data-url='" . html_escape_url("$filename?action=item_remove&id=" . $item['id'] . "&nostate=true&$url_data") . "'></a>";
print '</td>';
}
print '</tr>';
$i++;
}
} else {
print "<tr class='tableRow odd'><td colspan='" . cacti_sizeof($display_text) . "'><em>" . __('No Items') . '</em></td></tr>';
}
}
/**
* is_menu_pick_active - determines if current selection is active
*
* @param string $menu_url - url of current page
*
* @return bool true if active, false if not
*/
function is_menu_pick_active(string $menu_url) : bool {
static $url_array, $url_parts;
$menu_parts = [];
// special case for host.php?action=edit&create=true
if (str_contains($_SERVER['REQUEST_URI'], 'host.php?action=edit&create=true')) {
if (str_contains($menu_url, 'host.php?action=edit&create=true')) {
return true;
} else {
return false;
}
} elseif (!is_array($url_array) || !cacti_sizeof($url_array)) {
// break out the URL and variables
$url_array = parse_url($_SERVER['REQUEST_URI']);
if (isset($url_array['query'])) {
parse_str($url_array['query'], $url_parts);
} else {
$url_parts = [];
}
}
// Host requires another check
if (str_contains($menu_url, 'host.php?action=edit&create=true')) {
return false;
}
$menu_array = parse_url($menu_url);
if ($menu_array === false) {
return false;
}
if (!array_key_exists('path', $menu_array)) {
return false;
}
$base_url_path = basename($url_array['path']);
if (empty($base_url_path)) {
cacti_log('INFO: Empty url path detceted - ' . json_encode($url_array), false, 'MENU', POLLER_VERBOSITY_DEBUG);
return false;
}
$base_menu_path = basename($menu_array['path']);
if (empty($base_menu_path)) {
cacti_log('INFO: Empty menu path detceted - ' . json_encode($menu_array), false, 'MENU', POLLER_VERBOSITY_DEBUG);
return false;
}
if ($base_url_path == $base_menu_path) {
if (isset($menu_array['query'])) {
parse_str($menu_array['query'], $menu_parts);
} else {
$menu_parts = [];
}
if (isset($menu_parts['id'])) {
if (isset($url_parts['id'])) {
if ($menu_parts['id'] == $url_parts['id']) {
return true;
}
}
} elseif (isset($menu_parts['action'])) {
if (isset($url_parts['action'])) {
if ($menu_parts['action'] == $url_parts['action']) {
return true;
}
}
} else {
return true;
}
}
return false;
}
/**
* draw_menu - draws the cacti menu for display in the console
*
* @param mixed $user_menu - the user menu to display
*
* @return void
*/
function draw_menu(mixed $user_menu = '') : void {
global $user_auth_realm_filenames, $menu, $menu_glyphs;
if (!is_array($user_menu)) {
$user_menu = $menu;
}
print "<div id='menu'><ul id='nav' role='menu'>";
// loop through each header
$i = 0;
$headers = [];
foreach ($user_menu as $header_name => $header_array) {
// pass 1: see if we are allowed to view any children
$show_header_items = false;
foreach ($header_array as $item_url => $item_title) {
$basename = explode('?', basename($item_url));
$basename = $basename[0];
if (preg_match('#link.php\?id=(\d+)#', $item_url, $matches)) {
if (is_realm_allowed($matches[1] + 10000)) {
$show_header_items = true;
} else {
$show_header_items = false;
}
} else {
$current_realm_id = ($user_auth_realm_filenames[basename($item_url)] ?? 0);
if (is_realm_allowed($current_realm_id)) {
$show_header_items = true;
} elseif (api_user_realm_auth($basename)) {
$show_header_items = true;
}
}
}
if ($show_header_items == true) {
// Let's give our menu li's a unique id
$id = 'menu_' . cacti_strtolower(clean_up_name($header_name));
if (isset($headers[$id])) {
$id .= '_' . $i++;
}
$headers[$id] = true;
if (isset($menu_glyphs[$header_name])) {
$glyph = '<i class="menu_glyph ' . $menu_glyphs[$header_name] . '"></i>';
} else {
$glyph = '<i class="menu_glyph ti ti-folder-filled"></i>';
}
print "<li class='menuitem' role='menuitem' aria-haspopup='true' id='$id'><a class='menu_parent active' href='#'>$glyph<span>$header_name</span></a>";
print "<ul role='menu' id='{$id}_div' style='display:block;'>";
// pass 2: loop through each top level item and render it
foreach ($header_array as $item_url => $item_title) {
$basename = explode('?', basename($item_url));
$basename = $basename[0];
$current_realm_id = ($user_auth_realm_filenames[$basename] ?? 0);
/**
* if this item is an array, then it contains sub-items. if not, is just
* the title string and needs to be displayed
*/
if (is_array($item_title)) {
$i = 0;
if ($current_realm_id == -1 || is_realm_allowed($current_realm_id) || !isset($user_auth_realm_filenames[$basename])) {
// if the current page exists in the sub-items array, draw each sub-item
if (array_key_exists(get_current_page(), $item_title) == true) {
$draw_sub_items = true;
} else {
$draw_sub_items = false;
}
foreach ($item_title as $item_sub_url => $item_sub_title) {
if (str_starts_with($item_sub_url, 'EXTERNAL::')) {
$item_sub_external = true;
$item_sub_url = substr($item_sub_url, 10);
} else {
$item_sub_external = false;
$item_sub_url = CACTI_PATH_URL . $item_sub_url;
}
/* always draw the first item (parent), only draw the children if we are viewing a page
that is contained in the sub-items array */
if (($i == 0) || ($draw_sub_items)) {
if (is_menu_pick_active($item_sub_url)) {
print "<li><a role='menuitem' class='pic selected' href='";
print htmle($item_sub_url) . "'";
if ($item_sub_external) {
print " target='_blank' rel='noopener'";
}
print ">$item_sub_title</a></li>";
} else {
print "<li><a role='menuitem' class='pic' href='";
print htmle($item_sub_url) . "'";
if ($item_sub_external) {
print " target='_blank' rel='noopener'";
}
print ">$item_sub_title</a></li>";
}
}
$i++;
}
}
} else {
if ($current_realm_id == -1 || is_realm_allowed($current_realm_id) || !isset($user_auth_realm_filenames[$basename])) {
// draw normal (non sub-item) menu item
if (str_starts_with($item_url, 'EXTERNAL::')) {
$item_external = true;
$item_url = substr($item_url, 10);
} else {
$item_external = false;
$item_url = CACTI_PATH_URL . $item_url;
}
if (is_menu_pick_active($item_url)) {
print "<li><a role='menuitem' class='pic selected' href='";
print htmle($item_url) . "'";
if ($item_external) {
print " target='_blank' rel='noopener'";
}
print ">$item_title</a></li>";
} else {
print "<li><a role='menuitem' class='pic' href='";
print htmle($item_url) . "'";
if ($item_external) {
print " target='_blank' rel='noopener'";
}
print ">$item_title</a></li>";
}
}
}
}
print '</ul></li>';
}
}
print '</ul></div>';
}
/**
* draw_actions_dropdown - draws a table the allows the user to select an action to perform
* on one or more data elements
*
* @param array $actions_array - an array that contains a list of possible actions. this array should
* be compatible with the form_dropdown() function
* @param int $delete_action - if there is a delete action that should suppress removal of rows
* specify it here. If you don't want any delete actions, set to 0.
*
* @return void
*/
function draw_actions_dropdown(array $actions_array, int $delete_action = 1) : void {
global $form_id;
if ($actions_array === null || cacti_sizeof($actions_array) == 0) {
return;
}
if (!isset($actions_array[0])) {
$my_actions[0] = __('Choose an action');
$my_actions += $actions_array;
$actions_array = $my_actions;
}
?>
<div class='actionsDropdown'>
<div>
<span class='actionsDropdownArrow'><img src='<?php print get_theme_paths('%s', 'images/arrow.gif') ?>' alt=''></span>
<?php form_dropdown('drp_action', $actions_array, '', '', '0', '', ''); ?>
<span class='actionsDropdownButton'><button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' id='submit' value='<?php print __esc('Go'); ?>' title='<?php print __esc('Execute Action'); ?>' form='<?php print $form_id; ?>'><?php print __esc('Go'); ?></button></span>
</div>
</div>
<input type='hidden' id='action' name='action' value='actions' form='<?php print $form_id; ?>'/>
<script type='text/javascript' <?php print CactiSecureHeaders::getNonceAttribute(); ?>>
function setDisabled() {
$('tr[id^="line"]').addClass('selectable').prop('disabled', false).removeClass('disabled_row').unbind('click').prop('disabled', false);
if ($('#drp_action').val() == '<?php print $delete_action; ?>') {
$(':checkbox.disabled').each(function(data) {
$(this).closest('tr').addClass('disabled_row');
if ($(this).is(':checked')) {
$(this).prop('checked', false).removeAttr('aria-checked').removeAttr('data-prev-check');
$(this).closest('tr').removeClass('selected');
}
$(this).prop('disabled', true).closest('tr').removeClass('selected');
});
$('#submit').each(function() {
if ($(this).button === 'function') {
$(this).button('enable');
} else {
$(this).prop('disabled', false);
}
});
} else if ($('#drp_action').val() == 0) {
$(':checkbox.disabled').each(function(data) {
$(this).prop('disabled', false);
});
$('#submit').each(function() {
if ($(this).button === 'function') {
$(this).button('disable');
} else {
$(this).prop('disabled', true);
}
});
} else if ('<?php print $delete_action; ?>' != 0) {
$('#submit').each(function() {
if ($(this).button === 'function') {
$(this).button('enable');
} else {
$(this).prop('disabled', false);
}
});
}
$('tr[id^="line"]').filter(':not(.disabled_row)').off('click').on('click', function(event) {
selectUpdateRow(event, $(this));
});
}
$(function() {
setDisabled();
$('#drp_action').change(function() {
setDisabled();
});
});
</script>
<?php
}
/**
* Draws a matrix header item in an HTML table.
*
* @deprecated This function is deprecated
*
* @param string $matrix_name The name to be displayed in the matrix header.
* @param string $matrix_text_color The color of the text in the matrix header.
* @param int $column_span The number of columns the header item should span. Default is 1.
*
* @return void
*/
function DrawMatrixHeaderItem(string $matrix_name, string $matrix_text_color, int $column_span = 1) : void {
?>
<th style='height:1px;' colspan='<?php print $column_span; ?>'>
<div class='textSubHeaderDark'><?php print $matrix_name; ?></div>
</th>
<?php
}
/**
* Generates an HTML table row with a single cell containing the provided text.
*
* This function creates a table row (`<tr>`) with a single table data cell (`<td>`)
* that contains the provided text. The text is escaped using the `htmle` function
* to prevent XSS attacks.
*
* @param string $text The text to be displayed inside the table cell.
*
* @return void
*/
function form_area(string $text) : void {
?>
<tr>
<td class='textArea'>
<?php print htmle($text); ?>
</td>
</tr>
<?php
}
/**
* is_console_page - determines if current passed url is considered to be a console page
*
* @param string $url - url to be checked
*
* @return bool true if console page, false if not
*/
function is_console_page(string $url) : bool {
global $menu;
$basename = basename($url);
if ($basename == 'index.php') {
return true;
}
if ($basename == 'about.php') {
return true;
}
if ($basename == 'rrdcleaner.php') {
return true;
}
if ($basename == 'clog.php') {
return false;
}
if (api_plugin_hook_function('is_console_page', $url) != $url) {
return true;
}
if (cacti_sizeof($menu)) {
foreach ($menu as $children) {
if (cacti_sizeof($children)) {
foreach ($children as $page => $name) {
if (basename($page) == $basename) {
return true;
}
}
}
}
}
return false;
}
function html_show_tabs_left() : void {
global $tabs_left;
$realm_allowed = [];
$realm_allowed[7] = is_realm_allowed(7);
$realm_allowed[8] = is_realm_allowed(8);
$realm_allowed[18] = is_realm_allowed(18);
$realm_allowed[19] = is_realm_allowed(19);
$realm_allowed[21] = is_realm_allowed(21);
$realm_allowed[22] = is_realm_allowed(22);
if ($realm_allowed[8]) {
$show_console_tab = true;
} else {
$show_console_tab = false;
}
if ($show_console_tab) {
$tabs_left[] =
[
'title' => __('Console'),
'id' => 'tab-console',
'url' => CACTI_PATH_URL . 'index.php',
];
}
if ($realm_allowed[7]) {
if (POLLER_ID > 1 && CACTI_CONNECTION != 'online') { // @phpstan-ignore-line
// Don't show the graphs tab when offline
} else {
$tabs_left[] =
[
'title' => __('Graphs'),
'id' => 'tab-graphs',
'url' => CACTI_PATH_URL . 'graph_view.php',
];
}
}
if ($realm_allowed[21] || $realm_allowed[22]) {
if (POLLER_ID > 1) {
// Don't show the reports tab on other pollers
} else {
$tabs_left[] =
[
'title' => __('Reporting'),
'id' => 'tab-reports',
'url' => CACTI_PATH_URL . 'reports.php'
];
}
}
if ($realm_allowed[18] || $realm_allowed[19]) {
$tabs_left[] =
[
'title' => __('Logs'),
'id' => 'tab-logs',
'url' => CACTI_PATH_URL . ($realm_allowed[18] ? 'clog.php' : 'clog_user.php'),
];
}
// Get Plugin Text Out of Band
ob_start();
api_plugin_hook('top_graph_header_tabs');
$tab_text = trim((string) ob_get_clean());
$tab_text = str_replace('<a', '', $tab_text);
$tab_text = str_replace('</a>', '|', $tab_text);
$tab_text = str_replace('<img', '', $tab_text);
$tab_text = str_replace('<', '', $tab_text);
$tab_text = str_replace('"', "'", $tab_text);
$tab_text = str_replace('>', '', $tab_text);
$elements = explode('|', $tab_text);
$count = 0;
foreach ($elements as $p) {
$p = trim($p);
if ($p == '') {
continue;
}
$altpos = strpos($p, 'alt=');
$hrefpos = strpos($p, 'href=');
$idpos = strpos($p, 'id=');
if ($altpos !== false) {
$alt = substr($p, $altpos + 4);
$parts = explode("'", $alt);
if ($parts[0] == '') {
$alt = $parts[1];
} else {
$alt = $parts[0];
}
} else {
$alt = __('Title');
}
if ($hrefpos !== false) {
$href = substr($p, $hrefpos + 5);
$parts = explode("'", $href);
if ($parts[0] == '') {
$href = $parts[1];
} else {
$href = $parts[0];
}
} else {
$href = 'unknown';
}
if ($idpos !== false) {
$id = substr($p, $idpos + 3);
$parts = explode("'", $id);
if ($parts[0] == '') {
$id = $parts[1];
} else {
$id = $parts[0];
}
} else {
$id = 'unknown' . $count;
$count++;
}
$tabs_left[] = ['title' => ucwords($alt), 'id' => 'tab-' . $id, 'url' => $href];
}
if (POLLER_ID > 1 && CACTI_CONNECTION != 'online') { // @phpstan-ignore-line
// Only show external links when online
} else {
$external_links = db_fetch_assoc('SELECT id, title
FROM external_links
WHERE style="TAB"
AND enabled="on"
ORDER BY sortorder');
if (cacti_sizeof($external_links)) {
foreach ($external_links as $tab) {
if (is_realm_allowed($tab['id'] + 10000)) {
$tabs_left[] =
[
'title' => $tab['title'],
'id' => 'tab-link' . $tab['id'],
'url' => CACTI_PATH_URL . 'link.php?id=' . $tab['id']
];
}
}
}
}
$i = 0;
$me_base = get_current_page();
foreach ($tabs_left as $tab) {
$tab_base = basename($tab['url']);
if ($tab_base == 'graph_view.php' && ($me_base == 'graph_view.php' || $me_base == 'graph.php')) {
$tabs_left[$i]['selected'] = true;
} elseif (isrv('id') && ($tab_base == 'link.php?id=' . gnrv('id')) && $me_base == 'link.php') {
$tabs_left[$i]['selected'] = true;
} elseif ($tab_base == 'index.php' && is_console_page($me_base)) {
$tabs_left[$i]['selected'] = true;
} elseif ($tab_base == $me_base) {
$tabs_left[$i]['selected'] = true;
} else {
$tabs_left[$i]['selected'] = false;
}
$i++;
}
$i = 0;
print "<div class='maintabs'><nav><ul role='tablist'>";
foreach ($tabs_left as $tab) {
if (isset($tab['id'])) {
$id = $tab['id'];
} else {
$id = 'anchor' . $i;
$i++;
}
print "<li><a id='$id' role='tab' class='lefttab" . ($tab['selected'] === true ? " selected' aria-selected='true'" : "' aria-selected='false'") . " href='" . htmle($tab['url']) . "'><span class='fa glyph_$id'></span><span class='text_$id'>" . htmle($tab['title']) . "</span></a><a id='menu-$id' class='maintabs-submenu' href='#'><i class='ti ti-chevron-down'></i></a></li>";
}
print "<li class='ellipsis maintabs-submenu-ellipsis'><a id='menu-ellipsis' role='tab' aria-selected='false' class='submenu-ellipsis' href='#'><i class='ti ti-chevron-down'></i></a></li>";
print '</ul></nav></div>';
}
function html_graph_tabs_right() : void {
global $tabs_right;
$theme = get_selected_theme();
$tabs_right = [];
if (is_view_allowed('show_tree')) {
$tabs_right[] = [
'title' => __('Tree View'),
'image' => get_theme_paths('%s', 'images/tab_tree.gif'),
'id' => 'tree',
'url' => 'graph_view.php?action=tree',
];
}
if (is_view_allowed('show_list')) {
$tabs_right[] = [
'title' => __('List View'),
'image' => get_theme_paths('%s', 'images/tab_list.gif'),
'id' => 'list',
'url' => 'graph_view.php?action=list',
];
}
if (is_view_allowed('show_preview')) {
$tabs_right[] = [
'title' => __('Preview'),
'image' => get_theme_paths('%s', 'images/tab_preview.gif'),
'id' => 'preview',
'url' => 'graph_view.php?action=preview',
];
}
$i = 0;
foreach ($tabs_right as $tab) {
if ($tab['id'] == 'tree') {
if (isrv('action') && gnrv('action') == 'tree') {
$tabs_right[$i]['selected'] = true;
}
} elseif ($tab['id'] == 'list') {
if (isrv('action') && gnrv('action') == 'list') {
$tabs_right[$i]['selected'] = true;
}
} elseif ($tab['id'] == 'preview') {
if (isrv('action') && gnrv('action') == 'preview') {
$tabs_right[$i]['selected'] = true;
}
} elseif (strstr(get_current_page(false), $tab['url'])) {
$tabs_right[$i]['selected'] = true;
}
if (!isset($tabs_right[$i]['selected'])) {
$tabs_right[$i]['selected'] = false;
}
$i++;
}
print "<div class='tabs' style='float:right;'><nav><ul role='tablist'>";
foreach ($tabs_right as $tab) {
switch($tab['id']) {
case 'tree':
if ($tab['image'] != '') {
print "<li><a id='treeview' role='tab' title='" . htmle($tab['title']) . "' class='righttab " . ($tab['selected'] === true ? " selected' aria-selected='true'" : "' aria-selected='false'") . " href='" . $tab['url'] . "'><img src='" . $tab['image'] . "' alt='' style='vertical-align:bottom;'></a></li>";
} else {
print "<li><a role='tab' title='" . htmle($tab['title']) . "' class='righttab " . ($tab['selected'] === true ? " selected' aria-selected='true'" : "' aria-selected='false'") . " href='" . $tab['url'] . "'>" . $tab['title'] . '</a></li>';
}
break;
case 'list':
if ($tab['image'] != '') {
print "<li><a id='listview' role='tab' title='" . htmle($tab['title']) . "' class='righttab " . ($tab['selected'] === true ? " selected' aria-selected='true'" : "' aria-selected='false'") . " href='" . $tab['url'] . "'><img src='" . $tab['image'] . "' alt='' style='vertical-align:bottom;'></a></li>";
} else {
print "<li><a role='tab' title='" . htmle($tab['title']) . "' class='righttab " . ($tab['selected'] === true ? " selected' aria-selected='true'" : "' aria-selected='false'") . " href='" . $tab['url'] . "'>" . $tab['title'] . '</a></li>';
}
break;
case 'preview':
if ($tab['image'] != '') {
print "<li><a role='tab' id='preview' title='" . htmle($tab['title']) . "' class='righttab " . ($tab['selected'] === true ? " selected' aria-selected='true'" : "' aria-selected='false'") . " href='" . $tab['url'] . "'><img src='" . $tab['image'] . "' alt='' style='vertical-align:bottom;'></a></li>";
} else {
print "<li><a role='tab' title='" . htmle($tab['title']) . "' class='righttab " . ($tab['selected'] === true ? " selected' aria-selected='true'" : "' aria-selected='false'") . " href='" . $tab['url'] . "'>" . $tab['title'] . '</a></li>';
}
break;
}
}
print '</ul></nav></div>';
}
function html_transform_graph_template_ids(mixed $ids) : string {
$return_ids = [];
if (str_contains($ids, ',')) {
$ids = explode(',', $ids);
} else {
$ids = [$ids];
}
foreach ($ids as $id) {
if (is_numeric($id)) {
$return_ids[] = intval($id);
} elseif (str_contains($id, 'cg_')) {
$cg_id = str_replace('cg_', '', $id);
// non-numeric remainder would coerce to 0 (not templated); skip it instead
if (is_numeric($cg_id)) {
$return_ids[] = intval($cg_id);
}
} else {
$id = str_replace('dq_', '', $id);
$graph_template_id = db_fetch_cell_prepared('SELECT graph_template_id
FROM snmp_query_graph
WHERE id = ?',
[$id]);
// a missing row returns false; skip it so id 0 (not templated) is not injected
if (is_numeric($graph_template_id)) {
$return_ids[] = intval($graph_template_id);
}
}
}
return implode(',', $return_ids);
}
function html_make_device_where() : string {
$sql_where = '';
if (isrv('site_id') && gfrv('site_id') > 0) {
$sql_where .= ($sql_where != '' ? ' AND ' : ' (') . 'h.site_id = ' . grv('site_id');
}
if (isrv('location') && gnrv('location') != '-1') {
$sql_where .= ($sql_where != '' ? ' AND ' : ' (') . 'h.location = ' . db_qstr(gnrv('location'));
}
if (isrv('host_template_id') && gfrv('host_template_id') > 0) {
$sql_where .= ($sql_where != '' ? ' AND ' : ' (') . 'h.host_template_id = ' . grv('host_template_id');
}
if (isrv('external_id') && gnrv('external_id') != '-1') {
$sql_where .= ($sql_where != '' ? ' AND ' : ' (') . 'h.external_id = ' . db_qstr(gnrv('external_id'));
}
if ($sql_where != '') {
$sql_where .= ')';
}
return $sql_where;
}
function html_graph_order_filter_array() : array {
$return = [];
if (read_config_option('dsstats_enable') == '') {
$data_sources = ['-1' => __('Enable Data Source Statistics to Sort')];
$return['graph_source'] = [
'method' => 'drop_array',
'friendly_name' => __('Source'),
'filter' => FILTER_CALLBACK,
'filter_options' => ['options' => 'sanitize_search_string'],
'array' => $data_sources,
'value' => '',
'default' => ''
];
} else {
$mode = read_config_option('dsstats_mode'); // 0 - Peak/Average only, 1 - Kitchen Sink
$peak = read_config_option('dsstats_peak'); // '' - Average CF Only, 'on' - Average and Max CF's
if (isrv('graph_template_id')) {
$graph_templates = html_transform_graph_template_ids(gnrv('graph_template_id'));
if (str_contains($graph_templates, ',') || $graph_templates == '' || $graph_templates <= 0) {
$show_sort = false;
$data_sources = ['-1' => __('Select a Single Template')];
} else {
$show_sort = true;
$data_sources = array_rekey(
db_fetch_assoc_prepared('SELECT DISTINCT data_source_name AS graph_source
FROM graph_templates_item AS gti
INNER JOIN data_template_rrd AS dtr
ON dtr.id = gti.task_item_id
WHERE graph_template_id = ?
AND local_graph_id = 0
ORDER BY data_source_name',
[$graph_templates]),
'graph_source', 'graph_source'
);
if (gnrv('graph_source') == '' ||
gnrv('graph_source') == '-1' ||
!in_array(gnrv('graph_source'), $data_sources, true)) {
if (cacti_sizeof($data_sources)) {
srv('graph_source', array_keys($data_sources)[0]);
srv('graph_order', 'desc');
}
}
}
$return['graph_source'] = [
'method' => 'drop_array',
'friendly_name' => __('Source'),
'filter' => FILTER_CALLBACK,
'filter_options' => ['options' => 'sanitize_search_string'],
'array' => $data_sources,
'value' => ''
];
if ($show_sort) {
$options = [
'asc' => __('Ascending'),
'desc' => __('Descending')
];
$return['graph_order'] = [
'method' => 'drop_array',
'friendly_name' => __('Order'),
'filter' => FILTER_CALLBACK,
'filter_options' => ['options' => 'sanitize_search_string'],
'default' => 'desc',
'array' => $options,
'value' => 'desc'
];
if ($peak == 'on') {
$options = [
'0' => __esc('Average'),
'1' => __esc('Maximum')
];
$return['cf'] = [
'method' => 'drop_array',
'friendly_name' => __('CF'),
'filter' => FILTER_VALIDATE_INT,
'default' => '0',
'array' => $options,
'value' => gnrv('cf')
];
}
if ($mode == 0) {
$options = [
'average' => __esc('Average'),
'peak' => __esc('Maximum')
];
$return['measure'] = [
'method' => 'drop_array',
'friendly_name' => __('Measure'),
'filter' => FILTER_CALLBACK,
'filter_options' => ['options' => 'sanitize_search_string'],
'default' => 'average',
'array' => $options,
'value' => gnrv('measure')
];
} else {
$options = [
'average' => __esc('Average'),
'peak' => __esc('Maximum'),
'p25n' => __esc('25th Percentile'),
'p50n' => __esc('50th Percentile (Median)'),
'p75n' => __esc('75th Percentile'),
'p90n' => __esc('90th Percentile'),
'p95n' => __esc('95th Percentile'),
'sum' => __esc('Total/Sum/Bandwidth')
];
$return['measure'] = [
'method' => 'drop_array',
'friendly_name' => __('Measure'),
'filter' => FILTER_CALLBACK,
'filter_options' => ['options' => 'sanitize_search_string'],
'default' => 'average',
'array' => $options,
'value' => gnrv('measure')
];
}
}
} else {
$data_sources = ['-1' => __('Select a Single Template')];
$return['graph_source'] = [
'method' => 'drop_array',
'friendly_name' => __('Source'),
'filter' => FILTER_CALLBACK,
'filter_options' => ['options' => 'sanitize_search_string'],
'array' => $data_sources,
'value' => ''
];
}
}
foreach (array_keys($return) as $return_key) {
$return[$return_key]['bar'] = true;
}
return $return;
}
function html_thumbnails_filter(string $callBack = 'applyGraphFilter') : string {
$checked = (grv('thumbnails') == 'true' ? ' checked' : '');
$output = "<span class='nowrap'>";
$output .= "<label class='checkboxSwitch'><input class='formCheckbox' id='thumbnails' name='thumbnails' type='checkbox' onClick='$callBack()'$checked><span class='checkboxSlider checkboxRound'></span></label>";
$output .= "<label class='checkboxLabelWanted' for='thumbnails'>" . __('Thumbnails') . '</label>';
$output .= '</span>';
return $output;
}
function html_business_hours_filter(string $callBack = 'applyGraphFilter') : string {
if (read_config_option('business_hours_enable') == 'on') {
$checked = (grv('business_hours') == 'true' ? ' checked' : '');
$output = "<span class='nowrap'>";
$output .= "<label class='checkboxSwitch'><input class='formCheckbox' id='business_hours' name='business_hours' type='checkbox' onClick='$callBack()'$checked><span class='checkboxSlider checkboxRound'></span></label>";
$output .= "<label class='checkboxLabelWanted' for='business_hours'>" . __('Business Hours') . '</label>';
$output .= '</span>';
return $output;
}
return '';
}
/**
* Generates an HTML host filter dropdown or input field based on configuration.
*
* @param mixed $host_id - The ID of the host to be selected by default. Defaults to '-1'.
* @param string $call_back - The JavaScript function to call when the selection changes. Defaults to 'applyFilter'.
* @param string $sql_where - Not applied: the option list is populated via the shared
* 'ajax_hosts' action, which is not safe to pass caller-supplied
* raw SQL to over the client/server boundary. Callers that need to
* constrain the device list must add their own SQL-safe filtering
* (e.g. by request var) inside their page's own 'ajax_hosts' handler.
* @param bool $noany - Whether to exclude the 'Any' option from the dropdown. Defaults to false.
* @param bool $nonone - Whether to exclude the 'None' option from the dropdown. Defaults to false.
*
* @return void
*/
function html_host_filter(mixed $host_id = -1, string $call_back = 'applyFilter', string $sql_where = '', bool $noany = false, bool $nonone = false) : void {
if (!str_contains($call_back, '()')) {
$call_back .= '()';
}
if ($host_id == '-1' && isrv('host_id')) {
$host_id = gfrv('host_id');
}
if ($host_id > 0) {
$hostname = db_fetch_cell_prepared('SELECT description
FROM host
WHERE id = ?',
[$host_id]);
} elseif ($host_id == 0) {
$hostname = __('None');
} else {
$hostname = __('Any');
}
?>
<td>
<?php print __('Device'); ?>
</td>
<td>
<select id='host_id' name='host_id' class='select2-callback' data-action='ajax_hosts' data-variables='site_id' data-noany='<?php print $noany ? '1' : '0'; ?>' data-nonone='<?php print $nonone ? '1' : '0'; ?>' data-callback='<?php print html_escape_attr($call_back); ?>'>
<option value='<?php print html_escape_attr($host_id); ?>' selected><?php print htmle($hostname); ?></option>
</select>
</td>
<?php
}
/**
* Generates an HTML dropdown filter for selecting a site.
*
* @param mixed $site_id - The ID of the site to be selected by default. Defaults to '-1'.
* @param string $call_back - The JavaScript function to call when the selection changes. Defaults to 'applyFilter'.
* @param string $sql_where - Additional SQL WHERE clause to filter the sites. Defaults to an empty string.
* @param bool $noany - Whether to exclude the 'Any' option from the dropdown. Defaults to false.
* @param bool $nonone - Whether to exclude the 'None' option from the dropdown. Defaults to false.
*
* @return void
*/
function html_site_filter(mixed $site_id = -1, string $call_back = 'applyFilter', string $sql_where = '', bool $noany = false, bool $nonone = false) : void {
$theme = get_selected_theme();
if (!str_contains($call_back, '()')) {
$call_back .= '()';
}
if ($site_id == '-1' && isrv('site_id')) {
$site_id = gfrv('site_id');
}
?>
<td>
<?php print __('Site'); ?>
</td>
<td>
<select id='site_id' onChange='<?php print $call_back; ?>' data-defaultLabel='<?php print __('Site'); ?>'>
<?php if (!$noany) {?><option value='-1'<?php if ($site_id == '-1') {?> selected<?php }?>><?php print __('Any'); ?></option><?php }?>
<?php if (!$nonone) {?><option value='0'<?php if ($site_id == '0') {?> selected<?php }?>><?php print __('None'); ?></option><?php }?>
<?php
$sites = get_allowed_sites($sql_where);
if (cacti_sizeof($sites)) {
foreach ($sites as $site) {
print "<option value='" . $site['id'] . "'" . ($site_id == $site['id'] ? ' selected' : '') . '>' . htmle($site['name']) . '</option>';
}
}
?>
</select>
</td>
<?php
}
/**
* Generates an HTML dropdown filter for selecting a location.
*
* @param string $location The currently selected location value. Default is an empty string.
* @param string $call_back The JavaScript function to call when the selection changes. Default is 'applyFilter'.
* @param string $sql_where Additional SQL WHERE clause to filter the locations. Default is an empty string.
* @param bool $noany If true, the "Any" option will not be included in the dropdown. Default is false.
* @param bool $nonone If true, the "None" option will not be included in the dropdown. Default is false.
*
* @return void
*/
function html_location_filter(string $location = '', string $call_back = 'applyFilter', string $sql_where = '', bool $noany = false, bool $nonone = false) : void {
$theme = get_selected_theme();
if (!str_contains($call_back, '()')) {
$call_back .= '()';
}
?>
<td>
<?php print __('Location'); ?>
</td>
<td>
<select id='location' onChange='<?php print $call_back; ?>'>
<?php if (!$noany) {?><option value='-1'<?php if ($location == '-1') {?> selected<?php }?>><?php print __('Any'); ?></option><?php }?>
<?php if (!$nonone) {?><option value='0'<?php if ($location == '0') {?> selected<?php }?>><?php print __('None'); ?></option><?php }?>
<?php
if ($sql_where != '' && !str_contains($sql_where, 'WHERE')) {
$sql_where = 'WHERE ' . $sql_where;
}
$locations = array_rekey(
db_fetch_assoc("SELECT DISTINCT location
FROM host AS h
$sql_where
ORDER BY location ASC"),
'location', 'location'
);
if (cacti_sizeof($locations)) {
foreach ($locations as $l) {
if ($l == '') {
continue;
}
print "<option value='" . htmle($l) . "'" . ($location == $l ? ' selected' : '') . '>' . htmle($l) . '</option>';
}
}
?>
</select>
</td>
<?php
}
/**
* Generates the HTML for spike kill actions.
*
* This function is responsible for creating the HTML elements
* necessary for spike kill actions within the application.
*
* @return void
*/
function html_spikekill_actions() : void {
switch(gnrv('action')) {
case 'spikemenu':
html_spikekill_menu(gfrv('local_graph_id'));
break;
case 'spikesave':
switch(gnrv('setting')) {
case 'ravgnan':
$id = gnrv('id');
switch($id) {
case 'avg':
case 'last':
case 'nan':
set_user_setting('spikekill_avgnan', $id);
break;
}
break;
case 'rstddev':
set_user_setting('spikekill_deviations', gfrv('id'));
break;
case 'rkills':
set_user_setting('spikekill_number', gfrv('id'));
break;
case 'rabsmax':
set_user_setting('spikekill_absmax', gfrv('id'));
break;
}
break;
}
}
/**
* Retrieves the spike kill setting for a given name.
*
* This function reads a user-specific setting for the provided name. If the user-specific
* setting is not available, it falls back to the default configuration option.
*
* @param string $name The name of the setting to retrieve.
*
* @return mixed The value of the spike kill setting.
*/
function html_spikekill_setting(string $name) : mixed {
return read_user_setting($name, read_config_option($name), true);
}
/**
* Generates an HTML list item for a spike kill menu.
*
* @param string $text - The text content of the menu item.
* @param string $icon - The icon class for the menu item.
* @param string $class - Additional CSS classes for the menu item.
* @param string $id - The ID attribute for the menu item.
* @param mixed $data_graph - The data-graph attribute for the menu item.
* @param string $subitem - Submenu items in HTML format.
*
* @return string The generated HTML for the menu item.
*/
function html_spikekill_menu_item(string $text, string $icon = '', string $class = '', string $id = '', mixed $data_graph = '', string $subitem = '') : string {
$output = '<li ';
if (!empty($id)) {
$output .= "id='$id' ";
}
if (!empty($data_graph)) {
$output .= "data-graph='$data_graph' ";
}
$output .= 'class=\'' . (empty($class) ? '' : " $class") . '\'>';
$output .= '<span class=\'spikeKillMenuItem\'>';
if (!empty($icon)) {
$output .= "<i class='$icon'></i>";
}
$output .= "$text</span>";
if (!empty($subitem)) {
$output .= "<ul>$subitem</ul>";
}
$output .= '</li>';
return $output;
}
/**
* Generates the HTML for the SpikeKill menu.
*
* This function creates a menu for configuring the SpikeKill settings in Cacti.
* It includes options for various replacement methods, standard deviations, variance percentages,
* variance outliers, kills per RRA, and absolute maximum values.
*
* @param int $local_graph_id The ID of the local graph.
*
* @return void
*/
function html_spikekill_menu(int $local_graph_id) : void {
global $settings;
$ravgnan1 = html_spikekill_menu_item(__('Average'), html_spikekill_setting('spikekill_avgnan') == 'avg' ? 'ti ti-check' : 'fa', 'skmethod', 'method_avg');
$ravgnan2 = html_spikekill_menu_item(__('NaN\'s'), html_spikekill_setting('spikekill_avgnan') == 'nan' ? 'ti ti-check' : 'fa', 'skmethod', 'method_nan');
$ravgnan3 = html_spikekill_menu_item(__('Last Known Good'), html_spikekill_setting('spikekill_avgnan') == 'last' ? 'ti ti-check' : 'fa', 'skmethod', 'method_last');
$ravgnan = html_spikekill_menu_item(__('Replacement Method'), '', '', '', '', $ravgnan1 . $ravgnan2 . $ravgnan3);
$rstddev = '';
foreach ($settings['spikes']['spikekill_deviations']['array'] as $key => $value) {
$rstddev .= html_spikekill_menu_item($value, html_spikekill_setting('spikekill_deviations') == $key ? 'ti ti-check' : 'fa', 'skstddev', 'stddev_' . $key);
}
$rstddev = html_spikekill_menu_item(__('Standard Deviations'), '', '', '', '', $rstddev);
$rkills = '';
foreach ($settings['spikes']['spikekill_number']['array'] as $key => $value) {
$rkills .= html_spikekill_menu_item($value,html_spikekill_setting('spikekill_number') == $key ? 'ti ti-check' : 'fa', 'skills', 'kills_' . $key);
}
$rkills = html_spikekill_menu_item(__('Kills Per RRA'), '', '', '', '', $rkills);
$rabsmax = '';
foreach ($settings['spikes']['spikekill_absmax']['array'] as $key => $value) {
$rabsmax .= html_spikekill_menu_item($value, html_spikekill_setting('spikekill_absmax') == $key ? 'ti ti-check' : 'fa', 'skabsmax', 'absmax_' . $key);
}
$rabsmax = html_spikekill_menu_item(__('Absolute Max Value'), '', '', '', '', $rabsmax);
?>
<div class='spikekillParent' style='display:none;z-index:20;position:absolute;text-align:left;white-space:nowrap;padding-right:2px;'>
<ul class='spikekillMenu' style='font-size:1em;'>
<?php
print html_spikekill_menu_item(__('Remove StdDev'), 'deviceUp ti ti-lifebuoy-filled', 'rstddev', '', $local_graph_id);
print html_spikekill_menu_item(__('Gap Fill Range'), 'deviceUnknown ti ti-lifebuoy-filled', 'rfill', '', $local_graph_id);
print html_spikekill_menu_item(__('Float Range'), 'deviceDown ti ti-lifebuoy-filled', 'rfloat', '', $local_graph_id);
print html_spikekill_menu_item(__('Absolute Maximum'), 'deviceError ti ti-lifebuoy-filled', 'rabsolute', '', $local_graph_id);
print html_spikekill_menu_item(__('Dry Run StdDev'), 'deviceUp ti ti-check', 'dstddev', '', $local_graph_id);
print html_spikekill_menu_item(__('Dry Run Gap Fill Range'), 'deviceUnknown ti ti-check', 'dfill', '', $local_graph_id);
print html_spikekill_menu_item(__('Dry Run Float Range'), 'deviceDown ti ti-check', 'dfloat', '', $local_graph_id);
print html_spikekill_menu_item(__('Dry Run Absolute Maximum'), 'deviceError ti ti-check', 'dabsolute', '', $local_graph_id);
print html_spikekill_menu_item(__('Settings'), 'ti ti-settings-filled', '', '', '', $ravgnan . $rstddev . $rkills . $rabsmax);
}
function html_spikekill_js() : void {
?>
<script type='text/javascript' <?php print CactiSecureHeaders::getNonceAttribute(); ?>>
var spikeKillOpen = false;
$(function() {
$(document).click(function() {
spikeKillClose();
});
$('a.spikekill').children().contextmenu(function() {
return false;
});
$('a.spikekill').unbind().click(function(event) {
event.preventDefault();
if (spikeKillOpen == false) {
local_graph_id = $(this).attr('data-graph');
$.get('?action=spikemenu&local_graph_id='+local_graph_id)
.done(function(data) {
$('#sk'+local_graph_id).after(data);
menuAnchor = $('#sk'+local_graph_id).offset().left;
pageWidth = $(document).width();
if (pageWidth - menuAnchor < 180) {
$('.spikekillMenu').css({ position: 'absolute', top: 0, left: -180 });
}
$('.spikekillMenu').menu({
select: function(event, ui) {
$(this).menu('focus', event, ui.item);
},
delay: 1000
});
$('.spikekillParent').show();
spikeKillActions();
spikeKillOpen = true;
})
.fail(function(data) {
getPresentHTTPError(data);
});
} else {
spikeKillClose();
}
});
});
function spikeKillClose() {
if (spikeKillOpen) {
$(document).find('.spikekillMenu').menu('destroy').parent().remove();
spikeKillOpen = false;
}
}
function spikeKillActions() {
$('.rstddev').unbind().click(function() {
removeSpikes('stddev', false, $(this).attr('data-graph'));
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
});
$('.dstddev').unbind().click(function() {
removeSpikes('stddev', true, $(this).attr('data-graph'));
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
});
$('.rfill').unbind().click(function() {
removeSpikes('fill', false, $(this).attr('data-graph'));
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
});
$('.dfill').unbind().click(function() {
removeSpikes('fill', true, $(this).attr('data-graph'));
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
});
$('.rfloat').unbind().click(function() {
removeSpikes('float', false, $(this).attr('data-graph'));
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
});
$('.dfloat').unbind().click(function() {
removeSpikes('float', true, $(this).attr('data-graph'));
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
});
$('.rabsolute').unbind().click(function() {
removeSpikes('absolute', false, $(this).attr('data-graph'));
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
});
$('.dabsolute').unbind().click(function() {
removeSpikes('absolute', true, $(this).attr('data-graph'));
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
});
$('.skmethod').unbind().click(function() {
$('.skmethod').find('i').removeClass('ti ti-check');
$(this).find('i:first').addClass('ti ti-check');
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
strURL = '?action=spikesave&setting=ravgnan&id='+$(this).attr('id').replace('method_','');
$.get(strURL)
.fail(function(data) {
getPresentHTTPError(data);
});
});
$('.skills').unbind().click(function() {
$('.skills').find('i').removeClass('ti ti-check');
$(this).find('i:first').addClass('ti ti-check');
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
strURL = '?action=spikesave&setting=rkills&id='+$(this).attr('id').replace('kills_','');
$.get(strURL)
.fail(function(data) {
getPresentHTTPError(data);
});
});
$('.skstddev').unbind().click(function() {
$('.skstddev').find('i').removeClass('ti ti-check');
$(this).find('i:first').addClass('ti ti-check');
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
strURL = '?action=spikesave&setting=rstddev&id='+$(this).attr('id').replace('stddev_','');
$.get(strURL)
.fail(function(data) {
getPresentHTTPError(data);
});
});
$('.skabsmax').unbind().click(function() {
$('.skabsmax').find('i').removeClass('ti ti-check');
$(this).find('i:first').addClass('ti ti-check');
$(this).find('.spikekillMenu').menu('destroy').parent().remove();
strURL = '?action=spikesave&setting=rabsmax&id='+$(this).attr('id').replace('absmax_','');
$.get(strURL)
.fail(function(data) {
getPresentHTTPError(data);
});
});
}
</script>
<?php
}
/**
* html_common_header - prints a common set of header, css and javascript links
*
* @param string $title - the title of the page to place in the browser
* @param string $selectedTheme - optionally sets a specific theme over the current one
*
* @return void
*/
function html_common_header(string $title, string $selectedTheme = '') : void {
global $path2calendar, $path2timepicker, $path2colorpicker, $path2ms, $path2msfilter;
if ($selectedTheme == '') {
$selectedTheme = get_selected_theme();
}
print "<meta content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5' name='viewport'>" . PHP_EOL;
$alternates = htmle(read_config_option('content_security_alternate_sources'));
$script_src = CactiSecureHeaders::scriptSrc($alternates);
?>
<meta http-equiv='X-UA-Compatible' content='IE=Edge,chrome=1'>
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name='description' content='Monitoringauth tool of the Internet'>
<meta name='mobile-web-app-capable' content='yes'>
<meta name="theme-color" content="#161616"/>
<meta http-equiv="Content-Security-Policy" content="default-src *; img-src 'self' https://api.qrserver.com <?php print $alternates; ?> data: blob:; style-src 'self' 'unsafe-inline' <?php print $alternates; ?>; <?php print $script_src; ?>; worker-src 'self' <?php print $alternates; ?>;">
<title><?php print html_escape($title); ?></title>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>
<link rel='manifest' href='/manifest.json'>
<script type='text/javascript' <?php print CactiSecureHeaders::getNonceAttribute(); ?>>
var urlPath='<?php print CACTI_PATH_URL; ?>';
var aboutCacti = '<?php print __esc('About Cacti'); ?>';
var cactiCharts = '<?php print __esc('Charts'); ?>';
var cactiClient = '<?php print __esc('Client'); ?>';
var cactiCommunityForum = '<?php print __esc('User Community'); ?>';
var cactiConsole = '<?php print __esc('Console'); ?>';
var cactiConsoleAllowed=<?php print(is_realm_allowed(8) ? 'true' : 'false'); ?>;
var cactiContributeTo = '<?php print __esc('Contribute to the Cacti Project'); ?>';
var cactiDashboards = '<?php print __esc('Panels'); ?>';
var cactiDevHelp = '<?php print __esc('Help in Developing'); ?>';
var cactiDocumentation = '<?php print __esc('Documentation'); ?>';
var cactiDonate = '<?php print __esc('Donation & Sponsoring'); ?>';
var cactiGeneral = '<?php print __esc('General'); ?>';
var cactiGraphsAllowed=<?php print(is_realm_allowed(7) ? 'true' : 'false'); ?>;
var cactiHome = '<?php print __esc('Cacti Home'); ?>';
var cactiKeyboard = '<?php print __esc('Keyboard'); ?>';
var cactiMisc = '<?php print __esc('Miscellaneous'); ?>';
var cactiProfile = '<?php print __esc('Profile'); ?>';
var cactiProjectPage = '<?php print __esc('Cacti Project Page'); ?>';
var cactiRRDProxy = '<?php print __esc('RRDProxy'); ?>';
var cactiShortcuts = '<?php print __esc('Shortcuts'); ?>';
var cactiSpine = '<?php print __esc('Spine'); ?>';
var cactiTheme = '<?php print __esc('Theme'); ?>';
var cactiTranslate = '<?php print __esc('Help in Translating'); ?>';
var cactiUser = '<?php print __esc('User'); ?>';
var changePassword = '<?php print __esc('Change Password'); ?>';
var clearFilterTitle = '<?php print __esc('Clear Current Filter'); ?>';
var clipboard = '<?php print __esc('Clipboard'); ?>';
var clipboardCopyFailed = '<?php print __esc('Failed to find data to copy!'); ?>';
var clipboardID = '<?php print __esc('Clipboard ID'); ?>';
var clipboardNotAvailable = '<?php print __esc('Copy operation is unavailable at this time'); ?>';
var clipboardNotUpdated = '<?php print __esc('Sorry, your clipboard could not be updated at this time'); ?>';
var clipboardUpdated = '<?php print __esc('Clipboard has been updated'); ?>';
var compactGraphicalUserInterface = '<?php print __esc('Compact Mode'); ?>';
var keyup_delay = <?php print get_keyup_delay(); ?>;
var darkColorMode = '<?php print __esc('Dark Color Mode'); ?>';
var defaultSNMPAuthProtocol = '<?php print read_config_option('snmp_auth_protocol'); ?>';
var defaultSNMPPrivProtocol = '<?php print read_config_option('snmp_priv_protocol'); ?>';
var defaultSNMPSecurityLevel = '<?php print read_config_option('snmp_security_level'); ?>';
var editProfile = '<?php print __esc('Edit Profile'); ?>';
var errorNumberPrefix = '<?php print __esc('Error:'); ?>';
var errorOnPage = '<?php print __esc('Sorry, we could not process your last action.'); ?>';
var errorReasonPrefix = '<?php print __esc('Reason:'); ?>';
var errorReasonTitle = '<?php print __esc('Action failed'); ?>';
var errorReasonUnexpected = '<?php print __esc('The response to the last action was unexpected.'); ?>';
var filterSettingsSaved = '<?php print __esc('Filter Settings Saved'); ?>';
var hScroll=<?php print read_user_setting('enable_hscroll', '') == 'on' ? 'true' : 'false'; ?>;
var help = '<?php print __esc('Help'); ?>';
var ignorePreferredColorTheme = '<?php print __esc('Ignore System Color'); ?>';
var justCacti = '<?php print __esc('Cacti'); ?>';
var lightColorMode = '<?php print __esc('Light Color Mode'); ?>';
var listView = '<?php print __esc('List View'); ?>';
var logout = '<?php print __esc('Logout'); ?>';
var mixedOnPage = '<?php print __esc('Note, we could not process all your actions. Details are below.'); ?>';
var mixedReasonTitle = '<?php print __esc('Some Actions failed'); ?>';
var noFileSelected = '<?php print __esc('No file selected'); ?>';
var passwordMinChars = <?php print read_config_option('secpass_minlen'); ?>;
var passwordMatch = '<?php print __esc('Passphrases match'); ?>';
var passwordMatchTooShort = '<?php print __esc('Passphrase matches but too short'); ?>';
var passwordNotMatch = '<?php print __esc('Passphrases do not match'); ?>';
var passwordNotMatchTooShort = '<?php print __esc('Passphrase too short and not matching'); ?>';
var passwordPass = '<?php print __esc('Passphrase length meets 8 character minimum'); ?>';
var passwordTooShort = '<?php print __esc('Passphrase too short'); ?>';
var passwordValid = '<?php print __esc('Password Validation Passes'); ?>';
var previewView = '<?php print __esc('Preview View'); ?>';
var realtimeClickOff = '<?php print __esc('Click again to take this Graph out of Realtime'); ?>';
var realtimeClickOn = '<?php print __esc('Click to view just this Graph in Realtime'); ?>';
var reportABug = '<?php print __esc('Report a bug'); ?>';
var searchFilter = '<?php print __esc('Enter a search term'); ?>';
var searchRFilter = '<?php print __esc('Enter a regular expression'); ?>';
var searchSelect = '<?php print __esc('Select to Search'); ?>';
var searchPlaceholder = '<?php print __esc('Search'); ?>';
var searchEnterKeyword = '<?php print __esc('Enter keyword'); ?>';
var sessionMessageCancel = '<?php print __esc('Cancel'); ?>';
var sessionMessageContinue = '<?php print __esc('Continue'); ?>';
var sessionMessageOk = '<?php print __esc('Ok'); ?>';
var sessionMessagePause = '<?php print __esc('Pause'); ?>';
var sessionMessageSave = '<?php print __esc('The Operation was successful. Details are below.'); ?>';
var sessionMessageTitle = '<?php print __esc('Operation successful'); ?>';
var showHideFilter = '<?php print __esc('Click to Show/Hide Filter'); ?>';
var spikeKillResults = '<?php print __esc('SpikeKill Results'); ?>';
var standardGraphicalUserInterface = '<?php print __esc('Standard Mode'); ?>';
var tableConstraints = '<?php print __esc('Allow or limit the table columns to extend beyond the current windows limits.'); ?>';
var testFailed = '<?php print __esc('Connection Failed'); ?>';
var testSuccessful = '<?php print __esc('Connection Successful'); ?>';
var theme = '<?php print $selectedTheme; ?>';
var timeGraphView = '<?php print __esc('Time Graph View'); ?>';
var treeView = '<?php print __esc('Tree View'); ?>';
var usePreferredColorTheme = '<?php print __esc('Use System Color'); ?>';
var userSettings=<?php print is_view_allowed('graph_settings') ? 'true' : 'false'; ?>;
var utilityView = '<?php print __esc('Utility View'); ?>';
var allSelectedText = '<?php print __('All Graph Templates'); ?>';
var templatesSelected = '<?php print __esc('Templates Selected'); ?>';
var notTemplated = '<?php print __esc('Not Templated'); ?>';
var multiSelectAllText = <?php print cacti_js_encode(__('All Selected')); ?>;
var multiSelectCountText = <?php print cacti_js_encode(__('Selected')); ?>;
var allText = '<?php __esc('All'); ?>';
var noneText = '<?php print __esc('None'); ?>';
var zoom_i18n_3rd_button = '<?php print __esc('3rd Mouse Button'); ?>';
var zoom_i18n_advanced = '<?php print __esc('Advanced'); ?>';
var zoom_i18n_auto = '<?php print __esc('Auto'); ?>';
var zoom_i18n_begin = '<?php print __esc('Begin with'); ?>';
var zoom_i18n_center = '<?php print __esc('Center'); ?>';
var zoom_i18n_close = '<?php print __esc('Close'); ?>';
var zoom_i18n_copy_graph = '<?php print __esc('Copy graph'); ?>';
var zoom_i18n_copy_graph_link = '<?php print __esc('Copy graph link'); ?>';
var zoom_i18n_disabled = '<?php print __esc('Disabled'); ?>';
var zoom_i18n_end = '<?php print __esc('End with'); ?>';
var zoom_i18n_graph = '<?php print __esc('Graph'); ?>';
var zoom_i18n_mode = '<?php print __esc('Zoom Mode'); ?>';
var zoom_i18n_newTab = '<?php print __esc('Open in new tab'); ?>';
var zoom_i18n_off = '<?php print __esc('Always Off'); ?>';
var zoom_i18n_on = '<?php print __esc('Always On'); ?>';
var zoom_i18n_quick = '<?php print __esc('Quick'); ?>';
var zoom_i18n_save_graph = '<?php print __esc('Save graph'); ?>';
var zoom_i18n_settings = '<?php print __esc('Settings'); ?>';
var zoom_i18n_timestamps = '<?php print __esc('Timestamps'); ?>';
var zoom_i18n_zoom_2 = '<?php print __esc('2x'); ?>';
var zoom_i18n_zoom_4 = '<?php print __esc('4x'); ?>';
var zoom_i18n_zoom_8 = '<?php print __esc('8x'); ?>';
var zoom_i18n_zoom_16 = '<?php print __esc('16x'); ?>';
var zoom_i18n_zoom_32 = '<?php print __esc('32x'); ?>';
var zoom_i18n_zoom_in = '<?php print __esc('Zoom In'); ?>';
var zoom_i18n_zoom_out = '<?php print __esc('Zoom Out'); ?>';
var zoom_i18n_zoom_out_factor = '<?php print __esc('Zoom Out Factor'); ?>';
var zoom_i18n_zoom_out_positioning = '<?php print __esc('Zoom Out Positioning'); ?>';
var zoom_outOfRangeTitle='<?php print __esc('Zoom Window Out of Range'); ?>';
var zoom_outOfRangeMessage='<?php print __esc('Zoom Dates before January 1, 1993 are not supported in Cacti! Pick a more recent date.'); ?>';
</script>
<?php
// Global icons
print get_md5_include_icon('images', theme: $selectedTheme, file: 'favicon.ico', rel: 'shortcut icon');
print get_md5_include_icon('images', theme: $selectedTheme, file: 'cacti_logo.gif', rel: 'icon', sizes: '96x96');
// Global styles that can be overloaded by themes css as required.
print get_md5_include_css('include/css/jquery.contextMenu.css');
print get_md5_include_css('include/css/billboard.css');
print get_md5_include_css('include/fa/css/all.css');
print get_md5_include_css('include/tabler/dist/tabler-icons.min.css');
print get_md5_include_css('include/vendor/lipis/flag-icons/css/flag-icons.css');
// Theme-based styles
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'jquery.zoom.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'jquery-ui.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'default/style.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'jquery.multiselect.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'jquery.multiselect.filter.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'jquery.timepicker.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'jquery.colorpicker.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'select2.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'pace.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'Diff.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'jquery.toast.css');
print get_md5_include_css('include/css/', theme: $selectedTheme, file: 'main.css');
// Global styles
print get_md5_include_css('include/themes/' . $selectedTheme . '/main.css');
// Global scripts
print get_md5_include_js('include/js/screenfull.js', true);
print get_md5_include_js('include/js/jquery.js');
// htmx loads after jQuery so jQuery-based plugins initialised below remain
// the primary navigation/AJAX layer. htmx is opt-in per element via hx-*
// attributes; hx-boost is deliberately not set globally to avoid interfering
// with Cacti's existing form submission and loadUrl() patterns.
print htmx_script_tag();
print get_md5_include_js('include/js/jquery-ui.js');
print get_md5_include_js('include/js/jquery.ui.touch.punch.js', true);
print get_md5_include_js('include/js/jquery.cookie.js');
print get_md5_include_js('include/js/js.storage.js');
print get_md5_include_js('include/js/jstree.js');
print get_md5_include_js('include/js/jquery.toast.js');
print get_md5_include_js('include/js/jquery.hotkeys.js', true);
print get_md5_include_js('include/js/jquery.tablednd.js');
print get_md5_include_js('include/js/jquery.zoom.js', true);
print get_md5_include_js('include/js/jquery.contextMenu.js', true);
print get_md5_include_js('include/js/jquery.multiselect.js');
print get_md5_include_js('include/js/jquery.multiselect.filter.js');
print get_md5_include_js('include/js/jquery.timepicker.js');
print get_md5_include_js('include/js/jquery.colorpicker.js', true);
print get_md5_include_js('include/js/select2.js');
print get_md5_include_js('include/js/jquery.tablesorter.js');
print get_md5_include_js('include/js/jquery.tablesorter.widgets.js', true);
print get_md5_include_js('include/js/jquery.tablesorter.pager.js', true);
print get_md5_include_js('include/js/jquery.validate/jquery.validate.js', true);
print get_md5_include_js('include/js/Chart.js', true);
print get_md5_include_js('include/js/d3.js');
print get_md5_include_js('include/js/billboard.js');
print get_md5_include_js('include/layout.js');
print get_md5_include_js('include/js/pace.js');
print get_md5_include_js('include/js/purify.js');
print get_md5_include_js('include/realtime.js');
print get_md5_include_js('include/js/ui-notices.js');
print get_md5_include_js('include/js/lzjs.js');
print get_md5_include_js('include/js/big.js');
// Main theme based scripts (included last to allow overrides)
print get_md5_include_js('include/css/', theme: $selectedTheme, file: 'main.js');
// Language based scripts
if (isset($path2calendar) && file_exists($path2calendar)) {
print get_md5_include_js($path2calendar);
}
if (isset($path2timepicker) && file_exists($path2timepicker)) {
print get_md5_include_js($path2timepicker);
}
if (isset($path2colorpicker) && file_exists($path2colorpicker)) {
print get_md5_include_js($path2colorpicker);
}
if (isset($path2ms) && file_exists($path2ms)) {
print get_md5_include_js($path2ms);
}
if (isset($path2msfilter) && file_exists($path2msfilter)) {
print get_md5_include_js($path2msfilter);
}
if (file_exists('include/css/custom.css')) {
print get_md5_include_css('include/css/custom.css');
}
api_plugin_hook('page_head');
}
/**
* Generates the URL for the help page corresponding to the given page.
*
* This function maps a given page to its corresponding help documentation URL.
* It uses a predefined array of page-to-help mappings and allows for plugin
* hooks to modify or extend these mappings.
*
* @param string $page The page for which the help URL is to be generated.
*
* @return mixed - The URL to the help documentation if the page is found, false otherwise.
*/
function html_help_page(string $page) : mixed {
global $help;
$help = [
'aggregates.php' => 'Aggregates.html',
'aggregate_templates.php' => 'Aggregate-Templates.html',
'automation_networks.php' => 'Automation-Networks.html',
'cdef.php' => 'CDEFs.html',
'color_templates.php' => 'Color-Templates.html',
'color.php' => 'Colors.html',
'pollers.php' => 'Data-Collectors.html',
'data_debug.php' => 'Data-Debug.html',
'data_input.php' => 'Data-Input-Methods.html',
'data_source_profiles.php' => 'Data-Profiles.html',
'data_queries.php' => 'Data-Queries.html',
'data_templates.php' => 'Data-Source-Templates.html',
'data_sources.php' => 'Data-Sources.html',
'host.php' => 'Devices.html',
'automation_templates.php' => 'Device-Rules.html',
'host_templates.php' => 'Device-Templates.html',
'automation_devices.php' => 'Discovered-Devices.html',
'templates_export.php' => 'Export-Template.html',
'links.php' => 'External-Links.html',
'gprint_presets.php' => 'GPRINTs.html',
'graphs_new.php' => 'Graph-a-Single-SNMP-OID.html',
'graph_view.php' => 'Graph-Overview.html',
'automation_graph_rules.php' => 'Graph-Rules.html',
'graph_templates.php' => 'Graph-Templates.html',
'graphs.php' => 'Graphs.html',
'templates_import.php' => 'Import-Template.html',
'plugins.php' => 'Plugins.html',
'automation_snmp.php' => 'SNMP-Options.html',
'settings.php:authentication' => 'Settings-Auth.html',
'settings.php:data' => 'Settings-Data.html',
'settings.php:snmp' => 'Settings-Device-Defaults.html',
'settings.php:general' => 'Settings-General.html',
'settings.php:mail' => 'Settings-Mail-Reporting-DNS.html',
'settings.php:path' => 'Settings-Paths.html',
'settings.php:boost' => 'Settings-Performance.html',
'settings.php:poller' => 'Settings-Poller.html',
'settings.php:spikes' => 'Settings-Spikes.html',
'settings.php:visual' => 'Settings-Visual.html',
'sites.php' => 'Sites.html',
'automation_tree_rules.php' => 'Tree-Rules.html',
'tree.php' => 'Trees.html',
'login_providers.php' => 'Login-Providers.html',
'user_group_admin.php' => 'User-Group-Management.html',
'user_admin.php' => 'User-Management.html',
'vdef.php' => 'VDEFs.html',
'reports.php' => 'Reports-Admin.html',
'reports.php:details' => 'Reports-Admin.html',
'reports.php:items' => 'Reports-Items.html',
'reports.php:preview' => 'Reports-Preview.html',
'reports.php:events' => 'Reports-Events.html',
'clog.php' => 'Cacti-Log.html',
'clog_user.php' => 'Cacti-Log.html',
];
$help = api_plugin_hook_function('help_page', $help);
if (isset($help[$page])) {
return CACTI_PATH_URL . 'docs/' . $help[$page];
}
return false;
}
/**
* Generates the HTML authentication header.
*
* This function outputs the HTML structure for the authentication header, including
* the DOCTYPE declaration, HTML head, and body content. It also integrates plugin hooks
* for customization.
*
* @param string $section The section identifier for the authentication header.
* @param string $browser_title The title to be displayed in the browser's title bar.
* @param string $legend The legend text to be displayed in the authentication area.
* @param string $title The title text to be displayed in the authentication form.
* @param array $hook_args Optional. Additional arguments to be passed to plugin hooks. Default is an empty array.
*
* @return void
*/
function html_auth_header(string $section, string $browser_title, string $legend, string $title, array $hook_args = []) : void {
global $themes;
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<!-- <?php print "{$section}_title"; ?> -->
<?php html_common_header(api_plugin_hook_function("{$section}_title", $browser_title)); ?>
</head>
<body>
<div class='cactiAuthBody'>
<div class='cactiAuthCenter'>
<div class='cactiAuthArea'>
<legend><?php print $legend; ?></legend><hr />
<form id='auth' name='auth' method='post' action='<?php print get_current_page(); ?>'>
<input type='hidden' name='action' value='<?php print $section; ?>'>
<?php api_plugin_hook_function("{$section}_before", $hook_args); ?>
<div class='cactiAuthTitle'>
<table class='cactiAuthTable'>
<tr><td><?php print $title; ?></td></tr>
</table>
</div>
<div class='cactiAuth'>
<table class='cactiAuthTable'>
<?php
}
/**
* Renders the footer section of the authentication HTML page.
*
* This function outputs the closing HTML tags for the authentication page,
* including any error messages, version information, and additional HTML content.
* It also triggers a plugin hook and includes the global session file.
*
* @param string $section The section identifier used for the plugin hook.
* @param string $error Optional. The error message to display. Default is an empty string.
* @param string $html Optional. Additional HTML content to include. Default is an empty string.
*
* @return void
*/
function html_auth_footer(string $section, string $error = '', string $html = '') : void {
?>
</table>
</div>
<?php api_plugin_hook("{$section}_after"); ?>
</form>
<hr />
<div class='cactiAuthErrors'>
<?php print htmle($error); ?>
</div>
<div class='versionInfo'>
<?php print __('Version %s | %s', CACTI_VERSION_BRIEF, COPYRIGHT_YEARS_SHORT); ?>
</div>
</div>
<div class='cactiAuthLogo'></div>
</div>
<?php
print $html;
include_once(__DIR__ . '/../include/global_session.php');
?>
</div>
</body>
</html>
<?php
}