5958 Commits
Author SHA1 Message Date
Thomas VincentandTheWitness 769c59ec9e fix(forms): encode form action URLs and configured attributes (#8437)
* fix(forms): preserve action URLs in HTML and JavaScript

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

* docs(changelog): record form action URL fix for issue 8438

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

* fix(forms): encode configured form name and enctype attributes

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-10-20 13:18:37 -07:00
Thomas Vincent 2d58b2477f fix(spikekill): preserve valid reference samples during repair (#8427)
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-19 09:32:59 -04:00
TheWitness b95226918e fix: Update the deepness favicon (#8419)
* fix: Update the deepness favicon

(cherry picked from commit 3a59295162)

* QA: If the SVG is present, it's all we need.

(cherry picked from commit ee0b4c84ed)

* fix: Update such that the modern svg favicon can render
2026-10-18 11:51:18 -04:00
Thomas Vincent 57c0fe63d4 Fix restrictive graph permission display for default-deny templates (#8422)
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-18 10:39:20 -04:00
1997691de0 Render RRDtool Tune Info as balanced div boxes and POST the Data Source repair actions (#8412)
* data_sources: render RRDtool Tune Info as balanced div boxes

rrdtool_tune() emitted an unbalanced mix of html_header() and
html_end_box() calls wrapped around raw table rows inside the single
box opened by data_sources.php.  The premature and extra closing tags
corrupted the DOM, which (depending on theme) clipped the Save button
at the bottom of the Data Source edit page while in Data Source Info
mode.

- Separate the execute, CLI and HTML code paths in rrdtool_tune() and,
  for HTML, render each section as a self contained, balanced div box.
- Extract and rename the helper print_leaves() to the standalone
  rrdtool_tune_print_leaves(), matching the rrdtool_tune_* naming
  convention, and emit div rows instead of table rows.
- Switch the data_sources.php wrapper box to div mode to match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* data_debug: require POST for the Data Source repair actions

Applying RRDfile repairs and rerunning the debugger are state changing
operations, so they should not be reachable with a GET.  Driving them
through a GET also meant the follow up redirect was pulled into the
content area as a full page, losing the theme and navigation on
rerender.

Register data_debug.php run_repair and run_debug in the
cactiIsPostActionUrl() allowlist so loadUrl() upgrades them to a
token-bearing POST, and add the matching csrf_require_post() guard so
the browser transport and server side guard stay in sync.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* rrd: split rrdtool_tune into testable render and execute helpers

Break rrdtool_tune() into small single-purpose helpers so each path can
be exercised directly by unit tests (the web display path is otherwise
unreachable under CACTI_CLI):

- rrdtool_tune_print_leaves() takes an explicit $html flag instead of
  branching on CACTI_CLI, so both the div and plain-text branches are
  testable.
- rrdtool_tune_execute() applies the tune/resize commands for a repair.
- rrdtool_tune_print_cli() prints the source commands as plain text.
- rrdtool_tune_print_html() renders the balanced div boxes.
- rrdtool_tune() becomes a thin dispatcher over the three.

Add tests/Unit/RrdTuneRenderTest.php covering the leaf printer, the HTML
and CLI printers, the execute path and the dispatcher, asserting the
output is balanced div markup with no table rows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* review: post Data Source Troubleshooter actions via cactiPostAction

Address PR review feedback:

- Switch the Apply Suggested Fixes and Rerun Analysis links to the
  existing cactiPostAction/data-url idiom (as data_sources.php already
  does for ds_repair) instead of upgrading a loadUrl() GET through the
  cactiIsPostActionUrl allowlist. The allowlist path replayed the
  query-free mutation URL into browser history, so a refresh reopened
  the list instead of the selected Data Source; a full cactiPostAction
  submission follows the server redirect to the read-only view URL and
  records that in history. The run_repair / run_debug csrf_require_post
  guards are retained, and the data_debug.php allowlist entry (and its
  now redundant click handlers) are removed.

- Add a callable $executor test seam to rrdtool_tune_execute() (matching
  rrdtool_dump_document) so the unit tests assert the tune and resize
  commands are actually executed, with resize.rrd produced only when the
  resize command runs, rather than passing on the rename alone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* review: move RrdTuneRenderTest into the Unit/Core/Rrd category folder

Per AGENTS.md, new unit tests belong in a category subfolder rather than
directly under tests/Unit. Move RrdTuneRenderTest there and update the
UnitStubs include depth accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-16 11:23:07 -04:00
dependabot[bot]andThomas Vincent 2be8a7f5fe deps: update Composer tooling and resolve PHPStan 2.3 diagnostics (#8357)
* deps(deps-dev): bump the composer group with 2 updates

Bumps the composer group with 2 updates: [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) and [rector/rector](https://github.com/rectorphp/rector).


Updates `phpstan/phpstan` from 2.2.17 to 2.3.0
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

Updates `rector/rector` from 2.6.7 to 2.7.0
- [Release notes](https://github.com/rectorphp/rector/releases)
- [Commits](https://github.com/rectorphp/rector/compare/2.6.7...2.7.0)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: composer
- dependency-name: rector/rector
  dependency-version: 2.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: resolve PHPStan 2.3 dependency upgrade diagnostics

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-16 08:49:23 -04:00
TheWitness b2ed6ec337 Legacy filter rendering: show only the classic inline filter (#8404)
The page_filter_format setting is meant to be either/or: Modern shows the
Layouts selector with its Edit dialog, Legacy shows the classic inline
filter. Legacy also rendered the Layouts selector row (and its JavaScript),
so the new and the classic filters appeared together.

Stop rendering the Layouts selector in Legacy mode: create_filter() no
longer appends create_layouts(), and create_javascript() only emits the
layouts script in modern mode. The now-unused create_layouts() and
create_layouts_javascript() helpers are removed, and the setting help text
is updated to say the Layouts selector is available in Modern only.
2026-10-16 08:11:50 -04:00
Thomas Vincent 3bcb4e4f49 fix(graphs): [develop] clear empty query fields in rendered text (#8349)
* fix(graphs): clear empty query fields in rendered text

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

* ci(coverage): enforce changed PHP statement coverage

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

* ci: share changed-code coverage contracts and enforce shell policy

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

* fix(ci): run request coverage with portable path matching

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

* ci: retain bootstrap coverage and validate request dispatch

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

* test: synchronize native coverage fixture dispatch

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

* test: measure coverage at the exact PR revision

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-15 20:38:03 -04:00
TheWitness 433af99661 Fix plugin archive restore skipping not-yet-created nested directories (#8398)
Restoring a plugin from an archive, or installing one downloaded from
GitHub into the database, extracts the tgz to a temp file and writes each
entry under plugins/<name>/. validate_relative_path_within() anchored a
not-yet-created entry on its immediate parent directory, so every entry in
a subdirectory the restore had not created yet (includes/, css/, tests/,
docs/, ...) resolved against a missing path, failed the containment check
and was skipped. The plugin was left with only its top-level files and
setup.php then fatally required the missing includes/database.php.

Judge a not-yet-created entry by its deepest existing ancestor instead,
mirroring data_source_path_within_rra(). Every path segment is already a
validated plain name and no walked segment may be a symlink, so the full
candidate still stays within the base; traversal, absolute, drive-letter,
null-byte and symlink entries remain rejected.
2026-10-15 14:14:19 -04:00
Thomas Vincent cf70ba4cf2 fix(plugins): [develop] restore lifecycle diagnostics, compatibility and error attribution (#8297)
* fix(plugins): honor version range boundaries and compatibility status

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

* fix(plugins): preserve null hook diagnostics and enforce dependency versions

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

* fix(plugins): report config failures and confine error attribution

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

* fix(plugins): guard entrypoint frames and bind realm filenames

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

* fix(plugins): propagate install hook failures on develop

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

* fix(plugins): match realm filenames literally

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

* docs(changelog): defer release entries to final backlog PR

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

* test(plugins): add standard GPL headers

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

* ci(coverage): require 80 percent of changed PHP statements

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

* ci: share changed-code coverage contracts and enforce shell policy

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

* fix(ci): run request coverage with portable path matching

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

* ci: retain bootstrap coverage and validate request dispatch

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

* test: synchronize native coverage fixture dispatch

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

* test: measure coverage at the exact PR revision

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 22:53:44 -04:00
Thomas Vincent 62063a29e0 fix(theme): [develop] correct CSS cascade, invalid values and responsive selectors (#8293)
* fix(themes): correct CSS cascade, font fallbacks and responsive selectors

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

* fix(themes): remove validated empty and redundant CSS rules

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

* style(css): remove reviewed inactive rules and redundant separators

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

* style(themes): consolidate two reviewed duplicate selectors

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

* fix(themes): remove identical whitespace declaration after consolidation

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

* fix(theme): keep context menu fallbacks in Cacti overrides

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

* test(theme): measure native CSS and header coverage

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

* ci: retain bootstrap coverage and validate request dispatch

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

* test: synchronize native coverage fixture dispatch

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

* test: measure coverage at the exact PR revision

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

* test: cover Midwinter and graph heading CSS repairs in Chromium

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

* ci: take the theme coverage base from the merge commit parent

- github.event.pull_request.base.sha can predate the develop tip the checkout was merged onto, which counted develop's own CSS lines as the PR's.

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 22:51:15 -04:00
32467ddcab perf: reuse graph option arrays and document validation results (#8361)
* perf: reuse bootstrapped graph option arrays and document validation

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

* test: record the installed RRDtool version in profiles

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

* docs: record final validation scope and profiling runtime limits

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

* docs: clarify the scope of changed-code coverage

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

* ci: repair relocated PHPUnit configs and the deletion-only coverage gate

- Point the five relocated configs that still carried a contributor's local
  autoload path (#8376) at ../include/vendor/autoload.php.
- Pass --configuration=tests/phpunit.xml to the CSV window workflow and the
  README example now that phpunit.xml no longer lives in the repository root.
- Let the changed-lines gate pass deletion-only PHP changes. Without added
  lines the generated config has no source filter, PHPUnit writes no clover
  report, and the gate failed on the missing file instead of reporting zero
  executable changed lines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: TheWitness <thewitness@cacti.net>

* ci: keep the missing request-report check for deletion-only changes

Only short-circuit the changed-lines gate when there are no added PHP lines
and PHPUnit wrote no clover report, so an empty request-coverage directory
is still rejected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: TheWitness <thewitness@cacti.net>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: TheWitness <thewitness@cacti.net>
Co-authored-by: TheWitness <thewitness@cacti.net>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-14 22:45:42 -04:00
Thomas Vincent ebd16105c5 fix: drop backslash and line breaks in sanitize_uri (#8388)
* fix: drop backslash and line breaks in sanitize_uri

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

* docs: add changelog entry for sanitize_uri fix

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 22:39:32 -04:00
Thomas Vincent 0f06608175 fix: reject dangling symlinks in validate_relative_path_within (#8389)
* fix: reject dangling symlinks in validate_relative_path_within

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

* docs: add changelog entry for dangling symlink fix

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 22:38:41 -04:00
TheWitnessandCopilot 76a916cf03 fix(boost): never re-stage poller output when boost_redirect is on (#8374)
* fix(boost): never re-stage poller output when boost_redirect is on

#8332 gated the boost_redirect bypass in boost_poller_on_demand() on the
caller not passing $staged. process_poller_output() always passes it, so
the bypass never fired and the main poller re-inserted every row spine or
cmd.php had already written to poller_output_boost, flooding the log with
"Boost staging ignored N of N duplicate sample keys" warnings.

When boost_redirect is enabled the poller now never inserts into
poller_output_boost and reports the rows as staged so they are cleared
from poller_output. poller_recovery.php writes recovered remote rows via
boost_flush_output_batch() directly and is unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: TheWitness <thewitness@cacti.net>

* chore(changelog): use changelog.d fragment for #8374

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: TheWitness <thewitness@cacti.net>

* fix(boost): log failed poller_output_boost staging as an ERROR

boost_flush_output_batch() returned false silently when the database did
not acknowledge a chunk. cmd.php's boost_redirect staging ignores that
return, so a failed collector-side write left no trace. Log a BOOST ERROR
with the sample count from the helper so every caller's failure is visible.

Adds in-process coverage for the failure log and for boost_poller_on_demand()
never re-staging rows when boost_redirect is on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: TheWitness <thewitness@cacti.net>

---------

Signed-off-by: TheWitness <thewitness@cacti.net>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-14 21:13:43 -04:00
TheWitness ffd7a81109 Scope the tree Graph Templates filter to the drilled Device Template (#8370)
* Scope the tree Graph Templates filter to the drilled Device Template

When drilling a Site Branch into a Device Template (host_id=-1, host_template_id set), create_tree_filter() fell through to listing every Graph Template. Scope the dropdown to the Graph Templates present on that Device Template's devices (within the drilled Site when set), matching the graphs the branch actually shows.

* docs(CHANGELOG): add issue#8370 entry

* test: cover the tree Device Template graph-template scope; fix CHANGELOG ordering

Add an isolated source-slice probe that drives create_tree_filter()'s new host_template_id branch so the changed lines meet the >=80% changed-line coverage gate, wired into run_request_coverage.sh. Move the issue#8370 CHANGELOG entry into the issue block so it no longer precedes the security entries.

* docs(CHANGELOG): append issue#8370 at the end of the issue block
2026-10-14 20:56:12 -04:00
TheWitness 75f92c2d86 Detect and repair RRDfile Data Source count mismatches via RRDtune (#8368)
* Detect and repair RRDfile Data Source count mismatches via RRDtune

Report when a Graph requires more or fewer Data Sources than its RRDfile
contains, generate the rrdtool tune DS:/DEL: command to correct it, and run
it when the RRDfile is writable.  Applies to data_debug (rrdtool_cacti_compare)
and the RRDchecker batch process (do_rrdcheck).  The decision logic lives in
pure helpers (rrdtool_cacti_compare_ds, rrdcheck_ds_findings) with unit tests.

Resolves #8366

* rrdcheck: only probe local writability for local storage

Under the storage proxy the RRDfile is remote, so is_resource_writable()
checks the wrong filesystem.  Skip the local probe when using the proxy and
let it enforce remote permissions and report failures via its response.

* rrdcheck: validate tune args, enforce RRA containment, fix failure detection

Validate every tune specification against RRDtool's documented DS grammar
(ds-name 1-19 [a-zA-Z0-9_], known DST, integer heartbeat, numeric-or-U min/max)
and escape it before use.  For local storage require the RRDfile to resolve
inside the RRA directory via data_source_path_within_rra().  Detect failures
from the real return value (false/null/empty or missing OK) instead of casting
to string, so a failed tune is no longer logged as success.

* rrd: normalize a 0/0 data source range to 0:U when building a tune add spec

rrdtool_function_info_from_ds() passes min/max through unchanged, so a source
stored as 0/0 produced DS:name:type:hb:0:0 which RRDtool rejects.  Mirror RRD
creation and emit 0:U so the repair paths can restore those sources.
2026-10-14 20:55:52 -04:00
TheWitness 356f4ae813 fix(graphs): tree content reveal, filter dialog columns, single-column centering (#8365)
* fix(graphs): reveal graph content area after tree branch loads

The jstree select_node handler hides .cactiGraphContentArea (#navigation_right) before loading a branch's graphs via AJAX to avoid flashing the previous branch, but the only re-show was coupled to a window resize event that does not fire after the AJAX load. On themes that hide .cactiGraphContentArea by default (e.g. Deepness) the content area stayed display:none and the graphs/filters were invisible. Re-show it on loadUrl completion.

* fix(filter): keep the Edit Filter dialog columns from collapsing

The modern filter Edit dialog is created with jQuery UI width:'auto'. Its body is a table-based label/field grid, which jQuery UI measures at min-content, collapsing the dialog content to a sliver and pushing the fields off to the right. Floor the content min-width so the columns lay out. Validated in Deepness and Dark.

* fix(graphs): centre single-column graphs in the graph view

graphWrapperOuter > div:first-child is width:fit-content inside a (full-width for one column) flex wrapper, so the graph rendered flush-left. Add auto horizontal margins so it centres; the absolutely-positioned drilldown toggle continues to track the graph.

* fix(graphs): don't re-show content area on aborted branch load

A superseding tree branch click aborts the in-flight loadUrl; its .always() must not re-show the content area the new selection just hid, or stale graphs flash while the new branch loads. Skip completions whose textStatus is 'abort'.
2026-10-14 17:16:42 -04:00
TheWitness 2fdc8747af Cross-filter Device and Graph Template on Preview and List views (#8363)
* Cross-filter Device and Graph Template on Preview and List views

Forward the selected Graph Template(s) to the ajax_hosts Device callback and filter the device list accordingly, so choosing a Graph Template narrows the Device list. Device-to-template narrowing already exists on these views.

* Use line comment style to satisfy php-cs-fixer

* fix(graph_view): validate raw graph_template_id against filter allowlist in html_make_device_where

The ajax_hosts endpoint calls html_make_device_where() before the graph filter is sanitized, so gnrv('graph_template_id') is raw request data. An array value such as graph_template_id[]=cg_1 reached html_transform_graph_template_ids()'s str_contains() and raised a TypeError (HTTP 500). Reject non-string input and validate against the same regexp allowlist the filter uses before transforming.

* test(graph_view): add request-coverage probe for the device/template cross-filter

The pr-php-coverage gate requires >=80 percent of changed executable PHP lines to be exercised. The new html_make_device_where() graph_template_id branch and the request_vars changes in create_graphs_preview_filter()/create_listview_filter() are only reached when a graph_template_id filter is present, which the page-level e2e suites do not drive. Add an isolated source-slice probe that runs those production functions with stubbed I/O boundaries and wire it into run_request_coverage.sh so the changed lines are covered.
2026-10-14 13:21:22 -04:00
Thomas Vincent 4d11070ced feat: expose RRDtool 1.11 graph axis controls (#8356)
* feat: expose RRDtool 1.11 graph axis controls

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

* fix: constrain axis template inheritance checkboxes

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

* test(rrd): exercise native aggregate validation and enforce coverage

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

* ci: share changed-code coverage contracts and enforce shell policy

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

* fix(ci): run request coverage with portable path matching

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

* ci: retain bootstrap coverage and validate request dispatch

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

* test: synchronize native coverage fixture dispatch

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

* test: measure coverage at the exact PR revision

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 12:10:28 -04:00
Thomas VincentandPetr Macek 930b085703 feat(auth): log administrative account actions after confirmed writes (#8360)
* log user action (create, delete, locked, ..)

* change log

* update changelog

* style(auth): apply coding standards to user action logging

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

* Audit password changes only after successful account saves

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

* fix(auth): sanitize audit fields and confirm account removals

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

* test: cover native account audit state transitions

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

* test: synchronize native coverage fixture dispatch

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

* fix(auth): audit only confirmed account persistence

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

* test: measure coverage at the exact PR revision

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Petr Macek <petr.macek@kostax.cz>
2026-10-14 09:54:14 -04:00
TheWitnessandTheWitness 8acc5ec916 feature: theme-aware graph drilldown action menu (per-theme CSS palette) (#8355)
Label the open graph drilldown strip (layout.js labelGraphDrilldownIcons), shorten the action titles, and restyle the strip as a table menu. Each theme's main.css declares the --cacti-dd-* custom properties (foreground, hamburger/panel backgrounds, hairline border, hover accent) that include/css/graph_drilldown.css consumes via var() with a dark fallback, so each theme gets an appropriate light or dark look with its own accent and the hamburger reads correctly on light themes. Themes without a block (e.g. midwinter) use the dark fallback. Regenerates the affected locales/po/cacti.pot entries for the shortened titles.

Co-authored-by: TheWitness <1439914+TheWitness@users.noreply.github.com>
2026-10-13 12:33:14 -04:00
Thomas Vincent ce6ab6e0b3 fix(auth): [develop] preserve SAML POST callback sessions (#8333)
* fix(auth): preserve SAML POST callback sessions

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

* fix(auth): filter SAML inputs before bootstrap

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 15:42:00 -07:00
Thomas Vincent a12fe59627 fix(poller): [develop] retain output until bounded storage batches succeed (#8332)
* fix(poller): retain output until bounded storage batches succeed

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

* test(poller): refresh SQL audit after exact-key acknowledgements

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

* test(boost): preserve handoff contracts with explicit acknowledgements

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

* fix(poller): retain failed timestamp ordering across batches

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

* fix(poller): acknowledge complete timestamp groups together

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

* fix(poller): retry failed source metadata reads on develop

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

* fix(poller): preload standalone script exclusions once per process

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

* fix(poller): bound metadata failures and validate acknowledgement queries

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 15:41:53 -07:00
TheWitnessandThomas Vincent f39e74c65c Graph drilldown: keep hamburger fixed, icons reachable, hide on graph leave (#8315)
* Graph drilldown: keep hamburger fixed, icons reachable, hide on graph leave

The action-icon strip now slides down below the hamburger (flex column) instead
of the hamburger dropping to the bottom of the icons. Closing is driven by
leaving the whole graph rather than the small icon strip, so the mouse can move
from the hamburger onto the icons without the strip closing. Leaving the graph
also blurs the hamburger so it no longer lingers after interacting with it.

* changelog: graph drilldown interaction fixes (#8315)

* Graph drilldown: tolerate tooltip-induced mouseleave, drop hamburger title

A native tooltip appearing over a drilldown icon fires a spurious mouseleave on
the graph (relatedTarget null), which closed the strip. Defer the close and
re-check the graph is genuinely not hovered first, so the icon tooltips no longer
destabilize it. Also drop the hamburger's redundant title (its aria-label stays).

* Graph drilldown: widen the close grace period to 500ms

* Graph drilldown: tie hamburger visibility to the graph image

Reveal the hamburger while the pointer is over the graph image (or the toggle
overlaid on it) rather than anywhere in the graph cell, so moving off the image
hides it after the grace period.  Entering a different graph now immediately
hides and closes any other graph's open strip.

* Graph drilldown: lift the open graph so its strip clears thumbnails below

With thumbnails a short graph's open icon strip overflows onto the graph below;
sibling graphs paint in DOM order, so that graph rendered on top of the strip,
making the icons unclickable and stealing the hover (which closed the strip).
Raise the open graph (.graphDrillActive, position:relative; z-index) so its
overflowing strip stays on top and the graph beneath is not hovered.

* Graph drilldown: reveal via :focus-visible and stop blurring to preserve keyboard focus

* fix(ui): preserve graph action focus during pointer navigation

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

* test(ui): cover graph drilldown interactions and gate PR coverage

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

* ci: measure committed PR changes on a policy-compliant runner

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 06:29:12 -04:00
TheWitnessandThomas Vincent 34fc3f8642 Stop flagging a not-yet-created RRA subdirectory as a data_source_path escape (#8313)
* Stop data_source_path_within_rra() flagging a not-yet-created intermediate directory as an RRA escape

get_data_source_path() routed a legitimate <path_rra>/<host_id>/<local_data_id>.rrd through the generated-path fallback and logged a bogus 'SECURITY: ... escapes the RRA directory' message whenever the host_id subdirectory did not exist yet (first poll, or host_id 0). data_source_path_within_rra() only tolerated a missing final RRD file: for a missing file it resolved realpath(dirname(path)), which returns false when that parent directory has not been created, so the lexically-contained path was wrongly rejected.

Walk to the deepest ancestor that actually exists and confirm it resolves inside the RRA root instead. Lexical containment (no .., '.', or empty segments) and per-segment symlink rejection are unchanged, so traversal, lookalike-prefix, absolute, and symlink-pivot paths are still rejected. Adds a regression test for a not-yet-created intermediate directory.

* Drop explanatory comments from the RRA containment fix

* Add CHANGELOG fragment for the RRA containment fix

* Reject a dangling-symlink segment independently of file_exists()

* test(paths): cover RRA filesystem boundaries and gate PR coverage

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

* ci: measure committed PR changes on a policy-compliant runner

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 06:28:45 -04:00
Thomas Vincent 76579e6292 Fix dsstats partition dispatch and profile retention fallbacks (#8304)
* fix(core): preserve dsstats type and render retention fallbacks

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

* test(auth): use certificate expiry truth in rendering assertions

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 21:32:00 -04:00
Thomas Vincent af9d1591a3 fix(aggregate): honor skipped item sequences in totals and percentiles (#8300)
* fix(aggregate): honor skipped item sequences in totals and percentiles

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

* test: tighten SQL baseline for aggregate prepared queries

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

* docs(changelog): defer release entries to final backlog PR

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 21:27:25 -04:00
Thomas VincentandTheWitness 97e67c6777 security: harden browser rendering and review CodeQL boundaries (#8298)
* security: harden browser history and dependency option merging

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

* docs: record CodeQL triage decisions and follow-up scope

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

* test: wait for history reload before inspecting browser storage

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

* security: isolate HTMX history from the legacy navigation handler

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

* ci: build CodeQL assets through controlled dependency installs

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

* security: render form validation messages as plain text

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

* test: verify drag rows and color input security boundaries

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

* test: verify tree navigation rename and datetime input boundaries

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

* security: harden menu rendering and complete CodeQL caller review

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

* security: centralize menu rendering and stabilize certificate CI

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

* test: align certificate expectation with parallel web PR

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-10-11 21:26:38 -04:00
Thomas Vincent 08d0f94f24 Fix develop client behavior, popup isolation and PHP method visibility (#8292)
* fix(core): correct client state and declare public method contracts

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

* fix(security): isolate Midwinter external homepage popup

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

* fix(client): isolate menu state and simplify validated JavaScript guards

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

* test(auth): use certificate expiry truth in rendering assertions

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

* refactor(js): remove unused local storage while retaining initializer calls

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

* fix(js): encode inline translated values through the shared JSON boundary

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

* style(js): wrap encoded translation literals without changing messages

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

* fix(client): remove unused checkbox reads from graph resizing

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

* refactor(client): declare repeated function locals once

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

* test(ui): run column width regression in browser CI

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 21:15:39 -04:00
Thomas VincentandTheWitness 391c6ead18 Add scoped poller regressions and weekly branch backstop (develop) (#8286)
* test(poller): gate regressions by changes and run weekly scale checks

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

* test(poller): satisfy workflow policy and bound cold-cache queries

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

* fix(poller): retry failed template metadata reads

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

* docs(changelog): defer release entries to final backlog PR

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-10-11 21:15:06 -04:00
Thomas Vincent 71f2d55b6a refactor(html): share sortable header state resolution (#8307)
* refactor(html): share sortable column state and sort history

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

* refactor(html): normalize sortable metadata before resolving state

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 20:32:37 -04:00
TheWitness d7ec67c4cf Persist table column widths across request actions (#8309)
* Persist table column widths across request actions

Column widths are stored in localStorage keyed by the table id, but
html_start_box() derives that id from the request action/report/tab, so a
filter refresh (action change/go) reloads the same table under a different
id and the saved widths are not restored. Emit a stable, action-independent
data-colsizing key on each cactiTable and use it in saveTableWidths(), while
keeping the DOM id unchanged.

* changelog: 8309 table column width persistence

* Column width persist: keep per-view key, normalize only filter-refresh actions

The previous key dropped the action/report/tab entirely, so distinct table schemas at the same page+ordinal (e.g. support.php processes vs background tabs, utilities.php view_snmp_cache vs view_poller_cache) collapsed into one localStorage entry and applied widths from the wrong schema.

Retain the stable logical-view discriminator (tab/report, or a non-transient action) in the data-colsizing key and normalize only the transient filter-refresh actions (go/change/clear/reset), so widths still survive a filter change but never cross unrelated schemas.

* Use short array syntax for the colsizing normalization list (php-cs-fixer)
2026-10-11 17:26:48 -04:00
TheWitness e25a67c31f Common graph drilldown hamburger toggle across all themes (#8308)
* Graph drilldown: common hamburger toggle across all themes

The graph action icons were revealed by hovering to the right of the graph, implemented per-theme (dark/midwinter/sunrise main.js) and always-on elsewhere.

Hide the icons by default behind a small fa-bars hamburger overlaid on the graph's top-right. Clicking it opens the icon strip, which stays until a second click, leaving the strip, or an outside click. The behaviour is now centralized in include/layout.js (initGraphDrilldown) and include/css/graph_drilldown.css, and the per-theme hover JS is removed.

* changelog: 8308 feature fragment

* Graph drilldown: address review (a11y, positioning, spikekill, POT)

- Anchor the toggle to the shrink-wrapped graph holder (width:fit-content) so the hamburger sits on the graph's top-right, not the full-width column edge.

- Keep the hamburger focusable while visually hidden (opacity instead of display:none); reveal on hover, keyboard focus, open, and hover-less devices.

- Expose aria-expanded on the toggle and update it in every open/close path; add aria-haspopup/aria-label.

- Close any open SpikeKill submenu (spikeKillClose) when the strip closes or switches, matching the removed per-theme handlers.

- Remove the now-duplicate Midwinter top-right ellipsis pseudo-element.

- Add the new 'Graph Actions' msgid to locales/po/cacti.pot.
2026-10-11 17:20:06 -04:00
Thomas Vincent 06c32ab13f fix(installer): preserve upgrade state and propagate failures (#8266)
* fix(installer): preserve upgrade state and propagate failures

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

* docs(changelog): record installer recovery fixes for #8266

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

* test(security): refresh installer sink inventories

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

* fix(installer): retain merge precedence for browser parameters

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

* fix(installer): defer debug settings until the process lock is held

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

* fix(installer): enforce table conversion failures and postconditions

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

* test(installer): refresh SQL audit baseline after clause refactor

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 17:08:12 -04:00
Thomas Vincent bcb4aaf4ae fix(auth): render password reset links outside login error text (#8289)
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-10 20:24:49 -04:00
TheWitness bfa3efb7a9 Add a jQuery rebind() helper and roll it out across the core UI (#8279)
* Add a jQuery rebind() helper and roll it out across the core UI

Add $.fn.rebind(events[, selector], handler...) to include/layout.js as a
shorthand for the common off(events).on(events, ...) re-attach idiom, then
convert the matching chained calls across the core JavaScript and inline
PHP handlers to use it. Only pairs whose off() and on() events (and
delegated selector) match are converted; off() with no arguments is left
as-is since it intentionally clears every handler.

* changelog: add 8279.feature fragment

* tests: update layout.js JS tests for the rebind() helper

* tests: assert the cactiPostAction handler via rebind()

* rebind: preserve the delegated selector for the two-argument event-map overload; add helper tests
2026-10-10 17:01:49 -04:00
TheWitnessandTheWitness ce707ccd79 Fix false-positive RRA-escape warning when path_rra has a trailing slash (#8258)
* Fix false-positive RRA-escape warning when path_rra has a trailing slash

A path_rra setting saved with a trailing slash makes CACTI_PATH_RRA end in
'/', so the '<path_rra>/' -> CACTI_PATH_RRA . '/' expansion that every data
source consumer performs yields a doubled slash ('<rra>//0/1457.rrd').

data_source_path_within_rra() split the expanded path on '/' and rejected any
empty segment, so the doubled slash was read as a traversal and a perfectly
legitimate RRD file was reported as escaping the RRA directory:

  SECURITY: Data source 1457 has a data_source_path that escapes the RRA
  directory (<path_rra>/0/1457.rrd).  Using the generated path instead.

get_data_source_path() then returned the generated fallback path, which the
poller had never created, producing the follow-on 'RRD file does not exist'
graph warnings (note the tell-tale 'rra//0' doubled slash in those lines).

Collapse repeated slashes and strip a trailing slash from the RRA root before
the containment comparison. A '//' is an empty segment, not a parent
reference, so normalising it keeps the file contained while '..' traversal,
absolute paths elsewhere, lookalike prefixes and symlink pivots are still
rejected (asserted by the expanded regression test).

* Keep data_source_path_within_rra() normalisation strictly string-typed

preg_replace() is typed array<string>|string|null, so feeding its result to
rtrim()/strlen() tripped PHPStan (level 6/8). Cast CACTI_PATH_RRA and both
preg_replace() results to string; runtime behaviour is unchanged.

* Cover the trailing-slash CACTI_PATH_RRA half of the fix in an isolated process

The existing cases insert '//' into the target only; data_source_path_test_base()
uses realpath(), which strips the trailing slash, so they never exercise the
rtrim() on \. CACTI_PATH_RRA is immutable, so define it with a trailing
slash in a fresh PHP process and assert a clean/doubled-slash file stays
contained while a traversal is still rejected.

---------

Co-authored-by: TheWitness <1439914+TheWitness@users.noreply.github.com>
2026-10-09 17:05:11 -04:00
TheWitness 2780258e7c Add 'Deepness' dark emerald-green theme to develop (with billboard overload CSS) (#8206)
* Add 'Deepness' theme to develop; midwinter billboard + deviceStatus hook

Forward-ports the Deepness theme (1.2.x #8202 + #8205) to develop as one change: the full include/themes/deepness/ theme (emerald-dark :root palette, Paw layout/top-tab JS, full billboard.js 3.13 overload CSS, dark widget CSS), the themes allowlist entry, the i18n_themes name, the midwinter billboard overload rules, and the shared 'deviceStatus' hook class in get_colored_device_status() that the theme styles as status pills. Paper-plane is intentionally untouched (worked separately). The lib/html.php favicon/arrow tweaks from 1.2.x are omitted - develop handles those via get_md5_include_icon()/get_theme_paths() and the theme's own .actionsDropdownArrow CSS.

* deepness: align main.js with Paw parent (drop stale host_id autocomplete & graphDrillDown, fix search-icon/handler leaks)

* deepness: drop bundled billboard.css (obsolete per-theme file; central include/css/billboard.css + main.css .bb overloads cover it)

* i18n: add 'Deepness' theme name to cacti.pot translation template

* Deepness: apply php-cs-fixer formatting and refresh sink inventory baseline

php-cs-fixer fixes the single-quote and // comment style in the deepness theme index.php/rrdtheme.php files, and the sink inventory baseline records the header_redirect sinks in the new deepness index.php files.

* tests: stop the boost upgrade integration test leaking IN_CACTI_INSTALL

BoostProcessTableUpgradeIntegrationTest defined IN_CACTI_INSTALL at file (collection) scope, so it was set for the whole shared pest process and made CactiErrorHandler() take its install-mode early-return. That broke CactiErrorHandlerSuppressionTest ('an enabled level is logged...' expected false, got true) in the pest coverage gate and the locked integration job. The upgrade files required here do not check the constant at load time (only lib/database.php does, at runtime inside its schema probes), so define it in beforeEach after the DB connects instead -- it is never set at collection time and, with no DB in CI, the test skips without defining it. Full pest suite: 2664 passed, 0 failed.
2026-10-07 14:16:56 -04:00
TheWitness 2a9659c1c6 html: remove obsolete actions-dropdown arrow.gif image (mirrors 1.2.x) (#8208) 2026-10-07 13:24:34 -04:00
TheWitness a800fd0546 Fetch plugin default branch instead of hard-coded develop (#8201)
plugin_fetch_latest_plugins() now follows each plugin repository's default_branch (fallback develop) for the second fetch, instead of always using the develop branch.
2026-10-07 06:45:49 -04:00
TheWitness b5a388094d Honor @ error suppression in CactiErrorHandler on PHP 8 (#8197)
* Honor @ error suppression in CactiErrorHandler on PHP 8

CactiErrorHandler() guarded against the @ operator with error_reporting() == 0,
which was the PHP 7 semantics.  On PHP 8 the @ operator sets error_reporting()
to a non-zero bitmask that merely excludes the suppressed level, so the guard
never matched and deliberately suppressed warnings still reached the log.

This surfaced after the Symfony mailer swap: Symfony's SendmailTransport writes
to the sendmail pipe with @fwrite(), and when sendmail is misconfigured the
broken-pipe notice leaked into the Cacti log instead of being swallowed and
reported as the handled TransportException.

Mask error_reporting() against the incoming level so the @ operator is honored
on every supported PHP version.

* Add regression test for CactiErrorHandler @-suppression mask

Exercises both paths of the error_reporting() mask check: a notice and a
warning raised under PHP 8's @-operator bitmask return before logging, while an
enabled level is logged and reported back to PHP as unhandled.  The suppressed
cases would have logged under the previous error_reporting() == 0 guard, so the
test fails closed if that regression returns.
2026-10-07 06:44:07 -04:00
TheWitness 7de50e8fa5 Widen form_checkbox_cell() $id parameter to mixed (#8198)
Callers pass the row id straight from query results, which may be an int as
well as a string.  Declaring the second parameter as string forced an implicit
scalar coercion and was inconsistent with the sibling cell helpers
(form_selectable_cell(), form_selectable_ecell()) that already type $id as
mixed.  Accept mixed and update the docblock to match.
2026-10-06 14:21:41 -04:00
MurisandTheWitness 2c791b3db8 Fix printing of the detailed graph view on develop (#8193)
* Fix detailed graph print layout on develop

* Reference develop PR 8193 in changelog fragment

---------

Co-authored-by: TheWitness <thewitness@cacti.net>
2026-10-06 13:56:38 -04:00
TheWitnessandTheWitness 4298f2ae33 feature: Search box on the modern filter bar + null-safe tablesorter trim (#8194)
* Surface the Search box on the modern filter bar

Route the Search textbox (filter/rfilter) onto the modern filter bar, just right of the Rows selector, instead of only inside the Edit dialog. Pressing Enter in it applies the filter via the page's configured change function (filter=/rfilter= as appropriate), bound with event delegation so it survives ajax content reloads. field_is_bar() now keeps the search textbox on the bar and it is ordered Rows, Search, Refresh.

* Null-safe tablesorter $.trim rewrite in the sync-js transform

The jQuery 4 compatibility rewrite turned \$.trim(x) into (x).trim(), which throws when a custom textExtraction/parser returns a non-string or null (jQuery coerced values and mapped nullish to ''). Rewrite to String((x) ?? '').trim() - the inner parens keep || expressions from forming an illegal unparenthesized 'a || b ?? ''' - and regenerate include/js/jquery.tablesorter.js. Updates the sync-js fixture test accordingly.

* Add changelog entries for the modern-bar Search and tablesorter trim fix

* Add a Clear glyph button to the modern filter bar

Place an eraser (ti ti-eraser) Clear button just right of Refresh on the modern bar, wired to the page's clear action, so the filter can be reset without opening the Edit dialog. The eraser matches the timespan/generic Clear glyph and stays visually distinct from Delete's trash glyph.

* Give bar-routed Search a persistent label when a page omits friendly_name

The log viewer (clog) defines rfilter without a friendly_name because it shared the adjacent matches field's Search label. Routed onto the modern bar it would show only the placeholder, which disappears after entry. Default a bar-routed filter/rfilter without a friendly_name to Search, scoped to the modern bar so legacy pages are unchanged.

* Apply the filter when the search glyph is clicked

The ti ti-search magnifying glass added after filter/rfilter inputs was decorative. A delegated click now submits the input's form, or on the modern bar (no form) triggers Enter, so clicking it applies the filter.

* Use the legacy filter on Console > Management > Sites

The Sites list gains nothing from saved layouts; set render_layouts = false so it renders the classic legacy filter.

* Restore Search in the Edit dialog and glyph-ify Purge

Mirror the bar Search into the Edit dialog (synced to the canonical bar input on open and on edit) so a saved layout's search term is visible and editable there without duplicating the input id; the bar stays the source of truth and nothing persists to a layout until it is saved. Also force a ti ti-flame glyph for the page-action Purge button.

* Order Search before Rows on the modern bar to match legacy filters

* Narrow the modern-bar regex Search to the normal filter width

The rfilter (regex Search) input on the modern bar renders at size=55, much wider than the size=30 normal filter. Cap it at 30ch via .cactiFilterModernBar input#rfilter in every theme except Midwinter, which the user asked to leave as-is.

* Document the modern-bar Clear, Purge glyph, dialog Search, and Sites legacy filter in the changelog

* Glyph-ify the clog Rotate button

Force a ti ti-rotate glyph for the page-action Rotate button (clog log rotation), matching the Purge/Save/Clear glyph treatment.

---------

Co-authored-by: TheWitness <TheWitness@users.noreply.github.com>
2026-10-06 13:56:15 -04:00
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
TheWitnessandTheWitness 3b3544db03 Add MySQL/MariaDB variable capability matrix to Database section (#8188)
* Add MySQL/MariaDB variable capability matrix to Database section

* Add changelog fragment for PR #8188

* Address review: correct version boundaries, escaping, valid values, focusable hint, unit tests

* Normalize server version suffixes and cache the capability matrix

* Add capability matrix strings to the translation template

* Treat an unknown (null/empty) server version as supported to avoid a TypeError

* Cover the capability helpers in the installer coverage suite

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
2026-10-05 20:16:01 -04:00
TheWitnessandTheWitness 1c96e16a3f support: recommend supported DB releases and fix MariaDB detection (#8186)
* support: recommend supported DB releases and fix MariaDB detection

- Update the database version recommendation to the supported release streams (MariaDB 10.5+, MySQL 8.0+, e.g. the defaults on RHEL 9) and make the recommended '>=' threshold database-aware instead of a flat 5.6.
- Fix MariaDB being mis-detected as MySQL when the version suffix capitalization differs: use case-insensitive matching (stripos/str_ireplace) in get_mysql_info() and the connection setup. This drives the '%s Tuning' header and all MySQL/MariaDB branching on the Technical Support page.
- Remove dead code that special-cased MySQL < 8.0 (now unsupported): the '(MySQL && < 8.0) || MariaDB' guards in support.php and lib/utility.php collapse to just 'MariaDB'.

* support: reference Rocky Linux 9 instead of RHEL 9 in DB recommendation

* support: change RHEL font-path comment to Rocky

* docs: update System Requirements and DB recommendation for 1.3.x

- README: raise minimums (develop: MariaDB 10.11+, MySQL 8.4+, PHP 8.3+; 1.2.x column: MariaDB 10.5+, MySQL 8.0+, PHP 8.2+, RRDtool 1.7+), set Net-SNMP 5.9+ floor, mark php-snmp fully supported, and note upcoming PHP 8.6 support speeding up Script/Script Server collection.
- Bump the develop DB recommendation to MariaDB 10.11+/MySQL 8.4+ to match the 1.3.x minimum.

---------

Co-authored-by: TheWitness <TheWitness@users.noreply.github.com>
2026-10-05 19:50:42 -04:00
5403cf75bb feature: user-defined filter layouts with administrator publishing (#8185)
* feature: user-defined filter layouts with administrator publishing

Add a render_layouts toggle to the CactiTableFilter class so list pages
offer Save / New / Rename / Delete controls for saved filter layouts,
modelled on the monitor plugin. Layouts persist to a new user_layouts
table (id, user_id, page, name, url). The toggle defaults on for every
page except the Console > Presets pages (CDEFs, VDEFs, Colors, GPRINTs,
Data Profiles, SNMP), where it is off.

Each user manages their own layouts from any filter. Administrators who
hold the Settings/Utilities realm can publish a layout to all users
(moving it to user_id = 0) from the filter, and from a new
Console > Presets > Filters management page (layouts.php) where they can
view and manage every user's layouts. Published layouts appear for all
users on that page.

Layout create/rename/delete/publish requests are handled centrally by
filter_layouts_handle_request(), invoked from include/auth.php after the
user has been authorized for the current page, so the controls work on any
filter page while publishing stays gated on the realm. layouts.php itself
is mapped to the Settings/Utilities realm so the Presets > Filters menu
pick is administrator-only.

Schema added to cacti.sql, install/upgrades/1_3_0.php, and
docs/audit_schema.sql. Adds tests/Unit/FilterLayoutsTest.php covering the
URL-safety validation, preset-page defaults, and the request/permission
wiring.

* changelog: 8185.feature fragment for filter layouts

* chore: enforce LF line endings via .gitattributes and normalize new files

* fix: address review - attribute-escape layout options, honor failed/non-editable layout ops, regenerate cacti.pot, move test to Unit/Ui/Html

* chore: renormalize pre-existing CRLF files to LF per .gitattributes

* chore: keep vendored JS (big.js, tablesorter) byte-identical to upstream and exclude from LF normalization

* style: apply php-cs-fixer formatting and resolve PHPStan level 8 findings in filter layouts

* ci: re-trigger checks on current head

* refactor: store filter layouts as a versioned JSON document in a TEXT data column

Replace the user_layouts.url varchar(1024) column with a data TEXT column holding a versioned {version,page,filter} document. The navigable url is rebuilt server-side from the document (page basename + rebuilt query) plus the filter_layout marker, removing the stored-url redirect/XSS surface and making the backend forward-compatible with a structured query builder without a further schema change.

* feature: add Modern/Legacy page filter format with an Edit dialog for filters

Modern (default) collapses the filter to a Layouts selector plus an Edit dialog holding the Filter Name and all filter fields; Save and Publish live in the dialog, with Edit/Rename/Delete/Save As on the bar and time controls/refresh kept on the bar. Legacy keeps the classic inline filter. The format is a per-user General setting (page_filter_format). Field rendering is factored into emit_field() so both layouts share one source.

* fix: render page-action buttons (import, export, sort) on the modern filter bar

Import, Export, Purge, Save, and sort ascending/descending are page actions, not filter inputs, so they render on the bar beside the Layouts selector instead of inside the Edit dialog. Only go/clear are dropped from the bar; the dialog keeps the Search/Clear footer.

* feature: render the modern Edit dialog one filter variable per row

The dialog no longer mirrors the inline filter's multi-field row packing; each filter variable renders on its own row (label + control) for a clean, responsive form. Hidden/validate fields stay in the form without an empty visible row. Rows carry a cactiFilterEditRow hook for label-column alignment styling.

* feature: align the modern edit dialog one-per-row and skin it per theme

Render all dialog fields in a single table so the label column aligns via the existing table-cell layout. Tag the dialog widget with cactiFilterDialog and add a scoped skin to the modern theme (slate titlebar, styled button pane, accented primary action). Add a bar separator between layout and page-action buttons.

* feature: skin the modern filter Edit dialog across the classic themes

Add the scoped .cactiFilterDialog skin (accent titlebar, white title/close, styled button pane with an accented primary action) to cacti, carrot, dark, hollyberry, paper-plane, paw, raspberry, and sunrise, each using its own brand accent. The skin is accent-only and inherits each theme's dialog surface, so it works on light and dark themes. MidWinter is intentionally skipped (its variable-based CSS is handled in a separate Epic).

* fix: delete a user's saved filter layouts when the user is removed

user_remove() now deletes the user's own user_layouts rows alongside their other per-user data. Scoped to user_id so published (user_id = 0) layouts remain available to everyone.

* fix: address PR review — PHPStan return type, escaping, callbacks, SQL columns, mb truncation, contrast, POT

- create_filter(): cast both ob_get_clean() returns to string and drop the now-unused PHPStan baseline entry (fixes Level 6 failure).
- Edit dialog title now uses html_escape_attr() (attribute-context double-encoding).
- Modern Search/Clear honor the page's configured go/clear callbacks instead of hardcoding applyFilter()/clearFilter().
- filter_layouts_get() and layouts.php select explicit columns instead of *.
- Layout name truncation uses mb_substr() to avoid cutting multibyte codepoints.
- layouts.php management callback checks result.ok before reloading.
- carrot/paper-plane dialog accents darkened to meet WCAG AA (>=4.5:1) with white text.
- Added the new translatable msgids (Page Filter Format, Filters, Edit Filter, Filter Name, Save As, ...) to cacti.pot.

* fix: resolve Semgrep reflected-xss by reading filter_layout inside layout_select

The request var is no longer passed into layout_select() as a parameter (which Semgrep tracked parameter-to-return to the print sink); it is read internally like create_filter(), and the option markup remains escaped. No behavior change.

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
Co-authored-by: TheWitness <TheWitness@users.noreply.github.com>
2026-10-05 19:49:42 -04:00
TheWitness f79dc15c08 Port #8166 SNMP credential cache + procedural SNMPv3 (with #8171/#8172 follow-ups) to develop (#8174)
* Port #8166 SNMP credential cache + procedural SNMPv3 to develop

Ports the shared SNMPv3 credential cache and procedural SNMPv3 routing from the
1.2.x feature (merged as #8166) onto develop, adapted to develop's refactored
SNMP layer. Parts of the 1.2.x change that do not apply to develop are omitted:
the cacti_exec() idle-timeout rewrite (develop has no cacti_exec()), the PHP
floor bump and composer/doc changes (develop is already 8.3+), and the cmd.php
max_oids batching (develop's cmd.php already collects SNMP in-process through
persistent sessions, so the per-item spawn the batching removed does not occur).

- lib/cache.php: new cross-process \Cacti\Cache\SharedCache (file + POSIX
  shared-memory + process-local tiers, optional AES-256-GCM sealing). Loaded via
  lib/snmp.php (the Cacti\Cache\ namespace is not PSR-4 autoloaded). The crypto
  helpers it seals with already live in develop's lib/auth.php, loaded by
  global.php, so no duplicate was added.
- lib/snmp.php: snmp_get_method() now routes SNMPv3 get/getnext to the
  procedural snmp3_*() calls when the running PHP supports the configured
  auth/privacy tokens and no context/engine id is set (SHA-224/384 and
  AES-192/256[C] stay on the binary before PHP 8.6); the three single-get
  functions gain the procedural v3 path with \Throwable handling; adds the
  credential-cache subsystem, protocol-option/MD5-DES helpers,
  snmp_native_protocol(), snmp_format_agent() and cacti_snmp_get_multi(); the
  binary v3 auth builder consults the cache when enabled.
- poller.php / script_server.php: prime / load the shared cache.
- poller_maintenance.php: daily out-of-band cache rebuild on every collector
  (per-poller lastrun marker).
- lib/api_device.php / lib/utility.php: bump snmp_cred_version only on v3
  credential add/change (and propagate the token to the affected remote poller).
- include/global.php: resolve and create the private cache/ directory.
- include/global_settings.php: add 'Enable Credential Cache' and 'Enable MD5 and
  DES for SNMPv3' settings (both off by default); default new-device v3
  auth/privacy to SHA/AES instead of legacy MD5/DES.
- host.php / automation_snmp.php: drop MD5/DES from the pickers when disabled and
  default new records to SHA/AES on upgraded installs.
- include/config.php.dist: document $config['cache_dir'].
- tests/Unit/Core/Cache/SharedCacheTest.php: SharedCache behaviour coverage.
- tests/security/baselines: baseline the two new lib/cache.php sinks.

* Incorporate #8171: binary fallback for unsupported SNMPv3 protocols

Layers PR #8171 onto the develop port so an SNMPv3 device whose auth/privacy
protocols the running PHP's ext-snmp cannot service natively (e.g. AES-256
before PHP 8.6) no longer fatals the poller or device-edit screen.

- lib/snmp.php: add CactiSnmpBinarySession, a session stand-in exposing the
  get/getnext/walk/close interface the cacti_snmp_session_*() helpers use and
  delegating each call to cacti_snmp_*() -> the Net-SNMP binary.
  cacti_snmp_session() returns it when php-snmp is absent or the v3 protocol is
  not natively serviceable, so cmd.php/host.php collect these devices via the
  binary on pre-8.6 and switch to the in-process path on 8.6 with no caller
  change. v1/v2 snmpget/snmp2_get now go through snmp_format_agent(). The
  ERRNO_TIMEOUT check tolerates php-snmp being absent. Drop the vendored
  classSNMP.php compatibility shim and its include (and the Yandex attribution).
- cmd.php: open_snmp_session() returns the down sentinel when a host is already
  marked down this run, instead of returning an unset (null) []
  that cacti_snmp_session_get() then dereferenced (TypeError).
- lib/ping.php: ping_snmp() runs the availability check through the procedural
  cacti_snmp_get()/getnext() path so an unsupported protocol falls back to the
  binary instead of raising a ValueError in SNMP::setSecurity().

* Incorporate #8172: retire include/vendor/phpsnmp, relocate into lib

Follow-up to #8171 on the develop port. After classSNMP.php was removed, the
include/vendor/phpsnmp/ directory held only Cacti-owned files; this retires it.

- lib/snmp.php: the ext-snmp subclass that carried Cacti's bulk_walk_size /
  value_output_format (previously vendor/phpsnmp/extension.php) is now declared
  directly as CactiSnmpNativeSession, guarded by CACTI_PHP_SNMP; the include and
  the 'use phpsnmp\SNMP' alias are gone, and new SNMP(...) / SNMP::VERSION_*
  now reference the native \SNMP and the new subclass.
- lib/mib_parser.php: moved from include/vendor/phpsnmp/mib_parser.php;
  lib/mib_cache.php includes it via __DIR__.
- Remove include/vendor/phpsnmp/extension.php and index.php (directory retired).
- tests: SnmpCoverageTest uses \SNMP::ERRNO_TIMEOUT now that phpsnmp\SNMP is
  gone; drop the stale include/vendor/phpsnmp/ entry from the vendor-policy
  allowlist.

develop's sink baseline tracks constant-based includes differently than 1.2.x,
so no baseline refresh was needed here.

* Address PR #8174 review: snmp_get_method params, v3 walk, getnext agent, cache bump, CHANGELOG

- lib/snmp.php: restore $php_snmp as the 6th snmp_get_method() parameter and move
  the new $auth_proto/$priv_proto after it (callers pass them by name), so the
  positional $php_snmp used by SnmpCoverageTest and others keeps working.
- lib/snmp.php: keep every SNMPv3 operation except get/getnext on the Net-SNMP
  binary - the native walk branch implements only v1/v2, so a v3 walk reaching
  the procedural gate would have silently returned empty when path_snmpbulkwalk
  is missing.
- lib/snmp.php: cacti_snmp_getnext() v1/v2 branches now use snmp_format_agent()
  like get/get_raw, so an IPv6 literal / non-default port is bracketed.
- lib/api_device.php: bump snmp_cred_version only when the host's v3 membership
  or one of the cached credential fields actually changed, instead of on every
  save of a v3 device.
- CHANGELOG: move the #8171 entry into the issue block and the #8166 entries into
  the feature block, matching the established section ordering.

* Fix CI: unbracket IPv4/DNS SNMP targets, inline auth-cache SQL columns, apply phpcsfixer standard

* Refresh sink inventory baseline after short-array normalization

* Restore lib/snmp.php 100% coverage after aligning v3 routing to 1.2.x

- update the three SnmpCoverageTest assertions that still encoded develop's
  pre-port behavior: v3 snmp_get_method now returns PHP, an unsupported-
  protocol v3 session returns the binary-delegating session object, and the
  v3 cacti_snmp_get path runs natively
- add a v3 noAuthNoPriv user to the snmp-coverage snmpd fixture so native
  snmp3_get is exercised deterministically
- add tests covering the ported subsystems to 100%: shared auth cache,
  binary-delegating session, protocol pickers, v3 credential args,
  cacti_snmp_get_multi, and the cacti_get_snmpv3_auth cache path
- snmp_auth_cache_load uses a resettable process guard so its enabled and
  disabled branches are both coverable in a single process

* Resolve phpstan errors in the relocated SNMP MIB parser and ported snmp.php

- lib/mib_parser.php: type the MibParser properties, add parameter/return
  types, guard file_get_contents()/preg_split() against false, correct the
  parse_bracket_token delimiter params (string, not integer), and drop the
  unreachable return after trigger_error(E_USER_ERROR) - now phpstan clean
- lib/mib_cache.php: drop the redundant isset($mp->oids) guard (the typed
  property is always initialized; $mp->mib is the real check)
- lib/snmp.php: type the CactiSnmpBinarySession and CactiSnmpNativeSession
  properties/methods and pass bool to snmp_set_quick_print()
- .phpstan.neon: ignore greaterOrEqual.alwaysFalse alongside the existing
  greater.alwaysFalse (the PHP_VERSION_ID >= 80600 forward-compat gate)
- regenerate phpstan-baseline.neon for the irreducible ext-snmp stub
  strictness (snmp3_get literal-union protocols, int<-1,max> timeouts) in
  the ported procedural v3 path, the new lib/cache.php shmop stubs, and the
  $config array-shape offsets in include/global.php

* Make the v3 getter catch coverage net-snmp-build-independent

The DES-rejection trick that drove the procedural snmp3_get()/snmp3_getnext()
try/catch blocks only works on net-snmp builds compiled without DES; CI's
Debian net-snmp accepts DES, so those catches went uncovered (98.3%).

- extend the existing native_get injection seam (already on cacti_snmp_get)
  to cacti_snmp_get_raw, cacti_snmp_getnext, and cacti_snmp_get_multi, and
  drive the catch blocks with a throwing native getter in the test
- mark the seam-less cacti_snmp_session() setSecurity catch with
  @codeCoverageIgnore (it only throws for a protocol the running net-snmp
  rejects, which is not portably reproducible in the fixture)

* Exclude ext-snmp/PHP-8.6-gated defensive branches from snmp coverage

Two branches in lib/snmp.php are unreachable on the PHP 8.3 + ext-snmp
coverage runner and were only "covered" locally via opcache dead-code
elimination:

- snmp_get_method()'s !function_exists('snmp3_get') fallback (ext-snmp
  always provides snmp3_get)
- snmp_php_v3_protocols_supported()'s PHP_VERSION_ID >= 80600 block
  (SHA-224/384 and AES-192/256 tokens are only accepted on PHP 8.6+)

Mark both with @codeCoverageIgnore so the 100% gate is deterministic
across runners regardless of opcache settings.

* Account for the #8166 SNMPv3 credential-cache gate query in the batching test

poller_update_poller_cache_from_buffer() now runs one SELECT per flush to
decide whether the flush touched SNMPv3 poller items before bumping the shared
credential-cache version (matching 1.2.x). UpdateDataQueryCacheBatchingIntegrationTest
counted 4 statements per flush; update it to 5 (and 3 per-row flushes to 15,
1 batched flush to 5). The guard's intent - batching issues fewer statements
than per-row flushes - still holds.

* Port #8175: group SNMP options under an SNMP Behavior section

Mirror the 1.2.x settings reorganization (#8175) on develop. develop already
keeps the SNMP options in a dedicated SNMP tab, so add an "SNMP Behavior"
spacer there, order the behavior options as md5/des, credential cache, max
get size, bulkwalk fetch size, and move "Disable increasing OID Check" out of
the Poller tab to sit last in that group. Display-only; no settings added or
removed and no behavior change. Adds the "SNMP Behavior" heading to cacti.pot.

* fix: complete dependabot htmx 2.0.10 to 2.0.11 bump

Dependabot bumped package.json and package-lock.json to htmx.org 2.0.11
(PR 8179) but left the vendored include/js/htmx.js, its version marker,
and the pinned SRI constant at 2.0.10. The htmx-filters CI workflow runs
npm ci, which regenerates include/js/htmx.js from the 2.0.11 package, so
HtmxTest's runtime sha384 no longer matches the HTMX_2_0_10_SRI constant
and the integrity assertion fails.

Complete the bump: regenerate the vendored include/js/htmx.js to the
2.0.11 build, bump include/js/htmx.js.version to 2.0.11, rename
HTMX_2_0_10_SRI to HTMX_2_0_11_SRI with the recomputed sha384 hash, and
update the version/integrity assertions in tests/Unit/Ui/Html/HtmxTest.php
and tests/HandOff/HtmxLoaderHandOffTest.php. package.json and
package-lock.json match the values dependabot already landed on develop.

* settings: move SNMP Agent Support under the SNMP Behavior spacer

Relocate the enable_snmp_agent setting (SNMP Agent Support Enabled) so it
is the first item under the SNMP Behavior spacer instead of sitting in the
Poller General group, grouping it with the other SNMP behavior options.

* Address PR #8174 review: keep SHA-224/384 on the binary, honor port on transport targets

Resolves the two still-valid Copilot review findings on lib/snmp.php that were
left unresolved (only marked outdated after later commits shifted their lines).

- snmp_php_v3_protocols_supported(): drop SHA224/SHA384 from the native auth
  allowlist. ext-snmp's SNMP::setSecurity()/snmp3_*() only map MD5/SHA/SHA256/
  SHA512 - there is no SHA-224/SHA-384 mapping on any build, including PHP 8.6
  (verified against php-src ext/snmp/snmp.c), so routing those tokens to the
  procedural path raised a ValueError instead of using the binary. They now
  always fall back to the Net-SNMP binary. The AES-192/256 privacy tokens that
  ext-snmp does add on 8.6 stay gated behind PHP_VERSION_ID >= 80600.
- snmp_format_agent(): a udp/tcp transport-qualified target that omits a port
  now has the configured non-default port appended (udp6:[2001:db8::1] + 1161 ->
  udp6:[2001:db8::1]:1161), matching the Net-SNMP binary path. Targets that
  already carry a port, and unix: sockets (no port), are left untouched.
- SnmpCoverageTest: cover the new transport-target branches; lib/snmp.php stays
  at 100%.
2026-10-05 10:18:41 -04:00