Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

--- 3.3 ---

* security: Replace the confirmation-page Cancel buttons' inline `onClick='cactiReturnTo()'` with the CSP-safe `cactiReturnTo` class so the pages no longer trip Cacti's Content-Security-Policy script-src-attr directive; see the README for the one-time `applySkin()` snippet needed when running Cacti below 1.2.31 (1.2.31+ binds the class automatically)
* compat: Lower the minimum Cacti compatibility floor from 1.2.32 to 1.2.29
* issue: Re-introduce the `plugin_webseer_contacts` `user_id_type` UNIQUE key (on `user_id`, `type`) that Cacti 1.2.29-1.2.31's `db_update_table()` silently dropped - those releases rebuild a table's indexes only from its `keys` definition and ignore `unique_keys`. The repair runs automatically on upgrade when running one of those releases and is a no-op on 1.2.32+. WARNING: if duplicate `(user_id, type)` contact rows were inserted while the constraint was absent, re-adding the key fails; the upgrade logs the affected table and leaves the recorded version unchanged so it retries on the next request. To recover, de-duplicate `plugin_webseer_contacts` (keep a single row per `user_id`/`type`) and re-run the upgrade. Sites already upgraded to 1.2.32+ that lost the key on an earlier 1.2.29-1.2.31 run are not auto-repaired
* feature: Add a root `manifest.json` and an upgrade-time file prune that removes dev-only bundled files (e.g. `tests/`) on a version change, refuses any path resolving outside the plugin directory, and logs anything it cannot remove
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ plugin so you can setup maintenance schedules so that known times when a service
is going to be down can be configured so that escallation does not needlessly
take place during maintenance periods.

## Cacti compatibility

If you are running a version of Cacti below 1.2.31, please add the function
below to the `applySkin()` function in `include/layout.js` to enable the Cancel
buttons on forms to work:

```js
$(document).off('click.cactiReturnTo', '.cactiReturnTo')
.on('click.cactiReturnTo', '.cactiReturnTo', function(event) {
event.preventDefault();
cactiReturnTo($(this).attr('data-url'));
});
```

## Installation

To install the webseer plugin, simply copy the plugin_webseer directory to
Expand Down
50 changes: 50 additions & 0 deletions tests/Security/CspCancelButtonsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Regression coverage for the CSP migration of the confirmation-page Cancel
* buttons. The three entry points below are exempt from the patch-coverage
* gate, so without this source-level assertion CI would not catch a missing
* `cactiReturnTo` class or a reintroduced inline `onClick='cactiReturnTo()'`
* handler (which trips Cacti's Content-Security-Policy script-src-attr
* directive).
*
* Each entry's value is the number of Cancel buttons converted in that file;
* the total must stay at eight.
*/

$cancel_button_files = array(
'webseer.php' => 4,
'webseer_servers.php' => 3,
'webseer_proxies.php' => 1,
);

it('has removed every inline cactiReturnTo onClick handler', function () use ($cancel_button_files) {
foreach (array_keys($cancel_button_files) as $file) {
$source = plugin_test_read_source($file);

expect($source)->not->toMatch('/onClick\s*=\s*[\'"]cactiReturnTo\(/i', "{$file} still contains an inline cactiReturnTo onClick handler");
}
});

