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

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

- Page: https://pluginprobe.com/plugins/xspeed/1.0.2/code/includes/class-admin.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.2/raw/includes/class-admin.php
- Modified: 2026-06-01T17:33:22+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.2/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' ) );
	}

	/**
	 * 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
		);

		// Rename the auto-generated first submenu (which inherits the
		// toplevel title "xSpeed") to "Dashboard" — keeps the same slug so
		// it stays the default landing page.
		add_submenu_page(
			self::PAGE_SLUG,
			/* translators: %s = brand name (xSpeed by default; agencies may white-label). */
			sprintf( __( '%s Dashboard', 'xspeed' ), $brand['name'] ),
			__( 'Dashboard', 'xspeed' ),
			'manage_options',
			self::PAGE_SLUG,
			array( $this, 'render' )
		);

		// 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. Keep the list
	 * short — one entry per top-level concern, not per module. Anything
	 * over ~5 entries clutters the WP admin menu rail.
	 *
	 * @return array<string,string> map of hash → label.
	 */
	private static function deep_link_items() {
		return array(
			'cache'    => __( 'Cache', 'xspeed' ),
			'health'   => __( 'Health', 'xspeed' ),
			'minify'   => __( 'Performance', 'xspeed' ),
			'database' => __( 'Tools', '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
			);
		}

		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,
			),
			// 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;
	}
}

```
