Commit Graph
3646 Commits
Author SHA1 Message Date
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
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
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 c749790d84 fix(security): [develop] require CSRF-protected POST for link and plugin reordering (#8278)
* security: require CSRF-protected POST for link and plugin reordering (#7454)

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

* fix(csrf): share POST and token guards for reorder actions

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-13 07:52:20 -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 5f72f1a03c fix(security): [develop] require protected POSTs for administrative mutation endpoints (#8331)
* fix(csrf): require POST for administrative mutation endpoints

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

* test(csrf): support token-checking mutation helper

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 15:41:29 -07:00
Thomas Vincent 49c95bef63 fix(deps): track legacy browser assets and remove patch-package (#8330)
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 15:41:22 -07:00
Thomas Vincent ac50e0e0d0 fix(csrf): restrict XHR tokens to same-origin requests (#8329)
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 15:41:16 -07:00
Thomas Vincent 9a3a7dfce0 fix(graphs): [develop] correct opacity, redraw and realtime retries (#8328)
* fix(theme): enable graph opacity without selectmenu

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

* docs(changelog): describe theme fix

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

* fix(graph): restore redraw URLs and retry realtime failures

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 15:41:10 -07:00
Thomas Vincent 6e5078c929 fix(theme): [develop] restore Midwinter navigation, search and filter controls (#8327)
* fix(theme): restore Midwinter navigation and kiosk controls

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

* docs(changelog): describe theme fix

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

* fix(navigation): preserve filters and restore search and keyboard controls

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 15:41:04 -07:00
Thomas Vincent f28071eb67 fix(theme): [develop] restore Midwinter login controls, form state and notifications (#8326)
* fix(theme): preserve Midwinter authentication controls

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

* docs(changelog): describe theme fix

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

* fix(ui): preserve form state and notification lifecycles

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

* fix: replace the blinking timer during skin setup

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 15:40:56 -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 49e14c9785 Filter dialog: theme-aware close glyph, titlebar height matches table header, Deepness input theming (#8312)
* Filter dialog: theme-aware close glyph, titlebar height matches tableHeader, Deepness input theming

- Replace the non-theme-aware inverted sprite close icon with a theme-coloured x glyph (color: inherit) across all 10 themes that style cactiFilterDialog.
- Set the titlebar height/line-height to match each theme's cactiTableTitleRow height instead of 10px padding that made the dialog header taller than table headers.
- Deepness: re-apply theme field colours to dialog inputs/selects/textareas with higher specificity so jQuery UI .ui-widget-content .ui-state-default no longer overrides them.

* changelog: filter dialog theme consistency (#8312)

* Filter dialog: theme select focus border; raspberry titlebar 28px to match table header box

* test(ui): cover filter dialogs across all changed themes

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: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
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
TheWitness 533f3b5a04 Harmonize DateTimePicker styling with each theme palette (#8287)
* Harmonize DateTimePicker styling with each theme palette

The jQuery UI datepicker and timepicker addon rendered with the stock ThemeRoller texture-image backgrounds and a generic gray/green look that did not match any theme (most visible in the deepness dark theme). Add per-theme, picker-scoped overrides to every theme's jquery.timepicker.css so the popup surface, header, weekday labels, day cells, selected/hover/today states, Now/Done buttons, and the time sliders use each theme's own accent and content colors. Rules are scoped to .ui-datepicker so other jQuery UI widgets are unaffected.

* Address DateTimePicker a11y review (develop)

- Reset the nav-icon filter on prev/next hover (cacti, paw, raspberry) so the dark jQuery UI sprite stays visible on the pale hover tint.

- Give carrot and hollyberry a dark header title + dark nav icons so the month/year text clears AA on the light-ish accent header.

- Carrot: move to the theme's darker orange (#d2691e) for the today outline and sliders, and use a dark foreground on the orange selected-day/button fills.

- cacti and hollyberry: dark foreground on the accent selected-day/button fills to clear 4.5:1.

- paper-plane and sunrise: invert the icon sprite to white so the arrows are visible on their dark headers.

- dark and modern: add an explicit focus outline on the time-slider handles (both themes suppress the browser outline globally).

- midwinter: rebuild the override on the theme palette variables so the picker follows dark/light mode, with the theme blue as a fixed AA accent.
2026-10-11 13:32:41 -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
TheWitness f54da33bef themes(deepness): harmonize all greens to the emerald palette (#8234)
* themes(deepness): harmonize all greens to the emerald palette

* docs: changelog.d/8234.issue (deepness green harmonization)

* themes(deepness): distinct active-state green + sync themeroller url

Address review: give .ui-state-active a darker emerald (#083f2e) so persistent selection stays distinct from the #0c5f44 hover/focus state, and update the themeroller URL's hover/active colors and textures (flat) to match the de-olived declarations.
2026-10-08 19:51:03 -04:00
TheWitness c4a8c552a6 security: require POST+CSRF for GET-triggered purge/disable actions (GHSA-x664-x4hf-p9xh) (#8236)
* security: require POST+CSRF for GET-triggered purge/disable actions (GHSA-x664-x4hf-p9xh)

* style: align rrdcheck purge array operators for php-cs-fixer
2026-10-08 19:42:34 -04:00
TheWitness 9df1923c83 themes: use a clean green for .deviceUp instead of muddy olive (#8231)
* themes: use a clean green for .deviceUp instead of muddy olive

The device-status/good-message color .deviceUp was rgba(148,179,107,0.8), a desaturated olive that reads as a muddy warm tone (easily confused with warning/bad) - most visibly on the jQuery UI message dialog, where core wraps MESSAGE_LEVEL_INFO text in <span class=deviceUp>. Align these themes with the clean green rgba(40,180,40) already used by the deepness, modern and paw themes. Applies to device-status text everywhere the class is used.

* changelog: add fragment for .deviceUp green recolor (#8231)
2026-10-08 14:30:45 -04:00
TheWitness 22ab16cac9 themes: unify login page styling across all core themes (#8230)
* themes: unify login page styling across all core themes

Flatten the dark themes' login page/card gradients (dark, paper-plane) to flat surfaces, give every theme a clean bordered login card, and add a browser -webkit-autofill override per theme so saved credentials keep the theme's input colors instead of the browser's olive tint. Re-point the Deepness and MidWinter themes' login styling from the legacy .login*/.cactiLogin* classes to develop's .cactiAuth* markup (including .cactiAuthLeft, which auth_login.php targets) so their login pages render correctly. Light themes keep their signature login backdrops.

* changelog: add fragment for login page theme unification (#8230)
2026-10-08 13:32:01 -04:00
TheWitness 6f57c319dd midwinter: colour the graph drilldown action icons from the theme palette (develop port of #8228) (#8229)
* midwinter: colour the graph drilldown action icons using theme-palette variables (distinct task-appropriate hues, dark + light modes)

* changelog: add fragment for midwinter graph drilldown icon colours (#8229)
2026-10-08 13:30:50 -04:00
TheWitness 30db858d3f deepness: deep-dark login page with a bordered login box (#8224)
* deepness: deep-dark login background with a bordered, slightly-greyer login box

Drop the emerald loginBody gradient for a flat deep-dark page (var(--dp-page)), give the login box a flat slightly-greyer tint (var(--dp-panel-alt)) with a thin var(--dp-border) border, and override -webkit-autofill so the username/password fields no longer show the olive autofill tint.

* docs: add CHANGELOG entry for #8224 (Deepness login restyle)

* deepness: use input surface for login autofill override

The -webkit-autofill inset box-shadow used var(--dp-panel-alt) (the login card surface) instead of var(--dp-panel) used by the standard text/password inputs. Autofilled credentials therefore took on the card color rather than the normal input appearance. Use var(--dp-panel) so the override only removes Chrome's autofill tint.
2026-10-08 09:10:28 -04:00
TheWitnessandTheWitness 4d618e1869 Dark theme: theme-appropriate host status pills (develop) (#8223)
* dark theme: add theme-appropriate host status pills

Port the translucent host status pills (.deviceStatus) to develop's Dark theme with colors anchored to the theme's olive/sage palette: Up olive rgba(148,179,107)/rgba(167,217,134), Down muted brick red, Recovering theme orange #ff8f1e, Error theme purple #750F7D, Unknown theme blue #0072B5, Disabled theme grey #a1a1a1. The deviceStatus hook already ships in get_colored_device_status() on develop.

* changelog: 8223.feature dark theme host status pills

* dark: opaque dark backing for status pills on selected/hover rows

On tr.selected (opaque light-olive gradient) and tr.selectable:hover the translucent .deviceStatus pills let the bright row show through, dropping the light !important status text below WCAG contrast. Back the pills with the theme base (#161616) in those states; per-state border/text colors still convey status.

---------

Co-authored-by: TheWitness <larry@thewitness.org>
2026-10-08 09:10:18 -04:00
TheWitness 2e8afef8f7 issue: align Deepness console icon sizing/alignment with Modern (#8221)
* deepness: match modern console icon sizing/alignment and drop sub-menu indent

Port modern's .menu_glyph size/alignment (18px, 22px width, padding-right:0, inline-block), add line-height:20px to .menuitem span, bump legacy i.fa to 15px with text-align:left, and remove the 32px sub-menu indent (#nav li ul li a now flush at 5px like the top-level items).

* docs: add CHANGELOG entry for #8221 (Deepness console icon alignment)
2026-10-08 08:52:02 -04:00
TheWitness 35e7181ded issue: skin the modern filter Edit dialog for the Deepness theme (#8220)
* issue: skin the modern filter Edit dialog for the Deepness theme

The Deepness theme received the modern filter *bar* rules (#8219) but not
the .ui-dialog.cactiFilterDialog skin the other themes carry, so the filter
Edit dialog fell back to the default jQuery UI themeroller look (mismatched
titlebar gradient, unstyled buttonpane). Port the scoped dialog block used
by the other themes, using Deepness's emerald palette variables
(--dp-accent-deep titlebar/primary button, --dp-border buttonpane divider).

* issue#8220: reference PR number in CHANGELOG entry to satisfy the format gate
2026-10-07 22:07:58 -04:00
TheWitness 4430210a85 issue: fix Deepness theme modern filter bar stacking vertically (#8219)
* issue: fix Deepness theme modern filter bar stacking vertically

The Deepness theme (added in #8206) shipped include/themes/deepness/main.css without the modern filter-bar display rules the other themes carry, so .filterColumn/.filterColumnButton divs fell back to display:block and the Layouts/Search/Rows controls and action buttons stacked vertically. Port the .filterRow/.filterColumn/.filterColumnButton table-display rules and the .cactiFilterModernBar rules from the modern theme.

* docs: add CHANGELOG entry for #8219 (Deepness filter bar fix)

* issue#8219: port icon-rotate and barSep modern-bar rules to Deepness

html_filter.php adds icon-rotate to the Refresh glyph and emits empty barSep separator spans. Deepness lacked both rules, so the refresh action had no spin feedback and the separators were invisible. Add .icon-rotate and .cactiFilterModernBar .barSep (using the dark-theme rgba(128,128,128,0.45) separator colour, matching the dark/carrot/paper-plane themes).
2026-10-07 21:35:29 -04:00
TheWitness e7d389f4a1 deepness: make left-menu glyphs the light text colour always (#8216)
The .menu_parent fa/fas/fab/far/fal icons used var(--dp-accent) (emerald). Switch to var(--dp-text) so the console navigation glyphs stay light in every state.
2026-10-07 16:03:37 -04:00
TheWitness f66bf80c41 themes: render legacy raw filter checkboxes as toggle switches (#8214)
The common filter class already renders filter checkboxes as text-first toggle sliders. This adds the equivalent styling for legacy plugin filters that still emit a raw <input type=checkbox> inside .filterTable, so they match until they adopt the new filter class. Each theme's filter toggle mirrors that theme's own checkboxSlider (colours, radius, 0.4s). form_checkbox()/common-filter sliders (class formCheckbox) are excluded via :not(.formCheckbox). Track radius is !important to beat .ui-corner-all.
2026-10-07 15:43:12 -04:00
40d648cef3 fix: The horizontal scroll bar should not be visible unless it has to be (#8212)
* fix: horizontal scroll bar should not be visible unless it has to be

Port of #8211 to develop. Let overflow-x fall back to 'auto' instead of 'visible' so a horizontal scroll bar only appears when content actually overflows, and drop the resizable handle from the last visible column in applyTableSizing() so resizing it cannot force the page wider than the viewport.

* Fix resizable destruction for visible table headers

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

---------

Co-authored-by: TheWitness <larry@thewitness.org>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-07 15:38:22 -04:00
TheWitnessandTheWitness 16f62713c5 paper-plane: dark-mode refinements (readable forms/rows, consistent button system) (#8207)
* paper-plane: dark-mode refinements (readable forms/rows, consistent button system)

* tests: make CactiErrorHandler suppression test hermetic against leaked IN_CACTI_INSTALL

The three suppression cases eval a sandboxed copy of CactiErrorHandler to exercise the error_reporting() mask and logging path. Pest runs the whole suite in one process, and tests/integration/BoostProcessTableUpgradeIntegrationTest defines the global IN_CACTI_INSTALL constant at collection time. A constant cannot be unset, so the handler short-circuited to true before reaching the branch under test, failing 'an enabled level is logged and reported to PHP as unhandled'. Strip the install-mode guard from the sandboxed copy so the case is independent of collection order.

---------

Co-authored-by: TheWitness <larry@thewitness.org>
2026-10-07 14:17:13 -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
Petr Macek 1dc9961e03 fix displayin js code after last collapsible row (#8200) 2026-10-06 15:42:27 -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 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
TheWitness 7b551eea72 chore: normalize CRLF line endings to LF across Cacti-owned files (#8156)
Excludes vendored packages, third-party JS, patches, and the Win32 probe fixture. Pure EOL change; guards pass and php -l is clean.
2026-10-02 17:22:42 -04:00
TheWitness 41e6d9e033 security: consolidate verified GHSA fixes for develop (batch 4) (#8145)
* security: consolidate verified GHSA fixes for develop (batch 4)

Develop counterparts of the 1.2.x batch-4 fixes in #8144, re-verified against
current develop HEAD.

- GHSA-4rmr-wvjq-qxc2: include/auth.php Basic Auth and guest branches fall
  through to the per-page realm check instead of returning early.
- GHSA-j67j-wpm4-9g3x: the pre-dispatch update_policy branch in user_admin.php
  and user_group_admin.php requires a CSRF-token POST. The denylist -> allowlist
  redesign is tracked for 1.3 in #8143.
- GHSA-2p3x-f5jm-rg6p: the View-Graphs -> poller RCE chain is broken by the
  realm check (4rmr), the update_policy guard (j67j) and shell-free webhits (x5pw).
- GHSA-x5pw-cg7j-mj4r: scripts/webhits.pl runs wc through a shell-free list-form open.
- GHSA-9737-rqh3-h7fh: names interpolated into raise_message() now use __esc()
  across the UI; URL- and HTML-bearing messages are excluded.
- GHSA-9x9h-2577-9w86: the colordropdown input is built as a node and the label
  is no longer round-tripped through parseHTML (the list item already used
  createTextNode on develop).
- GHSA-h96j-8xhc-2p3w: remote_agent.php authorizes on $_SERVER['REMOTE_ADDR'],
  not get_client_addr(), which honours client-settable proxy headers.

Regression guards under tests/Unit/Security/{Auth,Csrf,Xss,ShellInjection}.
php -l, check-changelog.py and the sink/architectural baselines all pass.

* chore: reference PR #8145 on the batch-4 1.3.0-dev CHANGELOG entries

* security: resolve develop merge - keep id-only xmlerror unescaped (matches #8141 h745 test)

* security(GHSA-j67j): use REQUEST_METHOD POST guard instead of raw \ (hardening-pattern-guard)

* security(GHSA-9737): fix review findings - drop double-encoding in __esc, escape missed name sinks, correct plugins format-arg count
2026-10-02 13:10:42 -04:00
TheWitness 0055dd00cc security: consolidate verified GHSA fixes for develop (batch 3) + CHANGELOG CVE reconciliation (#8141)
* security: consolidate verified GHSA fixes for develop (batch 3)

Develop counterparts of the 1.2.x fixes in #8138, each re-verified against
develop HEAD.

- GHSA-77pr-g949-4v5f: spikekill.php also requires is_graph_allowed() on the
  target graph, not just the Spike Kill realm.
- GHSA-w3qw-762w-6g9v: the include/global.php GET action guard fails closed on
  an array-form action[] before the scalar denylist comparison.
- GHSA-xhpr-w454-cc9w: update_poller_cache() casts the stored output_type to
  int before interpolation.
- GHSA-j3px-vw6r-g25x: graphs_new.php escapes stored snmp_index via db_qstr()
  in the data-query IN() pivot.
- GHSA-h745-3r76-g253: graphs_new.php escapes the data-query and field names in
  the XML parse error messages (__ -> __esc).

GHSA-297h-95ff-r767 does not apply on develop: the clog log-file selector
renders through the drop_array filter framework, which escapes.

tests/Unit/Security/ConsolidatedBatch3Test.php adds source-scanning regression
guards. CHANGELOG (1.3.0-dev) updated; php -l, the security baselines, and
check-changelog.py all pass.

* chore: lead 1.3.0-dev security CHANGELOG entries with the GHSA id

Normalize the 1.3.0-dev -security entries to the approved
"-security: GHSA-xxxx [CVE-xxxx] Description" form, moving the advisory id to
the front. CVE tokens are omitted because none of these advisories have a CVE
issued yet (verified via the repository security-advisories API). Covers the
five batch-3 entries plus the pre-existing entries that still used the trailing
(GHSA-xxxx) form.

* chore: use the -security#GHSA-xxxx: form for the 1.3.0-dev CHANGELOG entries

Correct the batch-3 and reconciled 1.3.0-dev -security entries to the approved
"-security#GHSA-xxxx: CVE-yyyy Description" form (GHSA id in the prefix). No CVE
tokens are added because none of these advisories have a CVE issued yet
(verified via the repository security-advisories API).

* test: split batch-3 security guards into vulnerability-class files; reconcile CHANGELOG

- Move the ConsolidatedBatch3Test.php guards into the matching Auth, Csrf,
  SqlInjection and Xss folders with behavior-based filenames, and strengthen
  them per review: the GET-guard test asserts the array branch actually returns
  405 and exits; the snmp_index test rejects both raw concatenation branches;
  the parse-error test asserts every name-bearing message uses __esc().
- Append this pull request number to the five new 1.3.0-dev security entries.
- Add issued CVE identifiers to the older -security#GHSA CHANGELOG entries
  (verified via the repository security-advisories API). Entries with no CVE
  yet, entries that already carried a CVE, and non-GHSA lines are unchanged.
2026-10-02 11:03:52 -04:00
TheWitnessandTheWitness 3ab4c69fb7 PHPStan Level 8 - Phase 1 Conversion (#8135)
* PHPStan Level 8 - Phase 1 Conversion

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

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

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

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

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

* Remove redundant @phpstan-ignore comments

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

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

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

* ci: satisfy validator ratchet and refresh security sink baselines

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
2026-10-02 10:08:56 -04:00
TheWitness 6251fed1dc Make the drop_multi form method use Select2 by default (#8130)
* form_multi_dropdown: default drop_multi to select2, keep legacy opt-in

The drop_multi form method now renders a select2 'N Selected' multi dropdown by default instead of the legacy jquery-multiselect widget, closing a gap left from the earlier select2-multi-count work (html_graph/html_tree already opted in explicitly). A caller keeps the legacy jquery-multiselect widget by including 'multiselect' in its field class; the Scheduler fields used by Automation Networks (lib/api_scheduler.php), Data Source Profiles consolidation functions (global_form.php) and the Settings selective-debug/spikekill pickers (global_settings.php) are marked accordingly so their existing per-page .multiselect() initializers keep working unchanged. Also drops the duplicate class concatenation the old code emitted.

* settings: convert selective debug and spikekill template pickers to select2

Mirror of the 1.2.x conversion. Drop the inline .multiselect() initializers for the Logging-tab Selective File/Plugin Debug pickers and the Spikes-tab 'Graph Templates to Spike Kill' picker, and remove their 'multiselect' opt-in class, so applySkin() renders them as the select2 'N Selected' multi dropdown. Each keeps its wording via per-field select_all_text/select_count_text (the select2-multi-count data-attribute mechanism used for graph_template_id): 'Select File(s)'/'Files Selected', 'Select Plugin(s)'/'Plugins Selected', and 'Select Template(s)'/'Templates Selected'. Empty stays the placeholder text (poller_spikekill.php treats no selection as none).

* settings: align array operators in debug/spikekill defs (php-cs-fixer)

* html_form: legacy opt-in strips select2-multi-count; add drop_multi tests; fix stale docblock

When a drop_multi field carries both 'multiselect' and 'select2-multi-count', the legacy opt-in now wins: the select2-multi-count token is stripped so applySkin() does not also decorate the field and collide with the page's own .multiselect() init. Adds tests/Unit/Ui/Html/FormMultiDropdownSelect2Test.php covering the default injection, legacy opt-in, preserved plugin class, no duplication, and the both-token precedence (either order collapses to the legacy widget). Also drops the stale 'Settings selective debug' example from the helper comment since this PR converts those fields to select2.
2026-10-01 19:53:32 -04:00
TheWitnessandTheWitness d4b8cd3814 fix: align Force Uninstall icon (.piforceremove) on the Plugins page (#8110)
* fix: align Force Uninstall icon by adding .piforceremove to icon-padding rule

The orphaned-plugin (-5) control introduced in #8107 renders a .piforceremove anchor, but that class was missing from the plugin-action icon padding rule, so the icon lost its 0 3px padding and misaligned in the Actions column. Add .piforceremove alongside .pierror in every theme stylesheet.

* fix: align Force Uninstall confirm wording to standard 'Otherwise click Cancel.' pattern

develop counterpart of the #8109 dialog copy fix. develop already uses the non-modal displayDialog() with a plain-text message, so only the closing sentence is aligned to the house pattern (with trailing period). cacti.pot msgid updated to match.

* fix: bump midwinter core.css cache-buster hash to match the updated file

The Force Uninstall (.piforceremove) alignment change edited
include/themes/midwinter/css/media/core.css, changing its content md5 to
afd5bc6f4d45884846ab715f1583123c, but main.css still imported it with the old
?ad4ed321... query string. Browsers holding the pre-change stylesheet in cache
would keep the icon misaligned. Update the import hash so the change is picked up.

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
2026-09-29 16:32:46 -05:00
TheWitness 8a73fc6703 tree: Tabler glyphs for jsTree nodes with per-type theme colors and glyph carets (#8101)
* tree: replace jsTree node PNGs with Tabler glyphs, per-type theme colors, and glyph open/close carets

* tree: center carets to 20px node row, remove indent dotted lines, add dots:false to editor source tree

* tree: center jsTree carets with flexbox so alignment is glyph-height independent

* tree: vertically center node glyph with its label text (vertical-align middle)

* tree: preserve jsTree loading throbber and add default folder glyph

Scope the .jstree-ocl reset and caret glyph content to :not(.jstree-loading) so AJAX expansion keeps its loading indicator, and register a 'ti ti-folder' glyph (with per-theme color) for the untyped 'default' branch type so branch nodes are not left with a blank icon.
2026-09-29 10:42:29 -04:00
TheWitness e29581b68a Make package/diff viewer colors theme-aware (#8093)
* Make package/diff viewer colors theme-aware

The jfcherng/php-diff Inline renderer output is styled by each theme's
Diff.css, but every theme shipped an identical copy that hard-coded a
light palette (color: black, background: #fff, opaque #fbe1e1/#e1fbe1
add/remove tints). Inside the diff dialog this painted a light box with
dark text over dark themes (dark, sunrise, paper-plane) and over
midwinter's dark color-mode, so the modal did not match the theme.

Derive the base surface from the themed dialog instead of hard-coding it
(color: inherit, background: transparent, neutral translucent borders and
gutters) and express the add/remove/replace highlights as translucent
rgba overlays that read correctly on both light and dark surfaces. Light
themes render essentially as before; dark themes now inherit their dark
surface and light text. Applied to every theme so the shared stylesheet
stays identical.

* Band diff code columns on dark themes

On the dark themes the unchanged (context) code column and the
line-number gutter read too similarly, making the actual diff on the
right hard to pick out. Give the context old/new code cells a subtle
dark band (rgba(0,0,0,.25)) so the code reads as a panel distinct from
the lighter line-number gutter. Applied to dark, sunrise and paper-plane;
for midwinter the band is scoped to its dark color-mode only. Light
themes are unchanged.
2026-09-28 15:21:51 -04:00
TheWitness 46908e350b fix(ci): pin htmx.org back to 2.0.10 to match vendored asset and SRI (#8091)
* fix(ci): pin htmx.org back to 2.0.10 to match vendored asset and SRI

The npm group bump #8085 raised htmx.org to 4.0.0, but the vendored
include/js/htmx.js, the HTMX_2_0_10_SRI integrity constant, and the
htmx.js.version file were not regenerated. CI's 'Build managed JavaScript'
step (build/sync-js.mjs) copies node_modules/htmx.org/dist/htmx.js, so it
shipped 4.0.0 while htmx_script_tag() still emitted the 2.0.10 SRI, failing
tests/Unit/Ui/Html/HtmxTest. Revert htmx.org to the vendored/pinned 2.0.10;
a real htmx 4.x upgrade should be a separate, tested PR.

* fix(htmx): sync version file, tests and docs to vendored 2.0.10
2026-09-28 15:02:18 -04:00