PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-my-jetpack / src / class-initializer.php

class-initializer.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-my-jetpack/src/class-initializer.php

946 lines 30.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP Admin page with information and configuration shared among all Jetpack stand-alone plugins
4 *
5 * @package automattic/my-jetpack
6 */
7
8 namespace Automattic\Jetpack\My_Jetpack;
9
10 use Automattic\Jetpack\Admin_UI\Admin_Menu;
11 use Automattic\Jetpack\Assets;
12 use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
13 use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
14 use Automattic\Jetpack\Connection\Client;
15 use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
16 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
17 use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
18 use Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT;
19 use Automattic\Jetpack\Constants as Jetpack_Constants;
20 use Automattic\Jetpack\ExPlat;
21 use Automattic\Jetpack\JITMS\JITM;
22 use Automattic\Jetpack\Licensing;
23 use Automattic\Jetpack\Menu_Badges\Menu_Badges;
24 use Automattic\Jetpack\Menu_Badges\Notification_Counts;
25 use Automattic\Jetpack\Modules;
26 use Automattic\Jetpack\Plugins_Installer;
27 use Automattic\Jetpack\Status;
28 use Automattic\Jetpack\Status\Host as Status_Host;
29 use Automattic\Jetpack\Sync\Functions as Sync_Functions;
30 use Automattic\Jetpack\Terms_Of_Service;
31 use Automattic\Jetpack\Tracking;
32 use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
33 use Jetpack;
34 use WP_Error;
35
36 /**
37 * The main Initializer class that registers the admin menu and eneuque the assets.
38 */
39 class Initializer {
40
41 /**
42 * My Jetpack package version
43 *
44 * @var string
45 */
46 const PACKAGE_VERSION = '6.2.1';
47
48 /**
49 * HTML container ID for the IDC screen on My Jetpack page.
50 */
51 private const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container';
52
53 public const JETPACK_PLUGIN_SLUGS = array(
54 'jetpack-backup',
55 'jetpack-boost',
56 'zerobscrm',
57 'jetpack',
58 'jetpack-protect',
59 'jetpack-social',
60 'jetpack-videopress',
61 'jetpack-search',
62 );
63
64 private const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
65
66 /**
67 * Holds info/data about the site (from the /sites/%d endpoint)
68 *
69 * @var object
70 */
71 public static $site_info;
72
73 /**
74 * Initialize My Jetpack
75 *
76 * @return void
77 */
78 public static function init() {
79 if ( ! self::should_initialize() || did_action( 'my_jetpack_init' ) ) {
80 return;
81 }
82
83 // Extend jetpack plugins action links.
84 Products::extend_plugins_action_links();
85
86 // Set up the REST authentication hooks.
87 Connection_Rest_Authentication::init();
88
89 if ( self::is_licensing_ui_enabled() ) {
90 Licensing::instance()->initialize();
91 }
92
93 // Initialize Boost Speed Score
94 new Speed_Score( array(), 'jetpack-my-jetpack' );
95
96 // Add custom WP REST API endoints.
97 add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
98
99 add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) );
100
101 add_action( 'admin_init', array( __CLASS__, 'setup_historically_active_jetpack_modules_sync' ) );
102 // Registered on admin_menu (not admin_init) and well before priority 100000, so the
103 // counts it registers exist before the menu-badges renderer runs on admin_menu 100000.
104 add_action( 'admin_menu', array( __CLASS__, 'maybe_show_red_bubble' ), 30 );
105
106 // Set up the ExPlat package endpoints
107 ExPlat::init();
108
109 // Sets up JITMS.
110 JITM::configure();
111
112 // Add "Jetpack Manage" menu item.
113 Jetpack_Manage::init();
114
115 /**
116 * Fires after the My Jetpack package is initialized
117 *
118 * @since 0.1.0
119 */
120 do_action( 'my_jetpack_init' );
121 }
122
123 /**
124 * Acts as a feature flag, returning a boolean for whether we should show the licensing UI.
125 *
126 * @since 1.2.0
127 *
128 * @return boolean
129 */
130 public static function is_licensing_ui_enabled() {
131 // Default changed to true in 1.5.0.
132 $is_enabled = true;
133
134 /*
135 * Bail if My Jetpack is not enabled,
136 * and thus the licensing UI shouldn't be enabled either.
137 */
138 if ( ! self::should_initialize() ) {
139 $is_enabled = false;
140 }
141
142 /**
143 * Acts as a feature flag, returning a boolean for whether we should show the licensing UI.
144 *
145 * @param bool $is_enabled Defaults to true.
146 *
147 * @since 1.2.0
148 * @since 1.5.0 Update default value to true.
149 */
150 return apply_filters(
151 'jetpack_my_jetpack_should_enable_add_license_screen',
152 $is_enabled
153 );
154 }
155
156 /**
157 * Add My Jetpack menu item to the admin menu.
158 *
159 * @return void
160 */
161 public static function add_my_jetpack_menu_item() {
162 $page_suffix = Admin_Menu::add_menu(
163 __( 'My Jetpack', 'jetpack-my-jetpack' ),
164 __( 'My Jetpack', 'jetpack-my-jetpack' ),
165 'edit_posts',
166 'my-jetpack',
167 array( __CLASS__, 'admin_page' ),
168 -10
169 );
170 add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
171 }
172
173 /**
174 * Callback for the load my jetpack page hook.
175 *
176 * @return void
177 */
178 public static function admin_init() {
179 $connection = new Connection_Manager();
180
181 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce needed for redirect flow control
182 $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : '';
183
184 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking for partner coupon redemption flow
185 $show_coupon_redemption = isset( $_GET['showCouponRedemption'] );
186
187 // Redirect to Jetpack dashboard for partner coupon redemption
188 if ( $show_coupon_redemption ) {
189 wp_safe_redirect( admin_url( 'admin.php?page=jetpack&showCouponRedemption=1#/dashboard' ) );
190 exit( 0 );
191 }
192
193 // Handle onboarding redirects based on connection status
194 $redirect_args = self::get_onboarding_redirect_args( $step, $connection->is_connected(), self::is_onboarding_available() );
195
196 if ( null !== $redirect_args ) {
197 $admin_page = add_query_arg( $redirect_args, admin_url( 'admin.php' ) );
198 $location = wp_sanitize_redirect( $admin_page );
199
200 // Remove wp_get_referer filter applied in `fix_redirect` method of `Jetpack_Admin` class
201 remove_filter( 'wp_redirect', 'wp_get_referer' );
202 wp_safe_redirect( $location );
203
204 exit( 0 );
205 }
206
207 // If the user reaches the onboarding page, add a class to the body
208 if ( $step === 'onboarding' ) {
209 add_filter( 'admin_body_class', array( __CLASS__, 'add_onboarding_admin_body_class' ) );
210 }
211
212 self::$site_info = self::get_site_info();
213 add_filter( 'identity_crisis_container_id', array( static::class, 'get_idc_container_id' ) );
214 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
215 }
216
217 /**
218 * Whether the My Jetpack onboarding flow is available on this site.
219 *
220 * WordPress.com Simple sites are connected by definition and don't manage their
221 * connection through My Jetpack, so the onboarding flow (which asks the user to
222 * connect) never applies there.
223 *
224 * @internal Not part of the package's public API.
225 *
226 * @return bool
227 */
228 public static function is_onboarding_available() {
229 return ! ( new Status_Host() )->is_wpcom_simple();
230 }
231
232 /**
233 * Decide whether the current My Jetpack request should redirect, and where to.
234 *
235 * @internal Not part of the package's public API.
236 *
237 * @param string $step The current `step` query param.
238 * @param bool $is_connected Whether the site is connected to WordPress.com.
239 * @param bool $onboarding_available Whether the onboarding flow is available on this site.
240 * @return array|null Query args for the redirect, or null to stay on the current page.
241 */
242 public static function get_onboarding_redirect_args( $step, $is_connected, $onboarding_available ) {
243 if ( $onboarding_available && ! $is_connected && $step !== 'onboarding' ) {
244 // Redirect to onboarding if not connected
245 return array(
246 'page' => 'my-jetpack',
247 'step' => 'onboarding',
248 );
249 }
250
251 if ( $step === 'onboarding' && ( ! $onboarding_available || $is_connected ) ) {
252 // Redirect away from onboarding if already connected or onboarding is not available on this site
253 return array( 'page' => 'my-jetpack' );
254 }
255
256 return null;
257 }
258
259 /**
260 * Add a body class to the My Jetpack onboarding page.
261 * This class hides the WP Admin toolbar and the sidebar menu.
262 *
263 * @param string $classes The body classes.
264 * @return string The modified body classes.
265 */
266 public static function add_onboarding_admin_body_class( $classes ) {
267 $classes .= 'jetpack-admin-full-screen';
268 return $classes;
269 }
270
271 /**
272 * Returns whether we are in condition to track to use
273 * Analytics functionality like Tracks, MC, or GA.
274 */
275 public static function can_use_analytics() {
276 $status = new Status();
277 $connection = new Connection_Manager();
278 $tracking = new Tracking( 'jetpack', $connection );
279
280 return $tracking->should_enable_tracking( new Terms_Of_Service(), $status );
281 }
282
283 /**
284 * Register polyfills for the wp-notices / wp-private-apis / wp-rich-text / wp-theme
285 * handles the My Jetpack app bundle depends on but WP < 7.0 does not ship (or ships
286 * with an incomplete allowlist).
287 *
288 * `wp-rich-text` is needed because the bundle reaches `@wordpress/dataviews` (via
289 * `@wordpress/ui`), whose dataform controls unlock rich-text's `privateApis` at module
290 * scope. WP 6.9 exports none, so without the polyfill the bundle throws "Cannot unlock
291 * an undefined object" and the page renders blank.
292 *
293 * @return void
294 */
295 public static function register_wp_build_polyfills() {
296 if ( ! class_exists( WP_Build_Polyfills::class ) ) {
297 return;
298 }
299
300 WP_Build_Polyfills::register(
301 'my-jetpack',
302 array( 'wp-notices', 'wp-private-apis', 'wp-rich-text', 'wp-theme' )
303 );
304 }
305
306 /**
307 * Enqueue admin page assets.
308 *
309 * @return void
310 */
311 public static function enqueue_scripts() {
312 // Register the wp-build-polyfills shim before the extension hook below or
313 // the app script can enqueue against wp-theme / wp-private-apis / wp-notices.
314 // WP_Build_Polyfills registers synchronously on its first caller, so calling
315 // it after a hook consumer would leave our handles recorded but unregistered
316 // for this request.
317 self::register_wp_build_polyfills();
318
319 /**
320 * Fires after the My Jetpack page is initialized.
321 * Allows for enqueuing additional scripts only on the My Jetpack page.
322 *
323 * @since 4.35.7
324 */
325 do_action( 'myjetpack_enqueue_scripts' );
326 add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) );
327 Assets::register_script(
328 'my_jetpack_main_app',
329 '../build/index.js',
330 __FILE__,
331 array(
332 'enqueue' => true,
333 'in_footer' => true,
334 'textdomain' => 'jetpack-my-jetpack',
335 )
336 );
337 $modules = new Modules();
338 $connection = new Connection_Manager();
339 $speed_score_history = new Speed_Score_History( get_site_url() );
340 $latest_score = $speed_score_history->latest();
341 $previous_score = array();
342 if ( $speed_score_history->count() > 1 ) {
343 $previous_score = $speed_score_history->latest( 1 );
344 }
345 $latest_score['previousScores'] = $previous_score['scores'] ?? array();
346
347 $sandboxed_domain = '';
348 $is_dev_version = false;
349 if ( class_exists( 'Jetpack' ) ) {
350 $is_dev_version = Jetpack::is_development_version();
351 $sandboxed_domain = defined( 'JETPACK__SANDBOX_DOMAIN' ) ? JETPACK__SANDBOX_DOMAIN : '';
352 }
353
354 wp_localize_script(
355 'my_jetpack_main_app',
356 'myJetpackInitialState',
357 array(
358 'products' => array(
359 'items' => Products::get_products(),
360 ),
361 'plugins' => Plugins_Installer::get_plugins(),
362 'themes' => Sync_Functions::get_themes(),
363 'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ),
364 'myJetpackCheckoutUri' => admin_url( 'admin.php?page=my-jetpack' ),
365 'topJetpackMenuItemUrl' => Admin_Menu::get_top_level_menu_item_url(),
366 'siteSuffix' => ( new Status() )->get_site_suffix(),
367 'siteUrl' => esc_url( get_site_url() ),
368 'blogID' => Connection_Manager::get_site_id( true ),
369 'myJetpackVersion' => self::PACKAGE_VERSION,
370 'myJetpackFlags' => self::get_my_jetpack_flags(),
371 'fileSystemWriteAccess' => self::has_file_system_write_access(),
372 'loadAddLicenseScreen' => self::is_licensing_ui_enabled(),
373 'adminUrl' => esc_url( admin_url() ),
374 'IDCContainerID' => static::get_idc_container_id(),
375 'userIsAdmin' => current_user_can( 'manage_options' ),
376 'lifecycleStats' => array(
377 'jetpackPlugins' => self::get_installed_jetpack_plugins(),
378 'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
379 'brokenModules' => Red_Bubble_Notifications::check_for_broken_modules(),
380 'isSiteConnected' => $connection->is_connected(),
381 'isUserConnected' => $connection->is_user_connected(),
382 'modules' => self::get_active_modules(),
383 ),
384 'recommendedModules' => array(
385 'modules' => self::get_recommended_modules(),
386 'isFirstRun' => \Jetpack_Options::get_option( 'recommendations_first_run', true ),
387 'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
388 ),
389 'isStatsModuleActive' => $modules->is_active( 'stats' ),
390 'canUserViewStats' => current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ),
391 'sandboxedDomain' => $sandboxed_domain,
392 'isDevVersion' => $is_dev_version,
393 'isAtomic' => ( new Status_Host() )->is_woa_site(),
394 'isJetpackPluginActive' => class_exists( 'Jetpack' ),
395 'latestBoostSpeedScores' => $latest_score,
396 'seoOptIn' => self::get_seo_opt_in_state(),
397 )
398 );
399
400 wp_localize_script(
401 'my_jetpack_main_app',
402 'myJetpackRest',
403 array(
404 'apiRoot' => esc_url_raw( rest_url() ),
405 'apiNonce' => wp_create_nonce( 'wp_rest' ),
406 )
407 );
408
409 // Connection Initial State.
410 Connection_Initial_State::render_script( 'my_jetpack_main_app' );
411
412 // Required for Analytics.
413 if ( self::can_use_analytics() ) {
414 Tracking::register_tracks_functions_scripts( true );
415 }
416 }
417
418 /**
419 * Add My Jetpack data to the unified script data object.
420 *
421 * @param array $data The script data.
422 * @return array
423 */
424 public static function add_script_data( $data ) {
425 $block_availability = class_exists( '\Jetpack_Gutenberg' )
426 ? \Jetpack_Gutenberg::get_cached_availability()
427 : array();
428
429 $data['myJetpack']['siteEditor'] = array(
430 'isBlockTheme' => function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(),
431 'isSharingBlockAvailable' => isset( $block_availability['sharing-buttons'] )
432 && $block_availability['sharing-buttons']['available'],
433 'activeThemeStylesheet' => get_stylesheet(),
434 );
435
436 return $data;
437 }
438
439 /**
440 * Get installed Jetpack plugins
441 *
442 * @return array
443 */
444 public static function get_installed_jetpack_plugins() {
445 $plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
446 $plugin_slugs = array_map(
447 static function ( $slug ) {
448 $parts = explode( '/', $slug );
449 // Return the last segment of the filepath without the PHP extension
450 return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
451 },
452 $plugin_slugs
453 );
454
455 return array_values( array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs ) );
456 }
457
458 /**
459 * Get active modules (except ones enabled by default)
460 *
461 * @return array
462 */
463 public static function get_active_modules() {
464 $modules = new Modules();
465 $active_modules = $modules->get_active();
466
467 // if the Jetpack plugin is active, filter out the modules that are active by default
468 if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) {
469 $active_modules = array_diff( $active_modules, Jetpack::get_default_modules() );
470 }
471 return array_values( $active_modules );
472 }
473
474 /**
475 * Determine if the current user is "new" to Jetpack
476 * This is used to vary some messaging in My Jetpack
477 *
478 * On the front-end, purchases are also taken into account
479 *
480 * @return bool
481 */
482 public static function is_jetpack_user_new() {
483 // is the user connected?
484 $connection = new Connection_Manager();
485 if ( $connection->is_user_connected() ) {
486 return false;
487 }
488
489 // TODO: add a data point for the last known connection/ disconnection time
490
491 // are any modules active?
492 $active_modules = self::get_active_modules();
493 if ( ! empty( $active_modules ) ) {
494 return false;
495 }
496
497 // check for other Jetpack plugins that are installed on the site (active or not)
498 // If there's more than one Jetpack plugin active, this user is not "new"
499 $plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
500 $plugin_slugs = array_map(
501 static function ( $slug ) {
502 $parts = explode( '/', $slug );
503 // Return the last segment of the filepath without the PHP extension
504 return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
505 },
506 $plugin_slugs
507 );
508 $installed_jetpack_plugins = array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs );
509 if ( is_countable( $installed_jetpack_plugins ) && count( $installed_jetpack_plugins ) >= 2 ) {
510 return false;
511 }
512
513 // Does the site have any purchases?
514 $purchases = Wpcom_Products::get_site_current_purchases();
515 if ( ! empty( $purchases ) && ! is_wp_error( $purchases ) ) {
516 return false;
517 }
518
519 return true;
520 }
521
522 /**
523 * Build flags for My Jetpack UI
524 *
525 * @return array
526 */
527 public static function get_my_jetpack_flags() {
528 $flags = array(
529 'videoPressStats' => Jetpack_Constants::is_true( 'JETPACK_MY_JETPACK_VIDEOPRESS_STATS_ENABLED' ),
530 'showFullJetpackStatsCard' => class_exists( 'Jetpack' ),
531 // Only says which destination `manage_url` is: the legacy Stats page
532 // caches its report and wants a `force_refresh` hint the dashboard does not.
533 'premiumAnalyticsEnabled' => Products\Stats::is_premium_analytics_enabled(),
534 'showAiModuleToggle' => Products\Jetpack_Ai::is_feature_ui_enabled(),
535 );
536
537 return $flags;
538 }
539
540 /**
541 * Build the state the My Jetpack "try the new SEO experience" opt-in card hydrates from.
542 *
543 * The card invites an existing self-hosted install to switch over to the new Jetpack SEO
544 * dashboard (JETPACK-1700). Gating lives server-side, where the signals actually are. The
545 * card shows only when all of:
546 *
547 * - the new SEO product is available — the `rsm_jetpack_seo` feature filter is on (the SEO
548 * package autoloads regardless, so `class_exists()` alone isn't enough; the filter is the
549 * real availability switch and the same one the SEO package gates its own surface behind);
550 * - the site is self-hosted — WordPress.com (Simple + Atomic) decides its own SEO surface, so
551 * the opt-in card is for self-hosted installs only;
552 * - the install hasn't opted in yet — `jetpack_seo_surface_visible` is still false. On wpcom
553 * the SEO package's `is_seo_surface_visible()` short-circuits to `true`, so the
554 * "not visible yet" check also doubles as the self-hosted guard, but we check the platform
555 * explicitly for clarity.
556 *
557 * Referenced through `class_exists()` rather than a hard composer dependency: both packages
558 * ship inside the Jetpack plugin and the SEO surface is feature-flagged, so a guarded read of
559 * its public API keeps this from adding plumbing to consumers that don't load SEO.
560 *
561 * The on-success destination is computed by the opt-in endpoint itself; we only seed the card
562 * with the same admin URL so the button has a sensible fallback before the request resolves.
563 *
564 * @return array{showCard: bool, redirect: string}
565 */
566 public static function get_seo_opt_in_state() {
567 // Guard with method_exists rather than class_exists: an older bundled SEO package can ship
568 // the Initializer class without this method, and class_exists alone would still fatal.
569 $seo_initializer = 'Automattic\Jetpack\SEO\Initializer';
570 // @phan-suppress-next-line PhanUndeclaredClassReference -- optional SEO package, guarded by method_exists.
571 $show_card = method_exists( $seo_initializer, 'is_optin_available' ) && $seo_initializer::is_optin_available();
572
573 return array(
574 'showCard' => $show_card,
575 'redirect' => admin_url( 'admin.php?page=jetpack-seo' ),
576 );
577 }
578
579 /**
580 * Echoes the admin page content.
581 *
582 * @return void
583 */
584 public static function admin_page() {
585 $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
586
587 // No connection check needed here: admin_init() has already redirected connected users away from onboarding.
588 // Availability IS re-checked on purpose: this render can run even when that redirect did not,
589 // and the check below is what keeps the onboarding route off WordPress.com Simple sites.
590 $is_onboarding = $step === 'onboarding' && self::is_onboarding_available();
591
592 // Add data attribute for onboarding, otherwise render normal container
593 echo '<div id="my-jetpack-container" ' . ( $is_onboarding ? 'data-route="onboarding"' : '' ) . '></div>';
594 }
595
596 /**
597 * Register the REST API routes.
598 *
599 * @return void
600 */
601 public static function register_rest_endpoints() {
602 new REST_Products();
603 new REST_Purchases();
604 new REST_Zendesk_Chat();
605 ( new REST_Jetpack_AI_JWT() )->register_rest_route();
606 new REST_Recommendations_Evaluation();
607
608 Products::register_product_endpoints();
609 Historically_Active_Modules::register_rest_endpoints();
610 Jetpack_Manage::register_rest_endpoints();
611 Red_Bubble_Notifications::register_rest_endpoints();
612
613 register_rest_route(
614 'my-jetpack/v1',
615 'site',
616 array(
617 'methods' => \WP_REST_Server::READABLE,
618 'callback' => __CLASS__ . '::get_site',
619 'permission_callback' => __CLASS__ . '::permissions_callback',
620 )
621 );
622
623 register_rest_route(
624 'my-jetpack/v1',
625 'site/dismiss-welcome-banner',
626 array(
627 'methods' => \WP_REST_Server::EDITABLE,
628 'callback' => __CLASS__ . '::dismiss_welcome_banner',
629 'permission_callback' => __CLASS__ . '::permissions_callback',
630 )
631 );
632 }
633
634 /**
635 * Check user capability to access the endpoint.
636 *
637 * @access public
638 * @static
639 *
640 * @return true|WP_Error
641 */
642 public static function permissions_callback() {
643 return current_user_can( 'manage_options' );
644 }
645
646 /**
647 * Return true if we should initialize the My Jetpack admin page.
648 */
649 public static function should_initialize() {
650 $should = true;
651
652 // All options presented in My Jetpack require a connection to WordPress.com.
653 if ( ( new Status() )->is_offline_mode() ) {
654 $should = false;
655 }
656
657 /**
658 * Allows filtering whether My Jetpack should be initialized.
659 *
660 * @since 0.5.0-alpha
661 *
662 * @param bool $shoud_initialize Should we initialize My Jetpack?
663 */
664 return apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
665 }
666
667 /**
668 * Hook into several connection-based actions to update the historically active Jetpack modules
669 * If the transient that indicates the list needs to be synced, update it and delete the transient
670 *
671 * @return void
672 */
673 public static function setup_historically_active_jetpack_modules_sync() {
674 // yummmm. ham.
675 $ham = new Historically_Active_Modules();
676 if ( get_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
677 $ham::update_historically_active_jetpack_modules();
678 delete_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
679 }
680
681 $actions = array(
682 'jetpack_site_registered',
683 'jetpack_user_authorized',
684 'activated_plugin',
685 );
686
687 foreach ( $actions as $action ) {
688 add_action( $action, array( $ham, 'queue_historically_active_jetpack_modules_update' ), 5 );
689 }
690
691 // Modules are often updated async, so we need to update them right away as there will sometimes be no page reload.
692 add_action( 'jetpack_activate_module', array( $ham, 'update_historically_active_jetpack_modules' ), 5 );
693 }
694
695 /**
696 * Site full-data endpoint.
697 *
698 * @return object Site data.
699 */
700 public static function get_site() {
701 $site_id = \Jetpack_Options::get_option( 'id' );
702 $wpcom_endpoint = sprintf( '/sites/%d?force=wpcom', $site_id );
703 $wpcom_api_version = '1.1';
704 $response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version );
705 $response_code = wp_remote_retrieve_response_code( $response );
706 $body = json_decode( wp_remote_retrieve_body( $response ) );
707
708 if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
709 return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
710 }
711
712 return rest_ensure_response( $body );
713 }
714
715 /**
716 * Populates the self::$site_info var with site data from the /sites/%d endpoint
717 *
718 * @return object|WP_Error
719 */
720 public static function get_site_info() {
721 static $site_info = null;
722
723 if ( $site_info !== null ) {
724 return $site_info;
725 }
726
727 // Check for a cached value before doing lookup
728 $stored_site_info = get_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY );
729 if ( $stored_site_info !== false ) {
730 return $stored_site_info;
731 }
732
733 $response = self::get_site();
734 if ( is_wp_error( $response ) ) {
735 return $response;
736 }
737 $site_info = $response->data;
738 set_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY, $site_info, DAY_IN_SECONDS );
739
740 return $site_info;
741 }
742
743 /**
744 * Returns whether a site has been determined "commercial" or not.
745 *
746 * @return bool|null
747 */
748 public static function is_commercial_site() {
749 if ( is_wp_error( self::$site_info ) ) {
750 return null;
751 }
752
753 return empty( self::$site_info->options->is_commercial ) ? false : self::$site_info->options->is_commercial;
754 }
755
756 /**
757 * Check if site is registered (has been connected before).
758 *
759 * @return bool
760 */
761 public static function is_registered() {
762 return (bool) \Jetpack_Options::get_option( 'id' );
763 }
764
765 /**
766 * Dismiss the welcome banner.
767 *
768 * @return \WP_REST_Response
769 */
770 public static function dismiss_welcome_banner() {
771 \Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
772 return rest_ensure_response( array( 'success' => true ) );
773 }
774
775 /**
776 * Returns "yes" if the site has file write access to the plugins folder, "no" otherwise.
777 *
778 * @return string
779 **/
780 public static function has_file_system_write_access() {
781
782 $cache = get_transient( 'my_jetpack_write_access' );
783
784 if ( false !== $cache ) {
785 return $cache;
786 }
787
788 if ( ! function_exists( 'get_filesystem_method' ) ) {
789 require_once ABSPATH . 'wp-admin/includes/file.php';
790 }
791
792 require_once ABSPATH . 'wp-admin/includes/template.php';
793
794 $write_access = 'no';
795
796 $filesystem_method = get_filesystem_method( array(), WP_PLUGIN_DIR );
797 if ( 'direct' === $filesystem_method ) {
798 $write_access = 'yes';
799 }
800
801 if ( 'no' === $write_access ) {
802 ob_start();
803 $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
804 ob_end_clean();
805
806 if ( $filesystem_credentials_are_stored ) {
807 $write_access = 'yes';
808 }
809 }
810
811 set_transient( 'my_jetpack_write_access', $write_access, 30 * MINUTE_IN_SECONDS );
812
813 return $write_access;
814 }
815
816 /**
817 * Get container IDC for the IDC screen.
818 *
819 * @return string
820 */
821 public static function get_idc_container_id() {
822 return static::IDC_CONTAINER_ID;
823 }
824
825 /**
826 * Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show.
827 *
828 * On My Jetpack page: Uses blocking behavior to fetch fresh data.
829 * On other admin pages: Uses cached data only to avoid blocking, with async JS fetch if cache is empty.
830 *
831 * @return void
832 */
833 public static function maybe_show_red_bubble() {
834 global $pagenow;
835
836 // Don't show red bubble alerts for non-admin users
837 // These alerts are generally only actionable for admins
838 if ( ! current_user_can( 'manage_options' ) ) {
839 return;
840 }
841
842 // Don't show any red bubbles when Jetpack is disconnected
843 // Users can't act on most alerts without a connection
844 $connection = new Connection_Manager();
845 if ( ! $connection->is_connected() ) {
846 return;
847 }
848
849 // Check if we're on the My Jetpack page.
850 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
851 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
852 $is_my_jetpack_page = $pagenow === 'admin.php' && $page === 'my-jetpack';
853
854 if ( $is_my_jetpack_page ) {
855 // On My Jetpack page: use blocking behavior for fresh data.
856 add_filter( 'my_jetpack_red_bubble_notification_slugs', array( Red_Bubble_Notifications::class, 'add_red_bubble_alerts' ) );
857 $red_bubble_alerts = Red_Bubble_Notifications::get_red_bubble_alerts();
858 } else {
859 // On other pages: use cached data only to avoid blocking.
860 $cached_alerts = Red_Bubble_Notifications::get_cached_alerts();
861
862 if ( false === $cached_alerts ) {
863 // No cache: warm it asynchronously via JS. Register a hidden zero-count
864 // placeholder so Menu_Renderer emits a `my-jetpack` badge element the
865 // warmer can reveal (see async-notification-bubble.ts) without a reload.
866 Menu_Badges::init(); // idempotent; wires the renderer.
867 Notification_Counts::register(
868 'my-jetpack',
869 array(
870 'menu_slug' => 'my-jetpack',
871 'count' => 0,
872 'type' => 'count',
873 )
874 );
875 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_red_bubble_script' ) );
876 return;
877 }
878
879 $red_bubble_alerts = $cached_alerts;
880 }
881
882 // Filter out silent alerts.
883 $red_bubble_alerts = array_filter(
884 $red_bubble_alerts,
885 function ( $alert ) {
886 return empty( $alert['is_silent'] );
887 }
888 );
889
890 // Report each non-silent alert to the central menu-badges registry as an
891 // attention entry (count 1). The registry + renderer own the badge.
892 Menu_Badges::init(); // idempotent; wires the renderer.
893 foreach ( array_keys( $red_bubble_alerts ) as $slug ) {
894 // Protect reports its own count directly to the registry, but only when its
895 // standalone plugin is active (see class-jetpack-protect.php::admin_page_init()).
896 // If the standalone plugin isn't active, nobody else registers this count, so we
897 // must not skip it here or the alert silently disappears from the menu total.
898 if ( 'protect_has_threats' === $slug && Products\Protect::is_standalone_plugin_active() ) {
899 continue;
900 }
901 Notification_Counts::register(
902 'my-jetpack-' . $slug,
903 array(
904 'menu_slug' => 'my-jetpack',
905 'type' => 'attention',
906 )
907 );
908 }
909 }
910
911 /**
912 * Enqueue the notification bubble script.
913 * Fetches fresh alert data via REST API without blocking page load.
914 *
915 * @return void
916 */
917 public static function enqueue_red_bubble_script() {
918 Assets::register_script(
919 'my-jetpack-notification-bubble',
920 '../build/async-notification-bubble.js',
921 __FILE__,
922 array(
923 'enqueue' => true,
924 'in_footer' => true,
925 )
926 );
927 }
928
929 /**
930 * Get list of module names sorted by their recommendation score
931 *
932 * @return array|null
933 */
934 public static function get_recommended_modules() {
935 $recommendations_evaluation = \Jetpack_Options::get_option( 'recommendations_evaluation', null );
936
937 if ( empty( $recommendations_evaluation ) || ! is_array( $recommendations_evaluation ) ) {
938 return null;
939 }
940
941 arsort( $recommendations_evaluation ); // Sort by scores in descending order
942
943 return array_keys( $recommendations_evaluation ); // Get only module names
944 }
945 }
946