}> */ private static function groups( bool $network ): array { if ( $network ) { return array( 'activity' => array( 'label' => __( 'Activity', 'logged-ai' ), 'pages' => array( 'logged-ai-network' => __( 'Events', 'logged-ai' ), 'logged-ai-network-priority' => __( 'Investigation priority', 'logged-ai' ), ), ), 'incidents' => array( 'label' => __( 'Cases & handover', 'logged-ai' ), 'pages' => array( 'logged-ai-network-investigations' => __( 'Network cases', 'logged-ai' ) ), ), 'alerts' => array( 'label' => __( 'Alerts', 'logged-ai' ), 'pages' => array( 'logged-ai-network-alerts' => __( 'Fixed alerts', 'logged-ai' ) ), ), 'reports' => array( 'label' => __( 'Reports & exports', 'logged-ai' ), 'pages' => array( 'logged-ai-network-exports' => __( 'Exports', 'logged-ai' ) ), ), 'settings' => array( 'label' => __( 'Settings / System', 'logged-ai' ), 'pages' => array( 'logged-ai-network-privacy' => __( 'Privacy and retention', 'logged-ai' ) ), ), ); } $activity = array(); // During schema recovery the same top-level slug serves the upgrade page. if ( false !== has_action( 'toplevel_page_logged-ai', array( Activity::class, 'page' ) ) ) { $activity['logged-ai'] = __( 'Events', 'logged-ai' ); } $activity['logged-ai-investigations'] = __( 'Investigation priority', 'logged-ai' ); $activity['logged-ai-baselines'] = __( 'Learning baselines', 'logged-ai' ); return array( 'overview' => array( 'label' => __( 'Overview', 'logged-ai' ), 'pages' => array( 'logged-ai-overview' => __( 'Overview', 'logged-ai' ) ), ), 'setup' => array( 'label' => __( 'Setup', 'logged-ai' ), 'pages' => array( 'logged-ai-setup' => __( 'Guided setup', 'logged-ai' ) ) ), 'activity' => array( 'label' => __( 'Activity', 'logged-ai' ), 'pages' => $activity, ), 'requests' => array( 'label' => __( 'Requests', 'logged-ai' ), 'pages' => array( 'logged-ai-requests' => __( 'Findings', 'logged-ai' ), 'logged-ai-request-source' => __( 'Source and verification', 'logged-ai' ), 'logged-ai-request-rules' => __( 'Custom rules', 'logged-ai' ), ), ), 'incidents' => array( 'label' => __( 'Cases & handover', 'logged-ai' ), 'pages' => array( 'logged-ai-cases' => __( 'Cases & handover', 'logged-ai' ), 'logged-ai-evidence-links' => __( 'Evidence links', 'logged-ai' ), 'logged-ai-incident-background' => __( 'Background processing', 'logged-ai' ), ), ), 'response' => array( 'label' => __( 'Response', 'logged-ai' ), 'pages' => array( 'logged-ai-response' => __( 'Manual policies', 'logged-ai' ), 'logged-ai-automatic' => __( 'Automatic policies', 'logged-ai' ), ), ), 'alerts' => array( 'label' => __( 'Alerts', 'logged-ai' ), 'pages' => array( 'logged-ai-alerts' => __( 'Fixed alerts', 'logged-ai' ), 'logged-ai-advanced-alerts' => __( 'Advanced alerts', 'logged-ai' ), ), ), 'reports' => array( 'label' => __( 'Reports & exports', 'logged-ai' ), 'pages' => array( 'logged-ai-exports' => __( 'Exports', 'logged-ai' ), 'logged-ai-reports' => __( 'Saved searches and reports', 'logged-ai' ), ), ), 'plans' => array( 'label' => defined('LOGGED_AI_PRO_FILE') ? __( 'Pro toolkit', 'logged-ai' ) : __( 'Explore Pro', 'logged-ai' ), 'pages' => array('logged-ai-plans'=>__( 'Pro workflows', 'logged-ai' )) ), 'plugins' => array( 'label' => __( 'More Plugins', 'logged-ai' ), 'pages' => array( 'logged-ai-more-plugins' => __( 'More Plugins', 'logged-ai' ) ) ), 'settings' => array( 'label' => __( 'Settings / System', 'logged-ai' ), 'pages' => array( 'logged-ai-collection' => __( 'Collection controls', 'logged-ai' ), 'logged-ai-settings' => __( 'Privacy and retention', 'logged-ai' ), 'logged-ai-health' => __( 'System status', 'logged-ai' ), 'logged-ai-upgrade' => __( 'Schema upgrade', 'logged-ai' ), 'logged-ai-commercial' => __( 'Licence & updates', 'logged-ai' ), 'logged-ai-commercial-updates' => __( 'Pro updates', 'logged-ai' ), ), ), ); } /** * Finds unique native registrations using their current native capabilities. * This controls navigation visibility, never controller authorization. * * @param bool $network Whether this is network administration. * @return array}> */ public static function available( bool $network ): array { global $submenu; $parent = $network ? 'logged-ai-network' : 'logged-ai'; $registered = array(); $duplicates = array(); foreach ( $submenu[ $parent ] ?? array() as $key => $row ) { if ( ! is_array( $row ) || ! isset( $row[1], $row[2] ) || ! is_string( $row[1] ) || ! is_string( $row[2] ) ) { continue; } if ( isset( $registered[ $row[2] ] ) ) { $duplicates[ $row[2] ] = true; } $classes = preg_split( '/\s+/', is_string( $row[4] ?? null ) ? trim( $row[4] ) : '' ); $classes = $classes ? $classes : array(); $hidden = in_array( 'hidden', $classes, true ) && ! in_array( 'lai-navigation-secondary', $classes, true ); $registered[ $row[2] ] = array( 'key' => $key, 'capability' => $row[1], 'hidden' => $hidden, ); } $result = array(); foreach ( self::groups( $network ) as $name => $group ) { $pages = array(); foreach ( $group['pages'] as $slug => $label ) { if ( isset( $registered[ $slug ] ) && ! $registered[ $slug ]['hidden'] && ! isset( $duplicates[ $slug ] ) && current_user_can( $registered[ $slug ]['capability'] ) ) { $pages[ $slug ] = array( 'label' => $label, 'key' => $registered[ $slug ]['key'], ); } } if ( $pages ) { $result[ $name ] = array( 'label' => $group['label'], 'pages' => $pages, ); } } // Present everyday work first, settings next, and optional discovery last. $ordered = array(); foreach ( array( 'overview', 'activity', 'requests', 'incidents', 'alerts', 'reports', 'response', 'setup', 'settings', 'plans', 'plugins' ) as $key ) { if ( isset( $result[ $key ] ) ) { $ordered[ $key ] = $result[ $key ]; } } return $ordered + $result; } /** Groups labels and display order without removing an authorization row. */ public static function group_menu(): void { global $submenu; $network = is_network_admin(); $parent = $network ? 'logged-ai-network' : 'logged-ai'; $ordered = array(); foreach ( self::available( $network ) as $group ) { $first = true; foreach ( $group['pages'] as $page ) { $key = $page['key']; $classes = preg_split( '/\s+/', trim( $submenu[ $parent ][ $key ][4] ?? '' ) ); $classes = $classes ? $classes : array(); if ( in_array( 'lai-navigation-secondary', $classes, true ) ) { $classes = array_values( array_diff( $classes, array( 'hidden', 'lai-navigation-secondary' ) ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Remove only Logged.ai's previous presentation classes from its registered submenu entries; capabilities and destinations remain unchanged. $submenu[ $parent ][ $key ][4] = implode( ' ', $classes ); } if ( $first ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Give the first registered Logged.ai route its visible group label; retain the original capability, destination and callback registration. $submenu[ $parent ][ $key ][0] = $group['label']; $ordered[ $key ] = $submenu[ $parent ][ $key ]; } else { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Mark only secondary registered Logged.ai routes for grouped presentation; routes and capability checks remain registered. $submenu[ $parent ][ $key ][4] = trim( implode( ' ', $classes ) . ' hidden lai-navigation-secondary' ); } $first = false; } } if ( $ordered ) { // Original keys, capabilities, slugs, titles and hook registrations survive. // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Order only the owned Logged.ai submenu while preserving original keys, registered capabilities, destinations and callbacks. $submenu[ $parent ] = $ordered + $submenu[ $parent ]; } } /** * Highlights the visible group when a secondary registered route is active. * * @param string|null $selected Native selected submenu. * @param string $parent_value Native parent menu. * @return string|null */ public static function selected_menu( ?string $selected, string $parent_value ): ?string { global $plugin_page; $network = is_network_admin(); if ( ( $network ? 'logged-ai-network' : 'logged-ai' ) !== $parent_value || ! is_string( $plugin_page ) ) { return $selected; } foreach ( self::available( $network ) as $group ) { if ( isset( $group['pages'][ $plugin_page ] ) ) { return array_key_first( $group['pages'] ); } } return $selected; } /** Renders ordinary, keyboard-operable links only for the current page group. */ public static function render(): void { global $plugin_page, $pagenow; if ( 'admin.php' !== $pagenow || ! is_string( $plugin_page ) ) { return; } $network = is_network_admin(); Design::shell( self::available( $network ), $network, $plugin_page ); } }