diff --git a/CHANGELOG.md b/CHANGELOG.md
index 305c64e7..884c0d0e 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. 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 `';
}
diff --git a/setup.php b/setup.php
index 0323b859..f4cbd5ee 100644
--- a/setup.php
+++ b/setup.php
@@ -57,9 +57,7 @@ function plugin_mactrack_install($operator_initiated = true): bool {
// device hook: Device Management Action dropdown selected: execute list of device
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);
+ mactrack_install_realms(true);
$site_ready = mactrack_setup_table_new($operator_initiated);
@@ -131,6 +129,73 @@ 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',
+ ];
+}
+
+/**
+ * Registers this plugin's permission realms with Cacti.
+ *
+ * Cacti's api_plugin_register_realm() maps a realm that has to be created to
+ * the configured admin and to the user making the request when its fourth
+ * argument is true. Repairs that run from an arbitrary Console page pass
+ * false so that a missing realm is never granted to whoever triggered them.
+ *
+ * The name must keep "install": Cacti only honors api_plugin_register_realm()
+ * when the calling function's name contains install, upgrade or setup.
+ *
+ * @param bool $grant_access Whether newly created realms are granted to the
+ * admin and current session user.
+ *
+ * @return void
+ */
+function mactrack_install_realms(bool $grant_access): void {
+ foreach (mactrack_realm_definitions() as $display => $files) {
+ api_plugin_register_realm('mactrack', $files, $display, $grant_access);
+ }
+}
+
+/**
+ * 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 +228,16 @@ function mactrack_check_upgrade(): void {
$old = (array) db_fetch_row("SELECT * FROM plugin_config WHERE directory='mactrack'");
- if (!cacti_sizeof($old) || $current != $old['version']) {
+ $version_changed = !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). Repair only the realms in that case; the
+ // full upgrade below rebuilds indexes and rewrites large tables.
+ if (!$version_changed && in_array((int) $old['status'], [1, 4], true) && mactrack_realms_are_stale()) {
+ mactrack_install_realms(false);
+ }
+
+ if ($version_changed) {
// 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/TestCase.php b/tests/TestCase.php
index 7b35c3c3..bb93374b 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -33,6 +33,8 @@ protected function setUp(): void {
$GLOBALS['__test_config_options'] = array();
$GLOBALS['__test_next_returns'] = array();
$GLOBALS['__test_matched_returns'] = array();
+ $GLOBALS['__test_graph_calls'] = array();
+ $GLOBALS['__test_graph_total'] = 0;
}
/**
diff --git a/tests/Unit/MactrackRealmAndGraphViewerTest.php b/tests/Unit/MactrackRealmAndGraphViewerTest.php
new file mode 100644
index 00000000..20cdb412
--- /dev/null
+++ b/tests/Unit/MactrackRealmAndGraphViewerTest.php
@@ -0,0 +1,258 @@
+ $plugin_config The plugin_config row to report.
+ * @param array> $registered The plugin_realms rows to report.
+ *
+ * @return void
+ */
+ private function runCheckUpgrade(array $plugin_config, array $registered): void {
+ mactrack_test_queue_return('get_current_page', 'index.php');
+ mactrack_test_queue_return('db_fetch_row', $plugin_config);
+ mactrack_test_queue_return('db_fetch_assoc_prepared', $registered);
+
+ $restore = $GLOBALS['config']['base_path'];
+ $base = sys_get_temp_dir() . '/mactrack-realm-' . uniqid();
+
+ mkdir($base . '/plugins/mactrack/includes', 0777, true);
+ mkdir($base . '/plugins/mactrack/lib', 0777, true);
+ file_put_contents($base . '/plugins/mactrack/INFO', "[info]\nversion = 9.9.9\nname = mactrack\nlongname = MacTrack\nauthor = x\nhomepage = x\n");
+ file_put_contents($base . '/plugins/mactrack/includes/database.php', ">
+ */
+ private function callsTo(string $fn): array {
+ return array_values(array_filter($GLOBALS['__test_db_calls'], function ($call) use ($fn) {
+ return $call['fn'] === $fn;
+ }));
+ }
+
+ /**
+ * @return array>
+ */
+ 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_realm_helper_name_satisfies_the_cacti_entrypoint_check(): void {
+ $this->assertMatchesRegularExpression('/(install|upgrade|setup)/i', (new ReflectionFunction('mactrack_install_realms'))->getName());
+ }
+
+ /**
+ * @return void
+ */
+ public function test_install_grants_new_realms(): void {
+ plugin_mactrack_install(false);
+
+ foreach ($this->callsTo('api_plugin_register_realm') as $call) {
+ $this->assertTrue($call['params'][1]);
+ }
+ }
+
+ /**
+ * @return void
+ */
+ public function test_same_version_repair_registers_realms_without_granting_access(): void {
+ $this->runCheckUpgrade(['version' => '9.9.9', 'status' => 1], []);
+
+ $realms = $this->callsTo('api_plugin_register_realm');
+
+ $this->assertCount(count(mactrack_realm_definitions()), $realms);
+
+ foreach ($realms as $call) {
+ $this->assertFalse($call['params'][1]);
+ }
+ }
+
+ /**
+ * @return void
+ */
+ public function test_same_version_repair_skips_the_full_upgrade(): void {
+ $this->runCheckUpgrade(['version' => '9.9.9', 'status' => 1], []);
+
+ $this->assertSame([], $this->callsTo('api_plugin_register_hook'));
+
+ foreach ($this->callsTo('db_execute_prepared') as $call) {
+ $this->assertStringNotContainsString('UPDATE plugin_config', $call['sql']);
+ }
+ }
+
+ /**
+ * @return void
+ */
+ public function test_same_version_with_current_realms_registers_nothing(): void {
+ $this->runCheckUpgrade(['version' => '9.9.9', 'status' => 1], $this->registeredRows());
+
+ $this->assertSame([], $this->callsTo('api_plugin_register_realm'));
+ }
+
+ /**
+ * @return void
+ */
+ public function test_same_version_repair_ignores_a_disabled_plugin(): void {
+ $this->runCheckUpgrade(['version' => '9.9.9', 'status' => 0], []);
+
+ $this->assertSame([], $this->callsTo('api_plugin_register_realm'));
+ }
+
+ /**
+ * @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);
+ }
+}
diff --git a/tests/bootstrap-unit.php b/tests/bootstrap-unit.php
index a780b492..11496ee2 100644
--- a/tests/bootstrap-unit.php
+++ b/tests/bootstrap-unit.php
@@ -271,7 +271,7 @@ function api_plugin_register_hook($plugin, $hook, $function, $file, $inline = ''
if (!function_exists('api_plugin_register_realm')) {
function api_plugin_register_realm($plugin, $files, $name, $alone = 0) {
- $GLOBALS['__test_db_calls'][] = array('fn' => 'api_plugin_register_realm', 'sql' => $name, 'params' => array($files));
+ $GLOBALS['__test_db_calls'][] = array('fn' => 'api_plugin_register_realm', 'sql' => $name, 'params' => array($files, $alone));
return true;
}
@@ -369,6 +369,20 @@ function raise_message($id, $text = '', $level = 0) {
}
}
+/*
+ * Records the (sql_where, order_by, limit) of each call in
+ * $GLOBALS['__test_graph_calls'] and reports $GLOBALS['__test_graph_total']
+ * rows; TestCase::setUp() resets both.
+ */
+if (!function_exists('get_allowed_graphs')) {
+ function get_allowed_graphs($sql_where = '', $order_by = '', $limit = '', &$total_rows = 0) {
+ $GLOBALS['__test_graph_calls'][] = array($sql_where, $order_by, $limit);
+ $total_rows = $GLOBALS['__test_graph_total'] ?? 0;
+
+ return array();
+ }
+}
+
if (!function_exists('get_current_page')) {
function get_current_page() {
return mactrack_test_next_return('get_current_page', '');