key, title and capability. The first one is also the top-level page. * * @return array */ public static function screens(): array { $screens = array( self::TOP_SLUG => array( 'key' => 'overview', 'title' => __( 'Overview', 'forensix-digital' ), 'capability' => Capabilities::VIEW_FINDINGS, ), self::SETTINGS_SLUG => array( 'key' => 'settings', 'title' => __( 'Settings & Licence', 'forensix-digital' ), 'capability' => Capabilities::MANAGE_SETTINGS, ), 'forensix-apps' => array( 'key' => 'apps', 'title' => __( 'Apps', 'forensix-digital' ), 'capability' => Capabilities::MANAGE_APPS, ), 'forensix-findings' => array( 'key' => 'findings', 'title' => __( 'Findings', 'forensix-digital' ), 'capability' => Capabilities::VIEW_FINDINGS, ), 'forensix-activity' => array( 'key' => 'activity', 'title' => __( 'Activity', 'forensix-digital' ), 'capability' => Capabilities::VIEW_ACTIVITY, ), ); /** * Lets active apps add their own page. Entries need key, title and capability. * * @param array $screens Screens by page slug. */ $screens = (array) apply_filters( 'forensix_admin_screens', $screens ); return self::settings_first( $screens ); } /** * Keeps Settings & Licence directly under Overview, also after apps added their pages. * Overview has to stay first: WordPress links the top-level "Forensix" item to the first entry. * * @param array $screens Screens by page slug. * @return array */ public static function settings_first( array $screens ): array { if ( ! isset( $screens[ self::SETTINGS_SLUG ] ) ) { return $screens; } $settings = array( self::SETTINGS_SLUG => $screens[ self::SETTINGS_SLUG ] ); unset( $screens[ self::SETTINGS_SLUG ] ); if ( ! isset( $screens[ self::TOP_SLUG ] ) ) { return $settings + $screens; } $top = array( self::TOP_SLUG => $screens[ self::TOP_SLUG ] ); unset( $screens[ self::TOP_SLUG ] ); return $top + $settings + $screens; } /** * Hooks the menu. Nothing else is added outside wp-admin. */ public function register_hooks(): void { add_action( 'admin_menu', array( $this, 'add_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'highlight_settings' ) ); } /** * Shows the Settings & Licence menu entry in bold on every admin page, so it stands out * in the Forensix menu. Pure styling: the link text, its translation and what screen * readers announce stay the same. */ public function highlight_settings(): void { $selector = '#adminmenu .wp-submenu a[href="admin.php?page=' . self::SETTINGS_SLUG . '"]'; wp_add_inline_style( 'admin-menu', $selector . '{font-weight:600;}' ); } /** * Registers the pages. */ public function add_menu(): void { foreach ( self::screens() as $slug => $screen ) { if ( self::TOP_SLUG === $slug ) { add_menu_page( __( 'Forensix Digital', 'forensix-digital' ), __( 'Forensix', 'forensix-digital' ), $screen['capability'], $slug, array( $this, 'render' ), 'dashicons-search', 58 ); } add_submenu_page( self::TOP_SLUG, $screen['title'], $screen['title'], $screen['capability'], $slug, array( $this, 'render' ) ); } } /** * Prints the mount point for the React app. */ public function render(): void { $key = self::current_key(); if ( null === $key ) { return; } echo '
'; if ( ! self::built() ) { printf( '

%s

', esc_html__( 'The Forensix admin screens have not been built yet. Run "npm run build" in the plugin folder.', 'forensix-digital' ) ); } echo '
'; } /** * Loads the app on Forensix pages only. * * @param string $hook_suffix Current admin page. */ public function enqueue( string $hook_suffix ): void { unset( $hook_suffix ); $key = self::current_key(); if ( null === $key || ! self::built() ) { return; } $asset = require FORENSIX_PATH . 'build/index.asset.php'; wp_enqueue_script( 'fxd-admin', FORENSIX_URL . 'build/index.js', $asset['dependencies'], $asset['version'], true ); wp_set_script_translations( 'fxd-admin', 'forensix-digital', FORENSIX_PATH . 'languages' ); if ( file_exists( FORENSIX_PATH . 'build/style-index.css' ) ) { wp_enqueue_style( 'fxd-admin', FORENSIX_URL . 'build/style-index.css', array( 'wp-components' ), $asset['version'] ); } $links = array(); foreach ( self::screens() as $slug => $screen ) { if ( current_user_can( $screen['capability'] ) ) { $links[ $screen['key'] ] = array( 'title' => $screen['title'], 'url' => admin_url( 'admin.php?page=' . $slug ), ); } } $data = array( 'screens' => $links, 'logo' => FORENSIX_URL . 'admin/images/logo.png', 'canUndo' => current_user_can( Capabilities::UNDO_CHANGES ), 'home' => home_url( '/' ), // Plain links to the plans and the customer account; nothing is sent from this site. 'plans' => 'https://forensix.digital/pricing/', 'account' => 'https://forensix.digital/account/', ); wp_add_inline_script( 'fxd-admin', 'window.forensixAdmin = ' . wp_json_encode( $data ) . ';', 'before' ); } /** * The screen key of the current admin page, or null when it is not a Forensix page. */ private static function current_key(): ?string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page routing; the menu capability is enforced by WordPress. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; $screens = self::screens(); return isset( $screens[ $page ] ) ? $screens[ $page ]['key'] : null; } /** * Whether the JavaScript build exists. */ private static function built(): bool { return file_exists( FORENSIX_PATH . 'build/index.asset.php' ) && file_exists( FORENSIX_PATH . 'build/index.js' ); } }