# xspeed/1.0.3/includes/class-admin.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.0.3. 402 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.0.3/code/includes/class-admin.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.3/raw/includes/class-admin.php
- Modified: 2026-06-09T06:20:14+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.0.3/code/includes/class-admin.php#L10-L20`.

```php
<?php
/**
 * Admin menu + asset enqueue.
 *
 * @package XSpeed
 */

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

class Admin {

	const PAGE_SLUG = 'xspeed';

	const THEME_COOKIE = 'xspeed_theme';

	public function __construct() {
		add_action( 'admin_menu', array( $this, 'register_menu' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_menu_styles' ) );
		add_filter( 'admin_body_class', array( __CLASS__, 'admin_body_class' ) );
		// Strip third-party admin notices on our screens only. Fires in
		// `in_admin_header` (after `get_current_screen()` is populated
		// but before notices render) so the screen check is reliable.
		add_action( 'in_admin_header', array( __CLASS__, 'suppress_foreign_notices' ), 0 );
	}

	/**
	 * Remove every third-party admin notice on xSpeed admin screens so the
	 * dashboard stays visually clean. Scoped via `is_plugin_page()` — runs
	 * nowhere else. Our own notices stay rendered: register them on the
	 * dedicated `xspeed_admin_notices` action below, which fires after
	 * this suppression and is wired to all three core notice hooks.
	 *
	 * Hooks cleared: `admin_notices`, `all_admin_notices`,
	 * `user_admin_notices`, `network_admin_notices`. WordPress's own
	 * settings-saved / updated messages are emitted via `settings_errors()`
	 * and printed inline by `options.php` — they are NOT on these hooks
	 * and are unaffected.
	 *
	 * @return void
	 */
	public static function suppress_foreign_notices() {
		if ( ! self::is_plugin_page() ) {
			return;
		}
		remove_all_actions( 'admin_notices' );
		remove_all_actions( 'all_admin_notices' );
		remove_all_actions( 'user_admin_notices' );
		remove_all_actions( 'network_admin_notices' );

		// Re-route the four standard notice hooks to a single namespaced
		// action so xSpeed (and any deliberate extender that opts in)
		// keeps a place to emit notices after the strip.
		$relay = static function () {
			/**
			 * Fires in place of WP's `admin_notices` family on xSpeed
			 * admin screens. Use this instead of `admin_notices` when
			 * you want a notice to survive xSpeed's third-party
			 * suppression.
			 *
			 * @since 1.0.3
			 */
			do_action( 'xspeed_admin_notices' );
		};
		add_action( 'admin_notices',         $relay );
		add_action( 'all_admin_notices',     $relay );
		add_action( 'user_admin_notices',    $relay );
		add_action( 'network_admin_notices', $relay );
	}

	/**
	 * Server-side theme detection from the cookie written by useTheme. Used
	 * to emit the `.dark` class on the React mount node and the
	 * `xspeed-dark` class on `<body>` during the initial render — kills the
	 * light→dark flash that happens when JS adds those classes after the
	 * page has already painted.
	 *
	 * @return string 'dark' | 'light'
	 */
	public static function user_theme() {
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- read-only string compare; value is sanitized via sanitize_key() on the next line before any use.
		$raw = isset( $_COOKIE[ self::THEME_COOKIE ] ) ? wp_unslash( $_COOKIE[ self::THEME_COOKIE ] ) : '';
		return 'dark' === sanitize_key( $raw ) ? 'dark' : 'light';
	}

	public static function is_plugin_page() {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( ! $screen ) {
			return false;
		}
		// Toplevel page and any submenu (e.g., the onboarding wizard) share
		// the `xspeed` prefix in their screen base.
		return 0 === strpos( (string) $screen->base, 'toplevel_page_' . self::PAGE_SLUG )
			|| 0 === strpos( (string) $screen->base, self::PAGE_SLUG . '_page_' )
			|| 0 === strpos( (string) $screen->base, 'admin_page_' . Onboarding::PAGE_SLUG );
	}

	public static function admin_body_class( $classes ) {
		if ( ! self::is_plugin_page() ) {
			return $classes;
		}
		$classes .= ' xspeed-page';
		if ( 'dark' === self::user_theme() ) {
			$classes .= ' xspeed-dark';
		}
		return $classes;
	}

	public function register_menu() {
		$brand = self::branding();
		add_menu_page(
			$brand['name'],
			$brand['name'],
			'manage_options',
			self::PAGE_SLUG,
			array( $this, 'render' ),
			self::menu_icon(),
			80
		);

		// Drop WP's auto-generated duplicate first submenu (which
		// inherits the toplevel "xSpeed" title). The group deep-links
		// below replace it — keeping it would render a redundant
		// "xSpeed" / "Dashboard" row that just re-links to the same
		// page as the toplevel entry.
		global $submenu;
		// $submenu may not yet be populated for this slug; the
		// `remove_submenu_page` call covers either case.
		remove_submenu_page( self::PAGE_SLUG, self::PAGE_SLUG );

		// Deep-link submenus — each points to the same dashboard page
		// with a section hash so React's App.tsx routing lands the
		// user inside the right module. WordPress's add_submenu_page
		// strips the hash, so we inject directly into $submenu where
		// it survives intact (the same trick Yoast / WooCommerce use
		// for their per-area shortcuts).
		global $submenu;
		$deep_links = self::deep_link_items();
		foreach ( $deep_links as $hash => $label ) {
			$submenu[ self::PAGE_SLUG ][] = array(
				$label,
				'manage_options',
				'admin.php?page=' . self::PAGE_SLUG . '#' . $hash,
			);
		}
	}

	/**
	 * Section deep-links rendered under the xSpeed menu. Mirrors the
	 * React sidebar's group manifest (see src/components/sidebarGroups.ts)
	 * so the WP admin rail reads as the same map as the in-app sidebar.
	 * Each entry points at the first module slug in that group; the
	 * React app's hash router lands the user on that module, which is
	 * the first row of that group's sidebar section — no manual scrolling.
	 *
	 * If you add a group to React's SIDEBAR_GROUPS, add it here too. The
	 * two arrays are intentionally co-located in PR review (same change
	 * touches both) rather than DRY'd through a generated config file —
	 * this is the only PHP↔TS coupling and it's tiny.
	 *
	 * @return array<string,string> map of first-module-hash → group label.
	 */
	private static function deep_link_items() {
		return array(
			'cache'      => __( 'Cache', 'xspeed' ),
			'minify'     => __( 'Performance', 'xspeed' ),
			'cdn'        => __( 'Network', 'xspeed' ),
			'health'     => __( 'Insights', 'xspeed' ),
			'database'   => __( 'Tools', 'xspeed' ),
			'multisite'  => __( 'Pro Add-ons', 'xspeed' ),
		);
	}

	/**
	 * Pluggable branding for the dashboard chrome. xspeed-pro's
	 * White-Label module hooks `xspeed_branding` to override these
	 * values from saved settings.
	 *
	 * @return array{name:string,footer_credit:?string,hide_help_links:bool,logo_svg:?string}
	 * @since 1.5.0
	 */
	public static function branding() {
		$defaults = array(
			'name'            => 'xSpeed',
			'footer_credit'   => null, // null = show the default WPDeveloper credit.
			'hide_help_links' => false,
			'logo_svg'        => null, // null = use the built-in brand mark.
		);
		$out = apply_filters( 'xspeed_branding', $defaults );
		if ( ! is_array( $out ) ) {
			return $defaults;
		}
		return array_merge( $defaults, $out );
	}

	/**
	 * URL of the SVG menu icon — the official xSpeed brand mark. Uses
	 * fill="currentColor" which renders black in <img> context; the inline
	 * style below recolors it via CSS filter for the WP admin menu states.
	 */
	private static function menu_icon() {
		return XSPEED_URL . 'assets/icon.svg';
	}

	public function render() {
		$dark = 'dark' === self::user_theme() ? ' dark' : '';
		printf( '<div id="xspeed-app" class="xspeed-root%s"></div>', esc_attr( $dark ) );
	}

	/**
	 * Enqueue the stylesheet that recolors the menu icon to match the WP
	 * admin color scheme. Loads on every admin page (not just the plugin's
	 * page) because the menu icon is visible site-wide.
	 */
	public function enqueue_menu_styles() {
		wp_enqueue_style(
			'xspeed-menu-icon',
			XSPEED_URL . 'assets/menu-icon.css',
			array(),
			XSPEED_VERSION
		);
	}

	public function enqueue( $hook ) {
		if ( 'toplevel_page_' . self::PAGE_SLUG !== $hook ) {
			return;
		}

		$asset_js  = XSPEED_DIR . 'assets/admin.js';
		$asset_css = XSPEED_DIR . 'assets/admin.css';

		if ( file_exists( $asset_js ) ) {
			// filemtime() cache-busts on every rebuild so a stable
			// VERSION constant never serves stale JS through browser
			// caches.
			wp_enqueue_script(
				'xspeed-admin',
				XSPEED_URL . 'assets/admin.js',
				array( 'wp-api-fetch', 'wp-i18n' ),
				XSPEED_VERSION . '.' . filemtime( $asset_js ),
				true
			);
			// Loads .mo files for the 'xspeed' text-domain into
			// window.wp.i18n so the React `__()` helper resolves.
			if ( function_exists( 'wp_set_script_translations' ) ) {
				wp_set_script_translations(
					'xspeed-admin',
					'xspeed',
					XSPEED_DIR . 'languages'
				);
			}
		}

		if ( file_exists( $asset_css ) ) {
			wp_enqueue_style(
				'xspeed-admin',
				XSPEED_URL . 'assets/admin.css',
				array(),
				XSPEED_VERSION . '.' . filemtime( $asset_css )
			);
		}

		/**
		 * Fires after the Free dashboard bundle is enqueued, before its
		 * config is localized. Pro hooks this to enqueue its own bundle
		 * with `xspeed-admin` as a dependency, so its panel
		 * registrations run after `window.XSpeedPro` is installed by
		 * Free's main.tsx.
		 *
		 * @since 1.5.0
		 */
		do_action( 'xspeed_admin_enqueue', $hook );

		wp_localize_script(
			'xspeed-admin',
			'XSpeedConfig',
			array(
				'restUrl'   => esc_url_raw( rest_url( Rest_Api::NAMESPACE_V1 ) ),
				'nonce'     => wp_create_nonce( 'wp_rest' ),
				'version'   => XSPEED_VERSION,
				'branding'  => self::branding(),
				// 'pro' when xspeed-pro is active + speaks our API
				// version (see Tier_Registry); 'free' otherwise.
				// 'trial' reserved for future license-server work.
				'tier'      => class_exists( '\\XSpeed\\Tier_Registry' ) && Tier_Registry::pro_active() ? 'pro' : 'free',
				'bootstrap' => self::bootstrap_payload(),
			)
		);
	}

	/**
	 * Pre-rendered settings + status payload, baked into the page so the
	 * React app can mount with real values instead of showing a loading state
	 * while it waits for /settings and /status REST calls.
	 */
	private static function bootstrap_payload() {
		$opts  = Settings::get();
		$stats = Cache::get_stats();
		// Static-rewrite probe state shipped to the React side so the
		// dashboard can show a persistent banner when nginx/Apache
		// hasn't been wired to bypass PHP yet. Cheap — Cache::probe…
		// is transient-throttled to one HTTP round-trip per 5 min.
		$server_type     = Server::type();
		$rewrite_capable = ( $server_type === Server::NGINX || $server_type === Server::APACHE || $server_type === Server::LITESPEED );
		$rewrite_probe   = null;
		if ( $opts['cache_enabled'] && $rewrite_capable ) {
			$probe         = Cache::probe_static_rewrite();
			$rewrite_probe = array(
				'active'       => (bool) ( $probe['active'] ?? false ),
				'server_type'  => $server_type,
				'snippet'      => Cache::nginx_snippet(), // null on non-nginx hosts
				'topology'     => Server::rewrite_topology(),
				// A reverse proxy / CDN in front (X-Forwarded-* present) usually
				// means the request-terminating nginx isn't user-editable on this
				// host — the banner uses this to switch to honest messaging
				// instead of dangling a snippet the user can't apply.
				'behind_proxy' => Server::is_behind_proxy(),
			);
		}

		return array(
			'settings' => $opts,
			'status'   => array(
				'enabled' => (bool) $opts['cache_enabled'],
				'stats'   => $stats,
				'server'  => array(
					'type'          => $server_type,
					'gzip_mode'     => Server::gzip_mode(),
					'gzip_active'   => Gzip::probe_active(),
					'nginx_snippet' => Gzip::nginx_snippet(),
				),
				'rewrite_probe' => $rewrite_probe,
				// One consolidated nginx server-block snippet aggregating
				// every enabled module's directives (Cache static-rewrite,
				// BrowserCache headers, GZIP, …). Null on non-nginx hosts
				// or when no module contributes directives. Replaces the
				// per-module "paste this snippet" notices.
				'nginx_server_block' => Cache::full_nginx_server_block(),
			),
			// Registered Modules (Free + Pro). The React app discovers them
			// here and renders one sidebar item + one panel per module that
			// declares a settings schema. Hidden modules are filtered.
			'modules'  => self::modules_payload(),
		);
	}

	/**
	 * Serialize every available Module for the React dashboard. Each entry
	 * carries enough to render: identity (slug + tier), UI metadata (label,
	 * icon, optional description), current settings, and the typed schema
	 * the panel uses to render controls.
	 *
	 * Modules that declare `hidden => true` in ui_metadata (e.g., engine
	 * modules with no user-facing settings) are skipped.
	 */
	private static function modules_payload() {
		if ( ! class_exists( 'XSpeed\\Module_Registry' ) ) {
			return array();
		}
		$out = array();
		foreach ( Module_Registry::available() as $slug => $module ) {
			$meta = $module->ui_metadata();
			if ( ! empty( $meta['hidden'] ) ) {
				continue;
			}
			$schema       = $module->settings_schema();
			$custom_panel = $meta['custom_panel'] ?? null;
			// Skip only when the module has neither a schema nor a custom
			// panel — i.e., truly nothing to render in the dashboard.
			if ( empty( $schema ) && empty( $custom_panel ) ) {
				continue;
			}
			$entry = array(
				'slug'         => $slug,
				'tier'         => $module->tier(),
				'version'      => $module->version(),
				'label'        => $meta['label'] ?? ucfirst( $slug ),
				'icon'         => $meta['icon'] ?? 'Square',
				'description'  => $meta['description'] ?? '',
				'settings'     => Settings_Manager::get( $slug ),
				'schema'       => $schema,
				'notices'      => $module->ui_notices(),
				'custom_panel' => $meta['custom_panel'] ?? null,
			);

			/**
			 * Last-mile descriptor filter. Lets Pro (or third-party
			 * extensions) override any field before the module is
			 * shipped to React. Primary use: xspeed-pro hooks this to
			 * swap `custom_panel` to LicenseLockedPanel for Pro modules
			 * when the license is invalid, so unlocked modules stay
			 * visible in the sidebar (good upsell UX) but the panel
			 * shows an activation prompt instead of the real surface.
			 */
			$out[] = apply_filters( 'xspeed_module_descriptor', $entry, $module );
		}
		return $out;
	}
}

```
