PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
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 4 days ago class-historically-active-modules.php 9 months ago class-initializer.php 4 days 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 2 weeks ago class-wpcom-products.php 4 months ago
class-initializer.php
950 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.44.0';
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-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 // Pre-release gate: only internal testing environments get the AI
535 // card's module toggle. The helper lives in the Jetpack plugin, so
536 // standalone installs resolve to false. Remove when the AI settings
537 // page goes public.
538 'showAiModuleToggle' => function_exists( 'jetpack_is_internal_testing_environment' ) && jetpack_is_internal_testing_environment(),
539 );
540
541 return $flags;
542 }
543
544 /**
545 * Build the state the My Jetpack "try the new SEO experience" opt-in card hydrates from.
546 *
547 * The card invites an existing self-hosted install to switch over to the new Jetpack SEO
548 * dashboard (JETPACK-1700). Gating lives server-side, where the signals actually are. The
549 * card shows only when all of:
550 *
551 * - the new SEO product is available — the `rsm_jetpack_seo` feature filter is on (the SEO
552 * package autoloads regardless, so `class_exists()` alone isn't enough; the filter is the
553 * real availability switch and the same one the SEO package gates its own surface behind);
554 * - the site is self-hosted — WordPress.com (Simple + Atomic) decides its own SEO surface, so
555 * the opt-in card is for self-hosted installs only;
556 * - the install hasn't opted in yet — `jetpack_seo_surface_visible` is still false. On wpcom
557 * the SEO package's `is_seo_surface_visible()` short-circuits to `true`, so the
558 * "not visible yet" check also doubles as the self-hosted guard, but we check the platform
559 * explicitly for clarity.
560 *
561 * Referenced through `class_exists()` rather than a hard composer dependency: both packages
562 * ship inside the Jetpack plugin and the SEO surface is feature-flagged, so a guarded read of
563 * its public API keeps this from adding plumbing to consumers that don't load SEO.
564 *
565 * The on-success destination is computed by the opt-in endpoint itself; we only seed the card
566 * with the same admin URL so the button has a sensible fallback before the request resolves.
567 *
568 * @return array{showCard: bool, redirect: string}
569 */
570 public static function get_seo_opt_in_state() {
571 // Guard with method_exists rather than class_exists: an older bundled SEO package can ship
572 // the Initializer class without this method, and class_exists alone would still fatal.
573 $seo_initializer = 'Automattic\Jetpack\SEO\Initializer';
574 // @phan-suppress-next-line PhanUndeclaredClassReference -- optional SEO package, guarded by method_exists.
575 $show_card = method_exists( $seo_initializer, 'is_optin_available' ) && $seo_initializer::is_optin_available();
576
577 return array(
578 'showCard' => $show_card,
579 'redirect' => admin_url( 'admin.php?page=jetpack-seo' ),
580 );
581 }
582
583 /**
584 * Echoes the admin page content.
585 *
586 * @return void
587 */
588 public static function admin_page() {
589 $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
590
591 // No connection check needed here: admin_init() has already redirected connected users away from onboarding.
592 // Availability IS re-checked on purpose: this render can run even when that redirect did not,
593 // and the check below is what keeps the onboarding route off WordPress.com Simple sites.
594 $is_onboarding = $step === 'onboarding' && self::is_onboarding_available();
595
596 // Add data attribute for onboarding, otherwise render normal container
597 echo '<div id="my-jetpack-container" ' . ( $is_onboarding ? 'data-route="onboarding"' : '' ) . '></div>';
598 }
599
600 /**
601 * Register the REST API routes.
602 *
603 * @return void
604 */
605 public static function register_rest_endpoints() {
606 new REST_Products();
607 new REST_Purchases();
608 new REST_Zendesk_Chat();
609 ( new WP_REST_Jetpack_AI_JWT() )->register_rest_route();
610 new REST_Recommendations_Evaluation();
611
612 Products::register_product_endpoints();
613 Historically_Active_Modules::register_rest_endpoints();
614 Jetpack_Manage::register_rest_endpoints();
615 Red_Bubble_Notifications::register_rest_endpoints();
616
617 register_rest_route(
618 'my-jetpack/v1',
619 'site',
620 array(
621 'methods' => \WP_REST_Server::READABLE,
622 'callback' => __CLASS__ . '::get_site',
623 'permission_callback' => __CLASS__ . '::permissions_callback',
624 )
625 );
626
627 register_rest_route(
628 'my-jetpack/v1',
629 'site/dismiss-welcome-banner',
630 array(
631 'methods' => \WP_REST_Server::EDITABLE,
632 'callback' => __CLASS__ . '::dismiss_welcome_banner',
633 'permission_callback' => __CLASS__ . '::permissions_callback',
634 )
635 );
636 }
637
638 /**
639 * Check user capability to access the endpoint.
640 *
641 * @access public
642 * @static
643 *
644 * @return true|WP_Error
645 */
646 public static function permissions_callback() {
647 return current_user_can( 'manage_options' );
648 }
649
650 /**
651 * Return true if we should initialize the My Jetpack admin page.
652 */
653 public static function should_initialize() {
654 $should = true;
655
656 // All options presented in My Jetpack require a connection to WordPress.com.
657 if ( ( new Status() )->is_offline_mode() ) {
658 $should = false;
659 }
660
661 /**
662 * Allows filtering whether My Jetpack should be initialized.
663 *
664 * @since 0.5.0-alpha
665 *
666 * @param bool $shoud_initialize Should we initialize My Jetpack?
667 */
668 return apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
669 }
670
671 /**
672 * Hook into several connection-based actions to update the historically active Jetpack modules
673 * If the transient that indicates the list needs to be synced, update it and delete the transient
674 *
675 * @return void
676 */
677 public static function setup_historically_active_jetpack_modules_sync() {
678 // yummmm. ham.
679 $ham = new Historically_Active_Modules();
680 if ( get_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
681 $ham::update_historically_active_jetpack_modules();
682 delete_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
683 }
684
685 $actions = array(
686 'jetpack_site_registered',
687 'jetpack_user_authorized',
688 'activated_plugin',
689 );
690
691 foreach ( $actions as $action ) {
692 add_action( $action, array( $ham, 'queue_historically_active_jetpack_modules_update' ), 5 );
693 }
694
695 // Modules are often updated async, so we need to update them right away as there will sometimes be no page reload.
696 add_action( 'jetpack_activate_module', array( $ham, 'update_historically_active_jetpack_modules' ), 5 );
697 }
698
699 /**
700 * Site full-data endpoint.
701 *
702 * @return object Site data.
703 */
704 public static function get_site() {
705 $site_id = \Jetpack_Options::get_option( 'id' );
706 $wpcom_endpoint = sprintf( '/sites/%d?force=wpcom', $site_id );
707 $wpcom_api_version = '1.1';
708 $response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version );
709 $response_code = wp_remote_retrieve_response_code( $response );
710 $body = json_decode( wp_remote_retrieve_body( $response ) );
711
712 if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
713 return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
714 }
715
716 return rest_ensure_response( $body );
717 }
718
719 /**
720 * Populates the self::$site_info var with site data from the /sites/%d endpoint
721 *
722 * @return object|WP_Error
723 */
724 public static function get_site_info() {
725 static $site_info = null;
726
727 if ( $site_info !== null ) {
728 return $site_info;
729 }
730
731 // Check for a cached value before doing lookup
732 $stored_site_info = get_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY );
733 if ( $stored_site_info !== false ) {
734 return $stored_site_info;
735 }
736
737 $response = self::get_site();
738 if ( is_wp_error( $response ) ) {
739 return $response;
740 }
741 $site_info = $response->data;
742 set_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY, $site_info, DAY_IN_SECONDS );
743
744 return $site_info;
745 }
746
747 /**
748 * Returns whether a site has been determined "commercial" or not.
749 *
750 * @return bool|null
751 */
752 public static function is_commercial_site() {
753 if ( is_wp_error( self::$site_info ) ) {
754 return null;
755 }
756
757 return empty( self::$site_info->options->is_commercial ) ? false : self::$site_info->options->is_commercial;
758 }
759
760 /**
761 * Check if site is registered (has been connected before).
762 *
763 * @return bool
764 */
765 public static function is_registered() {
766 return (bool) \Jetpack_Options::get_option( 'id' );
767 }
768
769 /**
770 * Dismiss the welcome banner.
771 *
772 * @return \WP_REST_Response
773 */
774 public static function dismiss_welcome_banner() {
775 \Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
776 return rest_ensure_response( array( 'success' => true ) );
777 }
778
779 /**
780 * Returns "yes" if the site has file write access to the plugins folder, "no" otherwise.
781 *
782 * @return string
783 **/
784 public static function has_file_system_write_access() {
785
786 $cache = get_transient( 'my_jetpack_write_access' );
787
788 if ( false !== $cache ) {
789 return $cache;
790 }
791
792 if ( ! function_exists( 'get_filesystem_method' ) ) {
793 require_once ABSPATH . 'wp-admin/includes/file.php';
794 }
795
796 require_once ABSPATH . 'wp-admin/includes/template.php';
797
798 $write_access = 'no';
799
800 $filesystem_method = get_filesystem_method( array(), WP_PLUGIN_DIR );
801 if ( 'direct' === $filesystem_method ) {
802 $write_access = 'yes';
803 }
804
805 if ( 'no' === $write_access ) {
806 ob_start();
807 $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
808 ob_end_clean();
809
810 if ( $filesystem_credentials_are_stored ) {
811 $write_access = 'yes';
812 }
813 }
814
815 set_transient( 'my_jetpack_write_access', $write_access, 30 * MINUTE_IN_SECONDS );
816
817 return $write_access;
818 }
819
820 /**
821 * Get container IDC for the IDC screen.
822 *
823 * @return string
824 */
825 public static function get_idc_container_id() {
826 return static::IDC_CONTAINER_ID;
827 }
828
829 /**
830 * Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show.
831 *
832 * On My Jetpack page: Uses blocking behavior to fetch fresh data.
833 * On other admin pages: Uses cached data only to avoid blocking, with async JS fetch if cache is empty.
834 *
835 * @return void
836 */
837 public static function maybe_show_red_bubble() {
838 global $pagenow;
839
840 // Don't show red bubble alerts for non-admin users
841 // These alerts are generally only actionable for admins
842 if ( ! current_user_can( 'manage_options' ) ) {
843 return;
844 }
845
846 // Don't show any red bubbles when Jetpack is disconnected
847 // Users can't act on most alerts without a connection
848 $connection = new Connection_Manager();
849 if ( ! $connection->is_connected() ) {
850 return;
851 }
852
853 // Check if we're on the My Jetpack page.
854 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
855 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
856 $is_my_jetpack_page = $pagenow === 'admin.php' && $page === 'my-jetpack';
857
858 if ( $is_my_jetpack_page ) {
859 // On My Jetpack page: use blocking behavior for fresh data.
860 add_filter( 'my_jetpack_red_bubble_notification_slugs', array( Red_Bubble_Notifications::class, 'add_red_bubble_alerts' ) );
861 $red_bubble_alerts = Red_Bubble_Notifications::get_red_bubble_alerts();
862 } else {
863 // On other pages: use cached data only to avoid blocking.
864 $cached_alerts = Red_Bubble_Notifications::get_cached_alerts();
865
866 if ( false === $cached_alerts ) {
867 // No cache: warm it asynchronously via JS. Register a hidden zero-count
868 // placeholder so Menu_Renderer emits a `my-jetpack` badge element the
869 // warmer can reveal (see async-notification-bubble.ts) without a reload.
870 Menu_Badges::init(); // idempotent; wires the renderer.
871 Notification_Counts::register(
872 'my-jetpack',
873 array(
874 'menu_slug' => 'my-jetpack',
875 'count' => 0,
876 'type' => 'count',
877 )
878 );
879 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_red_bubble_script' ) );
880 return;
881 }
882
883 $red_bubble_alerts = $cached_alerts;
884 }
885
886 // Filter out silent alerts.
887 $red_bubble_alerts = array_filter(
888 $red_bubble_alerts,
889 function ( $alert ) {
890 return empty( $alert['is_silent'] );
891 }
892 );
893
894 // Report each non-silent alert to the central menu-badges registry as an
895 // attention entry (count 1). The registry + renderer own the badge.
896 Menu_Badges::init(); // idempotent; wires the renderer.
897 foreach ( array_keys( $red_bubble_alerts ) as $slug ) {
898 // Protect reports its own count directly to the registry, but only when its
899 // standalone plugin is active (see class-jetpack-protect.php::admin_page_init()).
900 // If the standalone plugin isn't active, nobody else registers this count, so we
901 // must not skip it here or the alert silently disappears from the menu total.
902 if ( 'protect_has_threats' === $slug && Products\Protect::is_standalone_plugin_active() ) {
903 continue;
904 }
905 Notification_Counts::register(
906 'my-jetpack-' . $slug,
907 array(
908 'menu_slug' => 'my-jetpack',
909 'type' => 'attention',
910 )
911 );
912 }
913 }
914
915 /**
916 * Enqueue the notification bubble script.
917 * Fetches fresh alert data via REST API without blocking page load.
918 *
919 * @return void
920 */
921 public static function enqueue_red_bubble_script() {
922 Assets::register_script(
923 'my-jetpack-notification-bubble',
924 '../build/async-notification-bubble.js',
925 __FILE__,
926 array(
927 'enqueue' => true,
928 'in_footer' => true,
929 )
930 );
931 }
932
933 /**
934 * Get list of module names sorted by their recommendation score
935 *
936 * @return array|null
937 */
938 public static function get_recommended_modules() {
939 $recommendations_evaluation = \Jetpack_Options::get_option( 'recommendations_evaluation', null );
940
941 if ( ! $recommendations_evaluation ) {
942 return null;
943 }
944
945 arsort( $recommendations_evaluation ); // Sort by scores in descending order
946
947 return array_keys( $recommendations_evaluation ); // Get only module names
948 }
949 }
950