The diagnostic screens. */ private static $checks = array(); public static function boot() { add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue' ), 101 ); // Before any notice callback runs, so the panels this screen owns can be // captured and re-emitted inside the section they belong to. add_action( 'admin_notices', array( __CLASS__, 'capture_section_notices' ), 0 ); } private static function page() { return isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- admin routing only. } /** @return bool Whether this screen shares the Settings shell. */ public static function is_settings_area() { return in_array( self::page(), array( 'ai-o-optimiser-settings', 'ai-o-optimiser-notifications', 'ai-o-optimiser-logs', 'ai-o-optimiser-guidance', 'ai-o-optimiser-system-check', 'ai-o-optimiser-environment-check', 'ai-o-optimiser-deep-review-prompt' ), true ); } /** @return bool Whether this is the Settings screen itself. */ public static function is_settings_screen() { return 'ai-o-optimiser-settings' === self::page(); } /** * The section tabs, in navigation order. * * @return array */ public static function sections() { return array( array( 'key' => 'setup', 'label' => __( 'Site Setup', 'ai-o-optimiser' ), 'icon' => 'dashicons-admin-site-alt3' ), array( 'key' => 'tracking', 'label' => __( 'Tracking', 'ai-o-optimiser' ), 'icon' => 'dashicons-chart-line' ), array( 'key' => 'privacy', 'label' => __( 'Privacy & Data', 'ai-o-optimiser' ), 'icon' => 'dashicons-shield' ), array( 'key' => 'integrations', 'label' => __( 'Integrations', 'ai-o-optimiser' ), 'icon' => 'dashicons-admin-links' ), array( 'key' => 'health', 'label' => __( 'Health', 'ai-o-optimiser' ), 'icon' => 'dashicons-heart' ), array( 'key' => 'edition', 'label' => ( class_exists( 'AI_O_Optimiser_Extensions' ) ? AI_O_Optimiser_Extensions::label( 'entitlement', __( 'Edition', 'ai-o-optimiser' ) ) : __( 'Edition', 'ai-o-optimiser' ) ), 'icon' => 'dashicons-id-alt' ), ); } /** * The key of the last section tab, whose panel each edition names for * itself. * * Derived rather than written twice, so the build's one rename inside * sections() carries through to the panel that tab opens. * * @return string */ public static function edition_section_key() { $sections = self::sections(); $last = end( $sections ); return isset( $last['key'] ) ? (string) $last['key'] : 'setup'; } /** @return string[] The sections whose fields live in the Save Settings form. */ public static function form_sections() { return array( 'tracking', 'privacy', 'integrations', 'health' ); } /** * The open section, from the URL. Unknown or absent falls back to the first. * * @return string */ public static function current_section() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen state; nothing is written from it. $requested = isset( $_GET['section'] ) ? sanitize_key( wp_unslash( $_GET['section'] ) ) : ''; foreach ( self::sections() as $section ) { if ( $section['key'] === $requested ) { return $requested; } } return 'setup'; } /** * @param string $key Section key. * @return string The admin URL that opens it. */ public static function section_url( $key ) { return admin_url( 'admin.php?page=ai-o-optimiser-settings§ion=' . rawurlencode( (string) $key ) ); } /** @return bool Whether $key is the open section. */ public static function is_active( $key ) { return self::current_section() === (string) $key; } /** * Open a section panel. Only the current section is visible; the rest carry * the hidden attribute, which every browser honours without a stylesheet. * Their fields are still in the form, so saving from one section never * clears the others. * * @param string $key Section key. */ public static function open_panel( $key ) { printf( '
', esc_attr( (string) $key ), self::is_active( $key ) ? '' : ' hidden' ); } public static function close_panel() { echo '
'; } /** * A section heading, with the same icon its tab carries. * * @param string $key Section key. * @param string $text Heading text, already translated. */ public static function heading( $key, $text ) { $icon = ''; foreach ( self::sections() as $section ) { if ( $section['key'] === (string) $key ) { $icon = $section['icon']; break; } } printf( '

%2$s

', esc_attr( $icon ), esc_html( $text ) ); } /** * Record the diagnostic screens, so the Health section can list them even * when the navigation itself was printed by the shell. * * @param array $checks Diagnostic screens. */ public static function set_checks( array $checks ) { if ( $checks ) { self::$checks = $checks; } } /** @return array */ public static function checks() { return self::$checks; } /** * Print the Settings navigation: one row, never two. * * On the Settings screen that row is the section tabs. On one of the four * diagnostic screens it is those four, plus a link back to Settings, with * the open one marked. They used to share a row, which meant opening a * check moved the highlight onto a Settings section the reader had not * chosen. The checks are reached from the Health section instead. * * Printed once per request: normally by the primary navigation into the * shared secondary slot, before the page body renders. The call each screen * makes afterwards is then a no-op. * * @param string $active_slug 'settings', or a diagnostic screen slug. * @param array $diagnostics Diagnostic screens: slug => array{label, url}. */ public static function render( $active_slug, array $diagnostics ) { self::set_checks( $diagnostics ); if ( self::$rendered || ! current_user_can( AI_O_Optimiser_SENSITIVE_CAPABILITY ) ) { return; } self::$rendered = true; if ( 'settings' === (string) $active_slug ) { $current = self::current_section(); echo ''; return; } /* * No back-link as of 1.37.1. This row used to open with "< Settings", * because before the rail a diagnostic screen had no other way back. The * rail carries Settings under Configure on the same screen now, so the * link was a second copy of a rail item sitting in a row of three -- and * it made Settings read as a peer of the checks, which is what taking it * out of $tabs in 1.27.1 was meant to stop. */ echo ''; } /** Render the diagnostic screens as cards, inside the Health section. */ public static function render_checks_panel() { $checks = self::checks(); if ( ! $checks ) { return; } echo '
'; echo '

' . esc_html__( 'Checks & diagnostics', 'ai-o-optimiser' ) . '

'; echo '

' . esc_html__( 'Detailed reports, each on its own screen. Nothing here changes a setting.', 'ai-o-optimiser' ) . '

'; echo '
'; foreach ( $checks as $tab ) { printf( '%2$s', esc_url( $tab['url'] ), esc_html( $tab['label'] ) ); } echo '
'; } /** * AI-O panels that render through admin_notices, and the section each one * belongs to. * * They print before the page body, so on the Settings screen they used to * land above every section and show on all of them. The shared relocator * carries them into the section that owns them; Schema Dash uses it too. * * @return array> */ private static function notice_sections() { return array_merge( AI_O_Optimiser_Notice_Sections::site_setup_panels( 'setup' ), array( 'AI_O_Optimiser_System_Impact' => array( 'render' => 'health' ), 'AI_O_Optimiser_Schema_Diagnostics' => array( 'render_settings' => 'health' ), 'AI_O_Optimiser_Metadata_Diagnostics' => array( 'render_settings' => 'health' ), ) ); } /** Redirect those callbacks into their section's buffer. */ public static function capture_section_notices() { if ( ! self::is_settings_screen() ) { return; } AI_O_Optimiser_Notice_Sections::capture( self::notice_sections() ); } /** * Emit whatever was captured for a section. * * @param string $key Section key. */ public static function print_stashed( $key ) { AI_O_Optimiser_Notice_Sections::output( $key ); } public static function enqueue() { if ( ! self::is_settings_area() || ! current_user_can( AI_O_Optimiser_SENSITIVE_CAPABILITY ) ) { return; } $css_path = AI_O_Optimiser_PLUGIN_DIR . 'assets/admin-settings-navigation.css'; $css_ver = file_exists( $css_path ) ? AI_O_Optimiser_VERSION . '.' . filemtime( $css_path ) : AI_O_Optimiser_VERSION; wp_enqueue_style( 'ai-o-optimiser-admin-settings-navigation', AI_O_Optimiser_PLUGIN_URL . 'assets/admin-settings-navigation.css', array( 'ai-o-optimiser-admin' ), $css_ver ); } }