PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-search / src / dashboard / class-dashboard.php

class-dashboard.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-search/src/dashboard/class-dashboard.php

376 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A class that adds a search dashboard to wp-admin.
4 *
5 * @package automattic/jetpack
6 */
7
8 namespace Automattic\Jetpack\Search;
9
10 use Automattic\Jetpack\Admin_UI\Admin_Menu;
11 use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
12 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
13 use Automattic\Jetpack\Status;
14 use Automattic\Jetpack\Tracking;
15 use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
16 use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;
17 /**
18 * Responsible for adding a search dashboard to wp-admin.
19 *
20 * @package Automattic\Jetpack\Search
21 */
22 class Dashboard {
23 /**
24 * Slug emitted by `@wordpress/build` (`wpPlugin.pages[0].id`). Must differ from the
25 * `jetpack-search` menu slug: the generated page.php takes over, and exits, any request
26 * whose `page` matches this id.
27 */
28 const WP_BUILD_PAGE_ID = 'jetpack-search-dashboard';
29
30 /**
31 * Render function generated by `@wordpress/build` into
32 * `build/pages/jetpack-search-dashboard/page-wp-admin.php`. Naming convention:
33 * `{wpPlugin.name}_{page-with-underscores}_wp_admin_render_page`.
34 */
35 const WP_BUILD_RENDER_FN = 'jetpack_search_jetpack_search_dashboard_wp_admin_render_page';
36
37 /**
38 * Classic script handle with no source, registered only so the dashboard has
39 * something to hang {@see Initial_State} and the connection initial state on.
40 */
41 const DATA_SCRIPT_HANDLE = 'jetpack-search-dashboard-data';
42
43 /**
44 * Whether the class has been initialized
45 *
46 * @var boolean
47 */
48 private static $initialized = false;
49
50 /**
51 * The screen ID {@see self::alias_screen_id_for_wp_build()} replaced, until it is restored.
52 *
53 * @var string|null
54 */
55 private static $wp_build_original_screen_id = null;
56
57 /**
58 * Plan instance
59 *
60 * @var \Automattic\Jetpack\Search\Plan
61 */
62 protected $plan;
63
64 /**
65 * Connection manager instance
66 *
67 * @var \Automattic\Jetpack\Connection\Manager
68 */
69 protected $connection_manager;
70
71 /**
72 * Module_Control instance
73 *
74 * @var \Automattic\Jetpack\Search\Module_Control
75 */
76 protected $module_control;
77
78 /**
79 * Priority for the dashboard menu
80 * For Jetpack sites: Akismet uses 4, so we use 1 to ensure both menus are added when only they exist.
81 * For Simple sites: the value is overriden in a child class with value 100000 to wait for all menus to be registered.
82 *
83 * @var int
84 */
85 protected $search_menu_priority = 1;
86
87 /**
88 * Contructor
89 *
90 * @param \Automattic\Jetpack\Search\Plan $plan - Plan instance.
91 * @param \Automattic\Jetpack\Connection\Manager $connection_manager - Connection Manager instance.
92 * @param \Automattic\Jetpack\Search\Module_Control $module_control - Module_Control instance.
93 */
94 public function __construct( $plan = null, $connection_manager = null, $module_control = null ) {
95 $this->plan = $plan ? $plan : new Plan();
96 $this->connection_manager = $connection_manager ? $connection_manager : new Connection_Manager( Package::SLUG );
97 $this->module_control = $module_control ? $module_control : new Module_Control( $this->plan );
98 $this->plan->init_hooks();
99 }
100
101 /**
102 * Initialise hooks.
103 *
104 * We use the `config` package to initialize the search package, which ensures the package is
105 * only initialized once. However earlier versions of Jetpack would still forcely initialize the
106 * dashboard. As a result, there would be two `Search` submenus if we don't ensure the dashboard
107 * is initialized only once. So we use `$initialized` to ensure the class is only initialized once.
108 *
109 * Ref: https://github.com/Automattic/jetpack/pull/21888/files#diff-aae7d66951585fc55053a4d53b68552a41864d2c69aee900574ef4404b7ad5f7L42
110 */
111 public function init_hooks() {
112 if ( ! self::$initialized ) {
113 self::$initialized = true;
114 // Any priority works: render() reads the loaded build after admin_menu, and the
115 // wpcom subclass overrides this to 100000.
116 add_action( 'admin_menu', array( $this, 'maybe_load_wp_build' ), $this->search_menu_priority );
117 add_action( 'admin_menu', array( $this, 'add_wp_admin_submenu' ), $this->search_menu_priority );
118 // Check if the site plan changed and deactivate module accordingly.
119 add_action( 'current_screen', array( $this, 'check_plan_deactivate_search_module' ) );
120 }
121 }
122
123 /**
124 * Load the wp-build dashboard bundle for this request.
125 *
126 * A no-op unless this is the Search page and `build/build.php` exists.
127 */
128 public function maybe_load_wp_build() {
129 if ( ! $this->is_search_admin_request() ) {
130 return;
131 }
132
133 $build_index = $this->wp_build_index();
134 if ( ! file_exists( $build_index ) ) {
135 return;
136 }
137
138 self::require_wp_build_with_screen_alias( $build_index );
139
140 // wp-build hooks module registration to wp_default_scripts, which has already
141 // fired by admin_menu — call it directly or the init module never registers.
142 if ( function_exists( 'jetpack_search_register_script_modules' ) ) {
143 jetpack_search_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- guarded by function_exists(); defined in the generated build/modules.php, which Phan excludes.
144 }
145
146 WP_Build_Polyfills::register(
147 'jetpack-search',
148 array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
149 );
150 }
151
152 /**
153 * Require the generated build file with the screen ID aliased across its enqueue check.
154 *
155 * @see WP_Build_Screen_Id::load_with_alias()
156 * @param string $build_index Path to the generated `build.php`.
157 * @return void
158 */
159 private static function require_wp_build_with_screen_alias( $build_index ) {
160 // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
161 if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
162 WP_Build_Screen_Id::load_with_alias(
163 array( __CLASS__, 'alias_screen_id_for_wp_build' ),
164 array( __CLASS__, 'restore_screen_id_after_wp_build' ),
165 function () use ( $build_index ) {
166 require_once $build_index;
167 }
168 );
169 return;
170 }
171
172 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
173 require_once $build_index;
174 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
175 }
176
177 /**
178 * Path to the generated build entry point. A seam, like the render function below:
179 * tests point it at a stub so this runs without the package being built.
180 *
181 * @return string
182 */
183 protected function wp_build_index() {
184 return dirname( __DIR__, 2 ) . '/build/build.php';
185 }
186
187 /**
188 * Name of the generated render function. A seam: tests override it to render
189 * the dashboard without depending on whether the package happens to be built.
190 *
191 * @return string
192 */
193 protected function wp_build_render_function() {
194 return self::WP_BUILD_RENDER_FN;
195 }
196
197 /**
198 * Whether the current request targets the Search admin page.
199 *
200 * @return bool
201 */
202 protected function is_search_admin_request() {
203 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
204 if ( ! is_admin() || ! isset( $_GET['page'] ) ) {
205 return false;
206 }
207
208 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
209 return 'jetpack-search' === sanitize_text_field( wp_unslash( $_GET['page'] ) );
210 }
211
212 /**
213 * Alias the current screen id to wp-build's expected slug so its
214 * auto-generated enqueue callback fires for our user-facing page.
215 */
216 public static function alias_screen_id_for_wp_build() {
217 $screen = get_current_screen();
218 if ( ! $screen ) {
219 return;
220 }
221
222 self::$wp_build_original_screen_id = $screen->id;
223 $screen->id = self::WP_BUILD_PAGE_ID;
224 }
225
226 /**
227 * Undo alias_screen_id_for_wp_build(), since JITM builds its message path from the screen ID.
228 */
229 public static function restore_screen_id_after_wp_build() {
230 $screen = get_current_screen();
231 if ( ! $screen || null === self::$wp_build_original_screen_id ) {
232 return;
233 }
234
235 $screen->id = self::$wp_build_original_screen_id;
236 self::$wp_build_original_screen_id = null;
237 }
238
239 /**
240 * The page to be added to submenu
241 */
242 public function add_wp_admin_submenu() {
243 // Jetpack of version <= 10.5 would register `jetpack-search` submenu with its built-in search module.
244 $this->remove_search_submenu_if_exists();
245
246 if ( $this->should_add_search_submenu() ) {
247 $page_suffix = Admin_Menu::add_menu(
248 /** "Search" is a product name, do not translate. */
249 'Jetpack Search',
250 'Search',
251 'manage_options',
252 'jetpack-search',
253 array( $this, 'render' ),
254 null,
255 array(
256 'product' => 'search',
257 'key' => 'jetpack-search',
258 )
259 );
260 } else {
261 // always add the page, but hide it from the menu.
262 $page_suffix = add_submenu_page(
263 '',
264 /** "Search" is a product name, do not translate. */
265 'Jetpack Search',
266 'Search',
267 'manage_options',
268 'jetpack-search',
269 array( $this, 'render' )
270 );
271 }
272
273 if ( $page_suffix ) {
274 add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
275 }
276 }
277
278 /**
279 * Render the dashboard page.
280 *
281 * The generated render function is missing where the package was never built.
282 */
283 public function render() {
284 $render_function = $this->wp_build_render_function();
285 if ( function_exists( $render_function ) ) {
286 call_user_func( $render_function );
287 }
288 }
289
290 /**
291 * Test whether we should show Search menu.
292 *
293 * @return boolean Show search sub menu or not.
294 */
295 protected function should_add_search_submenu() {
296 /**
297 * The filter allows to ommit adding a submenu item for Jetpack Search.
298 *
299 * @since 0.11.2
300 *
301 * @param boolean $should_add_search_submenu Default value is true.
302 */
303 return apply_filters( 'jetpack_search_should_add_search_submenu', current_user_can( 'manage_options' ) );
304 }
305
306 /**
307 * Remove `jetpack-search` submenu page
308 */
309 protected function remove_search_submenu_if_exists() {
310 remove_submenu_page( 'jetpack', 'jetpack-search' );
311 }
312
313 /**
314 * Initialize the admin resources.
315 */
316 public function admin_init() {
317 add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
318 }
319
320 /**
321 * Enqueue admin scripts.
322 */
323 public function load_admin_scripts() {
324 if ( $this->should_enqueue_tracking_script() ) {
325 // Required for Analytics.
326 Tracking::register_tracks_functions_scripts( true );
327 }
328
329 // wp-build enqueues the app itself; this empty handle exists only to
330 // print the initial state before boot runs on DOMContentLoaded.
331 wp_register_script( self::DATA_SCRIPT_HANDLE, false, array(), Package::VERSION, true );
332 wp_enqueue_script( self::DATA_SCRIPT_HANDLE );
333
334 // The i18n loader is registered on every admin page but only enqueued
335 // when depended on; the esbuild bundle doesn't pull it in.
336 if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
337 wp_enqueue_script( 'wp-jp-i18n-loader' );
338 }
339
340 // Add objects to be passed to the initial state of the app.
341 // Use wp_add_inline_script instead of wp_localize_script, see https://core.trac.wordpress.org/ticket/25280.
342 wp_add_inline_script(
343 self::DATA_SCRIPT_HANDLE,
344 ( new Initial_State() )->render(),
345 'before'
346 );
347
348 // Connection initial state.
349 Connection_Initial_State::render_script( self::DATA_SCRIPT_HANDLE );
350 }
351
352 /**
353 * Check if we should enqueue the tracking script.
354 */
355 protected function should_enqueue_tracking_script() {
356 return ! ( new Status() )->is_offline_mode() && $this->connection_manager->is_connected();
357 }
358
359 /**
360 * Deactivate search module if plan doesn't support search.
361 *
362 * @param \WP_Screen $current_screen Creent screen object.
363 */
364 public function check_plan_deactivate_search_module( $current_screen ) {
365 // Only run on Jetpack admin pages.
366 // The first two checks for current screen are cheap to run on every page.
367 if (
368 property_exists( $current_screen, 'base' ) &&
369 strpos( $current_screen->base, 'jetpack_page_' ) !== false &&
370 ( ! $this->plan->supports_search() || $this->plan->must_upgrade() )
371 ) {
372 $this->module_control->deactivate();
373 }
374 }
375 }
376