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
← All changes | _inc/lib/admin-pages/class-jetpack-ai-page.php +35 -90 16.3-a.3 → 16.3-beta View file →
@@ -18,9 +18,8 @@
18 18 use Automattic\Jetpack\Status;
19 19 use Automattic\Jetpack\Status\Host;
20 20 use Automattic\Jetpack\Terms_Of_Service;
21 21 use Automattic\Jetpack\Tracking;
22 -use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
23 22
24 23 if ( ! defined( 'ABSPATH' ) ) {
25 24 exit( 0 );
26 25 }
@@ -26,8 +25,9 @@
26 25 }
27 26
28 27 require_once dirname( __DIR__ ) . '/class-jetpack-ai-feature-flags.php';
29 28 require_once dirname( __DIR__ ) . '/class-jetpack-ai-settings.php';
29 +require_once __DIR__ . '/class-jetpack-wp-build-page.php';
30 30
31 31 /**
32 32 * Builds the Jetpack AI admin page and its sidebar menu entry.
33 33 */
@@ -33,26 +33,15 @@
33 33 */
34 34 class Jetpack_AI_Page {
35 35
36 36 /**
37 - * The wp-build route's page id.
37 + * The wp-build route's page id, which must not be the `jetpack-ai` menu slug.
38 38 *
39 - * Deliberately not the `jetpack-ai` menu slug: the generated standalone page.php
40 - * intercepts `admin_init` for its own id and exits, which would bypass wp-admin's
41 - * chrome entirely. The generated wp-admin enqueue matches the screen ID against this.
42 - *
43 39 * @var string
44 40 */
45 41 const WP_BUILD_PAGE_ID = 'jetpack-ai-hub';
46 42
47 43 /**
48 - * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
49 - *
50 - * @var string|null
51 - */
52 - private static $wp_build_original_screen_id = null;
53 -
54 - /**
55 44 * Whether this request renders through wp-build.
56 45 *
57 46 * Checks the render function too: if the build output is missing, the request would
58 47 * otherwise get no bundle at all now that the legacy entry is gone.
@@ -83,82 +72,19 @@
83 72
84 73 /**
85 74 * Load wp-build for the AI Hub page.
86 75 *
87 - * Scoped to this page so WP_Build_Polyfills does not replace core scripts everywhere else.
88 - *
89 76 * @since 16.3
90 77 *
91 78 * @return void
92 79 */
93 80 public static function maybe_load_wp_build() {
94 - if ( ! self::is_ai_admin_request() ) {
95 - return;
81 + if ( self::is_ai_admin_request() ) {
82 + Jetpack_WP_Build_Page::load( self::WP_BUILD_PAGE_ID );
96 83 }
97 -
98 - $build_index = JETPACK__PLUGIN_DIR . 'build/build.php';
99 - if ( ! file_exists( $build_index ) ) {
100 - return;
101 - }
102 -
103 - // Hooked on either side of the require, so the alias holds only for the generated
104 - // enqueue check it registers at the same priority.
105 - add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
106 - require_once $build_index;
107 - add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
108 -
109 - // wp-build hooks module registration to wp_default_scripts, which has already fired by
110 - // admin_menu — call it directly or the init module never reaches the import map.
111 - if ( function_exists( 'jetpack_plugin_register_script_modules' ) ) {
112 - jetpack_plugin_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes.
113 - }
114 -
115 - if ( class_exists( WP_Build_Polyfills::class ) ) {
116 - // wp-rich-text is needed because the Scheduled tasks tab reaches @wordpress/dataviews
117 - // via @wordpress/ui, whose dataform controls unlock rich-text's privateApis at module
118 - // scope. WP 6.9 exports none, so without it the bundle throws and the page is blank.
119 - WP_Build_Polyfills::register(
120 - 'jetpack',
121 - array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
122 - );
123 - }
124 84 }
125 85
126 86 /**
127 - * Point the screen ID at the wp-build page while its generated enqueue check runs.
128 - *
129 - * @since 16.3
130 - *
131 - * @return void
132 - */
133 - public static function alias_screen_id_for_wp_build() {
134 - $screen = get_current_screen();
135 - if ( ! $screen ) {
136 - return;
137 - }
138 -
139 - self::$wp_build_original_screen_id = $screen->id;
140 - $screen->id = self::WP_BUILD_PAGE_ID;
141 - }
142 -
143 - /**
144 - * Undo alias_screen_id_for_wp_build(), since JITM builds its message path from the screen ID.
145 - *
146 - * @since 16.3
147 - *
148 - * @return void
149 - */
150 - public static function restore_screen_id_after_wp_build() {
151 - $screen = get_current_screen();
152 - if ( ! $screen || null === self::$wp_build_original_screen_id ) {
153 - return;
154 - }
155 -
156 - $screen->id = self::$wp_build_original_screen_id;
157 - self::$wp_build_original_screen_id = null;
158 - }
159 -
160 - /**
161 87 * Register the page and its page-specific hooks.
162 88 *
163 89 * The AI Hub owns its full React layout, so it does not need the legacy
164 90 * Jetpack_Admin_Page lifecycle. Keeping this controller independent also
@@ -205,12 +131,10 @@
205 131 'manage_options',
206 132 'jetpack-ai',
207 133 array( $this, 'render' ),
208 134 null,
209 - array(
210 - 'product' => 'jetpack-ai',
211 - 'key' => 'jetpack-ai',
212 - )
135 + // No product gate: the Hub also holds the MCP and Connectors tab, which works with AI off.
136 + array( 'key' => 'jetpack-ai' )
213 137 );
214 138 }
215 139
216 140 /**
@@ -361,9 +285,9 @@
361 285 * alone returns true on all of wpcom-platform even while the flag is off —
362 286 * it answers only the cohort half, and page registration requires both
363 287 * (see packages/seo Initializer::init()).
364 288 */
365 - $seo_settings_url = admin_url( 'admin.php?page=jetpack#/traffic' );
289 + $seo_settings_url = admin_url( 'admin.php?page=jetpack-settings#/traffic' );
366 290 $is_internal_test = jetpack_is_internal_testing_environment();
367 291 $show_scheduled_tasks_view = self::is_scheduled_tasks_enabled();
368 292 if (
369 293 // The exact-symbol guard matters: the autoloader can select an older
@@ -380,14 +304,8 @@
380 304 // inline settings below, which the app reads from `window.jetpackAiSettings`.
381 305 wp_register_script( 'jetpack-ai-admin', false, array(), $script_version, true );
382 306 wp_enqueue_script( 'jetpack-ai-admin' );
383 307
384 - // Registered on every admin page but only enqueued when depended on, and the esbuild
385 - // bundles don't pull it in — without it every string renders untranslated.
386 - if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
387 - wp_enqueue_script( 'wp-jp-i18n-loader' );
388 - }
389 -
390 308 // The Tracks sender (w.js); without it, queued events never leave the
391 309 // browser. Consent-gated like the other surfaces that load it.
392 310 $can_send_tracks = ( new Tracking( 'jetpack', new Connection_Manager() ) )->should_enable_tracking( new Terms_Of_Service(), $status );
393 311 if ( $can_send_tracks ) {
@@ -425,14 +343,15 @@
425 343 'masterEnabled' => Jetpack_AI_Settings::is_master_enabled(),
426 344 // The route, not a flag: each one documents a different hook.
427 345 'masterForcedOff' => Jetpack_AI_Settings::get_master_forced_off_route(),
428 346 'isOfflineMode' => $is_offline_mode,
347 + 'canConnectSite' => current_user_can( 'jetpack_connect' ),
429 348 // These three answer one question; a filter changing one alone leaves
430 349 // a label pointing at a page that is not there.
431 350 'hasMyJetpack' => $has_my_jetpack,
432 351 'userConnectionUrl' => $has_my_jetpack
433 352 ? 'admin.php?page=my-jetpack#/connection'
434 - : 'admin.php?page=jetpack#/connect-user',
353 + : 'admin.php?page=jetpack-settings#/connect-user',
435 354 'manageUrl' => $has_my_jetpack
436 355 ? 'admin.php?page=my-jetpack#/products'
437 356 : 'admin.php?page=jetpack_modules',
438 357 'mcpSettingsApi' => array(
@@ -449,8 +368,9 @@
449 368 $settings = array(
450 369 'blogId' => $blog_id ? (int) $blog_id : 0,
451 370 'activityLogUrl' => $activity_log_url,
452 371 'seoSettingsUrl' => $seo_settings_url,
372 + 'searchSettingsUrl' => self::get_search_settings_url(),
453 373 'siteAdminUrl' => admin_url(),
454 374 'userConnectionUrl' => esc_url_raw( $config['userConnectionUrl'] ?? '' ),
455 375 'manageUrl' => esc_url_raw( $config['manageUrl'] ?? '' ),
456 376 'hasMyJetpack' => ! empty( $config['hasMyJetpack'] ),
@@ -466,8 +386,9 @@
466 386 ),
467 387 true
468 388 ) ? $config['masterForcedOff'] : '',
469 389 'isOfflineMode' => ! empty( $config['isOfflineMode'] ),
390 + 'canConnectSite' => ! empty( $config['canConnectSite'] ),
470 391 'apiRoot' => esc_url_raw( rest_url() ),
471 392 'apiNonce' => wp_create_nonce( 'wp_rest' ),
472 393 'pluginUrl' => plugins_url( '', JETPACK__PLUGIN_FILE ),
473 394 // Images ship from the plugin directory, so the plugin version is what busts their cache.
@@ -568,8 +489,32 @@
568 489 ? strtolower( (string) $user_data['email'] )
569 490 : '';
570 491
571 492 return '' !== $email && '@automattic.com' === substr( $email, -15 );
493 + }
494 +
495 + /**
496 + * The Search dashboard page the AI Answers row links to, or '' once a host
497 + * has removed it. Removal is remove_submenu_page() on `admin_menu`, which only
498 + * unsets the registry entry — menu_page_url() still answers for the page and
499 + * wp-admin then denies it — so the registry is read after that hook has run.
500 + *
501 + * @return string
502 + */
503 + private static function get_search_settings_url() {
504 + global $submenu;
505 +
506 + // Search registers under the Jetpack menu, or menu-less (parent '') when
507 + // `jetpack_search_should_add_search_submenu` says no; both are reachable.
508 + foreach ( array( 'jetpack', '' ) as $parent ) {
509 + foreach ( (array) ( $submenu[ $parent ] ?? array() ) as $item ) {
510 + if ( isset( $item[2] ) && 'jetpack-search' === $item[2] ) {
511 + return admin_url( 'admin.php?page=jetpack-search#/ai-answers' );
512 + }
513 + }
514 + }
515 +
516 + return '';
572 517 }
573 518
574 519 /**
575 520 * Whether My Jetpack is loaded on this host.