From 2a2b006267ecd655a5f8c2a00ea545afd2323d1d Mon Sep 17 00:00:00 2001 From: TheWitness Date: Fri, 16 Oct 2026 11:40:36 -0400 Subject: [PATCH 1/3] fix: heal stale realms, polyfill graph filter helper, scope tab script - Dashboard returned "401 Permission Denied" after upgrading: the page was added to the Mactrack Viewer realm without a version bump, so mactrack_check_upgrade() never re-registered the realm and the file stayed unmapped (realm 0). Realm definitions now live in mactrack_realm_definitions() and mactrack_realms_are_stale() makes the upgrade check re-register them on installed plugins. - Graphs tab fataled with "Call to undefined function graph_view_has_untemplated_graphs()": Cacti 1.2.x defines it in graph_view.php but html_graph_preview_filter() calls it. Add a guarded polyfill that is a no-op where core defines it. - The MacTrack tab script rewrote the href of whichever main tab was selected; scope it to the MacTrack tab so it can never repoint Console. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 + lib/mactrack_functions.php | 24 ++- setup.php | 56 +++++- .../Unit/MactrackRealmAndGraphViewerTest.php | 170 ++++++++++++++++++ 4 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/MactrackRealmAndGraphViewerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 305c64e7..94f97eaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ * issue: Decode MAC/IP OIDs as hex in the Extreme scanner by correcting a misplaced xform_stripped_oid() argument * issue: Fix an undefined $ifindex reference (should be $ifIndex) in the Nortel Bay scanner * issue: Fix a misplaced parenthesis in the running-process stats parser +* issue: Fix "401 Permission Denied" on the Dashboard after upgrading: the Dashboard page was added to the Mactrack Viewer realm without a version bump, so upgraded installs never mapped it to a realm. The upgrade check now also compares the registered realm file lists against the code (`mactrack_realms_are_stale()`) and re-registers them, which carries existing Viewer grants over to the Dashboard +* issue: Fix the Graphs tab fatal error "Call to undefined function graph_view_has_untemplated_graphs()" on Cacti 1.2.x releases whose graph preview filter calls a helper defined in graph_view.php; the plugin now supplies a guarded polyfill, which is a no-op where core already defines it +* issue: Make the MacTrack tab script repoint only the MacTrack main tab rather than whichever main tab is selected, so it can never retarget the Console tab at a MacTrack page * security: Move every inline `onClick`/`onChange` handler (filter selects, Return/Cancel buttons, the device rescan icon, the utilities refresh controls) into `$(function() {})` handlers, using delegated handlers in `js/mactrack.js` for markup rendered by shared or AJAX-refreshed code, so pages work under a Content-Security-Policy that blocks inline event-handler attributes; all inline `'; } diff --git a/setup.php b/setup.php index 0323b859..6235e942 100644 --- a/setup.php +++ b/setup.php @@ -58,8 +58,9 @@ function plugin_mactrack_install($operator_initiated = true): bool { api_plugin_register_hook('mactrack', 'device_action_execute', 'mactrack_device_action_execute', 'mactrack_actions.php'); // Register our realms - api_plugin_register_realm('mactrack', 'mactrack_view_dashboard.php,mactrack_view_ips.php,mactrack_view_arp.php,mactrack_view_macs.php,mactrack_view_dot1x.php,mactrack_view_sites.php,mactrack_view_devices.php,mactrack_view_interfaces.php,mactrack_view_graphs.php,mactrack_ajax.php', 'Mactrack Viewer', 1); - api_plugin_register_realm('mactrack', 'mactrack_ajax_admin.php,mactrack_devices.php,mactrack_snmp.php,mactrack_sites.php,mactrack_device_types.php,mactrack_utilities.php,mactrack_macwatch.php,mactrack_macauth.php,mactrack_vendormacs.php', 'Mactrack Administrator', 1); + foreach (mactrack_realm_definitions() as $display => $files) { + api_plugin_register_realm('mactrack', $files, $display, 1); + } $site_ready = mactrack_setup_table_new($operator_initiated); @@ -131,6 +132,51 @@ function plugin_mactrack_upgrade(): bool { return false; } +/** + * Permission realms this plugin registers, keyed by realm display name, + * each with its comma-delimited list of protected files. Shared by the + * installer and by mactrack_realms_are_stale() so a page added to a realm + * (for example the Dashboard) reaches upgraded installs. + * + * @return array Realm display name => comma-delimited files. + */ +function mactrack_realm_definitions(): array { + return [ + 'Mactrack Viewer' => 'mactrack_view_dashboard.php,mactrack_view_ips.php,mactrack_view_arp.php,mactrack_view_macs.php,mactrack_view_dot1x.php,mactrack_view_sites.php,mactrack_view_devices.php,mactrack_view_interfaces.php,mactrack_view_graphs.php,mactrack_ajax.php', + 'Mactrack Administrator' => 'mactrack_ajax_admin.php,mactrack_devices.php,mactrack_snmp.php,mactrack_sites.php,mactrack_device_types.php,mactrack_utilities.php,mactrack_macwatch.php,mactrack_macauth.php,mactrack_vendormacs.php', + ]; +} + +/** + * Reports whether any file this plugin protects is missing from the realms + * stored in plugin_realms. The realm file lists are only written when the + * plugin is installed or its version changes, so a file added to a realm + * without a version bump stays unmapped on upgraded installs and Cacti + * denies it with "401 Permission Denied". + * + * @return bool True when at least one expected file is not registered. + */ +function mactrack_realms_are_stale(): bool { + $rows = db_fetch_assoc_prepared('SELECT file FROM plugin_realms WHERE plugin = ?', ['mactrack']); + $registered = []; + + foreach ((array) $rows as $row) { + foreach (explode(',', (string) $row['file']) as $file) { + $registered[trim($file)] = true; + } + } + + foreach (mactrack_realm_definitions() as $files) { + foreach (explode(',', $files) as $file) { + if (!isset($registered[$file])) { + return true; + } + } + } + + return false; +} + /** * Detects a version mismatch between the running plugin code and the * version recorded in plugin_config, and when found (and only on a @@ -163,7 +209,11 @@ function mactrack_check_upgrade(): void { $old = (array) db_fetch_row("SELECT * FROM plugin_config WHERE directory='mactrack'"); - if (!cacti_sizeof($old) || $current != $old['version']) { + // A realm file list can drift from the code without a version bump (the + // Dashboard was added that way), so verify it on installed plugins too. + $realms_stale = cacti_sizeof($old) && in_array((int) $old['status'], [1, 4], true) && mactrack_realms_are_stale(); + + if (!cacti_sizeof($old) || $current != $old['version'] || $realms_stale) { // if the plugin is installed and/or active if (!cacti_sizeof($old) || $old['status'] == 1 || $old['status'] == 4) { // re-register the hooks diff --git a/tests/Unit/MactrackRealmAndGraphViewerTest.php b/tests/Unit/MactrackRealmAndGraphViewerTest.php new file mode 100644 index 00000000..9efb3a01 --- /dev/null +++ b/tests/Unit/MactrackRealmAndGraphViewerTest.php @@ -0,0 +1,170 @@ +> + */ + private function registeredRows(): array { + $rows = []; + + foreach (mactrack_realm_definitions() as $files) { + $rows[] = ['file' => $files]; + } + + return $rows; + } + + /** + * @return void + */ + public function test_viewer_realm_protects_the_dashboard(): void { + $realms = mactrack_realm_definitions(); + + $this->assertArrayHasKey('Mactrack Viewer', $realms); + $this->assertStringContainsString('mactrack_view_dashboard.php', $realms['Mactrack Viewer']); + $this->assertStringContainsString('mactrack_view_graphs.php', $realms['Mactrack Viewer']); + $this->assertStringContainsString('mactrack_devices.php', $realms['Mactrack Administrator']); + } + + /** + * @return void + */ + public function test_install_registers_every_realm(): void { + plugin_mactrack_install(false); + + $registered = []; + + foreach ($GLOBALS['__test_db_calls'] as $call) { + if ($call['fn'] === 'api_plugin_register_realm') { + $registered[$call['sql']] = $call['params'][0]; + } + } + + $this->assertSame(mactrack_realm_definitions(), $registered); + } + + /** + * @return void + */ + public function test_realms_are_stale_when_the_dashboard_file_is_unregistered(): void { + $rows = $this->registeredRows(); + $rows[0]['file'] = str_replace('mactrack_view_dashboard.php,', '', $rows[0]['file']); + + mactrack_test_queue_return('db_fetch_assoc_prepared', $rows); + + $this->assertTrue(mactrack_realms_are_stale()); + } + + /** + * @return void + */ + public function test_realms_are_current_when_every_file_is_registered(): void { + mactrack_test_queue_return('db_fetch_assoc_prepared', $this->registeredRows()); + + $this->assertFalse(mactrack_realms_are_stale()); + } + + /** + * @return void + */ + public function test_realms_are_stale_when_none_are_registered(): void { + mactrack_test_queue_return('db_fetch_assoc_prepared', []); + + $this->assertTrue(mactrack_realms_are_stale()); + } + + /** + * @return void + */ + public function test_untemplated_graph_helper_reports_true_when_a_graph_exists(): void { + $GLOBALS['__test_graph_total'] = 3; + + $this->assertTrue(graph_view_has_untemplated_graphs('gt.id IN (1,2)')); + $this->assertSame( + ['(gl.graph_template_id = 0) AND gt.id IN (1,2)', '', '1'], + $GLOBALS['__test_graph_calls'][0] + ); + } + + /** + * @return void + */ + public function test_untemplated_graph_helper_reports_false_without_a_restriction(): void { + $this->assertFalse(graph_view_has_untemplated_graphs()); + $this->assertSame(['(gl.graph_template_id = 0)', '', '1'], $GLOBALS['__test_graph_calls'][0]); + } + + /** + * @return void + */ + public function test_tab_script_only_repoints_the_mactrack_tab(): void { + ob_start(); + mactrack_tabs(); + $html = (string) ob_get_clean(); + + $this->assertStringContainsString('plugins/mactrack/mactrack_view_', $html); + $this->assertStringNotContainsString('.maintabs a.selected', $html); + } +} From 646331c70f1e6525d509acc72cda89dec5d43957 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Fri, 16 Oct 2026 12:25:20 -0400 Subject: [PATCH 2/3] fix: repair stale realms without schema upgrade or access grants Same-version realm repair now only registers the realms with auto-grant disabled, so it no longer re-runs mactrack_database_upgrade() or hands a missing realm to the requesting user. Move the get_allowed_graphs() test stub into the shared bootstrap and reset its state in TestCase::setUp(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- setup.php | 35 ++++-- tests/TestCase.php | 2 + .../Unit/MactrackRealmAndGraphViewerTest.php | 107 +++++++++++++++--- tests/bootstrap-unit.php | 16 ++- 5 files changed, 140 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f97eaa..884c0d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ * issue: Decode MAC/IP OIDs as hex in the Extreme scanner by correcting a misplaced xform_stripped_oid() argument * issue: Fix an undefined $ifindex reference (should be $ifIndex) in the Nortel Bay scanner * issue: Fix a misplaced parenthesis in the running-process stats parser -* issue: Fix "401 Permission Denied" on the Dashboard after upgrading: the Dashboard page was added to the Mactrack Viewer realm without a version bump, so upgraded installs never mapped it to a realm. The upgrade check now also compares the registered realm file lists against the code (`mactrack_realms_are_stale()`) and re-registers them, which carries existing Viewer grants over to the Dashboard +* issue: Fix "401 Permission Denied" on the Dashboard after upgrading: the Dashboard page was added to the Mactrack Viewer realm without a version bump, so upgraded installs never mapped it to a realm. The upgrade check now also compares the registered realm file lists against the code (`mactrack_realms_are_stale()`) and re-registers them, which carries existing Viewer grants over to the Dashboard. The repair only registers the realms (no schema upgrade) and never grants a newly created realm to the requesting user * issue: Fix the Graphs tab fatal error "Call to undefined function graph_view_has_untemplated_graphs()" on Cacti 1.2.x releases whose graph preview filter calls a helper defined in graph_view.php; the plugin now supplies a guarded polyfill, which is a no-op where core already defines it * issue: Make the MacTrack tab script repoint only the MacTrack main tab rather than whichever main tab is selected, so it can never retarget the Console tab at a MacTrack page * security: Move every inline `onClick`/`onChange` handler (filter selects, Return/Cancel buttons, the device rescan icon, the utilities refresh controls) into `$(function() {})` handlers, using delegated handlers in `js/mactrack.js` for markup rendered by shared or AJAX-refreshed code, so pages work under a Content-Security-Policy that blocks inline event-handler attributes; all inline `