mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
* 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>
114 lines
5.6 KiB
PHP
114 lines
5.6 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
/*
|
|
* Batch-11 security consolidation regressions (develop), guarding the four
|
|
* sinks fixed in PR #8165:
|
|
*
|
|
* GHSA-fq9x-x3vf-3vf2 single-pass data-input <field> substitution
|
|
* GHSA-hh7r-464j-rf39 Content-Disposition filename sanitization
|
|
* GHSA-v99q-57w7-mpmg HTML <title> escaping
|
|
* GHSA-f5f5-h85v-6m3h user administration tooltip text encoding
|
|
*/
|
|
|
|
$root = dirname(__DIR__, 3);
|
|
|
|
$functionsSource = file_get_contents($root . '/lib/functions.php');
|
|
$xportSource = file_get_contents($root . '/graph_xport.php');
|
|
$htmlSource = file_get_contents($root . '/lib/html.php');
|
|
$userAdminSource = file_get_contents($root . '/user_admin.php');
|
|
$userGroupSource = file_get_contents($root . '/user_group_admin.php');
|
|
|
|
// =====================================================================
|
|
// GHSA-fq9x: single-pass <field> token substitution
|
|
// =====================================================================
|
|
|
|
test('GHSA-fq9x: substitute_script_path substitutes tokens in a single pass', function () use ($functionsSource) {
|
|
expect($functionsSource)->toContain('function substitute_script_path(');
|
|
expect($functionsSource)->toContain("preg_replace_callback('/<([A-Za-z0-9_]+)>/',");
|
|
expect($functionsSource)->toContain('array_key_exists($matches[1], $escaped_values)');
|
|
});
|
|
|
|
test('GHSA-fq9x: both path builders drop the iterative str_replace and call the helper', function () use ($functionsSource) {
|
|
expect($functionsSource)->not->toContain("\$full_path = str_replace('<' . \$item['data_name'] . '>', \$value, \$full_path);");
|
|
expect(substr_count($functionsSource, '$full_path = substitute_script_path($full_path, $escaped_values);'))->toBe(2);
|
|
});
|
|
|
|
/*
|
|
* Behavioral proof. The develop unit bootstrap loads lib/functions.php via
|
|
* include/global.php, so substitute_script_path() is normally already defined.
|
|
* As a defensive fallback (and to document the Test-only nature of the eval),
|
|
* the real function body is regex-extracted from this repo's own source and
|
|
* eval()'d only when it is not already present -- the extract-and-eval pattern
|
|
* used elsewhere in the suite. Input is this repo's source, never user input.
|
|
*/
|
|
if (!function_exists('substitute_script_path')) {
|
|
preg_match('/\nfunction substitute_script_path\b.*?\n\}/s', "\n" . $functionsSource, $sspMatch);
|
|
|
|
if (!empty($sspMatch)) {
|
|
eval($sspMatch[0]);
|
|
}
|
|
}
|
|
|
|
test('GHSA-fq9x: a field value that contains another field token is not re-substituted', function () {
|
|
expect(function_exists('substitute_script_path'))->toBeTrue();
|
|
|
|
$result = substitute_script_path('<field1>', [
|
|
'field1' => '<arg2>',
|
|
'arg2' => 'PAYLOAD',
|
|
]);
|
|
|
|
expect($result)->toBe('<arg2>');
|
|
expect($result)->not->toContain('PAYLOAD');
|
|
});
|
|
|
|
test('GHSA-fq9x: known tokens are replaced once and unknown tokens remain literal', function () {
|
|
expect(substitute_script_path('<unknown>', []))->toBe('<unknown>');
|
|
expect(substitute_script_path('<f>-<f>', ['f' => "'v'"]))->toBe("'v'-'v'");
|
|
expect(substitute_script_path('<a><b>', ['a' => 'X', 'b' => 'Y']))->toBe('XY');
|
|
});
|
|
|
|
// =====================================================================
|
|
// GHSA-hh7r: Content-Disposition filename sanitization
|
|
// =====================================================================
|
|
|
|
test('GHSA-hh7r: the export filename strips CR/LF/quote/backslash from the title', function () use ($xportSource) {
|
|
expect($xportSource)->toContain('$filename = str_replace(["\r", "\n", \'"\', \'\\\\\'], \'\', (string) $xport_array[\'meta\'][\'title_cache\']) . \'.csv\';');
|
|
// the raw, unsanitized concatenation must be gone.
|
|
expect($xportSource)->not->toContain('$filename = $xport_array[\'meta\'][\'title_cache\'] . \'.csv\';');
|
|
});
|
|
|
|
// =====================================================================
|
|
// GHSA-v99q: HTML <title> escaping
|
|
// =====================================================================
|
|
|
|
test('GHSA-v99q: the page title is html-escaped before entering the <title> element', function () use ($htmlSource) {
|
|
expect($htmlSource)->toContain('<title><?php print html_escape($title); ?></title>');
|
|
expect($htmlSource)->not->toContain('<title><?php print $title; ?></title>');
|
|
});
|
|
|
|
// =====================================================================
|
|
// GHSA-f5f5: user administration tooltip text encoding
|
|
// =====================================================================
|
|
|
|
test('GHSA-f5f5: the user-admin tooltip renders data-tooltip as escaped text', function () use ($userAdminSource) {
|
|
expect($userAdminSource)->toContain("return $('<div>').text($(this).attr('data-tooltip') || '').html();");
|
|
expect($userAdminSource)->not->toContain("return $(this).attr('data-tooltip');");
|
|
});
|
|
|
|
test('GHSA-f5f5: the user-group-admin tooltip also renders data-tooltip as escaped text', function () use ($userGroupSource) {
|
|
expect($userGroupSource)->toContain("return $('<div>').text($(this).attr('data-tooltip') || '').html();");
|
|
expect($userGroupSource)->not->toContain("return $(this).attr('data-tooltip');");
|
|
});
|