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 (#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>
This commit is contained in:
23 files changed
+2360
-163
No files matched your search
@@ -1,3 +1,32 @@
|
||||
# Normalize every text file to LF in the repository and the working tree, so a
|
||||
# checkout on Windows cannot reintroduce CRLF that php-cs-fixer then rejects.
|
||||
# Git auto-detects binary content; the explicit rules below guard common binary
|
||||
# types against accidental normalization.
|
||||
* text=auto eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.phar binary
|
||||
|
||||
# Patch files are applied byte-for-byte; never rewrite their line endings.
|
||||
*.patch -text
|
||||
|
||||
# Vendored third-party libraries are kept exactly as shipped upstream, including
|
||||
# their line endings, so they stay byte-identical to upstream and out of diffs
|
||||
# and code scanners. Do not normalize them.
|
||||
include/js/big.js -text
|
||||
include/js/jquery.tablesorter.js -text
|
||||
include/js/jquery.tablesorter.widgets.js -text
|
||||
|
||||
# Every pull request appends to the same two anchors in CHANGELOG: the top of
|
||||
# the issue block and the end of the feature block. Concurrent PRs therefore
|
||||
# collide there even when they touch nothing else. check-changelog.py enforces
|
||||
|
||||
@@ -3198,6 +3198,25 @@ CREATE TABLE `user_auth_row_cache` (
|
||||
-- Dumping data for table `user_auth_row_cache`
|
||||
--
|
||||
|
||||
--
|
||||
-- Table structure for table `user_layouts`
|
||||
--
|
||||
|
||||
CREATE TABLE `user_layouts` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL default '0',
|
||||
`page` varchar(64) NOT NULL default '',
|
||||
`name` varchar(128) NOT NULL default '',
|
||||
`data` text NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `page` (`page`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic COMMENT='Stores user defined and published filter layouts';
|
||||
|
||||
--
|
||||
-- Dumping data for table `user_layouts`
|
||||
--
|
||||
|
||||
--
|
||||
-- Table structure for table `user_log`
|
||||
--
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Add saved filter layouts to the filter class with administrator publishing and a Presets > Filters management page, plus a per-user Modern or Legacy page filter format where Modern presents the Layouts selector with an Edit dialog holding the filter fields
|
||||
@@ -1291,6 +1291,11 @@ INSERT INTO `table_columns` VALUES ('login_providers',8,'is_default','tinyint(3)
|
||||
INSERT INTO `table_columns` VALUES ('login_providers',9,'allow_auth_cookies','char(2)','NO','','on','');
|
||||
INSERT INTO `table_columns` VALUES ('login_providers',10,'user_id','int(10) unsigned','NO','','0','');
|
||||
INSERT INTO `table_columns` VALUES ('login_providers',11,'parameters','longtext','YES','',NULL,'');
|
||||
INSERT INTO `table_columns` VALUES ('user_layouts',1,'id','int(10) unsigned','NO','PRI',NULL,'auto_increment');
|
||||
INSERT INTO `table_columns` VALUES ('user_layouts',2,'user_id','int(10) unsigned','NO','MUL','0','');
|
||||
INSERT INTO `table_columns` VALUES ('user_layouts',3,'page','varchar(64)','NO','MUL','','');
|
||||
INSERT INTO `table_columns` VALUES ('user_layouts',4,'name','varchar(128)','NO','','','');
|
||||
INSERT INTO `table_columns` VALUES ('user_layouts',5,'data','text','NO','',NULL,'');
|
||||
INSERT INTO `table_columns` VALUES ('user_log',1,'username','varchar(50)','NO','PRI','0','');
|
||||
INSERT INTO `table_columns` VALUES ('user_log',2,'user_id','mediumint(8)','NO','PRI','0','');
|
||||
INSERT INTO `table_columns` VALUES ('user_log',3,'time','timestamp','NO','PRI','0000-00-00 00:00:00','');
|
||||
@@ -1739,6 +1744,9 @@ INSERT INTO `table_indexes` VALUES ('user_auth_row_cache',0,'PRIMARY',1,'user_id
|
||||
INSERT INTO `table_indexes` VALUES ('user_auth_row_cache',0,'PRIMARY',2,'class','A',0,NULL,NULL,'','BTREE','');
|
||||
INSERT INTO `table_indexes` VALUES ('user_auth_row_cache',0,'PRIMARY',3,'hash','A',0,NULL,NULL,'','BTREE','');
|
||||
INSERT INTO `table_indexes` VALUES ('login_providers',0,'PRIMARY',1,'id','A',0,NULL,NULL,'','BTREE','');
|
||||
INSERT INTO `table_indexes` VALUES ('user_layouts',1,'page',1,'page','A',0,NULL,NULL,'','BTREE','');
|
||||
INSERT INTO `table_indexes` VALUES ('user_layouts',0,'PRIMARY',1,'id','A',0,NULL,NULL,'','BTREE','');
|
||||
INSERT INTO `table_indexes` VALUES ('user_layouts',1,'user_id',1,'user_id','A',0,NULL,NULL,'','BTREE','');
|
||||
INSERT INTO `table_indexes` VALUES ('user_log',0,'PRIMARY',1,'username','A',2,NULL,NULL,'','BTREE','');
|
||||
INSERT INTO `table_indexes` VALUES ('user_log',0,'PRIMARY',2,'user_id','A',2,NULL,NULL,'','BTREE','');
|
||||
INSERT INTO `table_indexes` VALUES ('user_log',0,'PRIMARY',3,'time','A',2,NULL,NULL,'','BTREE','');
|
||||
|
||||
@@ -405,4 +405,9 @@ if (empty($_SESSION[SESS_USER_ID])) {
|
||||
FROM user_auth
|
||||
WHERE id = ?',
|
||||
[$_SESSION[SESS_USER_ID]]);
|
||||
|
||||
// Handle saved filter-layout AJAX (save/rename/delete/publish) posted to
|
||||
// any filter page, now that the user is authenticated and authorized for
|
||||
// the current page. Returns immediately for all non-layout requests.
|
||||
filter_layouts_handle_request();
|
||||
}
|
||||
@@ -1122,6 +1122,7 @@ if (POLLER_ID || $config['connection'] == 'online') { // @phpstan-ignore-line
|
||||
'vdef.php' => __('VDEFs'),
|
||||
'color.php' => __('Colors'),
|
||||
'gprint_presets.php' => __('GPRINTs'),
|
||||
'layouts.php' => __('Filters'),
|
||||
],
|
||||
__('Import/Export') => [
|
||||
'package_import.php' => __('Import Packages'),
|
||||
@@ -1437,6 +1438,7 @@ $user_auth_realm_filenames = [
|
||||
'rrdcheck.php' => 15,
|
||||
'settings.php' => 15,
|
||||
'links.php' => 15,
|
||||
'layouts.php' => 15,
|
||||
'data_queries.php' => 13,
|
||||
'templates_export.php' => 16,
|
||||
'templates_import.php' => 17,
|
||||
@@ -2306,6 +2308,12 @@ $navigation = [
|
||||
'url' => '',
|
||||
'level' => '2'
|
||||
],
|
||||
'layouts.php:' => [
|
||||
'title' => __('Filter Layouts'),
|
||||
'mapping' => 'index.php:',
|
||||
'url' => 'layouts.php',
|
||||
'level' => '1'
|
||||
],
|
||||
'cdef.php:' => [
|
||||
'title' => __('CDEFs'),
|
||||
'mapping' => 'index.php:',
|
||||
|
||||
@@ -3186,6 +3186,16 @@ $settings_user = [
|
||||
'array' => $graph_views,
|
||||
'default' => '1'
|
||||
],
|
||||
'page_filter_format' => [
|
||||
'friendly_name' => __('Page Filter Format'),
|
||||
'description' => __('How table filters are presented. Modern collapses the filter into a Layouts selector with an Edit dialog that holds the filter fields; Legacy shows the classic inline filter. The Layouts selector is available in both.'),
|
||||
'method' => 'drop_array',
|
||||
'default' => 'modern',
|
||||
'array' => [
|
||||
'modern' => __('Modern'),
|
||||
'legacy' => __('Legacy')
|
||||
]
|
||||
],
|
||||
'client_timezone_support' => [
|
||||
'friendly_name' => __('TimeZone Support'),
|
||||
'description' => __('How would you like Cacti to present dates? This setting will also change the way that Cacti Graphs dates are represented.'),
|
||||
|
||||
@@ -1266,6 +1266,82 @@ legend {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter dialog skin (page_filter_format = modern), scoped to the
|
||||
cactiFilterDialog widget so other dialogs are unaffected. */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: rgba(128, 128, 128, 0.45);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #2d4ca8;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #2d4ca8;
|
||||
color: #ffffff;
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1272,6 +1272,82 @@ legend {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter dialog skin (page_filter_format = modern), scoped to the
|
||||
cactiFilterDialog widget so other dialogs are unaffected. */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: rgba(128, 128, 128, 0.45);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #b34d16;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #b34d16;
|
||||
color: #ffffff;
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1457,6 +1457,82 @@ legend {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter dialog skin (page_filter_format = modern), scoped to the
|
||||
cactiFilterDialog widget so other dialogs are unaffected. */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: rgba(128, 128, 128, 0.45);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #31442b;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #31442b;
|
||||
color: #ffffff;
|
||||
border-color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1309,6 +1309,82 @@ legend {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter dialog skin (page_filter_format = modern), scoped to the
|
||||
cactiFilterDialog widget so other dialogs are unaffected. */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: rgba(128, 128, 128, 0.45);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #b3007d;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #b3007d;
|
||||
color: #ffffff;
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1480,6 +1480,85 @@ legend {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter (page_filter_format = modern): the Layouts bar and the
|
||||
jQuery UI Edit dialog (dialogClass cactiFilterDialog). */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: #c3ccd4;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border: 1px solid #9aa4ad;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #525e68;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid #d4dae0;
|
||||
background: #f2f4f6;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #525e68;
|
||||
color: #ffffff;
|
||||
border-color: #46525b;
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1464,6 +1464,82 @@ tr>td.textInfo.right {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter dialog skin (page_filter_format = modern), scoped to the
|
||||
cactiFilterDialog widget so other dialogs are unaffected. */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: rgba(128, 128, 128, 0.45);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #357a39;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #357a39;
|
||||
color: #ffffff;
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1297,6 +1297,82 @@ legend {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter dialog skin (page_filter_format = modern), scoped to the
|
||||
cactiFilterDialog widget so other dialogs are unaffected. */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: rgba(128, 128, 128, 0.45);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #1a195e;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #1a195e;
|
||||
color: #ffffff;
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1284,6 +1284,82 @@ legend {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter dialog skin (page_filter_format = modern), scoped to the
|
||||
cactiFilterDialog widget so other dialogs are unaffected. */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: rgba(128, 128, 128, 0.45);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #a3184b;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #a3184b;
|
||||
color: #ffffff;
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1501,6 +1501,82 @@ table.cactiTable {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
/* Modern page filter dialog skin (page_filter_format = modern), scoped to the
|
||||
cactiFilterDialog widget so other dialogs are unaffected. */
|
||||
.cactiFilterModernBar .barSep {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 6px;
|
||||
background-color: rgba(128, 128, 128, 0.45);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 14px;
|
||||
background: #04335b;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar .ui-dialog-titlebar-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-titlebar-close .ui-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-content {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditTable {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:first-child {
|
||||
min-width: 120px;
|
||||
padding-right: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child select,
|
||||
.ui-dialog.cactiFilterDialog .cactiFilterEditRow .filterColumn:last-child input[type="text"] {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane {
|
||||
margin: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane .ui-button:first-child {
|
||||
background: #04335b;
|
||||
color: #ffffff;
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.settingsFilter {
|
||||
display: table;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -30,6 +30,17 @@ function upgrade_to_1_3_0() : void {
|
||||
PRIMARY KEY (key_id)
|
||||
) ENGINE=InnoDB COMMENT='Serializes Cacti Process Registry Mutations'");
|
||||
|
||||
db_install_execute("CREATE TABLE IF NOT EXISTS user_layouts (
|
||||
id int(10) unsigned NOT NULL auto_increment,
|
||||
user_id int(10) unsigned NOT NULL default '0',
|
||||
page varchar(64) NOT NULL default '',
|
||||
name varchar(128) NOT NULL default '',
|
||||
data text NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY page (page)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic COMMENT='Stores user defined and published filter layouts'");
|
||||
|
||||
db_install_change_column('version', ['name' => 'cacti', 'type' => 'char(30)', 'NULL' => false, 'default' => '']);
|
||||
|
||||
db_install_add_column('user_auth', ['name' => 'tfa_enabled', 'type' => 'char(3)', 'NULL' => false, 'default' => '']);
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
| |
|
||||
| This program is distributed in the hope that it will be useful, |
|
||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
| GNU General Public License for more details. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
| This code is designed, written, and maintained by the Cacti Group. See |
|
||||
| about.php and/or the AUTHORS file for specific developer information. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| http://www.cacti.net/ |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
require('./include/auth.php');
|
||||
|
||||
// Saved-layout create/rename/delete/publish requests (posted here or to any
|
||||
// other filter page) are handled by filter_layouts_handle_request(), which is
|
||||
// invoked from include/auth.php before this point. Only the management view
|
||||
// remains here.
|
||||
|
||||
set_default_action();
|
||||
|
||||
switch (get_nfilter_request_var('action')) {
|
||||
default:
|
||||
top_header();
|
||||
|
||||
layouts_manage();
|
||||
|
||||
bottom_footer();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the filter-layout management page. Administrators holding the
|
||||
* Settings/Utilities realm manage every user's layouts and may publish them
|
||||
* globally; other users manage only their own saved layouts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function layouts_manage() : void {
|
||||
$is_admin = filter_layouts_can_manage_global();
|
||||
$user_id = isset($_SESSION['sess_user_id']) ? (int) $_SESSION['sess_user_id'] : 0;
|
||||
|
||||
if ($is_admin) {
|
||||
$layouts = db_fetch_assoc('SELECT ul.id, ul.user_id, ul.page, ul.name, ul.data, ua.username
|
||||
FROM user_layouts AS ul
|
||||
LEFT JOIN user_auth AS ua
|
||||
ON ua.id = ul.user_id
|
||||
ORDER BY ul.page, (ul.user_id = 0), ul.name');
|
||||
} else {
|
||||
$layouts = db_fetch_assoc_prepared('SELECT ul.id, ul.user_id, ul.page, ul.name, ul.data, ua.username
|
||||
FROM user_layouts AS ul
|
||||
LEFT JOIN user_auth AS ua
|
||||
ON ua.id = ul.user_id
|
||||
WHERE ul.user_id = ?
|
||||
ORDER BY ul.page, ul.name', [$user_id]);
|
||||
}
|
||||
|
||||
html_start_box(__('Filter Layouts'), '100%', true, 3, 'center', '');
|
||||
|
||||
$display_text = [
|
||||
'name' => ['display' => __('Name')],
|
||||
'page' => ['display' => __('Page')],
|
||||
'owner' => ['display' => __('Owner')],
|
||||
'url' => ['display' => __('Filter')],
|
||||
'nosort' => ['display' => __('Actions'), 'align' => 'right'],
|
||||
];
|
||||
|
||||
html_header($display_text, 1);
|
||||
|
||||
if (cacti_sizeof($layouts)) {
|
||||
foreach ($layouts as $layout) {
|
||||
if ($layout['user_id'] == 0) {
|
||||
$owner = '<em>' . __('Global') . '</em>';
|
||||
} elseif (!empty($layout['username'])) {
|
||||
$owner = html_escape($layout['username']);
|
||||
} else {
|
||||
$owner = __('User %d', $layout['user_id']);
|
||||
}
|
||||
|
||||
form_alternate_row('line' . $layout['id'], true);
|
||||
|
||||
$document = filter_layouts_decode($layout['data']);
|
||||
$summary = $document !== false ? filter_layouts_document_url($document) : '';
|
||||
|
||||
form_selectable_cell(filter_value($layout['name'], ''), $layout['id']);
|
||||
form_selectable_cell(html_escape($layout['page']), $layout['id']);
|
||||
form_selectable_cell($owner, $layout['id']);
|
||||
form_selectable_cell(html_escape($summary), $layout['id']);
|
||||
|
||||
$actions = "<a class='pic layoutAction' href='#' data-action='layout_delete' data-id='" . $layout['id'] . "' title='" . __esc('Delete') . "'><i class='fa fa-times deviceDown'></i></a>";
|
||||
|
||||
if ($is_admin) {
|
||||
if ($layout['user_id'] == 0) {
|
||||
$actions = "<span title='" . __esc('Published to all users') . "'><i class='fa fa-globe'></i></span> " . $actions;
|
||||
} else {
|
||||
$actions = "<a class='pic layoutAction' href='#' data-action='layout_publish' data-id='" . $layout['id'] . "' title='" . __esc('Publish to all users') . "'><i class='fa fa-upload'></i></a> " . $actions;
|
||||
}
|
||||
}
|
||||
|
||||
form_selectable_cell($actions, $layout['id'], '', 'right');
|
||||
|
||||
form_end_row();
|
||||
}
|
||||
} else {
|
||||
print "<tr class='tableRow'><td colspan='5'><em>" . __('No Filter Layouts Found') . '</em></td></tr>';
|
||||
}
|
||||
|
||||
html_end_box(false);
|
||||
|
||||
?>
|
||||
<script type='text/javascript'>
|
||||
$(function() {
|
||||
$('.layoutAction').click(function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var action = $(this).attr('data-action');
|
||||
var id = $(this).attr('data-id');
|
||||
|
||||
if (action == 'layout_delete' && !confirm(<?php print json_encode(__('Delete the selected layout?')); ?>)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('layouts.php', { action: action, id: id, __csrf_magic: csrfMagicToken }, function(result) {
|
||||
if (result && result.ok) {
|
||||
document.location.reload();
|
||||
} else {
|
||||
alert(<?php print json_encode(__('The layout operation failed.')); ?>);
|
||||
}
|
||||
}, 'json').fail(function() {
|
||||
alert(<?php print json_encode(__('The layout operation failed.')); ?>);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
@@ -646,6 +646,7 @@ function user_remove(int $user_id) : void {
|
||||
db_execute_prepared('DELETE FROM user_auth_group_members WHERE user_id = ?', [$user_id]);
|
||||
db_execute_prepared('DELETE FROM settings_user WHERE user_id = ?', [$user_id]);
|
||||
db_execute_prepared('DELETE FROM settings_tree WHERE user_id = ?', [$user_id]);
|
||||
db_execute_prepared('DELETE FROM user_layouts WHERE user_id = ?', [$user_id]);
|
||||
db_execute_prepared('DELETE FROM sessions WHERE user_id = ?', [$user_id]);
|
||||
|
||||
api_plugin_hook_function('user_remove', $user_id);
|
||||
|
||||
+1074
-157
File diff suppressed because it is too large.
Load diff
@@ -24971,3 +24971,131 @@ msgstr ""
|
||||
#: vdef.php
|
||||
msgid "No VDEFs"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Layouts"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "(New Layout)"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Overwrite the selected layout with the current filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Save the current filter as a new layout"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Rename the selected layout"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Delete the selected layout"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Publish the selected layout so all users can see it"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Layout Name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "New Layout"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Rename Layout"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "The layout operation failed."
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Please select a layout to rename."
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Please select a layout to delete."
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Please select a layout to publish."
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "You are not permitted to modify this layout."
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Delete the selected layout?"
|
||||
msgstr ""
|
||||
|
||||
#: layouts.php
|
||||
msgid "Filter Layouts"
|
||||
msgstr ""
|
||||
|
||||
#: layouts.php
|
||||
msgid "User %d"
|
||||
msgstr ""
|
||||
|
||||
#: layouts.php
|
||||
msgid "Published to all users"
|
||||
msgstr ""
|
||||
|
||||
#: layouts.php
|
||||
msgid "Publish to all users"
|
||||
msgstr ""
|
||||
|
||||
#: layouts.php
|
||||
msgid "No Filter Layouts Found"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php layouts.php
|
||||
msgid "Global"
|
||||
msgstr ""
|
||||
|
||||
#: include/global_settings.php
|
||||
msgid "Page Filter Format"
|
||||
msgstr ""
|
||||
|
||||
#: include/global_settings.php
|
||||
msgid "How table filters are presented. Modern collapses the filter into a Layouts selector with an Edit dialog that holds the filter fields; Legacy shows the classic inline filter. The Layouts selector is available in both."
|
||||
msgstr ""
|
||||
|
||||
#: include/global_arrays.php
|
||||
msgid "Filters"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Edit Filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Filter Name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Save As"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Edit the current filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Save this layout as a new personal layout"
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Please enter a filter name."
|
||||
msgstr ""
|
||||
|
||||
#: lib/html_filter.php
|
||||
msgid "Save the layout before publishing it."
|
||||
msgstr ""
|
||||
@@ -3420,12 +3420,6 @@ parameters:
|
||||
count: 1
|
||||
path: lib/html.php
|
||||
|
||||
-
|
||||
message: '#^Method CactiTableFilter\:\:create_filter\(\) should return string but returns string\|false\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: lib/html_filter.php
|
||||
|
||||
-
|
||||
message: '#^Argument of an invalid type array\|bool supplied for foreach, only iterables are supported\.$#'
|
||||
identifier: foreach.nonIterable
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<?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 |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/*
|
||||
* Coverage for the saved filter-layout feature added to the CactiTableFilter
|
||||
* class (lib/html_filter.php) and the layouts.php management page. The pure
|
||||
* helpers are executed directly; the request/permission wiring is asserted by
|
||||
* scanning source, matching the database-free unit-test convention.
|
||||
*/
|
||||
|
||||
$root = dirname(__DIR__, 4);
|
||||
|
||||
test('filter_layouts_page_key reduces a url to its page basename', function () {
|
||||
expect(filter_layouts_page_key('host.php?filter=x&rows=30'))->toBe('host.php');
|
||||
expect(filter_layouts_page_key('cdef.php'))->toBe('cdef.php');
|
||||
expect(filter_layouts_page_key('/var/www/host.php?a=b'))->toBe('host.php');
|
||||
});
|
||||
|
||||
test('filter_layouts_valid_page accepts only bare same-site page basenames', function () {
|
||||
expect(filter_layouts_valid_page('host.php'))->toBeTrue();
|
||||
expect(filter_layouts_valid_page('graph_view.php'))->toBeTrue();
|
||||
|
||||
// No query, scheme, host, traversal, or markup may name a page.
|
||||
expect(filter_layouts_valid_page('host.php?filter=a'))->toBeFalse();
|
||||
expect(filter_layouts_valid_page('http://evil.example/host.php'))->toBeFalse();
|
||||
expect(filter_layouts_valid_page('../host.php'))->toBeFalse();
|
||||
expect(filter_layouts_valid_page(''))->toBeFalse();
|
||||
});
|
||||
|
||||
test('filter_layouts_build_document normalizes a filter url into a stored document', function () {
|
||||
$document = filter_layouts_build_document('host.php?rfilter=down&rows=30&filter_layout=5');
|
||||
|
||||
expect($document)->toBeArray();
|
||||
expect($document['version'])->toBe(1);
|
||||
expect($document['page'])->toBe('host.php');
|
||||
// The re-selection marker is never persisted.
|
||||
expect($document['filter'])->toBe(['rfilter' => 'down', 'rows' => '30']);
|
||||
|
||||
// Scheme, host, traversal, or markup payloads cannot name a page.
|
||||
expect(filter_layouts_build_document('http://evil.example/host.php'))->toBeFalse();
|
||||
expect(filter_layouts_build_document('javascript:alert(1)'))->toBeFalse();
|
||||
expect(filter_layouts_build_document('../host.php?x=1'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('filter_layouts_document_url round-trips a document back to a safe url', function () {
|
||||
$document = filter_layouts_build_document('host.php?rfilter=down&rows=30');
|
||||
|
||||
expect(filter_layouts_document_url($document))->toBe('host.php?rfilter=down&rows=30');
|
||||
expect(filter_layouts_document_url(['version' => 1, 'page' => 'host.php', 'filter' => []]))->toBe('host.php');
|
||||
|
||||
// A document that does not name a safe page yields no navigable url.
|
||||
expect(filter_layouts_document_url(['version' => 1, 'page' => '../evil.php', 'filter' => []]))->toBe('');
|
||||
});
|
||||
|
||||
test('filter_layouts_decode rejects anything but a bounded version 1 document', function () {
|
||||
expect(filter_layouts_decode('{"version":1,"page":"host.php","filter":{"rows":"30"}}'))->toBeArray();
|
||||
|
||||
expect(filter_layouts_decode(''))->toBeFalse();
|
||||
expect(filter_layouts_decode('not json'))->toBeFalse();
|
||||
expect(filter_layouts_decode('{"version":2,"page":"host.php"}'))->toBeFalse();
|
||||
expect(filter_layouts_decode('{"version":1,"page":"../evil.php"}'))->toBeFalse();
|
||||
expect(filter_layouts_decode('{"version":1,"page":"host.php","filter":"x"}'))->toBeFalse();
|
||||
expect(filter_layouts_decode('{"version":1,"page":"host.php","filter":{}}' . str_repeat(' ', 8192)))->toBeFalse();
|
||||
});
|
||||
|
||||
test('preset pages disable layouts by default', function () {
|
||||
$presets = filter_layouts_preset_pages();
|
||||
|
||||
expect($presets)->toBeArray();
|
||||
expect($presets)->toContain('cdef.php');
|
||||
expect($presets)->toContain('vdef.php');
|
||||
expect($presets)->toContain('color.php');
|
||||
expect($presets)->toContain('gprint_presets.php');
|
||||
});
|
||||
|
||||
test('the filter class exposes a render_layouts toggle that preset pages clear', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
|
||||
expect($src)->toContain('public bool $render_layouts = true;');
|
||||
expect($src)->toContain('in_array($page, filter_layouts_preset_pages(), true)');
|
||||
expect($src)->toContain('$this->render_layouts = false;');
|
||||
expect($src)->toContain('if ($this->render_layouts) {');
|
||||
});
|
||||
|
||||
test('publishing and global management require the Settings/Utilities realm', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
|
||||
// Realm 15 is Settings/Utilities.
|
||||
expect($src)->toContain('return is_realm_allowed(15);');
|
||||
expect($src)->toContain('function filter_layouts_publish(int $id) : bool {');
|
||||
expect($src)->toContain('if (!filter_layouts_can_manage_global()) {');
|
||||
});
|
||||
|
||||
test('the layout request handler only acts on csrf-protected POST requests', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
|
||||
expect($src)->toContain('function filter_layouts_handle_request() : void {');
|
||||
expect($src)->toContain("strtoupper(\$_SERVER['REQUEST_METHOD']) !== 'POST'");
|
||||
expect($src)->toContain("case 'layout_save':");
|
||||
expect($src)->toContain("case 'layout_publish':");
|
||||
});
|
||||
|
||||
test('a per-user page filter format setting is registered and defaults to modern', function () use ($root) {
|
||||
$src = file_get_contents($root . '/include/global_settings.php');
|
||||
|
||||
expect($src)->toContain("'page_filter_format' => [");
|
||||
expect($src)->toContain("'modern' => __('Modern')");
|
||||
expect($src)->toContain("'legacy' => __('Legacy')");
|
||||
expect($src)->toContain("'default' => 'modern',");
|
||||
});
|
||||
|
||||
test('filter_layouts_user_format defaults to modern without an authenticated user', function () {
|
||||
unset($_SESSION['sess_user_id']);
|
||||
|
||||
expect(filter_layouts_user_format())->toBe('modern');
|
||||
});
|
||||
|
||||
test('the filter class renders a legacy or modern layout from the user preference', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
|
||||
expect($src)->toContain('public string $filter_format = \'modern\';');
|
||||
expect($src)->toContain('$this->filter_format = filter_layouts_user_format();');
|
||||
expect($src)->toContain('private function use_modern_filter() : bool {');
|
||||
expect($src)->toContain('return $this->render_layouts && $this->filter_format === \'modern\';');
|
||||
|
||||
// Modern short-circuits create_filter() before the inline rendering.
|
||||
expect($src)->toContain('if ($this->use_modern_filter()) {');
|
||||
expect($src)->toContain('print $this->create_modern_filter();');
|
||||
});
|
||||
|
||||
test('field rendering is shared so legacy and the modern dialog stay in sync', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
|
||||
expect($src)->toContain('private function emit_field(string $field_name, array $field_array) : string {');
|
||||
// Both the legacy loop and the modern dialog emit fields through the helper.
|
||||
expect(substr_count($src, 'print $this->emit_field($field_name, $field_array);'))->toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test('the modern filter keeps time controls on the bar and fields in the dialog', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
|
||||
expect($src)->toContain('private function field_is_bar(string $field_name, array $field_array) : bool {');
|
||||
expect($src)->toContain("=== 'timespan' || \$field_name === 'refresh'");
|
||||
expect($src)->toContain('cactiFilterEditDialog');
|
||||
expect($src)->toContain("\$this->layout_button('layout_edit',");
|
||||
expect($src)->toContain("\$this->layout_button('layout_saveas',");
|
||||
|
||||
// The dialog renders one filter variable per row rather than mirroring the
|
||||
// inline filter's multi-field row grouping.
|
||||
expect($src)->toContain('$dialog_fields[$field_name] = $field_array;');
|
||||
expect($src)->toContain("<div class='filterRow cactiFilterEditRow'>");
|
||||
expect($src)->not->toContain('$dialog_rows');
|
||||
});
|
||||
|
||||
test('page action buttons (import, export, sort) render on the bar, not the dialog', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
|
||||
// Non go/clear buttons are collected as bar actions and emitted on the bar.
|
||||
expect($src)->toContain('$bar_actions[$field_name] = $field_array;');
|
||||
expect($src)->toContain('foreach ($bar_actions as $field_name => $field_array) {');
|
||||
|
||||
// The dialog no longer carries a page-action button row.
|
||||
expect($src)->not->toContain('$dialog_buttons');
|
||||
});
|
||||
|
||||
test('the modern javascript wires the edit dialog save and publish actions', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
|
||||
expect($src)->toContain('private function create_modern_javascript(string $applyFilter, string $changeFunction, string $clearFunction) : string {');
|
||||
expect($src)->toContain('function layoutDialogSave(forceNew) {');
|
||||
expect($src)->toContain('function layoutUpdateButtons() {');
|
||||
expect($src)->toContain("layoutPost('layout_publish'");
|
||||
});
|
||||
|
||||
test('the edit dialog is tagged for theming and the modern theme skins it', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/html_filter.php');
|
||||
$css = file_get_contents($root . '/include/themes/modern/main.css');
|
||||
|
||||
// The widget wrapper is tagged so themes can skin the dialog chrome.
|
||||
expect($src)->toContain(".dialog('widget').addClass('cactiFilterDialog');");
|
||||
|
||||
expect($css)->toContain('.ui-dialog.cactiFilterDialog .ui-dialog-titlebar');
|
||||
expect($css)->toContain('.ui-dialog.cactiFilterDialog .ui-dialog-buttonpane');
|
||||
});
|
||||
|
||||
test('removing a user deletes their own layouts but not published ones', function () use ($root) {
|
||||
$src = file_get_contents($root . '/lib/auth.php');
|
||||
|
||||
// Scoped to user_id so published (user_id = 0) layouts are preserved.
|
||||
expect($src)->toContain("DELETE FROM user_layouts WHERE user_id = ?', [\$user_id]");
|
||||
});
|
||||
|
||||
test('auth invokes the layout handler after authorization', function () use ($root) {
|
||||
$src = file_get_contents($root . '/include/auth.php');
|
||||
|
||||
expect($src)->toContain('filter_layouts_handle_request();');
|
||||
});
|
||||
|
||||
test('layouts.php is admin-only and registered in the Presets menu', function () use ($root) {
|
||||
$arrays = file_get_contents($root . '/include/global_arrays.php');
|
||||
|
||||
expect($arrays)->toContain("'layouts.php' => 15,");
|
||||
expect($arrays)->toContain("'layouts.php' => __('Filters'),");
|
||||
});
|
||||
|
||||
test('user_layouts is defined consistently across the schema files', function () use ($root) {
|
||||
$sql = file_get_contents($root . '/cacti.sql');
|
||||
$audit = file_get_contents($root . '/docs/audit_schema.sql');
|
||||
$upg = file_get_contents($root . '/install/upgrades/1_3_0.php');
|
||||
|
||||
expect($sql)->toContain('CREATE TABLE `user_layouts`');
|
||||
expect($upg)->toContain('CREATE TABLE IF NOT EXISTS user_layouts');
|
||||
expect($audit)->toContain("('user_layouts',1,'id'");
|
||||
expect($audit)->toContain("('user_layouts',5,'data','text'");
|
||||
|
||||
foreach (['user_id', 'page', 'name', 'data'] as $column) {
|
||||
expect($sql)->toContain("`$column`");
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user