PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.2
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.2
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-my-jetpack / src / class-initializer.php
jetpack / jetpack_vendor / automattic / jetpack-my-jetpack / src Last commit date
products 2 months ago class-historically-active-modules.php 9 months ago class-initializer.php 3 weeks ago class-jetpack-manage.php 5 months ago class-products.php 2 months ago class-red-bubble-notifications.php 4 months ago class-rest-products.php 9 months ago class-rest-purchases.php 4 months ago class-rest-recommendations-evaluation.php 9 months ago class-rest-zendesk-chat.php 9 months ago class-wpcom-products.php 4 months ago
class-initializer.php
937 lines
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\Agents_Manager\WP_REST_Jetpack_AI_JWT;
12 use Automattic\Jetpack\Assets;
13 use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
14 use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
15 use Automattic\Jetpack\Connection\Client;
16 use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
17 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
18 use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
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 = '5.41.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 -1
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-theme handles the
285 * My Jetpack app bundle depends on but WP < 7.0 does not ship (or ships with an
286 * incomplete allowlist).
287 *
288 * @return void
289 */
290 public static function register_wp_build_polyfills() {
291 if ( ! class_exists( WP_Build_Polyfills::class ) ) {
292 return;
293 }
294
295 WP_Build_Polyfills::register(
296 'my-jetpack',
297 array( 'wp-notices', 'wp-private-apis', 'wp-theme' )
298 );
299 }
300
301 /**
302 * Enqueue admin page assets.
303 *
304 * @return void
305 */
306 public static function enqueue_scripts() {
307 // Register the wp-build-polyfills shim before the extension hook below or
308 // the app script can enqueue against wp-theme / wp-private-apis / wp-notices.
309 // WP_Build_Polyfills registers synchronously on its first caller, so calling
310 // it after a hook consumer would leave our handles recorded but unregistered
311 // for this request.
312 self::register_wp_build_polyfills();
313
314 /**
315 * Fires after the My Jetpack page is initialized.
316 * Allows for enqueuing additional scripts only on the My Jetpack page.
317 *
318 * @since 4.35.7
319 */
320 do_action( 'myjetpack_enqueue_scripts' );
321 add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) );
322 Assets::register_script(
323 'my_jetpack_main_app',
324 '../build/index.js',
325 __FILE__,
326 array(
327 'enqueue' => true,
328 'in_footer' => true,
329 'textdomain' => 'jetpack-my-jetpack',
330 )
331 );
332 $modules = new Modules();
333 $connection = new Connection_Manager();
334 $speed_score_history = new Speed_Score_History( get_site_url() );
335 $latest_score = $speed_score_history->latest();
336 $previous_score = array();
337 if ( $speed_score_history->count() > 1 ) {
338 $previous_score = $speed_score_history->latest( 1 );
339 }
340 $latest_score['previousScores'] = $previous_score['scores'] ?? array();
341
342 $sandboxed_domain = '';
343 $is_dev_version = false;
344 if ( class_exists( 'Jetpack' ) ) {
345 $is_dev_version = Jetpack::is_development_version();
346 $sandboxed_domain = defined( 'JETPACK__SANDBOX_DOMAIN' ) ? JETPACK__SANDBOX_DOMAIN : '';
347 }
348
349 wp_localize_script(
350 'my_jetpack_main_app',
351 'myJetpackInitialState',
352 array(
353 'products' => array(
354 'items' => Products::get_products(),
355 ),
356 'plugins' => Plugins_Installer::get_plugins(),
357 'themes' => Sync_Functions::get_themes(),
358 'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ),
359 'myJetpackCheckoutUri' => admin_url( 'admin.php?page=my-jetpack' ),
360 'topJetpackMenuItemUrl' => Admin_Menu::get_top_level_menu_item_url(),
361 'siteSuffix' => ( new Status() )->get_site_suffix(),
362 'siteUrl' => esc_url( get_site_url() ),
363 'blogID' => Connection_Manager::get_site_id( true ),
364 'myJetpackVersion' => self::PACKAGE_VERSION,
365 'myJetpackFlags' => self::get_my_jetpack_flags(),
366 'fileSystemWriteAccess' => self::has_file_system_write_access(),
367 'loadAddLicenseScreen' => self::is_licensing_ui_enabled(),
368 'adminUrl' => esc_url( admin_url() ),
369 'IDCContainerID' => static::get_idc_container_id(),
370 'userIsAdmin' => current_user_can( 'manage_options' ),
371 'lifecycleStats' => array(
372 'jetpackPlugins' => self::get_installed_jetpack_plugins(),
373 'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
374 'brokenModules' => Red_Bubble_Notifications::check_for_broken_modules(),
375 'isSiteConnected' => $connection->is_connected(),
376 'isUserConnected' => $connection->is_user_connected(),
377 'modules' => self::get_active_modules(),
378 ),
379 'recommendedModules' => array(
380 'modules' => self::get_recommended_modules(),
381 'isFirstRun' => \Jetpack_Options::get_option( 'recommendations_first_run', true ),
382 'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
383 ),
384 'isStatsModuleActive' => $modules->is_active( 'stats' ),
385 'canUserViewStats' => current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ),
386 'sandboxedDomain' => $sandboxed_domain,
387 'isDevVersion' => $is_dev_version,
388 'isAtomic' => ( new Status_Host() )->is_woa_site(),
389 'isJetpackPluginActive' => class_exists( 'Jetpack' ),
390 'latestBoostSpeedScores' => $latest_score,
391 'seoOptIn' => self::get_seo_opt_in_state(),
392 )
393 );
394
395 wp_localize_script(
396 'my_jetpack_main_app',
397 'myJetpackRest',
398 array(
399 'apiRoot' => esc_url_raw( rest_url() ),
400 'apiNonce' => wp_create_nonce( 'wp_rest' ),
401 )
402 );
403
404 // Connection Initial State.
405 Connection_Initial_State::render_script( 'my_jetpack_main_app' );
406
407 // Required for Analytics.
408 if ( self::can_use_analytics() ) {
409 Tracking::register_tracks_functions_scripts( true );
410 }
411 }
412
413 /**
414 * Add My Jetpack data to the unified script data object.
415 *
416 * @param array $data The script data.
417 * @return array
418 */
419 public static function add_script_data( $data ) {
420 $block_availability = class_exists( '\Jetpack_Gutenberg' )
421 ? \Jetpack_Gutenberg::get_cached_availability()
422 : array();
423
424 $data['myJetpack']['siteEditor'] = array(
425 'isBlockTheme' => function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(),
426 'isSharingBlockAvailable' => isset( $block_availability['sharing-buttons'] )
427 && $block_availability['sharing-buttons']['available'],
428 'activeThemeStylesheet' => get_stylesheet(),
429 );
430
431 return $data;
432 }
433
434 /**
435 * Get installed Jetpack plugins
436 *
437 * @return array
438 */
439 public static function get_installed_jetpack_plugins() {
440 $plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
441 $plugin_slugs = array_map(
442 static function ( $slug ) {
443 $parts = explode( '/', $slug );
444 // Return the last segment of the filepath without the PHP extension
445 return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
446 },
447 $plugin_slugs
448 );
449
450 return array_values( array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs ) );
451 }
452
453 /**
454 * Get active modules (except ones enabled by default)
455 *
456 * @return array
457 */
458 public static function get_active_modules() {
459 $modules = new Modules();
460 $active_modules = $modules->get_active();
461
462 // if the Jetpack plugin is active, filter out the modules that are active by default
463 if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) {
464 $active_modules = array_diff( $active_modules, Jetpack::get_default_modules() );
465 }
466 return array_values( $active_modules );
467 }
468
469 /**
470 * Determine if the current user is "new" to Jetpack
471 * This is used to vary some messaging in My Jetpack
472 *
473 * On the front-end, purchases are also taken into account
474 *
475 * @return bool
476 */
477 public static function is_jetpack_user_new() {
478 // is the user connected?
479 $connection = new Connection_Manager();
480 if ( $connection->is_user_connected() ) {
481 return false;
482 }
483
484 // TODO: add a data point for the last known connection/ disconnection time
485
486 // are any modules active?
487 $active_modules = self::get_active_modules();
488 if ( ! empty( $active_modules ) ) {
489 return false;
490 }
491
492 // check for other Jetpack plugins that are installed on the site (active or not)
493 // If there's more than one Jetpack plugin active, this user is not "new"
494 $plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
495 $plugin_slugs = array_map(
496 static function ( $slug ) {
497 $parts = explode( '/', $slug );
498 // Return the last segment of the filepath without the PHP extension
499 return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
500 },
501 $plugin_slugs
502 );
503 $installed_jetpack_plugins = array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs );
504 if ( is_countable( $installed_jetpack_plugins ) && count( $installed_jetpack_plugins ) >= 2 ) {
505 return false;
506 }
507
508 // Does the site have any purchases?
509 $purchases = Wpcom_Products::get_site_current_purchases();
510 if ( ! empty( $purchases ) && ! is_wp_error( $purchases ) ) {
511 return false;
512 }
513
514 return true;
515 }
516
517 /**
518 * Build flags for My Jetpack UI
519 *
520 * @return array
521 */
522 public static function get_my_jetpack_flags() {
523 $flags = array(
524 'videoPressStats' => Jetpack_Constants::is_true( 'JETPACK_MY_JETPACK_VIDEOPRESS_STATS_ENABLED' ),
525 'showFullJetpackStatsCard' => class_exists( 'Jetpack' ),
526 );
527
528 return $flags;
529 }
530
531 /**
532 * Build the state the My Jetpack "try the new SEO experience" opt-in card hydrates from.
533 *
534 * The card invites an existing self-hosted install to switch over to the new Jetpack SEO
535 * dashboard (JETPACK-1700). Gating lives server-side, where the signals actually are. The
536 * card shows only when all of:
537 *
538 * - the new SEO product is available — the `rsm_jetpack_seo` feature filter is on (the SEO
539 * package autoloads regardless, so `class_exists()` alone isn't enough; the filter is the
540 * real availability switch and the same one the SEO package gates its own surface behind);
541 * - the site is self-hosted — WordPress.com (Simple + Atomic) decides its own SEO surface, so
542 * the opt-in card is for self-hosted installs only;
543 * - the install hasn't opted in yet — `jetpack_seo_surface_visible` is still false. On wpcom
544 * the SEO package's `is_seo_surface_visible()` short-circuits to `true`, so the
545 * "not visible yet" check also doubles as the self-hosted guard, but we check the platform
546 * explicitly for clarity.
547 *
548 * Referenced through `class_exists()` rather than a hard composer dependency: both packages
549 * ship inside the Jetpack plugin and the SEO surface is feature-flagged, so a guarded read of
550 * its public API keeps this from adding plumbing to consumers that don't load SEO.
551 *
552 * The on-success destination is computed by the opt-in endpoint itself; we only seed the card
553 * with the same admin URL so the button has a sensible fallback before the request resolves.
554 *
555 * @return array{showCard: bool, redirect: string}
556 */
557 public static function get_seo_opt_in_state() {
558 // Guard with method_exists rather than class_exists: an older bundled SEO package can ship
559 // the Initializer class without this method, and class_exists alone would still fatal.
560 $seo_initializer = 'Automattic\Jetpack\SEO\Initializer';
561 // @phan-suppress-next-line PhanUndeclaredClassReference -- optional SEO package, guarded by method_exists.
562 $show_card = method_exists( $seo_initializer, 'is_optin_available' ) && $seo_initializer::is_optin_available();
563
564 return array(
565 'showCard' => $show_card,
566 'redirect' => admin_url( 'admin.php?page=jetpack-seo' ),
567 );
568 }
569
570 /**
571 * Echoes the admin page content.
572 *
573 * @return void
574 */
575 public static function admin_page() {
576 $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
577
578 // No connection check needed here: admin_init() has already redirected connected users away from onboarding.
579 // Availability IS re-checked on purpose: this render can run even when that redirect did not,
580 // and the check below is what keeps the onboarding route off WordPress.com Simple sites.
581 $is_onboarding = $step === 'onboarding' && self::is_onboarding_available();
582
583 // Add data attribute for onboarding, otherwise render normal container
584 echo '<div id="my-jetpack-container" ' . ( $is_onboarding ? 'data-route="onboarding"' : '' ) . '></div>';
585 }
586
587 /**
588 * Register the REST API routes.
589 *
590 * @return void
591 */
592 public static function register_rest_endpoints() {
593 new REST_Products();
594 new REST_Purchases();
595 new REST_Zendesk_Chat();
596 ( new WP_REST_Jetpack_AI_JWT() )->register_rest_route();
597 new REST_Recommendations_Evaluation();
598
599 Products::register_product_endpoints();
600 Historically_Active_Modules::register_rest_endpoints();
601 Jetpack_Manage::register_rest_endpoints();
602 Red_Bubble_Notifications::register_rest_endpoints();
603
604 register_rest_route(
605 'my-jetpack/v1',
606 'site',
607 array(
608 'methods' => \WP_REST_Server::READABLE,
609 'callback' => __CLASS__ . '::get_site',
610 'permission_callback' => __CLASS__ . '::permissions_callback',
611 )
612 );
613
614 register_rest_route(
615 'my-jetpack/v1',
616 'site/dismiss-welcome-banner',
617 array(
618 'methods' => \WP_REST_Server::EDITABLE,
619 'callback' => __CLASS__ . '::dismiss_welcome_banner',
620 'permission_callback' => __CLASS__ . '::permissions_callback',
621 )
622 );
623 }
624
625 /**
626 * Check user capability to access the endpoint.
627 *
628 * @access public
629 * @static
630 *
631 * @return true|WP_Error
632 */
633 public static function permissions_callback() {
634 return current_user_can( 'manage_options' );
635 }
636
637 /**
638 * Return true if we should initialize the My Jetpack admin page.
639 */
640 public static function should_initialize() {
641 $should = true;
642
643 // All options presented in My Jetpack require a connection to WordPress.com.
644 if ( ( new Status() )->is_offline_mode() ) {
645 $should = false;
646 }
647
648 /**
649 * Allows filtering whether My Jetpack should be initialized.
650 *
651 * @since 0.5.0-alpha
652 *
653 * @param bool $shoud_initialize Should we initialize My Jetpack?
654 */
655 return apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
656 }
657
658 /**
659 * Hook into several connection-based actions to update the historically active Jetpack modules
660 * If the transient that indicates the list needs to be synced, update it and delete the transient
661 *
662 * @return void
663 */
664 public static function setup_historically_active_jetpack_modules_sync() {
665 // yummmm. ham.
666 $ham = new Historically_Active_Modules();
667 if ( get_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
668 $ham::update_historically_active_jetpack_modules();
669 delete_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
670 }
671
672 $actions = array(
673 'jetpack_site_registered',
674 'jetpack_user_authorized',
675 'activated_plugin',
676 );
677
678 foreach ( $actions as $action ) {
679 add_action( $action, array( $ham, 'queue_historically_active_jetpack_modules_update' ), 5 );
680 }
681
682 // Modules are often updated async, so we need to update them right away as there will sometimes be no page reload.
683 add_action( 'jetpack_activate_module', array( $ham, 'update_historically_active_jetpack_modules' ), 5 );
684 }
685
686 /**
687 * Site full-data endpoint.
688 *
689 * @return object Site data.
690 */
691 public static function get_site() {
692 $site_id = \Jetpack_Options::get_option( 'id' );
693 $wpcom_endpoint = sprintf( '/sites/%d?force=wpcom', $site_id );
694 $wpcom_api_version = '1.1';
695 $response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version );
696 $response_code = wp_remote_retrieve_response_code( $response );
697 $body = json_decode( wp_remote_retrieve_body( $response ) );
698
699 if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
700 return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
701 }
702
703 return rest_ensure_response( $body );
704 }
705
706 /**
707 * Populates the self::$site_info var with site data from the /sites/%d endpoint
708 *
709 * @return object|WP_Error
710 */
711 public static function get_site_info() {
712 static $site_info = null;
713
714 if ( $site_info !== null ) {
715 return $site_info;
716 }
717
718 // Check for a cached value before doing lookup
719 $stored_site_info = get_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY );
720 if ( $stored_site_info !== false ) {
721 return $stored_site_info;
722 }
723
724 $response = self::get_site();
725 if ( is_wp_error( $response ) ) {
726 return $response;
727 }
728 $site_info = $response->data;
729 set_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY, $site_info, DAY_IN_SECONDS );
730
731 return $site_info;
732 }
733
734 /**
735 * Returns whether a site has been determined "commercial" or not.
736 *
737 * @return bool|null
738 */
739 public static function is_commercial_site() {
740 if ( is_wp_error( self::$site_info ) ) {
741 return null;
742 }
743
744 return empty( self::$site_info->options->is_commercial ) ? false : self::$site_info->options->is_commercial;
745 }
746
747 /**
748 * Check if site is registered (has been connected before).
749 *
750 * @return bool
751 */
752 public static function is_registered() {
753 return (bool) \Jetpack_Options::get_option( 'id' );
754 }
755
756 /**
757 * Dismiss the welcome banner.
758 *
759 * @return \WP_REST_Response
760 */
761 public static function dismiss_welcome_banner() {
762 \Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
763 return rest_ensure_response( array( 'success' => true ) );
764 }
765
766 /**
767 * Returns "yes" if the site has file write access to the plugins folder, "no" otherwise.
768 *
769 * @return string
770 **/
771 public static function has_file_system_write_access() {
772
773 $cache = get_transient( 'my_jetpack_write_access' );
774
775 if ( false !== $cache ) {
776 return $cache;
777 }
778
779 if ( ! function_exists( 'get_filesystem_method' ) ) {
780 require_once ABSPATH . 'wp-admin/includes/file.php';
781 }
782
783 require_once ABSPATH . 'wp-admin/includes/template.php';
784
785 $write_access = 'no';
786
787 $filesystem_method = get_filesystem_method( array(), WP_PLUGIN_DIR );
788 if ( 'direct' === $filesystem_method ) {
789 $write_access = 'yes';
790 }
791
792 if ( 'no' === $write_access ) {
793 ob_start();
794 $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
795 ob_end_clean();
796
797 if ( $filesystem_credentials_are_stored ) {
798 $write_access = 'yes';
799 }
800 }
801
802 set_transient( 'my_jetpack_write_access', $write_access, 30 * MINUTE_IN_SECONDS );
803
804 return $write_access;
805 }
806
807 /**
808 * Get container IDC for the IDC screen.
809 *
810 * @return string
811 */
812 public static function get_idc_container_id() {
813 return static::IDC_CONTAINER_ID;
814 }
815
816 /**
817 * Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show.
818 *
819 * On My Jetpack page: Uses blocking behavior to fetch fresh data.
820 * On other admin pages: Uses cached data only to avoid blocking, with async JS fetch if cache is empty.
821 *
822 * @return void
823 */
824 public static function maybe_show_red_bubble() {
825 global $pagenow;
826
827 // Don't show red bubble alerts for non-admin users
828 // These alerts are generally only actionable for admins
829 if ( ! current_user_can( 'manage_options' ) ) {
830 return;
831 }
832
833 // Don't show any red bubbles when Jetpack is disconnected
834 // Users can't act on most alerts without a connection
835 $connection = new Connection_Manager();
836 if ( ! $connection->is_connected() ) {
837 return;
838 }
839
840 // Check if we're on the My Jetpack page.
841 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
842 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
843 $is_my_jetpack_page = $pagenow === 'admin.php' && $page === 'my-jetpack';
844
845 if ( $is_my_jetpack_page ) {
846 // On My Jetpack page: use blocking behavior for fresh data.
847 add_filter( 'my_jetpack_red_bubble_notification_slugs', array( Red_Bubble_Notifications::class, 'add_red_bubble_alerts' ) );
848 $red_bubble_alerts = Red_Bubble_Notifications::get_red_bubble_alerts();
849 } else {
850 // On other pages: use cached data only to avoid blocking.
851 $cached_alerts = Red_Bubble_Notifications::get_cached_alerts();
852
853 if ( false === $cached_alerts ) {
854 // No cache: warm it asynchronously via JS. Register a hidden zero-count
855 // placeholder so Menu_Renderer emits a `my-jetpack` badge element the
856 // warmer can reveal (see async-notification-bubble.ts) without a reload.
857 Menu_Badges::init(); // idempotent; wires the renderer.
858 Notification_Counts::register(
859 'my-jetpack',
860 array(
861 'menu_slug' => 'my-jetpack',
862 'count' => 0,
863 'type' => 'count',
864 )
865 );
866 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_red_bubble_script' ) );
867 return;
868 }
869
870 $red_bubble_alerts = $cached_alerts;
871 }
872
873 // Filter out silent alerts.
874 $red_bubble_alerts = array_filter(
875 $red_bubble_alerts,
876 function ( $alert ) {
877 return empty( $alert['is_silent'] );
878 }
879 );
880
881 // Report each non-silent alert to the central menu-badges registry as an
882 // attention entry (count 1). The registry + renderer own the badge.
883 Menu_Badges::init(); // idempotent; wires the renderer.
884 foreach ( array_keys( $red_bubble_alerts ) as $slug ) {
885 // Protect reports its own count directly to the registry, but only when its
886 // standalone plugin is active (see class-jetpack-protect.php::admin_page_init()).
887 // If the standalone plugin isn't active, nobody else registers this count, so we
888 // must not skip it here or the alert silently disappears from the menu total.
889 if ( 'protect_has_threats' === $slug && Products\Protect::is_standalone_plugin_active() ) {
890 continue;
891 }
892 Notification_Counts::register(
893 'my-jetpack-' . $slug,
894 array(
895 'menu_slug' => 'my-jetpack',
896 'type' => 'attention',
897 )
898 );
899 }
900 }
901
902 /**
903 * Enqueue the notification bubble script.
904 * Fetches fresh alert data via REST API without blocking page load.
905 *
906 * @return void
907 */
908 public static function enqueue_red_bubble_script() {
909 Assets::register_script(
910 'my-jetpack-notification-bubble',
911 '../build/async-notification-bubble.js',
912 __FILE__,
913 array(
914 'enqueue' => true,
915 'in_footer' => true,
916 )
917 );
918 }
919
920 /**
921 * Get list of module names sorted by their recommendation score
922 *
923 * @return array|null
924 */
925 public static function get_recommended_modules() {
926 $recommendations_evaluation = \Jetpack_Options::get_option( 'recommendations_evaluation', null );
927
928 if ( ! $recommendations_evaluation ) {
929 return null;
930 }
931
932 arsort( $recommendations_evaluation ); // Sort by scores in descending order
933
934 return array_keys( $recommendations_evaluation ); // Get only module names
935 }
936 }
937