# jetpack/16.3/jetpack_vendor/automattic/jetpack-search/src/dashboard/class-dashboard.php

Jetpack – WP Security, Backup, Speed, &amp; Growth, version 16.3. 376 lines.

- Page: https://pluginprobe.com/plugins/jetpack/16.3/code/jetpack_vendor/automattic/jetpack-search/src/dashboard/class-dashboard.php
- Raw: https://pluginprobe.com/plugins/jetpack/16.3/raw/jetpack_vendor/automattic/jetpack-search/src/dashboard/class-dashboard.php
- Modified: 2026-09-29T02:50:08+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/jetpack/16.3/code/jetpack_vendor/automattic/jetpack-search/src/dashboard/class-dashboard.php#L10-L20`.

```php
<?php
/**
 * A class that adds a search dashboard to wp-admin.
 *
 * @package automattic/jetpack
 */

namespace Automattic\Jetpack\Search;

use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Tracking;
use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;
/**
 * Responsible for adding a search dashboard to wp-admin.
 *
 * @package Automattic\Jetpack\Search
 */
class Dashboard {
	/**
	 * Slug emitted by `@wordpress/build` (`wpPlugin.pages[0].id`). Must differ from the
	 * `jetpack-search` menu slug: the generated page.php takes over, and exits, any request
	 * whose `page` matches this id.
	 */
	const WP_BUILD_PAGE_ID = 'jetpack-search-dashboard';

	/**
	 * Render function generated by `@wordpress/build` into
	 * `build/pages/jetpack-search-dashboard/page-wp-admin.php`. Naming convention:
	 * `{wpPlugin.name}_{page-with-underscores}_wp_admin_render_page`.
	 */
	const WP_BUILD_RENDER_FN = 'jetpack_search_jetpack_search_dashboard_wp_admin_render_page';

	/**
	 * Classic script handle with no source, registered only so the dashboard has
	 * something to hang {@see Initial_State} and the connection initial state on.
	 */
	const DATA_SCRIPT_HANDLE = 'jetpack-search-dashboard-data';

	/**
	 * Whether the class has been initialized
	 *
	 * @var boolean
	 */
	private static $initialized = false;

	/**
	 * The screen ID {@see self::alias_screen_id_for_wp_build()} replaced, until it is restored.
	 *
	 * @var string|null
	 */
	private static $wp_build_original_screen_id = null;

	/**
	 * Plan instance
	 *
	 * @var \Automattic\Jetpack\Search\Plan
	 */
	protected $plan;

	/**
	 * Connection manager instance
	 *
	 * @var \Automattic\Jetpack\Connection\Manager
	 */
	protected $connection_manager;

	/**
	 * Module_Control instance
	 *
	 * @var \Automattic\Jetpack\Search\Module_Control
	 */
	protected $module_control;

	/**
	 * Priority for the dashboard menu
	 * For Jetpack sites: Akismet uses 4, so we use 1 to ensure both menus are added when only they exist.
	 * For Simple sites: the value is overriden in a child class with value 100000 to wait for all menus to be registered.
	 *
	 * @var int
	 */
	protected $search_menu_priority = 1;

	/**
	 * Contructor
	 *
	 * @param \Automattic\Jetpack\Search\Plan           $plan - Plan instance.
	 * @param \Automattic\Jetpack\Connection\Manager    $connection_manager - Connection Manager instance.
	 * @param \Automattic\Jetpack\Search\Module_Control $module_control - Module_Control instance.
	 */
	public function __construct( $plan = null, $connection_manager = null, $module_control = null ) {
		$this->plan               = $plan ? $plan : new Plan();
		$this->connection_manager = $connection_manager ? $connection_manager : new Connection_Manager( Package::SLUG );
		$this->module_control     = $module_control ? $module_control : new Module_Control( $this->plan );
		$this->plan->init_hooks();
	}

	/**
	 * Initialise hooks.
	 *
	 * We use the `config` package to initialize the search package, which ensures the package is
	 * only initialized once. However earlier versions of Jetpack would still forcely initialize the
	 * dashboard. As a result, there would be two `Search` submenus if we don't ensure the dashboard
	 * is initialized only once. So we use `$initialized` to ensure the class is only initialized once.
	 *
	 * Ref: https://github.com/Automattic/jetpack/pull/21888/files#diff-aae7d66951585fc55053a4d53b68552a41864d2c69aee900574ef4404b7ad5f7L42
	 */
	public function init_hooks() {
		if ( ! self::$initialized ) {
			self::$initialized = true;
			// Any priority works: render() reads the loaded build after admin_menu, and the
			// wpcom subclass overrides this to 100000.
			add_action( 'admin_menu', array( $this, 'maybe_load_wp_build' ), $this->search_menu_priority );
			add_action( 'admin_menu', array( $this, 'add_wp_admin_submenu' ), $this->search_menu_priority );
			// Check if the site plan changed and deactivate module accordingly.
			add_action( 'current_screen', array( $this, 'check_plan_deactivate_search_module' ) );
		}
	}

	/**
	 * Load the wp-build dashboard bundle for this request.
	 *
	 * A no-op unless this is the Search page and `build/build.php` exists.
	 */
	public function maybe_load_wp_build() {
		if ( ! $this->is_search_admin_request() ) {
			return;
		}

		$build_index = $this->wp_build_index();
		if ( ! file_exists( $build_index ) ) {
			return;
		}

		self::require_wp_build_with_screen_alias( $build_index );

		// wp-build hooks module registration to wp_default_scripts, which has already
		// fired by admin_menu — call it directly or the init module never registers.
		if ( function_exists( 'jetpack_search_register_script_modules' ) ) {
			jetpack_search_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- guarded by function_exists(); defined in the generated build/modules.php, which Phan excludes.
		}

		WP_Build_Polyfills::register(
			'jetpack-search',
			array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
		);
	}

	/**
	 * Require the generated build file with the screen ID aliased across its enqueue check.
	 *
	 * @see WP_Build_Screen_Id::load_with_alias()
	 * @param string $build_index Path to the generated `build.php`.
	 * @return void
	 */
	private static function require_wp_build_with_screen_alias( $build_index ) {
		// Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
		if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
			WP_Build_Screen_Id::load_with_alias(
				array( __CLASS__, 'alias_screen_id_for_wp_build' ),
				array( __CLASS__, 'restore_screen_id_after_wp_build' ),
				function () use ( $build_index ) {
					require_once $build_index;
				}
			);
			return;
		}

		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
		require_once $build_index;
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
	}

	/**
	 * Path to the generated build entry point. A seam, like the render function below:
	 * tests point it at a stub so this runs without the package being built.
	 *
	 * @return string
	 */
	protected function wp_build_index() {
		return dirname( __DIR__, 2 ) . '/build/build.php';
	}

	/**
	 * Name of the generated render function. A seam: tests override it to render
	 * the dashboard without depending on whether the package happens to be built.
	 *
	 * @return string
	 */
	protected function wp_build_render_function() {
		return self::WP_BUILD_RENDER_FN;
	}

	/**
	 * Whether the current request targets the Search admin page.
	 *
	 * @return bool
	 */
	protected function is_search_admin_request() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! is_admin() || ! isset( $_GET['page'] ) ) {
			return false;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return 'jetpack-search' === sanitize_text_field( wp_unslash( $_GET['page'] ) );
	}

	/**
	 * Alias the current screen id to wp-build's expected slug so its
	 * auto-generated enqueue callback fires for our user-facing page.
	 */
	public static function alias_screen_id_for_wp_build() {
		$screen = get_current_screen();
		if ( ! $screen ) {
			return;
		}

		self::$wp_build_original_screen_id = $screen->id;
		$screen->id                        = self::WP_BUILD_PAGE_ID;
	}

	/**
	 * Undo alias_screen_id_for_wp_build(), since JITM builds its message path from the screen ID.
	 */
	public static function restore_screen_id_after_wp_build() {
		$screen = get_current_screen();
		if ( ! $screen || null === self::$wp_build_original_screen_id ) {
			return;
		}

		$screen->id                        = self::$wp_build_original_screen_id;
		self::$wp_build_original_screen_id = null;
	}

	/**
	 * The page to be added to submenu
	 */
	public function add_wp_admin_submenu() {
		// Jetpack of version <= 10.5 would register `jetpack-search` submenu with its built-in search module.
		$this->remove_search_submenu_if_exists();

		if ( $this->should_add_search_submenu() ) {
			$page_suffix = Admin_Menu::add_menu(
				/** "Search" is a product name, do not translate. */
				'Jetpack Search',
				'Search',
				'manage_options',
				'jetpack-search',
				array( $this, 'render' ),
				null,
				array(
					'product' => 'search',
					'key'     => 'jetpack-search',
				)
			);
		} else {
			// always add the page, but hide it from the menu.
			$page_suffix = add_submenu_page(
				'',
				/** "Search" is a product name, do not translate. */
				'Jetpack Search',
				'Search',
				'manage_options',
				'jetpack-search',
				array( $this, 'render' )
			);
		}

		if ( $page_suffix ) {
			add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
		}
	}

	/**
	 * Render the dashboard page.
	 *
	 * The generated render function is missing where the package was never built.
	 */
	public function render() {
		$render_function = $this->wp_build_render_function();
		if ( function_exists( $render_function ) ) {
			call_user_func( $render_function );
		}
	}

	/**
	 * Test whether we should show Search menu.
	 *
	 * @return boolean Show search sub menu or not.
	 */
	protected function should_add_search_submenu() {
		/**
		 * The filter allows to ommit adding a submenu item for Jetpack Search.
		 *
		 * @since 0.11.2
		 *
		 * @param boolean $should_add_search_submenu Default value is true.
		 */
		return apply_filters( 'jetpack_search_should_add_search_submenu', current_user_can( 'manage_options' ) );
	}

	/**
	 * Remove `jetpack-search` submenu page
	 */
	protected function remove_search_submenu_if_exists() {
		remove_submenu_page( 'jetpack', 'jetpack-search' );
	}

	/**
	 * Initialize the admin resources.
	 */
	public function admin_init() {
		add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
	}

	/**
	 * Enqueue admin scripts.
	 */
	public function load_admin_scripts() {
		if ( $this->should_enqueue_tracking_script() ) {
			// Required for Analytics.
			Tracking::register_tracks_functions_scripts( true );
		}

		// wp-build enqueues the app itself; this empty handle exists only to
		// print the initial state before boot runs on DOMContentLoaded.
		wp_register_script( self::DATA_SCRIPT_HANDLE, false, array(), Package::VERSION, true );
		wp_enqueue_script( self::DATA_SCRIPT_HANDLE );

		// The i18n loader is registered on every admin page but only enqueued
		// when depended on; the esbuild bundle doesn't pull it in.
		if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
			wp_enqueue_script( 'wp-jp-i18n-loader' );
		}

		// Add objects to be passed to the initial state of the app.
		// Use wp_add_inline_script instead of wp_localize_script, see https://core.trac.wordpress.org/ticket/25280.
		wp_add_inline_script(
			self::DATA_SCRIPT_HANDLE,
			( new Initial_State() )->render(),
			'before'
		);

		// Connection initial state.
		Connection_Initial_State::render_script( self::DATA_SCRIPT_HANDLE );
	}

	/**
	 * Check if we should enqueue the tracking script.
	 */
	protected function should_enqueue_tracking_script() {
		return ! ( new Status() )->is_offline_mode() && $this->connection_manager->is_connected();
	}

	/**
	 * Deactivate search module if plan doesn't support search.
	 *
	 * @param \WP_Screen $current_screen Creent screen object.
	 */
	public function check_plan_deactivate_search_module( $current_screen ) {
		// Only run on Jetpack admin pages.
		// The first two checks for current screen are cheap to run on every page.
		if (
			property_exists( $current_screen, 'base' ) &&
			strpos( $current_screen->base, 'jetpack_page_' ) !== false &&
			( ! $this->plan->supports_search() || $this->plan->must_upgrade() )
		) {
			$this->module_control->deactivate();
		}
	}
}

```
