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>
414 lines
14 KiB
PHP
414 lines
14 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. |
|
|
| |
|
|
| 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/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
global $current_user;
|
|
|
|
require_once('global.php');
|
|
|
|
if (!isset($config['cacti_db_version'])) {
|
|
$version = get_cacti_db_version();
|
|
|
|
$config['cacti_db_version'] = $version;
|
|
|
|
if (!defined('CACTI_DB_VERSION')) {
|
|
define('CACTI_DB_VERSION', $version);
|
|
}
|
|
} else {
|
|
$version = $config['cacti_db_version'];
|
|
}
|
|
|
|
$auth_method = read_config_option('auth_method');
|
|
$cookie_user = false;
|
|
|
|
/**
|
|
* Check to see if Cacti authentication is disabled
|
|
* and force Local Authentication on if found
|
|
*/
|
|
check_reset_no_authentication($auth_method);
|
|
|
|
/**
|
|
* Check to see if the Database Cacti version is different
|
|
* from the installed Cacti version and start the install
|
|
* process if found to be different.
|
|
*/
|
|
if (is_install_needed() && !defined('IN_CACTI_INSTALL')) {
|
|
header('Location: ' . CACTI_PATH_URL . 'install/');
|
|
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* The logout page does not require authentication
|
|
* so, short cut the process. login_sso.php is the public SAML2/OpenID
|
|
* redirect+callback endpoint: every request to it is, by definition, still
|
|
* unauthenticated, so it must never hit the login-page redirect below.
|
|
*/
|
|
if (get_current_page() == 'logout.php' || get_current_page() == 'auth_changepassword.php' || get_current_page() == 'auth_resetpassword.php' || get_current_page() == 'login_sso.php') {
|
|
return true;
|
|
}
|
|
|
|
// handle alternate authentication realms
|
|
api_plugin_hook_function('auth_alternate_realms');
|
|
|
|
/**
|
|
* handle change password dialog and auth cookie if not using basic auth
|
|
*/
|
|
if ($auth_method != AUTH_METHOD_BASIC) {
|
|
if (isset($_SESSION[SESS_CHANGE_PASSWORD])) {
|
|
header('Location: ' . CACTI_PATH_URL . 'auth_changepassword.php?ref=' . rawurlencode(validate_redirect_url($_SERVER['HTTP_REFERER'] ?? '', 'index.php')));
|
|
exit;
|
|
}
|
|
|
|
// check for remember me functionality
|
|
if (!isset($_SESSION[SESS_USER_ID])) {
|
|
$cookie_user = check_auth_cookie();
|
|
|
|
if ($cookie_user !== false) {
|
|
if (cacti_auth_transition((int) $cookie_user, 'cookie_restore')) {
|
|
$_SESSION[SESS_USER_ID] = $cookie_user;
|
|
$_SESSION[SESS_USER_AGENT] = $_SERVER['HTTP_USER_AGENT'];
|
|
$_SESSION[SESS_CLIENT_ADDR] = get_client_addr();
|
|
} else {
|
|
// The account may have been locked after cookie validation.
|
|
clear_auth_cookie();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for basic auth, and if the user has been logged in via the server
|
|
* but their user_id is not set, include the auth_login.php script to
|
|
* process their log in.
|
|
*/
|
|
if ($auth_method == AUTH_METHOD_BASIC && !isset($_SESSION[SESS_USER_ID])) {
|
|
$username = get_basic_auth_username();
|
|
|
|
if ($username !== false) {
|
|
$current_user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE realm = 2
|
|
AND username = ?',
|
|
[$username]);
|
|
|
|
if (cacti_sizeof($current_user)) {
|
|
if ($current_user['enabled'] != 'on') {
|
|
auth_display_custom_error_message(__('Access Denied! User account disabled.'));
|
|
|
|
exit;
|
|
}
|
|
|
|
if (!auth_user_has_access($current_user)) {
|
|
auth_display_custom_error_message(__('You do not have access to any area of Cacti. Contact your administrator.'));
|
|
|
|
exit;
|
|
}
|
|
|
|
if (!cacti_auth_transition((int) $current_user['id'], 'basic_auth')) {
|
|
auth_display_custom_error_message(__('Access Denied! User account locked.'));
|
|
|
|
exit;
|
|
}
|
|
|
|
$_SESSION[SESS_USER_ID] = $current_user['id'];
|
|
$_SESSION[SESS_USER_AGENT] = $_SERVER['HTTP_USER_AGENT'];
|
|
$_SESSION[SESS_CLIENT_ADDR] = get_client_addr();
|
|
|
|
$client_addr = get_client_addr();
|
|
|
|
cacti_log("LOGIN: User '" . $current_user['username'] . "' authenticated via Basic Authentication from IP Address '" . $client_addr . "'", false, 'AUTH');
|
|
|
|
db_execute_prepared('INSERT IGNORE INTO user_log
|
|
(username, user_id, result, ip, time)
|
|
VALUES (?, ?, 1, ?, NOW())',
|
|
[$username, $current_user['id'], $_SESSION[SESS_CLIENT_ADDR]]);
|
|
|
|
// GHSA-4rmr-wvjq-qxc2: fall through to the per-page realm check below
|
|
// instead of returning, so a Basic Auth login still has its
|
|
// authorization verified for the requested page.
|
|
} else {
|
|
require_once(CACTI_PATH_BASE . '/auth_login.php');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check to see if Cacti is locked out and if it is, block
|
|
* users from accessing the site until lockout is cleared.
|
|
*/
|
|
if (CACTI_WEB && isset($_SESSION[SESS_USER_ID])) {
|
|
$lockout = read_config_option('cacti_lockout_status', true);
|
|
|
|
if ($lockout != '') {
|
|
$lockout = json_decode($lockout, true);
|
|
$admin_user = read_config_option('admin_user');
|
|
$enabled = read_config_option('auth_maint_lockout_type', true);
|
|
|
|
if ($lockout['session'] != session_id()) {
|
|
if (($enabled == 0 && $admin_user != $_SESSION[SESS_USER_ID]) || $enabled == 1) {
|
|
general_header();
|
|
print '<h1>' . __('Cacti is Locked Out for Maintenance') . '</h1>';
|
|
print '<p>' . __('The primary Cacti administrator has locked out the Cacti user interface for maintenance for activities such as upgrades. If you feel this is in error, please contact your Cacti adminnistrator. This lockout will stay in place for approximately 30 minutes.') . '</p>';
|
|
bottom_footer();
|
|
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the special boolean $guest_account is set for a page, then the guest
|
|
* account can be used. Where this may not be the case is with basic auth
|
|
* where to enter the Cacti website, you must first have a valid account.
|
|
* if that is the case, then use that valid accounts permissions and not
|
|
* the guest account.
|
|
*/
|
|
if (isset($guest_account)) {
|
|
$guest_user_id = get_guest_account();
|
|
|
|
// find guest user
|
|
if (!empty($guest_user_id)) {
|
|
if (empty($_SESSION[SESS_USER_ID])) {
|
|
$_SESSION[SESS_USER_ID] = $guest_user_id;
|
|
$_SESSION[SESS_USER_AGENT] = $_SERVER['HTTP_USER_AGENT'];
|
|
$_SESSION[SESS_CLIENT_ADDR] = get_client_addr();
|
|
}
|
|
|
|
$current_user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$_SESSION[SESS_USER_ID]]);
|
|
|
|
// GHSA-4rmr-wvjq-qxc2: fall through to the per-page realm check below
|
|
// instead of returning, so the guest account's access to this page is
|
|
// authorized rather than assumed.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If we are a guest user in a non-guest area, wipe credentials
|
|
* user will be redirected back to the login page.
|
|
*/
|
|
if (!isset($guest_account) && isset($_SESSION[SESS_USER_ID])) {
|
|
if (get_guest_account() === $_SESSION[SESS_USER_ID]) {
|
|
kill_session_var(SESS_USER_ID);
|
|
cacti_session_destroy();
|
|
cacti_session_start();
|
|
}
|
|
}
|
|
|
|
if (empty($_SESSION[SESS_USER_ID])) {
|
|
if (isset($auth_json) && $auth_json == true) {
|
|
print json_encode(
|
|
[
|
|
'status' => '500',
|
|
'statusText' => __('Not Logged In'),
|
|
'responseText' => __('You must be logged in to access this area of Cacti.')
|
|
]
|
|
);
|
|
} elseif (isset($auth_text) && $auth_text == true) {
|
|
// handle graph_image.php to respond with text.
|
|
print __('FATAL: You must be logged in to access this area of Cacti.');
|
|
} else {
|
|
require_once(CACTI_PATH_BASE . '/auth_login.php');
|
|
}
|
|
|
|
exit;
|
|
} else {
|
|
if (empty($_SESSION[SESS_USER_2FA]) && db_column_exists('user_auth', 'tfa_enabled')) {
|
|
if (read_config_option('secpass_2fa_enabled') == 'on') {
|
|
if (is_2fa_enabled($_SESSION[SESS_USER_ID])) {
|
|
header('Location: ' . CACTI_PATH_URL . 'auth_2fa.php');
|
|
exit;
|
|
} else {
|
|
$_SESSION[SESS_USER_2FA] = time();
|
|
}
|
|
}
|
|
}
|
|
|
|
$realm_id = 0;
|
|
|
|
if (isset($user_auth_realm_filenames[get_current_page()])) {
|
|
$realm_id = $user_auth_realm_filenames[get_current_page()];
|
|
}
|
|
|
|
// Are we upgrading from a version before 1.2 which has the Install/Upgrade realm 26
|
|
if ($realm_id == 26) {
|
|
// See if we can find any users that are allowed to upgrade
|
|
$install_sql_query = '
|
|
SELECT COUNT(*)
|
|
FROM (
|
|
SELECT realm_id
|
|
FROM user_auth_realm AS uar
|
|
WHERE uar.realm_id = ?';
|
|
|
|
$install_sql_params = [$realm_id];
|
|
|
|
// See if the group realms exist and if so, check if permission exists there too
|
|
if (db_table_exists('user_auth_group_realm') &&
|
|
db_table_exists('user_auth_group') &&
|
|
db_table_exists('user_auth_group_members')) {
|
|
$install_sql_query .= '
|
|
UNION
|
|
SELECT realm_id
|
|
FROM user_auth_group_realm AS uagr
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uagr.group_id=uagm.group_id
|
|
INNER JOIN user_auth_group AS uag
|
|
ON uag.id=uagr.group_id
|
|
WHERE uag.enabled="on"
|
|
AND uagr.realm_id = ?';
|
|
|
|
$install_sql_params = array_merge($install_sql_params, [$realm_id]);
|
|
}
|
|
|
|
$install_sql_query .= '
|
|
) AS authorized';
|
|
|
|
$has_install_user = db_fetch_cell_prepared($install_sql_query, $install_sql_params);
|
|
|
|
if (!$has_install_user) {
|
|
/* We did not find any existing users who can upgrade/install so add any admin *
|
|
* who has access to the system settings (realm 15) by default */
|
|
db_execute('INSERT INTO `user_auth_realm` (realm_id, user_id)
|
|
SELECT 26 as realm_id, ua.id
|
|
FROM user_auth ua
|
|
INNER JOIN user_auth_realm uar
|
|
ON uar.user_id=ua.id
|
|
LEFT JOIN user_auth_realm uar2
|
|
ON uar2.user_id=ua.id
|
|
AND uar2.realm_id=26
|
|
WHERE uar.realm_id=15
|
|
AND uar2.user_id IS NULL');
|
|
}
|
|
}
|
|
|
|
if ($realm_id > 0) {
|
|
$auth_sql_query = '
|
|
SELECT COUNT(*)
|
|
FROM (
|
|
SELECT realm_id
|
|
FROM user_auth_realm AS uar
|
|
WHERE uar.user_id = ?
|
|
AND uar.realm_id = ?';
|
|
|
|
$auth_sql_params = [$_SESSION[SESS_USER_ID], $realm_id];
|
|
|
|
/* Because we now expect installation to be done by authorized users, check the group_realm *
|
|
* exists before using it as this may not be present if upgrading from pre-1.x */
|
|
if (db_table_exists('user_auth_group_realm') &&
|
|
db_table_exists('user_auth_group') &&
|
|
db_table_exists('user_auth_group_members')) {
|
|
$auth_sql_query .= '
|
|
UNION
|
|
SELECT realm_id
|
|
FROM user_auth_group_realm AS uagr
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uagr.group_id = uagm.group_id
|
|
INNER JOIN user_auth_group AS uag
|
|
ON uag.id = uagr.group_id
|
|
WHERE uag.enabled = "on"
|
|
AND uagm.user_id = ?
|
|
AND uagr.realm_id = ?';
|
|
|
|
$auth_sql_params = array_merge($auth_sql_params, [$_SESSION[SESS_USER_ID], $realm_id]);
|
|
}
|
|
|
|
$auth_sql_query .= '
|
|
) AS authorized';
|
|
|
|
$authorized = db_fetch_cell_prepared($auth_sql_query, $auth_sql_params);
|
|
} else {
|
|
$authorized = false;
|
|
}
|
|
|
|
if ($realm_id != -1 && !$authorized) {
|
|
if (api_plugin_hook_function('custom_denied', OPER_MODE_NATIVE) == OPER_MODE_RESKIN) {
|
|
exit;
|
|
}
|
|
|
|
if (isset($_SERVER['HTTP_REFERER'])) {
|
|
$goBack = "<td colspan='2' class='center'>[<a href='" . validate_redirect_url($_SERVER['HTTP_REFERER'], $_SERVER['SCRIPT_NAME']) . "'>" . __('Return') . "</a> | <a href='" . CACTI_PATH_URL . "logout.php'>" . __('Login Again') . '</a>]</td>';
|
|
} elseif ($auth_method != AUTH_METHOD_BASIC) {
|
|
$goBack = "<td colspan='2' class='center'>[<a href='" . CACTI_PATH_URL . "logout.php'>" . __('Login Again') . '</a>]</td>';
|
|
} else {
|
|
$goBack = '';
|
|
}
|
|
|
|
raise_ajax_permission_denied();
|
|
|
|
$title_header = __('Permission Denied');
|
|
$title_body = '<p>' . __('You are not permitted to access this section of Cacti.') . '</p><p>' . __('If you feel that this is an error. Please contact your Cacti Administrator.');
|
|
|
|
if ($realm_id == 26) {
|
|
$title_header = __('Installation In Progress');
|
|
$title_body = '<p>' . __('There is an Installation or Upgrade in progress.') . '</p><p>' . __('Only Cacti Administrators with Install/Upgrade privilege may login at this time') . '</p>';
|
|
}
|
|
|
|
print "<!DOCTYPE html>\n";
|
|
print "<html>\n";
|
|
print "<head>\n";
|
|
html_common_header($title_header);
|
|
print "</head>\n";
|
|
print "<body class='logoutBody'>
|
|
<div class='logoutLeft'></div>
|
|
<div class='logoutCenter'>
|
|
<div class='logoutArea'>
|
|
<div class='cactiLogoutLogo'></div>
|
|
<legend>" . $title_header . "</legend>
|
|
<div class='logoutTitle'>
|
|
" . $title_body . '
|
|
</p>
|
|
<center>' . $goBack . "</center>
|
|
</div>
|
|
<div class='logoutErrors'></div>
|
|
</div>
|
|
<div class='versionInfo'>" . __('Version') . ' ' . $version . ' | ' . COPYRIGHT_YEARS_SHORT . "</div>
|
|
</div>
|
|
<div class='logoutRight'></div>
|
|
<script type='text/javascript' " . CactiSecureHeaders::getNonceAttribute() . ">
|
|
$(function() {
|
|
$('.loginLeft').css('width',parseInt($(window).width()*0.33)+'px');
|
|
$('.loginRight').css('width',parseInt($(window).width()*0.33)+'px');
|
|
});
|
|
</script>\n";
|
|
include_once('global_session.php');
|
|
print "</body>
|
|
</html>\n";
|
|
|
|
exit;
|
|
}
|
|
|
|
$current_user = db_fetch_row_prepared('SELECT *
|
|
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();
|
|
}
|