it('binds every Cancel button through the CSP-safe cactiReturnTo class', function () use ($cancel_button_files) {
$total = 0;

foreach ($cancel_button_files as $file => $expected) {
$source = plugin_test_read_source($file);

$count = preg_match_all('/class\s*=\s*[\'"]cactiReturnTo[\'"]/i', $source);

expect($count)->toBe($expected, "{$file} should expose {$expected} cactiReturnTo class button(s), found {$count}");

$total += $count;
}

expect($total)->toBe(8);
});
8 changes: 4 additions & 4 deletions webseer.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ function form_actions() {
</td>
</tr>\n";

$save_html = "<input type='button' value='" . __esc('Cancel') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Delete URL', 'Delete URLs', cacti_sizeof($url_array)) . "'>";
$save_html = "<input type='button' class='cactiReturnTo' value='" . __esc('Cancel') . "'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Delete URL', 'Delete URLs', cacti_sizeof($url_array)) . "'>";
Comment thread
TheWitness marked this conversation as resolved.
} elseif ($action == WEBSEER_ACTION_URL_DISABLE) {
print " <tr>
<td class='topBoxAlt'>
Expand All @@ -223,7 +223,7 @@ function form_actions() {
</td>
</tr>\n";

$save_html = "<input type='button' value='" . __esc('Cancel') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Disable URL', 'Disable URLs', cacti_sizeof($url_array)) . "'>";
$save_html = "<input type='button' class='cactiReturnTo' value='" . __esc('Cancel') . "'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Disable URL', 'Disable URLs', cacti_sizeof($url_array)) . "'>";
} elseif ($action == WEBSEER_ACTION_URL_ENABLE) {
print " <tr>
<td class='topBoxAlt'>
Expand All @@ -232,7 +232,7 @@ function form_actions() {
</td>
</tr>\n";

$save_html = "<input type='button' value='" . __esc('Cancel') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Enable URL', 'Enable URLs', cacti_sizeof($url_array)) . "'>";
$save_html = "<input type='button' class='cactiReturnTo' value='" . __esc('Cancel') . "'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Enable URL', 'Enable URLs', cacti_sizeof($url_array)) . "'>";
} elseif ($action == WEBSEER_ACTION_URL_DUPLICATE) {
print " <tr>
<td class='topBoxAlt'>
Expand All @@ -241,7 +241,7 @@ function form_actions() {
</td>
</tr>\n";

$save_html = "<input type='button' value='" . __esc('Cancel') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Duplicate URL', 'Duplicate URLs', cacti_sizeof($url_array)) . "'>";
$save_html = "<input type='button' class='cactiReturnTo' value='" . __esc('Cancel') . "'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Duplicate URL', 'Duplicate URLs', cacti_sizeof($url_array)) . "'>";
}
} else {
raise_message(40);
Expand Down
2 changes: 1 addition & 1 deletion webseer_proxies.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function proxy_form_actions() {
</td>
</tr>\n";

$save_html = "<input type='button' value='" . __esc('Cancel') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Delete Proxy', 'Delete Proxies', cacti_sizeof($proxy_array)) . "'>";
$save_html = "<input type='button' class='cactiReturnTo' value='" . __esc('Cancel') . "'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Delete Proxy', 'Delete Proxies', cacti_sizeof($proxy_array)) . "'>";
}
} else {
raise_message(40);
Expand Down
6 changes: 3 additions & 3 deletions webseer_servers.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function form_actions() {
</td>
</tr>";

$save_html = "<input type='button' value='" . __esc('Cancel') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Delete Server', 'Delete Servers', cacti_sizeof($server_array)) . "'>";
$save_html = "<input type='button' class='cactiReturnTo' value='" . __esc('Cancel') . "'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Delete Server', 'Delete Servers', cacti_sizeof($server_array)) . "'>";
} elseif ($action == WEBSEER_ACTION_SERVER_DISABLE) {
print " <tr>
<td class='topBoxAlt'>
Expand All @@ -180,7 +180,7 @@ function form_actions() {
</td>
</tr>";

$save_html = "<input type='button' value='" . __esc('Cancel') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Disable Server', 'Disable Servers', cacti_sizeof($server_array)) . "'>";
$save_html = "<input type='button' class='cactiReturnTo' value='" . __esc('Cancel') . "'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Disable Server', 'Disable Servers', cacti_sizeof($server_array)) . "'>";
} elseif ($action == WEBSEER_ACTION_SERVER_ENABLE) {
print " <tr>
<td class='topBoxAlt'>
Expand All @@ -189,7 +189,7 @@ function form_actions() {
</td>
</tr>";

$save_html = "<input type='button' value='" . __esc('Cancel') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Enable Server', 'Enable Servers', cacti_sizeof($server_array)) . "'>";
$save_html = "<input type='button' class='cactiReturnTo' value='" . __esc('Cancel') . "'>&nbsp;<input type='submit' value='" . __esc('Continue') . "' title='" . __esc_n('Enable Server', 'Enable Servers', cacti_sizeof($server_array)) . "'>";
}
} else {
raise_message(40);
Expand Down
Loading