837 Commits
Author SHA1 Message Date
a9c73cfc42 fix(themes): skin jQuery multiselect in Deepness and fix 3.x markup in four themes (develop) (#8443)
* fix(themes): skin jQuery multiselect in Deepness and the 3.x markup in four themes

Deepness: main.css repainted select2 onto its --dp-* palette but never
the jQuery multiselect button or popup menu, so those fell through to the
jquery-ui.css textured surfaces and the generic uppercase button rule and
looked like the Dark theme. Repaint them to match the select2 controls.

Cacti, Carrot, Hollyberry, Raspberry: these themes ship the old 1.x
jquery.multiselect.css while include/js/jquery.multiselect.js is 3.x, so
optgroup menus showed list bullets, a nested indent, compounded 0.9em
option text and an unstyled group label. Add the missing 3.x rules.

All changes extend each theme's main.css; the stock
jquery.multiselect.css files are left untouched.

Reported in Cacti/plugin_flowview#298.

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

* fix(themes): satisfy the develop CHANGELOG and theme coverage gates

- Move the CHANGELOG entries into changelog.d/8443.issue, as develop
  expects fragments rather than direct CHANGELOG edits.
- Drop the Deepness multiselect border-radius; .ui-corner-all forces
  border-radius with !important in this theme, so it never applied.
- Add Chromium computed-style tests for the new multiselect rules in
  Deepness, Cacti, Carrot, Hollyberry and Raspberry so the changed CSS
  lines are exercised by the theme correctness suite.

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

---------

Co-authored-by: TheWitness <copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-22 14:47:45 -04:00
TheWitness b95226918e fix: Update the deepness favicon (#8419)
* fix: Update the deepness favicon

(cherry picked from commit 3a59295162)

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

(cherry picked from commit ee0b4c84ed)

* fix: Update such that the modern svg favicon can render
2026-10-18 11:51:18 -04:00
TheWitness 57ef1c5f77 featture: Add modern favicon images to develop (#8424)
This will improve the look of Cacti in the various browsers.
2026-10-18 11:50:50 -04:00
TheWitness c226f2a1a5 Add a right scrollbar gutter to the console content area in legacy themes (#8403)
.cactiConsoleContentArea scrolls vertically, and with no right padding the
scrollbar sat flush against the right-aligned cells (for example the Data
Source Info values on data_sources.php), hiding the last digits. Give it a
15px right gutter in every legacy theme (cacti, carrot, dark, deepness,
hollyberry, modern, paper-plane, paw, raspberry, sunrise). midwinter already
reserves a scrollbar gutter and is unchanged.

Cover each changed rule with a develop-theme-correctness targeted test.
2026-10-16 08:16:58 -04:00
TheWitnessandCopilot 4e09327660 deepness: fix unreadable search-result highlight color (#8396)
* deepness: make search-result highlight readable and on-theme

The .filteredValue search highlight set a neon yellow (#F8D93D) background
with no text color, so the emerald link text (--dp-link) rendered on bright
yellow was effectively unreadable and clashed with the dark emerald theme.

Use a warm gold background that complements the emerald palette and a
near-black (--dp-page) text color for high-contrast, readable matches.

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

* tests: cover deepness .filteredValue highlight computed styles

Assert the deepness search-highlight background and text colors in the
theme-correctness suite so the changed .filteredValue CSS lines are
exercised and meet the changed-CSS coverage gate.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:09:23 -04:00
TheWitness 5c1577a382 Force plugins.php action icons over POST and enlarge Tabler icons (#8394)
The install, enable, remote enable, load and dialog-confirmed actions on
plugins.php are guarded by csrf_require_post() on the server. The anchors
were still activated by the generic GET anchor handler, so clicking them
returned a 405 on the develop branch. Submit these actions over POST:

- The dialog Continue button and the Check Latest control now post.
- A capture-phase document click listener intercepts the install, enable,
  remote enable and load icons before the bubble-phase generic handler, so
  they post, keep working after Ajax table refreshes, and bind only once.

Tabler (i.ti) icons rendered smaller than the FontAwesome glyphs used on
the 1.2.x branch, so give them an explicit 16px rule matching the fa
layout in each bundled theme, and cover the new rule with
develop-theme-correctness targeted tests.
2026-10-15 13:39:18 -04:00
Thomas Vincent 62063a29e0 fix(theme): [develop] correct CSS cascade, invalid values and responsive selectors (#8293)
* fix(themes): correct CSS cascade, font fallbacks and responsive selectors

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

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

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

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

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

* style(themes): consolidate two reviewed duplicate selectors

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

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

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

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

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

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

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

* ci: retain bootstrap coverage and validate request dispatch

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

* test: synchronize native coverage fixture dispatch

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

* test: measure coverage at the exact PR revision

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

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

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

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

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

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 22:51:15 -04:00
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
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 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 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 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 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 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
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
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
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 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
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 f956ea5a5c Convert generic <select> elements to select2(), with new class helpers (#8053)
* feat: convert generic <select> elements to select2, with new class helpers

Replaces the jQuery UI selectmenu() catch-all in setSelectMenus() with select2,
applied uniformly to single and multi selects. Fewer than 10 options disables
the search box (minimumResultsForSearch: Infinity); 10 or more enables it -
same threshold now applied to the existing select2/select2-multi classes.

New layout.js class helper: select2-multi-count, a multi-select whose
selection summary reads "N Selected"/"All Selected" instead of one chip per
option (generic multiSelectAllText/multiSelectCountText strings in html.php,
overridable per-field via data-select-all-text/data-select-count-text/
data-select-all-value). graph_template_id (html_graph.php x2, html_tree.php)
now uses it in place of the jquery.multiselect-based .graph-multiselect,
preserving the existing "All Graph Templates"/"N Templates Selected" wording
and the -1 "all" sentinel behavior (reimplemented via select2:select/unselect
events in setSelectMenus()). The old .graph-multiselect jquery.multiselect
init block is left in place (now unreachable for core's own markup) in case
a plugin/theme still emits that class itself.

select2-callback is reworked from its previously-unused static-URL contract
into an ajax-backed replacement for the legacy .drop-callback/makeCallbacks()
autocomplete widget: data-action + data-variables build the query the same
way makeCallbacks() already did (pageName + action + live values of other
named fields + search term), and data-callback still names a JS function to
invoke on selection, matching form_callback()'s existing $on_change/$class
contract. form_callback() only renders the new <select> markup when its
$class parameter contains select2-callback (opt-in); every existing caller
that doesn't request it keeps the exact legacy output. html_graph.php's
host_id filter (both places) opts in. form_multi_dropdown() gained optional
$select_all_text/$select_count_text/$select_all_value parameters (all
additive/backward compatible) to plumb the graph_template_id wording through.

Compatibility: an org-wide code search found no plugin depending on the
.select2-callback/.graph-multiselect/.drop-callback/makeCallbacks() internals
being changed here (form_callback()'s default output is unchanged; the
.graph-multiselect jquery.multiselect init stays available). Everywhere else
in core that assumed the old jQuery UI selectmenu widget on elements now
covered by the broadened catch-all (disableField()/enableField(), the SNMP
security-level/protocol refresh logic, realtime.js's ds_step/graph_start/size
refresh, api_aggregate.php's field enable/disable, and html_reports.php's
branch_id/site_id/host_template_id handling) now branches on
.select2-hidden-accessible and uses select2's own refresh mechanism
(.trigger('change.select2')) instead, alongside the untouched legacy
selectmenu branch. #user_language and #i18n_default_language (which get
their own bespoke flag-icon languageselect widget) are excluded from the
catch-all, same as before.

The install wizard (install/install.js) has its own independent selectmenu
initialization and is intentionally left untouched.

* fix: use json_encode for select2 multi-select JS translation strings

* fix: make select2 initializers idempotent to stop duplicate widgets

applySkin() (which calls setSelectMenus(), where every select2/select2-multi/
select2-multi-count/select2-callback/catch-all initializer lives) runs more
than once per page view - once from cactiReady() on initial load, and again
from the generic AJAX-completion handler after any loadUrl()/postUrl() call,
which most list/filter pages already trigger during their own initial load.

Since calling .select2() a second time on an element that already has it
initialized creates a second, independent .select2-container next to the
first (select2 does not no-op or tear down the previous instance), every
affected <select> ended up rendering twice. Guard every initializer (plus
the graph_template_id -1 sentinel's event bindings, now namespaced and
unbound first) so a second setSelectMenus() pass skips elements already
marked .select2-hidden-accessible.

* fix: match select2 dropdown styling to the dark theme's real button look

border-radius was 4px (should be 6px, matching .ui-corner-all used
everywhere else); the selection box and dropdown panel were flat black
instead of this theme's actual .ui-button/.ui-widget-content backgrounds
(a subtle gradient-highlight image and a repeating texture image,
respectively). Also add a min-width and force the selected text/dropdown
option list to a single line instead of wrapping.

* fix: match select2 dropdown styling to each theme's real button look

Same fix as the earlier dark theme commit, applied to the remaining themes.
Every theme's select2 override previously used generic placeholder colors
(border-radius: 4px, background: #ffffff/#222222) that did not match that
theme's actual .ui-state-default/.ui-button/.ui-widget-content styling:

- cacti, carrot, hollyberry, raspberry, modern, paw: selection box now uses
  the real button gradient-image background instead of flat white; dropdown
  panel background already matched and is unchanged.
- sunrise: selection box background corrected to transparent (matching its
  real button style) and the dropdown panel to its real black background,
  instead of flat white for both.
- paper-plane: selection box background corrected to this theme's real
  near-black button color instead of flat white.
- midwinter: switched from hardcoded generic colors to this theme's own
  CSS custom properties (var(--border-color), var(--background-content),
  var(--text-color-lighter), etc.), matching jquery-ui.midwinter.css.

border-radius is now 6px everywhere, matching .ui-corner-all. All themes
also gained a min-width and forced the selected text/dropdown option list
onto a single line instead of wrapping.

* fix: default select2's dropdownAutoWidth to true

The legacy jQuery UI selectmenu widget always sized its open dropdown to fit
the widest option, regardless of how narrow the closed control itself was.
Select2 defaults to matching the dropdown's width to the control's, so wide
option text was wrapping/truncating inside a narrow panel. Set this as a
global default once instead of touching every individual select2() call.

fix: destroy any existing select2 widget before re-initializing

applySkin() runs more than once per page view. The existing
:not(.select2-hidden-accessible) guards were meant to make re-running the
select2 initializers a no-op, but duplicate widgets were still being
reported. Explicitly tear down any select2 instance still attached right
before the initializers run again, so a widget can never end up duplicated
regardless of why applySkin() re-ran.

fix: don't select2-ify a select a plugin already widget-ified with selectmenu

Symmetric to the above: if a plugin's own script already deliberately
applied jQuery UI selectmenu to one of its selects (a few do, for their own
dynamically-created fields), skip converting it to select2 too.

* fix: convert the Device filter's legacy drop-callback widget to select2-callback

html_host_filter()'s autocomplete-enabled branch never got the select2
treatment applied to every other filter field - it still built its own
<input class='drop-callback'> wired up by the older makeCallbacks()/jQuery UI
autocomplete widget (styled with the legacy ui-selectmenu-button markup),
instead of opting into form_callback()'s newer select2-callback rendering like
everything else. Left side-by-side with now-modernized neighboring filters,
this old widget stood out as a leftover "old select". Switch it to the same
select2-callback select markup (data-action='ajax_hosts',
data-variables='site_id') the 1.2.x branch already uses, reusing the existing
ajax_hosts handlers and get_allowed_ajax_hosts()'s id/label response shape.

* fix: restore select2's gradient after closing without selecting, shrink its height, and drop the now-unneeded min-width

Every theme's active/open select2 style also matched .select2-container--focus,
but select2 leaves that class on the container after the dropdown closes (it
only drops --open), so the flat "active" background stuck around until the
next selection instead of the gradient returning right away. Scope the active
look to --open only.

Also shrink the single-select control from select2's stock 28px down to 24px
(and its arrow/rendered text to match) so it sits closer to the jQuery UI
button height it's replacing, and drop the min-width: 160px placeholder added
earlier to keep wide option text from being clipped - dropdownAutoWidth now
sizes the open dropdown to its content regardless of the closed control's
width, so the constraint is no longer needed.

* feat: add configurable select2 search threshold; drop autocomplete_enabled on develop

Add the same Select Search Minimum Rows setting added on 1.2.x, wiring it
through as the select2SearchRows JS global (include/global_session.php) and
replacing the hardcoded 10-option cutoff in every select2 initializer in
layout.js.

Remove Autocomplete Enabled entirely on develop: it only ever existed to let
the Classic theme opt out of the ajax-backed callback widgets, and Classic
doesn't exist as a develop theme. html_host_filter() and the Graph Items data
source field now always use the ajax-backed rendering (select2-callback /
drop_callback) unconditionally instead of branching on the removed setting.

* fix: keep the first 20 select2 results visible before scrolling

Select2's stock results list only guaranteed ~7-8 rows (200px) before
scrolling. Pin each result row to a consistent 24px (matching the shrunk
control height) and size the list's max-height to fit 20 of them before the
scrollbar kicks in.

* fix: repair layout.js corruption from the select2SearchRows commit, and exclude .multiselect from the catch-all

A faulty automated edit in 3cbc5a75 (feat: add configurable select2 search
threshold) matched the wrong text in 4 places and clobbered unrelated code
instead of the intended minimumResultsForSearch lines:
 - deleted the fa-remove icon-class swap and mangled the `if (!theme)` check
 - deleted setupPageTimeout()/CsrfMagic.end()
 - left a stray bare `select2SearchRows` statement before the themeReady() call
 - deleted the #password/#password_confirm delayKeyup wiring and broke the
   showPage deferred's closing brace

Restored all four to their pre-corruption behavior and applied the intended
select2SearchRows threshold to the actual 4 select2 init blocks this time
(select2, select2-multi, select2-multi-count, and the generic catch-all).

While in there: the catch-all's '.multi-select'/'.graph-multiselect'
exclusions never matched anything real - form_multi_dropdown() always tags
its <select multiple> with plain 'multiselect', so every field using the
older jquery.multiselect plugin (Automation's day/month pickers, Consolidation
Functions, Selective Debug, etc.) was getting select2-ified on top of its own
dedicated multiselect() init. Exclude '.multiselect' instead.

* feat: checkbox-styled option rows for select2-multi-count dropdowns

Gets closer to the old jquery-multiselect look the count-style multi-selects
(Graph Templates, etc.) replaced: each option row in the open dropdown now
shows a checkbox-style indicator instead of plain text, driven by
[aria-selected] via CSS so it stays in sync with select2's own selection
state without any extra JS bookkeeping. closeOnSelect stays off so multiple
options can still be picked without the dropdown closing between clicks; the
closed control still shows the "N Selected"/"All Selected" summary text.

* fix: match multi-select control height to single-selects and add its missing dropdown arrow

select2's stock multi-select box only had a min-height with no matching
override, so it stayed taller than every single-select control next to it;
select2-multi-count fields never actually show real per-option chips (the
rendered area always gets replaced with "N Selected"/"All Selected" text),
so it's safe to tighten this to match.

select2 also only renders a .select2-selection__arrow element for
single-selects - multi-selects (Templates, Days of Week, etc.) got none by
default, so they looked handle-less next to every single-select control. Add
a matching CSS-only arrow via ::after (flips on select2-container--open,
same as the single-select's).

* fix: stop select2's multi-select inline search box from collapsing to near-zero width

Select2 sizes the inline search input it embeds in .select2-selection__rendered
by measuring the box's natural layout; my earlier height-matching CSS
(zeroed padding, a fixed line-height, and extending the single-select's
overflow:hidden/text-overflow:ellipsis truncation to multi-selects too) broke
that measurement, so the search field rendered as a barely-visible sliver
sitting inline next to the "N Selected" text instead of expanding to a usable
width.

Back off to a lighter touch: only nudge min-height down a little (no padding/
line-height overrides), and keep the overflow/ellipsis truncation scoped to
single-selects, which is where it was actually needed (short "N Selected"/
"All Selected" labels on multi-selects don't need it and it wasn't there
before this session's changes).

* fix: hide the inline search box on select2-multi-count controls

Reducing padding/line-height (previous attempt) didn't stop the box from
showing - select2's inline search-while-typing input for multi-selects still
rendered as a tiny stray rectangle next to the "N Selected" text and kept
inflating the control's height, since that input's own margin/height are
part of the rendered area's natural content flow regardless of the parent's
box model.

select2-multi-count fields always replace the rendered area with summary
text and rely on checkboxes in the open dropdown for picking options, so the
inline search box serves no purpose here - hide it outright via the adjacent-
sibling selector (select.select2-multi-count + .select2-container), which
also lets the control's height settle back down to match single-selects
without needing to fight select2's own sizing math.

* fix: let select2-multi-count's search box work again once opened; fix vertical alignment

Hiding the inline search box outright also removed the only way to search a
multi-select's open dropdown (multi-select mode has no separate dropdown-only
search box - the inline one filters the open list too). Scope the hide to
:not(.select2-container--open) instead, so it stays out of the way while
closed but reappears once the dropdown is open.

Also wrap the "N Selected"/"All Selected" label in its own span
(select2-count-label) instead of writing bare text into
.select2-selection__rendered, so its line-height/truncation can be
controlled independently of whatever else (the search box) shares that
container - this was causing the text to sit off-center vertically.

* fix: keep the multi-select control's height fixed when its search box opens

Even scoped to select2-container--open, the inline search <li> was still
sitting inside .select2-selection__rendered as a normal block-level sibling
of the "N Selected" label span, so it pushed the label onto its own line and
grew/shifted the whole control upward whenever the dropdown opened - visible
proof the search box was structurally part of the "selection" element, not
just visually overlapping it.

Take it fully out of flow with position: absolute (sized to the control via
top/left/right/height: 100%, overriding select2's own inline width/height on
the actual <input>), so it can never affect the parent's layout regardless of
when it's shown. Also switch the control to flexbox with align-items: center
for reliable vertical centering of the label instead of a guessed
line-height, and give it the same 8px left inset the single-select's text
already has.

* fix: stop the multi-select's overlaid search field from painting over the label

The generic .select2-search--inline .select2-search__field rule gives search
boxes a solid background (matching the dropdown panel's search box, where
that's correct) - but once resized to fill the whole control via absolute
positioning, that same solid background completely hid the "N Selected"
label behind it while the dropdown was open. Make it transparent/borderless
instead, and hide the label (visibility, not display, so layout doesn't
shift) while open so there's no overlapping text underneath the cursor.

* fix: put the multi-select's search box on its own row instead of overlapping the selection

Overlaying it via position: absolute solved the layout-corruption bug but
traded it for a worse one - it now hid the "N Selected" label completely.
Let it flow normally below the label instead (its own full-width block, small
top margin for separation), matching how a search box should actually behave
- visible on its own line, not fighting the label for the same space. The
container only grows taller while the dropdown is actually open, since the
search row is hidden entirely while closed.

Also reset the stray margin select2 puts on #select2-<id>-container (the
same element as .select2-selection__rendered) - it was inflating the closed
control's height too.

* fix: multi-select search box wraps below label; vertically center label text

The row-direction flex box for select2-selection--multiple had no
flex-wrap, so once the search field was added as a second full-width flex
item its own line-item competed with the label item in the same row -
squeezing/overlapping both instead of the intended label-then-search
stacked layout. flex-wrap: wrap lets the label take row 1 and the
full-width search field naturally wrap to row 2, only while open.

Also give the label an explicit line-height matching the control's height
(same technique already used for the single-select boxes) - flex centering
alone left the text sitting a few pixels high due to the font's default
line-height metrics.

* fix: search select2-multi-count from the dropdown, not an inline box in the control

Root cause of the open-state height jump: select2 gives every multiple-select
a type-to-filter <input> INSIDE the selection control (its SelectionSearch
adapter). When the dropdown opens that input appears, growing the control and
pushing the surrounding filter rows down. No amount of CSS on an in-flow child
of the control avoids that - the search genuinely lives inside the control's
box.

Single-selects don't have this problem because their search lives in the
floating dropdown panel (DropdownSearch adapter), which is body-attached and
never affects page layout. So give the multi-count controls the same wiring:
a custom dropdownAdapter (Dropdown + DropdownSearch + MinimumResultsForSearch
+ AttachBody) that searches from the panel, and a selectionAdapter of just
MultipleSelection + EventRelay with no inline search box at all.

The control now keeps a fixed height whether open or closed, and the surrounding
form no longer shifts. Guarded so it falls back to default behavior if the
select2 AMD modules aren't reachable.

* fix: show 15 select2 dropdown rows before scrolling instead of 20

* fix: actually apply the multi-count dropdown-search adapters + trim control padding

Two bugs behind the "search still looks attached to the (green) control and the
control grows when open" report:

1. The adapters were required with the ARRAY form of select2's amd.require(),
   which almond defers via setTimeout - so multiCountDropdownAdapter/
   multiCountSelectionAdapter were still undefined when the init loop ran, and
   select2 silently fell back to its default inline search box inside the
   control. Fetch each module with the synchronous string form instead so the
   adapters are actually in place at init time. The search now lives in the
   floating dropdown (black), not inline in the green open-state control.

2. Vendor select2.css's .select2-container--default .select2-selection--multiple
   adds padding-bottom:5px (equal specificity, so it wasn't being reset), which
   left the count label sitting against the top of the control with a gap below.
   Set explicit padding: 0 20px 0 0 so the flex-centered label is truly centered
   and the closed control matches the single-select height.

* fix: size select2-multi-count control/dropdown to content, not the native listbox width

* fix: reload graphs after the Template select2 fully closes, not mid-teardown

* docs: track pending select2 upstream dropdown-search PR for future cleanup

* docs: link the actual select2 upstream PR (#6462)

* fix: sync checkbox visuals for graph_template_id's -1/others exclusivity; skip reload if nothing changed

Two bugs in the "All Templates" (-1) mutual-exclusivity sentinel:

1. Selecting -1 didn't visually uncheck the other checkboxes, and vice versa.
   The old code flipped the underlying <option>.selected directly and fired a
   plain 'change' event - but select2's own 'change' listener only refreshes
   the "N Selected" summary label, not the open dropdown's rendered results
   list. The checkbox marks are drawn from aria-selected on those already-
   rendered <li> elements, which never got touched. Fixed by routing through
   select2's own internal event bus (available via $el.data('select2')) with
   'select'/'unselect' triggers instead - the exact same path a real click
   takes, which also runs Results.setClasses() to redraw the checkboxes.

2. Opening the dropdown and closing it again without changing anything used
   to still reload the graphs. Now the selected value is snapshotted on open
   and compared on close; the reload only fires if it actually changed.

* fix: force checkbox refresh in graph_template_id sentinel to fix off-by-one visual lag

Reported symptom: click template T1 while "All Graphs & Templates" is
checked - the "All" box unchecks correctly, but T1 does not visually check.
Click T2 next - T1 (not T2) now shows checked. Click T3 - T2 shows checked.
The checkbox visual was always exactly one click behind the actual selection.

Rather than keep chasing the precise internal event-ordering that causes
this (select2's DataAdapter/Results adapters both listen to the same
internal 'select'/'unselect' events our sentinel re-triggers), force a
direct, unconditional call to the Results adapter's own setClasses() method
(exposed publicly as $el.data('select2').results) immediately after our
selection changes. This guarantees the open dropdown's checkboxes reflect
the current underlying <option> selected state before the handler returns,
independent of dispatch ordering.

* debug: temporary console diagnostics for graphTemplateSentinel checkbox lag

* fix: checkbox checkmark keys off --selected class, not aria-selected (hover was stealing it)

Root cause of the "checkbox always one click behind" bug, finally confirmed
via console diagnostics: our checkbox checkmark CSS keyed off the
aria-selected attribute, but select2's own results.js reuses that exact same
attribute for a second, unrelated purpose - tracking which row is currently
mouse-hovered/keyboard-highlighted (a legitimate ARIA listbox pattern). Every
time the mouse moves onto a new row, select2 clears aria-selected on whatever
row was previously "highlighted" and sets it on the newly-hovered row,
completely independent of actual selection state.

Since a user naturally moves the mouse toward the next item immediately after
clicking the current one, this hover-tracking code was stealing
aria-selected="true" away from the just-selected row and handing it to
whatever's now under the cursor - producing exactly the observed "previous
box shows checked, current one doesn't" pattern. Confirmed via console
logging: aria-selected was momentarily correct right after each click (when
our own forced setClasses() call ran), then immediately got reassigned by the
very next mouseenter as the user moved toward their next target.

Fix: key the checkmark off the select2-results__option--selected CSS class
instead. That class is exclusively added/removed by setClasses() based on the
real underlying <option>.selected state, and is never touched by the
mouseenter/highlight-tracking code - so it can't be hijacked by hovering.

Also removes the temporary console.log diagnostics added while tracking this
down; the forced setClasses() calls in the sentinel are kept as harmless extra
insurance.

* chore: apply php-cs-fixer alignment fixes

CI's php-cs-fixer dry-run flagged 4 files for pure whitespace/alignment
drift introduced by the select2 conversion: array `=>` operators and a
docblock's @param columns need to align to the widest key/name in their
respective blocks, and two inline PHP print tags were missing the space
before `?>`. No behavioral change.

* fix: address Copilot review findings on select2 conversion (develop)

- lib/html_form.php: form_callback()'s select2-callback opt-in path used
  htmle() (double_encode=false) for the select's id/name/class, its
  data-action/data-variables/data-callback attributes, and the
  pre-selected option's value attribute. A pre-encoded entity in any of
  these caller-supplied values would survive into the attribute and be
  decoded back into markup/quotes by the browser. Use html_escape_attr()
  (double_encode=true) for all of these attribute contexts; the option's
  text content stays on htmle() since that's body text, not an
  attribute. Same fix for form_multi_dropdown()'s three
  data-select-all-text/data-select-count-text/data-select-all-value
  attributes.
- lib/html_reports.php: changeTree()'s replaceWith(data) on #branch_id
  only swaps the <select> element itself, leaving a live Select2
  instance's sibling .select2-container orphaned in the DOM. Destroy the
  existing Select2 instance first so the re-initialization a few lines
  down doesn't produce a duplicate branch control.

include/layout.js's select2-callback CSRF finding: verified this is not
an actual bug. select2 4.0.13's AjaxAdapter.query() calls
options.data.call(...) and hands the result straight to $.ajax() via the
default transport; for type: 'post' with a plain-object `data` and no
processData:false/contentType override, jQuery serializes it as the POST
body via $.param(), and Cacti's csrf_check() reads the token via
filter_input(INPUT_POST, ...). The __csrf_magic data() callback already
present here (since the PR's first commit) does get sent correctly. No
global $.fn.select2.defaults override clobbers it. Replied with this
analysis rather than changing working code.

Verified: php -l on both changed PHP files; brace-balance check on
lib/html_reports.php (295 open / 295 close).

* fix: address remaining Copilot review findings on select2 conversion (develop)

- include/layout.js: move the select2-hidden-accessible teardown in
  applySkin() to run before setSelectMenus() instead of after it, so the
  catch-all initializer still re-enhances plain <select> elements that the
  teardown just destroyed (previously they stayed native after any
  post-initial-load applySkin() re-run).
- include/layout.js: namespace the select2-callback widget's
  select2:select listener and unbind it before rebinding, since destroying
  and recreating the Select2 instance on every AJAX filter reload does not
  remove a plain jQuery listener bound to the underlying <select>, which
  was accumulating duplicate invocations of the configured callback.
- include/layout.js, lib/auth.php, lib/html.php: thread $noany/$nonone
  from html_host_filter() through to its ajax-backed select2-callback
  widget (as data-noany/data-nonone attributes forwarded as request vars),
  and have get_allowed_ajax_hosts() honor them. $sql_where is intentionally
  left unapplied over the ajax path and documented as such: passing
  caller-supplied raw SQL across the client/server boundary would be a new
  SQL injection vector, unlike the boolean any/none toggles.
- lib/html.php: use html_escape_attr() instead of htmle() for the two
  attribute contexts in html_host_filter()'s markup (data-callback and the
  option value), since htmle()/html_escape() disables double_encode and so
  a pre-encoded quote entity in the input could survive and be decoded
  back into a live quote by the browser, breaking out of the attribute.
  htmle() is kept for the visible option text.
- lib/html.php: use cacti_js_encode() instead of plain json_encode() for
  the two new multi-select translation strings emitted into the inline
  <script> block, since json_encode() alone does not hex-escape </script>
  sequences.
- lib/html_reports.php: use the global select2SearchRows setting instead
  of a hard-coded 10-row threshold when reinitializing the branch_id
  Select2 control after an AJAX tree change, so the configured
  "Select Search Minimum Rows" setting (including "Always") is respected.
- include/themes/paper-plane/main.css, include/themes/sunrise/main.css:
  fix unreadable dark-on-dark Select2 control text (both the base control
  and its rendered value) by using each theme's existing light foreground
  convention instead of the near-black color left over from the plain
  jQuery UI selectmenu styling.

Verified: tests/Unit/InlineScriptDataEncodingTest,
tests/Unit/Security/Xss/HtmlEscapeTest, and reports/host/graph-filter
related Unit tests all pass; php -l and node --check clean on touched
files; empirically reproduced (jsdom + real select2 4.1.0 + jQuery 4.0)
that the select2-multi-count adapter combination does not create a
duplicate inline search box, contrary to one review comment - no code
change made for that finding, replied with the reproduction instead.
2026-09-23 11:58:48 -04:00
TheWitness 2e05b98b1f Add select2 for enhanced/searchable dropdowns (#8051)
* feat: add select2 for enhanced/searchable dropdowns

Vendors select2 4.1.0 (MIT, https://select2.org) as a new front-end
dependency, following this branch's existing npm-synced-to-include/js
convention:

- package.json: added "select2": "4.1.0" to dependencies (alongside the
  other npm-tracked front-end libs, so Dependabot can track it).
- build/sync-js.mjs: added node_modules/select2/dist/js/select2.full.js ->
  include/js/select2.js to the asset map (the "full" build bundles its
  jquery-mousewheel dependency, so no extra package is needed).
- include/js/select2.js: the synced output (select2.full.js, unminified,
  matching this repo's "vendor unminified where possible" convention).

CSS, following the existing per-theme vendor-CSS convention (jquery-ui.css,
jquery.multiselect.css, etc. - none of which are npm-synced either):

- include/themes/<theme>/select2.css: the upstream select2.css (structural/
  functional rules), copied unmodified into all 10 themes.
- lib/html.php: wired both the JS and (per-theme) CSS into the page header,
  next to the other jQuery UI plugin includes.
- include/themes/<theme>/main.css: since Select2 4.x publishes no jQuery UI
  theme/adapter, appended a small override block per theme (widget border/
  background/text, hover, and active/highlighted colors) reusing each
  theme's own jquery-ui.css/.ui-state-active colors, so Select2 dropdowns
  match that theme's native jQuery UI look instead of Select2's generic
  default styling.

* feat: initialize select2() widgets in applySkin()

Wires up the select2/select2-nosearch/select2-tags/select2-multi/
select2-multi-tags/select2-callback marker classes so pages can opt in
to a Select2-enhanced dropdown just by adding the class, matching the
existing markup-driven convention used by other jQuery UI widget
setup in this function.

Dialog-hosted selects pass dropdownParent so the dropdown attaches to
the ui-dialog widget instead of escaping under it (a well-known
Select2 + jQuery UI dialog z-index/stacking issue).

* fix: remove accidental theme:'bootstrap' from select2-multi init

* fix: address Copilot review findings on the select2 PR

- package-lock.json: added the select2@4.1.0 entry (registry resolved URL +
  integrity hash) so `npm ci` no longer rejects the package.json/lockfile
  mismatch.
- .gitignore / include/js/select2.js: select2.js is a generated npm-synced
  build output like the other managed libraries, so it belongs in the
  generated-asset ignore list, not committed. Untracked the copy that had
  been committed; `npm run build:js` regenerates it locally.
- build/README.md: documented select2 among the npm-managed libraries, and
  clarified that the per-theme select2.css copies are committed unmodified
  upstream CSS (unlike jquery-ui/jstree's Cacti-owned theme CSS).
- include/themes/paper-plane/main.css: highlighted/focused Select2 options
  were the same white as the dropdown background, giving no visible focus
  indicator; changed to the theme's own hover color (#dadada / #212121).
- include/themes/sunrise/main.css: same issue; changed to this theme's
  established menu focus color (#3C8A3C, matching .ui-menu .ui-state-focus)
  with white text.

* fix: address 3 material Copilot review findings on select2 init

- Exclude the select2/select2-nosearch/select2-tags/select2-multi/
  select2-multi-tags/select2-callback marker classes from
  setSelectMenus()'s generic $('select')... loop, so Select2-marked
  single-selects are no longer first hidden/wrapped by jQuery UI
  Selectmenu before Select2 wraps them again.
- select2-callback: Select2 calls the ajax.url callback with a context
  that is not the original <select>, so $(this).data('callback') read
  undefined inside it. Capture the element's callback URL into a local
  variable before calling .select2() and use that instead.
- select2-callback: it was the only initializer that never checked for a
  jQuery UI dialog ancestor, so its dropdown always appended to body and
  could be hidden behind a dialog's overlay. Set dropdownParent the same
  way the other five initializers do.

* fix: proactively apply 2 material findings from the 1.2.x PR review

Copilot's review of the companion 1.2.x PR (#8052) found 2 bugs in this
same select2-init code that also apply here unchanged:

- Select2 renders its replacement as <span class="select2 select2-container">,
  so the plain class selectors (.select2, .select2-nosearch, etc.) also
  matched Select2's own generated wrappers on every later applySkin() call
  (e.g. during AJAX navigation), re-initializing and nesting wrappers.
  Scoped all six selectors to `select.<marker-class>` so only the original
  <select> elements are ever matched.
- select2-callback's ajax response was never normalized: Cacti's existing
  callback endpoints (e.g. lib/auth.php) return a plain array of
  {label, value, id} objects, but Select2 requires {results: [{id, text}]}.
  Added a processResults mapping so callback-backed dropdowns actually
  populate.
- select2-callback now also sets dropdownParent unconditionally (dialog
  ancestor, or document.body otherwise) instead of leaving it unset outside
  dialogs.
2026-09-22 08:27:00 -04:00
TheWitness 1a37594ad6 Replace unmaintained phpdiff vendor lib with jfcherng/php-diff (develop) (#8050)
* chore: replace unmaintained phpdiff vendor lib with jfcherng/php-diff

The manually-vendored include/vendor/phpdiff (chrisboulton/php-diff v1.1,
2009 - unmaintained since ~2013) backed the package-import file diff
viewer (package_import.php's package_diff_file()), its only call site.

Replace it with jfcherng/php-diff (^6.16, BSD-3-Clause), an actively
maintained continuation of the same library, declared as a real Composer
dependency in composer.json/composer.lock (installed at build/CI time,
same as this branch's other composer.json dependencies - not committed
to include/vendor, unlike the old hand-vendored phpdiff).

package_import.php now uses Jfcherng\Diff\Differ + RendererFactory in
place of the old Diff/Diff_Renderer_Html_Inline classes. Preserved the
existing nosemgrep justification comment, updated to reference the new
library's AbstractHtml::htmlSafe() escaping.

Adopted the vendor's own diff-table.css as the Diff.css in all 10 themes,
replacing the bespoke per-theme stylesheets that were tied to the old
library's class names.

* fix: address review feedback on jfcherng/php-diff migration

- package_import.php: narrow $oldfile/$newfile to array before
  constructing Jfcherng\Diff\Differ so PHPStan can see the
  false branch is excluded (fixes level 6/8 analysis failure).
- tests/tools/check_vendor_policy.php: drop the removed
  include/vendor/phpdiff/ path from the legacy allowlist.
- tests/Unit/Security/Xss/PackageDiffRendererTest.php: rewrite
  against the new Jfcherng\Diff\Differ/RendererFactory API instead
  of the deleted vendored phpdiff classes.
2026-09-21 13:59:50 -04:00
TheWitness 5911a5f8d6 fix: Missed one padding adjustment (#7239) 2026-06-25 09:12:26 -04:00
Thomas Vincent 41eda4cd42 fix(xss): resolve the two first-party CodeQL JS findings (#7234)
* fix(xss): build textWidth span via .text() and load example iframe over https

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

* fix(xss): build DOM-sourced UI elements safely instead of via html string interpolation

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-06-25 08:17:25 -04:00
TheWitness c58a955edd fix: Change in resizable handles negates need for padding-right (#7233) 2026-06-24 18:14:51 -04:00
b2bf39b8dc fix(sqli): sanitize sort_column and enforce sort_direction in ORDER BY clauses (#7162)
* fix: correct grammar and possessive errors in English source strings (batch 2) (#1)

* fix: correct grammar and possessive errors in English source strings (batch 2)

Seven errors found via cacti.pot audit:

- automation_networks.php: "senders name" → "sender\'s name" (line 839)
- automation_networks.php: "senders Email" → "sender\'s Email" (line 848)
- automation_templates.php: "devices sysDescr" → "device\'s sysDescr" (line 1060)
- automation_templates.php: "devices sysName" → "device\'s sysName" (line 1067)
- automation_templates.php: "devices sysOid" → "device\'s sysOid" (line 1074)
- data_templates.php: garbled "it either it value...critical data Data Query" →
  "its value...Data Query" (line 903)
- package_repos.php: "Package Repositorites" → "Package Repositories" (line 436)

All changes are to UI description strings inside __() / __esc() calls.
cacti.pot should be regenerated via build_gettext.sh after merge.

* fix: correct grammar and possessive errors in English source strings (batch 2)

Seven errors found via cacti.pot audit:

- automation_networks.php: "senders name" → "sender\'s name" (line 839)
- automation_networks.php: "senders Email" → "sender\'s Email" (line 848)
- automation_templates.php: "devices sysDescr" → "device\'s sysDescr" (line 1060)
- automation_templates.php: "devices sysName" → "device\'s sysName" (line 1067)
- automation_templates.php: "devices sysOid" → "device\'s sysOid" (line 1074)
- data_templates.php: garbled string → "its value...Data Query" (line 903)
- package_repos.php: "Package Repositorites" → "Package Repositories" (line 436)

All changes are to UI description strings inside __() / __esc() calls.
cacti.pot should be regenerated via build_gettext.sh after merge.

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

---------

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

* ci: add dependabot config with npm and github-actions

* ci: add CodeQL (JS/TS, excludes PHP) with workflow_dispatch

* ci: update CodeQL for javascript-typescript, python

* fix(ci): use specific runner labels, fix codeql concurrency

* Improve IPv6 support in RRDtool proxy and ping utilities (#2)

* Improve IPv6 support in RRDtool proxy and ping utilities

- Fix RRDtool proxy to dynamically detect IPv6 addresses and use
  AF_INET6 sockets instead of hardcoded AF_INET (IPv4-only), enabling
  connections to IPv6 RRDtool proxy servers including backup servers
- Strip brackets from IPv6 addresses before passing to socket_connect
- Properly close failed sockets before creating new ones for failover
- Replace fragile str_contains(':') IPv6 detection in ping with
  filter_var(FILTER_FLAG_IPV6) for more robust address validation

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Fix JS code quality: implicit globals, deprecated APIs, loose equality

- Add var declarations to prevent implicit globals in install.js
  (element, enabled, button, buttonCheck)
- Remove console.log debug output left in production (install.js)
- Replace deprecated jQuery .unbind() with .off() (layout.js)
- Fix "depreciated" typo to "deprecated" in deprecation warnings
- Convert == / != to === / !== for null, boolean, string, typeof,
  and numeric comparisons across install.js and realtime.js

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Replace deprecated jQuery shorthand event methods in layout.js

Replace .click(), .keyup(), .keydown(), .mousedown(), .mouseenter(),
.mouseleave(), .submit(), .resize() with .on() equivalents. Replace
.focus(), .change() trigger calls with .trigger(). These shorthands
were deprecated in jQuery 3.5.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Replace deprecated jQuery methods in realtime.js

Replace .bind() with .on() and .change() trigger calls with
.trigger('change'). .bind() was deprecated in jQuery 3.0 and
shorthand triggers in jQuery 3.5.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Replace deprecated jQuery methods in install.js and fix ===/!== errors

Replace .click(), .change(), .focus() with .on()/.trigger() equivalents.
Also fix !=== and ==== operators that were incorrectly introduced by a
prior replace-all of == to === within existing !== and === expressions.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Restore == null where needed to catch both null and undefined

In JavaScript, == null matches both null and undefined, which is an
intentional idiom. The prior === null conversion broke cases where
values come from jQuery .val(), .data(), $.urlParam(), or object
property access that may return undefined rather than null. Revert
those specific cases while keeping === null where variables are
explicitly initialized to null.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Replace deprecated jQuery methods in all theme main.js files

Replace .unbind().click() with .off('click').on('click'), convert
.hover() to .on('mouseenter').on('mouseleave'), replace .change(),
.scroll(), .click() shorthands with .on() equivalents, and .blur()
with .trigger('blur') across all 10 theme files.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Convert string concatenation to template literals

Replace 'str' + var + 'str' patterns with ES6 template literals
in realtime.js and install.js. Improves readability especially for
URL construction and HTML building. Also replace $.parseJSON() with
native JSON.parse() in realtime.js.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Convert var to const for single-assignment variables

Replace var with const where the variable is assigned once and never
reassigned within its scope, in install.js and realtime.js. Keeps var
for variables that are conditionally reassigned (e.g. size, url).

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

---------

Co-authored-by: Claude <noreply@anthropic.com>

* fix(sqli): sanitize sort_column and enforce sort_direction in ORDER BY clauses

Closes GHSA-q9xg-p762-9jm3 and GHSA-72vr-jr4v-55vf.

- Add sanitize_sql_column() to lib/functions.php stripping all chars
  outside [a-zA-Z0-9_().] from user-supplied column names
- Apply sanitize_sql_column() at every ORDER BY concatenation in
  api_automation.php and html_reports.php
- Enforce direction to literal ASC/DESC in html_reports.php,
  api_automation.php, and html_utility.php (update_order_string,
  get_order_string)
- Add tests/Unit/SafeSortTest.php with 22 stub-based regression tests

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

* fix(docs): move sanitize_sql_column above sanitize_search_string docblock

The sanitize_search_string docblock was orphaned after sanitize_sql_column
was inserted between it and the function body. Reorder so each function
follows its own docblock directly.

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

* fix(sqli): escape apostrophes in automation_templates and harden sort_direction in display_matching_hosts

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

* fix(sqli): restore allowlist guard, sanitize session keys, add sanitize_sql_column default

Restore the preg_match allowlist guard in get_order_string() that was
removed by the PR, re-opening the ORDER BY injection surface. When the
request var is empty after sanitization, return '' rather than building
a malformed ORDER BY clause.

Sanitize sort_column before storing as SESSION array key in
update_order_string() and add read-time guard in the inplace loop that
rebuilds ORDER BY from stored keys.

Add $default parameter to sanitize_sql_column() so callers get a safe
fallback instead of an empty string that produces 'ORDER BY  ASC'.

Fix inconsistent INET_ATON substitution in display_matching_hosts().

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

* Improve sanitize_sql_column function

Fix regex to sanitize SQL column names properly.

* Potential fix for pull request finding

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-24 16:54:42 -04:00
Thomas VincentandTheWitness 255667b49d chore(assets): update jQuery UI and theme assets (#7082)
* chore(assets): update jQuery UI and theme assets

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

* fix(codex): restore jQuery UI compatibility and fix PHPStan issues

* fix(midwinter): restore full-width multiselect button rules dropped in jquery-ui rebuild

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

* ci: ignore phpstan function.alreadyNarrowedType identifier

Clears the level 6 alreadyNarrowedType error on lib/data_query.php:2591 that
develop currently emits, matching the sibling narrowing identifiers already ignored.

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-06-24 14:49:05 -04:00
J.P. Pasnak 5e4be9188e typo (#7064) 2026-04-25 17:59:44 -04:00
Browniebraun b814eb4b20 MidWinter - replacing third party libraries by native Vanilla (#7060)
replacing third party libraries by native Vanilla
2026-04-22 12:56:21 -04:00
Browniebraun d703edb6e6 Latest State of MidWinter Refactoring (#7043)
* support dynamic resizable rrdgraph

* introducing the newest version of navigationBox including plugin support. This allows reusable navigation Boxes e.g. different trees and so on.
2026-04-13 19:31:05 +02:00
3e1dc3c886 fix: replace strtolower/strtoupper with locale-independent wrappers (#6717)
* cleanup: remove unreachable defensive error logs in aggregate graph item validation

* fix: replace strtolower/strtoupper with locale-independent wrappers

PHP 8.5 deprecates locale-sensitive strtolower() and strtoupper().
Add cacti_strtolower() and cacti_strtoupper() wrappers that use
mb_strtolower/mb_strtoupper with explicit UTF-8 encoding, then
replace all 185 call sites across 65 files.

Existing mb_strtolower() calls (email normalization) are left as-is
since they already specify encoding explicitly.

Includes 18 Pest tests covering ASCII, UTF-8, protocol names, config
values, hex strings, NaN detection, and file extension patterns.

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

* fix: correct docblock references and month_shift() strpos check

Fix docblocks to reference strtolower()/strtoupper() (the deprecated
PHP builtins) instead of the wrapper names. Fix month_shift() to use
strpos() !== false instead of > 0 so 'month' at position 0 is matched.

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

* chore: drop unrelated lib/api_aggregate changes

Keep PR scope focused on the issue for this branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-03-05 19:51:02 -05:00