Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
86.47% |
652 / 754 |
|
72.46% |
50 / 69 |
CRAP | |
0.00% |
0 / 1 |
| Initializer | |
86.47% |
652 / 754 |
|
72.46% |
50 / 69 |
455.16 | |
0.00% |
0 / 1 |
| init | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
5 | |||
| use_local_module_options | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
6.05 | |||
| restore_module_options | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| is_licensing_ui_enabled | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
| add_my_jetpack_menu_item | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
| current_user_can_access_page | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
| admin_init | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
5.01 | |||
| is_onboarding_settled | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| maybe_redirect_to_onboarding | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
| get_onboarding_redirect_args_for_step | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
| is_onboarding_available | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| get_partner_coupon_screen | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
8 | |||
| get_onboarding_redirect_args | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
12 | |||
| silence_onboarding_notices | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| hide_onboarding_jitms | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| add_onboarding_admin_body_class | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| can_use_analytics | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| is_onboarding_wizard_enabled | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
| has_never_registered_jetpack | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_onboarding_wizard_state | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| is_my_jetpack_admin_request | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| is_onboarding_request | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| is_onboarding_takeover | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| alias_screen_id_for_wp_build | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| restore_screen_id_after_wp_build | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| should_load_wp_build | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| should_render_wp_build | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| maybe_load_wp_build | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
4.94 | |||
| load_wp_build | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
| require_wp_build_with_screen_alias | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
2.06 | |||
| enqueue_scripts | |
96.92% |
126 / 130 |
|
0.00% |
0 / 1 |
12 | |||
| get_header_state | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| add_script_data | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
| is_modules_rest_api_enabled | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
| get_modules_management_url | |
52.11% |
37 / 71 |
|
0.00% |
0 / 1 |
235.71 | |||
| print_modules_fallback | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
10 | |||
| get_products_section | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| add_admin_script_data | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
| get_registered_admin_page_slugs | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| is_admin_page_available | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| get_assets_url | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_installed_jetpack_plugins | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| get_active_modules | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| is_jetpack_user_new | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
| register_feature_flags | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
1 | |||
| is_header_controls_enabled | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| maybe_enqueue_header_controls_script | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
| get_my_jetpack_flags | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
| is_overview_v1_enabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_modules_to_features_enabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_seo_opt_in_state | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| admin_page | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| register_rest_endpoints | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
3 | |||
| permissions_callback | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| should_initialize | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
7 | |||
| is_offline_features_enabled | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| setup_historically_active_jetpack_modules_sync | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
| get_site | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
| get_site_info | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
5.16 | |||
| is_commercial_site | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
| is_registered | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| dismiss_welcome_banner | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| has_file_system_write_access | |
61.11% |
11 / 18 |
|
0.00% |
0 / 1 |
8.12 | |||
| get_idc_container_id | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| maybe_show_red_bubble | |
96.23% |
51 / 53 |
|
0.00% |
0 / 1 |
12 | |||
| has_connection_error | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
| get_alerts_counted_elsewhere | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
| enqueue_red_bubble_script | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
| get_recommended_modules | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| 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\Error_Handler as Connection_Error_Handler; |
| 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\Connection\REST_Jetpack_AI_JWT; |
| 20 | use Automattic\Jetpack\Constants as Jetpack_Constants; |
| 21 | use Automattic\Jetpack\ExPlat; |
| 22 | use Automattic\Jetpack\Feature_Flags\Feature_Flags; |
| 23 | use Automattic\Jetpack\JITMS\JITM; |
| 24 | use Automattic\Jetpack\Licensing; |
| 25 | use Automattic\Jetpack\Menu_Badges\Menu_Badges; |
| 26 | use Automattic\Jetpack\Menu_Badges\Notification_Counts; |
| 27 | use Automattic\Jetpack\Modules; |
| 28 | use Automattic\Jetpack\Partner_Coupon; |
| 29 | use Automattic\Jetpack\Plugins_Installer; |
| 30 | use Automattic\Jetpack\Status; |
| 31 | use Automattic\Jetpack\Status\Host as Status_Host; |
| 32 | use Automattic\Jetpack\Sync\Functions as Sync_Functions; |
| 33 | use Automattic\Jetpack\Terms_Of_Service; |
| 34 | use Automattic\Jetpack\Tracking; |
| 35 | use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills; |
| 36 | use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id; |
| 37 | use Jetpack; |
| 38 | use WP_Error; |
| 39 | |
| 40 | /** |
| 41 | * The main Initializer class that registers the admin menu and eneuque the assets. |
| 42 | */ |
| 43 | class Initializer { |
| 44 | |
| 45 | /** |
| 46 | * My Jetpack package version |
| 47 | * |
| 48 | * @var string |
| 49 | */ |
| 50 | const PACKAGE_VERSION = '6.14.0'; |
| 51 | |
| 52 | /** |
| 53 | * Feature flag that swaps the single-screen onboarding takeover for the setup wizard. |
| 54 | */ |
| 55 | const ONBOARDING_WIZARD_FEATURE_FLAG = 'my-jetpack-onboarding-wizard'; |
| 56 | |
| 57 | /** |
| 58 | * User option recording that this user chose to skip setup. |
| 59 | * |
| 60 | * Per user rather than per site, because skipping is one person saying "not |
| 61 | * now": a second admin arriving at a site nobody has set up should still be |
| 62 | * offered the flow. Finishing it is the site-wide fact, and that is |
| 63 | * ONBOARDING_COMPLETED_OPTION. |
| 64 | * |
| 65 | * A user option rather than plain user meta: `wp_usermeta` is network-wide, so |
| 66 | * without the blog prefix a skip on one site would silence setup on all of them. |
| 67 | */ |
| 68 | const ONBOARDING_DISMISSED_USER_OPTION = 'jetpack_my_jetpack_onboarding_dismissed'; |
| 69 | |
| 70 | /** |
| 71 | * Option recording that someone has finished setup on this site. |
| 72 | * |
| 73 | * Site-wide, because the flow connects the site and switches modules on, so a |
| 74 | * second admin does not need it again. Deliberately not autoloaded. |
| 75 | */ |
| 76 | const ONBOARDING_COMPLETED_OPTION = 'jetpack_my_jetpack_onboarding_completed'; |
| 77 | |
| 78 | /** |
| 79 | * Handle for the classic script that carries the React initial state. |
| 80 | * |
| 81 | * Plugins that render My Jetpack components on their own pages (Boost) depend on this name. |
| 82 | */ |
| 83 | const DATA_SCRIPT_HANDLE = 'my_jetpack_main_app'; |
| 84 | |
| 85 | /** |
| 86 | * Handle for the webpack bundle that renders the onboarding takeover. |
| 87 | */ |
| 88 | const ONBOARDING_SCRIPT_HANDLE = 'my_jetpack_onboarding'; |
| 89 | |
| 90 | /** |
| 91 | * Handle for the webpack bundle that gives dashboards outside My Jetpack the shared header's controls. |
| 92 | */ |
| 93 | const HEADER_CONTROLS_SCRIPT_HANDLE = 'my-jetpack-header-controls'; |
| 94 | |
| 95 | /** |
| 96 | * The wp-build page ID, which the generated enqueue check expects as the screen ID. |
| 97 | */ |
| 98 | const WP_BUILD_PAGE_ID = 'my-jetpack-dashboard'; |
| 99 | |
| 100 | /** |
| 101 | * HTML container ID for the IDC screen on My Jetpack page. |
| 102 | */ |
| 103 | private const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container'; |
| 104 | |
| 105 | public const JETPACK_PLUGIN_SLUGS = array( |
| 106 | 'jetpack-backup', |
| 107 | 'jetpack-boost', |
| 108 | 'zerobscrm', |
| 109 | 'jetpack', |
| 110 | 'jetpack-protect', |
| 111 | 'jetpack-social', |
| 112 | 'jetpack-videopress', |
| 113 | 'jetpack-search', |
| 114 | 'jetpack-stats', |
| 115 | ); |
| 116 | |
| 117 | private const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info'; |
| 118 | |
| 119 | /** |
| 120 | * Feature flag for connecting the WordPress.com account from a Features card that needs one. |
| 121 | * |
| 122 | * @var string |
| 123 | */ |
| 124 | public const ACCOUNT_CONNECT_FLAG = 'my-jetpack-account-connect'; |
| 125 | |
| 126 | /** |
| 127 | * Feature flag for the shared header's global controls. |
| 128 | * |
| 129 | * @var string |
| 130 | */ |
| 131 | public const HEADER_CONTROLS_FLAG = 'my-jetpack-header-controls'; |
| 132 | |
| 133 | /** |
| 134 | * Feature flag for Overview v1. |
| 135 | * |
| 136 | * @var string |
| 137 | */ |
| 138 | public const OVERVIEW_V1_FLAG = 'my-jetpack-overview-v1'; |
| 139 | |
| 140 | /** |
| 141 | * Feature flag for sending classic Modules links to Features. |
| 142 | * |
| 143 | * @var string |
| 144 | */ |
| 145 | public const MODULES_TO_FEATURES_FLAG = 'my-jetpack-modules-to-features'; |
| 146 | |
| 147 | /** |
| 148 | * Holds info/data about the site (from the /sites/%d endpoint) |
| 149 | * |
| 150 | * @var object |
| 151 | */ |
| 152 | public static $site_info; |
| 153 | |
| 154 | /** |
| 155 | * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored. |
| 156 | * |
| 157 | * @var string|null |
| 158 | */ |
| 159 | private static $wp_build_original_screen_id = null; |
| 160 | |
| 161 | /** |
| 162 | * Initialize My Jetpack |
| 163 | * |
| 164 | * @return void |
| 165 | */ |
| 166 | public static function init() { |
| 167 | self::register_feature_flags(); |
| 168 | |
| 169 | // Before the gate: the Jetpack plugin renders this package's connection screen and footer |
| 170 | // links even where My Jetpack is off, and `myJetpackInitialState` only exists on its own page. |
| 171 | add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_admin_script_data' ) ); |
| 172 | add_action( 'admin_head', array( __CLASS__, 'print_modules_fallback' ) ); |
| 173 | |
| 174 | if ( ! self::should_initialize() || did_action( 'my_jetpack_init' ) ) { |
| 175 | return; |
| 176 | } |
| 177 | |
| 178 | if ( self::is_offline_features_enabled() ) { |
| 179 | add_filter( 'jetpack_my_jetpack_should_enable_add_license_screen', '__return_false' ); |
| 180 | Connection_Rest_Authentication::init(); |
| 181 | add_filter( 'rest_request_before_callbacks', array( __CLASS__, 'use_local_module_options' ), 10, 3 ); |
| 182 | add_filter( 'rest_request_after_callbacks', array( __CLASS__, 'restore_module_options' ), 10, 3 ); |
| 183 | add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) ); |
| 184 | add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 ); |
| 185 | add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) ); |
| 186 | do_action( 'my_jetpack_init' ); |
| 187 | return; |
| 188 | } |
| 189 | |
| 190 | // Answer "is this product on?" for admin menu registration. |
| 191 | Menu_Visibility::init(); |
| 192 | |
| 193 | // Extend jetpack plugins action links. |
| 194 | Products::extend_plugins_action_links(); |
| 195 | |
| 196 | // Set up the REST authentication hooks. |
| 197 | Connection_Rest_Authentication::init(); |
| 198 | |
| 199 | if ( self::is_licensing_ui_enabled() ) { |
| 200 | Licensing::instance()->initialize(); |
| 201 | } |
| 202 | |
| 203 | // Initialize Boost Speed Score |
| 204 | new Speed_Score( array(), 'jetpack-my-jetpack' ); |
| 205 | |
| 206 | // Add custom WP REST API endoints. |
| 207 | add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) ); |
| 208 | |
| 209 | // Both of wp-build's deadlines fall later in this request: its enqueue check must be |
| 210 | // hooked before `admin_enqueue_scripts`, and its render function defined before |
| 211 | // the page callback. |
| 212 | add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 ); |
| 213 | |
| 214 | add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) ); |
| 215 | |
| 216 | add_action( 'admin_init', array( __CLASS__, 'setup_historically_active_jetpack_modules_sync' ) ); |
| 217 | |
| 218 | // Nothing else gets to talk over the takeover. See both methods for why it |
| 219 | // takes two hooks rather than one. |
| 220 | add_action( 'in_admin_header', array( __CLASS__, 'silence_onboarding_notices' ), PHP_INT_MAX ); |
| 221 | add_filter( 'jetpack_display_jitms_on_screen', array( __CLASS__, 'hide_onboarding_jitms' ), 10, 2 ); |
| 222 | // Registered on admin_menu (not admin_init) and well before priority 100000, so the |
| 223 | // counts it registers exist before the menu-badges renderer runs on admin_menu 100000. |
| 224 | add_action( 'admin_menu', array( __CLASS__, 'maybe_show_red_bubble' ), 30 ); |
| 225 | |
| 226 | // Set up the ExPlat package endpoints |
| 227 | ExPlat::init(); |
| 228 | |
| 229 | // Sets up JITMS. |
| 230 | JITM::configure(); |
| 231 | |
| 232 | // Add "Jetpack Manage" menu item. |
| 233 | Jetpack_Manage::init(); |
| 234 | |
| 235 | /** |
| 236 | * Fires after the My Jetpack package is initialized |
| 237 | * |
| 238 | * @since 0.1.0 |
| 239 | */ |
| 240 | do_action( 'my_jetpack_init' ); |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * Keep uncached module-list options local on the offline route. |
| 245 | * |
| 246 | * @since 6.10.0 |
| 247 | * @param mixed $response Response before the callback. |
| 248 | * @param array $handler Route handler. |
| 249 | * @param \WP_REST_Request $request REST request. |
| 250 | * @return mixed |
| 251 | */ |
| 252 | public static function use_local_module_options( $response, $handler, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- WordPress REST filter signature. |
| 253 | if ( '/jetpack/v4/module/all' === $request->get_route() ) { |
| 254 | // Keep module activation editable when the standalone plugin is not running; |
| 255 | // a stale autoloaded VideoPress class can still attach its forcing filter. |
| 256 | if ( ! Jetpack_Constants::is_defined( 'JETPACK_VIDEOPRESS_ROOT_FILE' ) |
| 257 | && ! Products\Videopress::is_standalone_plugin_active() |
| 258 | && remove_filter( 'jetpack_active_modules', array( \Automattic\Jetpack\VideoPress\Module_Control::class, 'add_videopress_to_array' ) ) |
| 259 | && class_exists( 'Jetpack_Modules_Overrides', false ) |
| 260 | ) { |
| 261 | \Jetpack_Modules_Overrides::instance()->clear_cache(); |
| 262 | } |
| 263 | add_filter( 'default_option_monitor_receive_notifications', '__return_false' ); |
| 264 | add_filter( 'default_option_post_by_email_address' . get_current_user_id(), '__return_false' ); |
| 265 | } |
| 266 | return $response; |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Restore normal option reads after the offline module-list callback. |
| 271 | * |
| 272 | * @since 6.10.0 |
| 273 | * @param mixed $response Response after the callback. |
| 274 | * @param array $handler Route handler. |
| 275 | * @param \WP_REST_Request $request REST request. |
| 276 | * @return mixed |
| 277 | */ |
| 278 | public static function restore_module_options( $response, $handler, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- WordPress REST filter signature. |
| 279 | if ( '/jetpack/v4/module/all' === $request->get_route() ) { |
| 280 | remove_filter( 'default_option_monitor_receive_notifications', '__return_false' ); |
| 281 | remove_filter( 'default_option_post_by_email_address' . get_current_user_id(), '__return_false' ); |
| 282 | } |
| 283 | return $response; |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * Acts as a feature flag, returning a boolean for whether we should show the licensing UI. |
| 288 | * |
| 289 | * @since 1.2.0 |
| 290 | * |
| 291 | * @return boolean |
| 292 | */ |
| 293 | public static function is_licensing_ui_enabled() { |
| 294 | // Default changed to true in 1.5.0. |
| 295 | $is_enabled = true; |
| 296 | |
| 297 | /* |
| 298 | * Bail if My Jetpack is not enabled, |
| 299 | * and thus the licensing UI shouldn't be enabled either. |
| 300 | */ |
| 301 | if ( ! self::should_initialize() ) { |
| 302 | $is_enabled = false; |
| 303 | } |
| 304 | |
| 305 | /** |
| 306 | * Acts as a feature flag, returning a boolean for whether we should show the licensing UI. |
| 307 | * |
| 308 | * @param bool $is_enabled Defaults to true. |
| 309 | * |
| 310 | * @since 1.2.0 |
| 311 | * @since 1.5.0 Update default value to true. |
| 312 | */ |
| 313 | return apply_filters( |
| 314 | 'jetpack_my_jetpack_should_enable_add_license_screen', |
| 315 | $is_enabled |
| 316 | ); |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * Add My Jetpack menu item to the admin menu. |
| 321 | * |
| 322 | * @return void |
| 323 | */ |
| 324 | public static function add_my_jetpack_menu_item() { |
| 325 | if ( self::is_offline_features_enabled() && ! self::current_user_can_access_page() ) { |
| 326 | return; |
| 327 | } |
| 328 | |
| 329 | $position = defined( Admin_Menu::class . '::POSITION_FIRST' ) ? Admin_Menu::POSITION_FIRST : -10; |
| 330 | $menu_slug = 'my-jetpack'; |
| 331 | $page_suffix = Admin_Menu::add_menu( |
| 332 | __( 'My Jetpack', 'jetpack-my-jetpack' ), |
| 333 | __( 'My Jetpack', 'jetpack-my-jetpack' ), |
| 334 | 'edit_posts', |
| 335 | $menu_slug, |
| 336 | array( __CLASS__, 'admin_page' ), |
| 337 | $position |
| 338 | ); |
| 339 | add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) ); |
| 340 | // Users who can edit posts but have no Jetpack menu get an admin_page_ hook instead. |
| 341 | add_action( 'load-admin_page_' . $menu_slug, array( __CLASS__, 'admin_init' ) ); |
| 342 | } |
| 343 | |
| 344 | /** |
| 345 | * Whether the current user may access the page selected by My Jetpack. |
| 346 | * |
| 347 | * @since 6.10.0 |
| 348 | * @return bool |
| 349 | */ |
| 350 | public static function current_user_can_access_page() { |
| 351 | return current_user_can( 'edit_posts' ) |
| 352 | && ( ! self::is_offline_features_enabled() || REST_Main_Features::permissions_callback() ); |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Callback for the load my jetpack page hook. |
| 357 | * |
| 358 | * @return void |
| 359 | */ |
| 360 | public static function admin_init() { |
| 361 | if ( self::is_offline_features_enabled() ) { |
| 362 | if ( ! self::current_user_can_access_page() ) { |
| 363 | wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'jetpack-my-jetpack' ), 403 ); |
| 364 | } |
| 365 | add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) ); |
| 366 | return; |
| 367 | } |
| 368 | |
| 369 | self::maybe_redirect_to_onboarding(); |
| 370 | |
| 371 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce needed for redirect flow control |
| 372 | $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : ''; |
| 373 | |
| 374 | // If the user reaches the onboarding page, add a class to the body |
| 375 | if ( $step === 'onboarding' ) { |
| 376 | add_filter( 'admin_body_class', array( __CLASS__, 'add_onboarding_admin_body_class' ) ); |
| 377 | } |
| 378 | |
| 379 | self::$site_info = self::get_site_info(); |
| 380 | add_filter( 'identity_crisis_container_id', array( static::class, 'get_idc_container_id' ) ); |
| 381 | add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) ); |
| 382 | } |
| 383 | |
| 384 | /** |
| 385 | * Whether this user has already settled setup, one way or the other. |
| 386 | * |
| 387 | * Two separate facts. Finishing is site-wide: the wizard connects the site and |
| 388 | * switches modules on, so once it is done nobody needs the takeover again. |
| 389 | * Skipping is this person saying "not now", and it must not answer for the next |
| 390 | * admin who arrives at a site nobody has set up. |
| 391 | * |
| 392 | * @internal Not part of the package's public API. |
| 393 | * |
| 394 | * @return bool |
| 395 | */ |
| 396 | public static function is_onboarding_settled() { |
| 397 | if ( get_option( self::ONBOARDING_COMPLETED_OPTION, false ) ) { |
| 398 | return true; |
| 399 | } |
| 400 | |
| 401 | return (bool) get_user_option( self::ONBOARDING_DISMISSED_USER_OPTION ); |
| 402 | } |
| 403 | |
| 404 | /** |
| 405 | * Send the user into setup when they land on My Jetpack without one. |
| 406 | * |
| 407 | * Runs from My Jetpack's own page load, which nothing but a request for that page |
| 408 | * reaches and which fires before any output, so it needs no guards of its own. |
| 409 | * |
| 410 | * @return void |
| 411 | */ |
| 412 | public static function maybe_redirect_to_onboarding() { |
| 413 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce needed for redirect flow control |
| 414 | $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : ''; |
| 415 | $redirect_args = self::get_onboarding_redirect_args_for_step( $step ); |
| 416 | |
| 417 | if ( null === $redirect_args ) { |
| 418 | return; |
| 419 | } |
| 420 | |
| 421 | $location = wp_sanitize_redirect( add_query_arg( $redirect_args, admin_url( 'admin.php' ) ) ); |
| 422 | |
| 423 | // Remove wp_get_referer filter applied in `fix_redirect` method of `Jetpack_Admin` class |
| 424 | remove_filter( 'wp_redirect', 'wp_get_referer' ); |
| 425 | wp_safe_redirect( $location ); |
| 426 | |
| 427 | exit( 0 ); |
| 428 | } |
| 429 | |
| 430 | /** |
| 431 | * Get the setup redirect for the current user and site at the requested step. |
| 432 | * |
| 433 | * @param string $step The requested setup step, or empty for the dashboard. |
| 434 | * @return array|null Query args for the redirect, or null to stay on the page. |
| 435 | */ |
| 436 | private static function get_onboarding_redirect_args_for_step( $step ) { |
| 437 | $wizard_enabled = self::is_onboarding_wizard_enabled(); |
| 438 | $connection = new Connection_Manager(); |
| 439 | |
| 440 | // Only the wizard connects a user, so only it may count a site-only connection as |
| 441 | // unfinished setup. The single screen can only register the site, so asking it for |
| 442 | // an owner leaves that site on a screen with no way back out. |
| 443 | $connection_done = $wizard_enabled ? $connection->has_connected_owner() : $connection->is_connected(); |
| 444 | |
| 445 | // Onboarding's only action is the connect request, which answers a user |
| 446 | // without `jetpack_connect` with a 403. |
| 447 | return self::get_onboarding_redirect_args( |
| 448 | $step, |
| 449 | $connection_done, |
| 450 | self::is_onboarding_available() && current_user_can( 'jetpack_connect' ), |
| 451 | $wizard_enabled, |
| 452 | // Only the wizard records a settled run, and only a registered site may act on one: |
| 453 | // My Jetpack requires the site connection that accepting the terms creates. |
| 454 | $wizard_enabled && $connection->is_connected() && self::is_onboarding_settled() |
| 455 | ); |
| 456 | } |
| 457 | |
| 458 | /** |
| 459 | * Whether the My Jetpack onboarding flow is available on this site. |
| 460 | * |
| 461 | * WordPress.com Simple sites are connected by definition and don't manage their |
| 462 | * connection through My Jetpack, so the onboarding flow (which asks the user to |
| 463 | * connect) never applies there. A pending partner coupon brings its own connect screen. |
| 464 | * |
| 465 | * @internal Not part of the package's public API. |
| 466 | * |
| 467 | * @return bool |
| 468 | */ |
| 469 | public static function is_onboarding_available() { |
| 470 | if ( self::is_offline_features_enabled() ) { |
| 471 | return false; |
| 472 | } |
| 473 | return ! ( new Status_Host() )->is_wpcom_simple() && null === self::get_partner_coupon_screen(); |
| 474 | } |
| 475 | |
| 476 | /** |
| 477 | * The partner coupon screen's data, when it should replace the dashboard. |
| 478 | * |
| 479 | * Coupons exist only with the Jetpack plugin, which supplies their products and images. |
| 480 | * |
| 481 | * @since 6.5.0 |
| 482 | * |
| 483 | * @return array{coupon: array, assetBaseUrl: string}|null |
| 484 | */ |
| 485 | public static function get_partner_coupon_screen() { |
| 486 | if ( |
| 487 | self::is_offline_features_enabled() |
| 488 | || ! self::should_initialize() |
| 489 | || ! current_user_can( 'manage_options' ) |
| 490 | || ! Jetpack_Constants::is_defined( 'JETPACK__PLUGIN_FILE' ) |
| 491 | ) { |
| 492 | return null; |
| 493 | } |
| 494 | |
| 495 | $coupon = Partner_Coupon::get_coupon(); |
| 496 | if ( ! $coupon ) { |
| 497 | return null; |
| 498 | } |
| 499 | |
| 500 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only picks which screen renders. |
| 501 | if ( ! isset( $_GET['showCouponRedemption'] ) && ( new Connection_Manager() )->has_connected_owner() ) { |
| 502 | return null; |
| 503 | } |
| 504 | |
| 505 | return array( |
| 506 | 'coupon' => $coupon, |
| 507 | 'assetBaseUrl' => plugins_url( '', Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) ), |
| 508 | ); |
| 509 | } |
| 510 | |
| 511 | /** |
| 512 | * Decide whether the current My Jetpack request should redirect, and where to. |
| 513 | * |
| 514 | * @internal Not part of the package's public API. |
| 515 | * |
| 516 | * @param string $step The current `step` query param. |
| 517 | * @param bool $connection_done Whether the connection the takeover asks for is in place: |
| 518 | * a connected owner with the wizard, a connected site without it. |
| 519 | * @param bool $onboarding_available Whether the onboarding flow is available on this site. |
| 520 | * @param bool $wizard_enabled Whether the takeover renders the setup wizard. |
| 521 | * @param bool $is_settled Whether this user has already finished or skipped setup. |
| 522 | * @return array|null Query args for the redirect, or null to stay on the current page. |
| 523 | */ |
| 524 | public static function get_onboarding_redirect_args( $step, $connection_done, $onboarding_available, $wizard_enabled = false, $is_settled = false ) { |
| 525 | /* |
| 526 | * Only the automatic redirect is suppressed once setup is settled. Asking for |
| 527 | * the onboarding page by its own URL still works, so someone who skipped can |
| 528 | * go back to it, and so can a second admin the site-wide flag does not cover. |
| 529 | */ |
| 530 | if ( $onboarding_available && ! $connection_done && ! $is_settled && $step !== 'onboarding' ) { |
| 531 | // The connection setup asks for is not in place, so setup is unfinished. |
| 532 | return array( |
| 533 | 'page' => 'my-jetpack', |
| 534 | 'step' => 'onboarding', |
| 535 | ); |
| 536 | } |
| 537 | |
| 538 | // The wizard connects mid-flow: the user authorizes on WordPress.com and is sent |
| 539 | // back here to finish setup, so arriving connected is not the same as finishing. |
| 540 | if ( $step === 'onboarding' && $connection_done && $onboarding_available && $wizard_enabled ) { |
| 541 | return null; |
| 542 | } |
| 543 | |
| 544 | if ( $step === 'onboarding' && ( ! $onboarding_available || $connection_done ) ) { |
| 545 | // Nothing left to set up here, or onboarding does not apply on this site. |
| 546 | return array( 'page' => 'my-jetpack' ); |
| 547 | } |
| 548 | |
| 549 | return null; |
| 550 | } |
| 551 | |
| 552 | /** |
| 553 | * Empty the notice channels while the takeover is on screen. |
| 554 | * |
| 555 | * The takeover hides `.notice`, `.error` and `.updated` in CSS, which only |
| 556 | * catches notices that use WordPress's own classes. Plenty do not, and a plugin |
| 557 | * announcing itself in its own markup lands on top of a full-screen flow that |
| 558 | * has no way to scroll past it. |
| 559 | * |
| 560 | * On `in_admin_header`, the last hook before `admin-header.php` prints notices, |
| 561 | * so a notice registered late is caught too. |
| 562 | * |
| 563 | * @return void |
| 564 | */ |
| 565 | public static function silence_onboarding_notices() { |
| 566 | if ( ! self::is_onboarding_request() ) { |
| 567 | return; |
| 568 | } |
| 569 | |
| 570 | remove_all_actions( 'admin_notices' ); |
| 571 | remove_all_actions( 'all_admin_notices' ); |
| 572 | remove_all_actions( 'network_admin_notices' ); |
| 573 | remove_all_actions( 'user_admin_notices' ); |
| 574 | } |
| 575 | |
| 576 | /** |
| 577 | * Keep Just In Time Messages off the takeover. |
| 578 | * |
| 579 | * Emptying the notice channels above would also drop the JITM, since that is how |
| 580 | * it renders, but only after it has enqueued its script and stylesheet. This is |
| 581 | * the package's own opt-out and it runs before any of that. |
| 582 | * |
| 583 | * The screen id cannot answer this on its own: the takeover and the My Jetpack |
| 584 | * dashboard are the same screen, and only the query arg separates them. |
| 585 | * |
| 586 | * @param bool $show Whether to show JITMs on this screen. |
| 587 | * @param string $screen_id The current screen's id. |
| 588 | * @return bool |
| 589 | */ |
| 590 | public static function hide_onboarding_jitms( $show, $screen_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Required by the filter signature. |
| 591 | return self::is_onboarding_request() ? false : $show; |
| 592 | } |
| 593 | |
| 594 | /** |
| 595 | * Add a body class to the My Jetpack onboarding page. |
| 596 | * This class hides the WP Admin toolbar and the sidebar menu. |
| 597 | * |
| 598 | * @param string $classes The body classes. |
| 599 | * @return string The modified body classes. |
| 600 | */ |
| 601 | public static function add_onboarding_admin_body_class( $classes ) { |
| 602 | $classes .= ' jetpack-admin-full-screen'; |
| 603 | return $classes; |
| 604 | } |
| 605 | |
| 606 | /** |
| 607 | * Returns whether we are in condition to track to use |
| 608 | * Analytics functionality like Tracks, MC, or GA. |
| 609 | */ |
| 610 | public static function can_use_analytics() { |
| 611 | $status = new Status(); |
| 612 | $connection = new Connection_Manager(); |
| 613 | $tracking = new Tracking( 'jetpack', $connection ); |
| 614 | |
| 615 | return $tracking->should_enable_tracking( new Terms_Of_Service(), $status ); |
| 616 | } |
| 617 | |
| 618 | /** |
| 619 | * Whether the onboarding takeover renders the setup wizard. |
| 620 | * |
| 621 | * Self-hosted only. On WordPress.com the platform owns the connection and nobody chose |
| 622 | * to install Jetpack, so a screen inviting them to start with it and connect an account |
| 623 | * does not apply. `is_wpcom_platform()` covers Simple and WordPress.com on Atomic, and |
| 624 | * requires wpcomsh, so other Atomic-hosted products still count as self-hosted here. |
| 625 | * |
| 626 | * @since 6.11.0 |
| 627 | * |
| 628 | * @return bool |
| 629 | */ |
| 630 | public static function is_onboarding_wizard_enabled() { |
| 631 | if ( ( new Status_Host() )->is_wpcom_platform() ) { |
| 632 | return false; |
| 633 | } |
| 634 | |
| 635 | if ( ! Feature_Flags::is_enabled( self::ONBOARDING_WIZARD_FEATURE_FLAG ) ) { |
| 636 | return false; |
| 637 | } |
| 638 | |
| 639 | // The wizard registers the site at its own first step, which writes the marker |
| 640 | // below, so asking again on the way back from WordPress.com would drop someone |
| 641 | // out of a run already under way. |
| 642 | if ( ( new Connection_Manager() )->is_connected() ) { |
| 643 | return true; |
| 644 | } |
| 645 | |
| 646 | return self::has_never_registered_jetpack(); |
| 647 | } |
| 648 | |
| 649 | /** |
| 650 | * Whether the Jetpack plugin has never registered this site. |
| 651 | * |
| 652 | * `unique_registrations` is written on `jetpack_site_registered` by the Jetpack plugin |
| 653 | * alone, so a site that has only ever run a standalone plugin still reads as new here. |
| 654 | * |
| 655 | * @internal Not part of the package's public API. |
| 656 | * |
| 657 | * @since 6.11.0 |
| 658 | * |
| 659 | * @return bool |
| 660 | */ |
| 661 | public static function has_never_registered_jetpack() { |
| 662 | return ! \Jetpack_Options::get_option( 'unique_registrations' ); |
| 663 | } |
| 664 | |
| 665 | /** |
| 666 | * The wizard's configuration, or null while the takeover stays on its single screen. |
| 667 | * |
| 668 | * @since 6.11.0 |
| 669 | * |
| 670 | * @return array{exitUrl: string, dashboardUrl: string}|null |
| 671 | */ |
| 672 | public static function get_onboarding_wizard_state() { |
| 673 | if ( ! self::is_onboarding_wizard_enabled() ) { |
| 674 | return null; |
| 675 | } |
| 676 | |
| 677 | return array( |
| 678 | // Where skipping the wizard lands: the admin menu is hidden, so leaving is always one click. |
| 679 | 'exitUrl' => admin_url( 'admin.php?page=my-jetpack' ), |
| 680 | // Where leaving Jetpack altogether lands: wp-admin's own dashboard. |
| 681 | 'dashboardUrl' => admin_url(), |
| 682 | ); |
| 683 | } |
| 684 | |
| 685 | /** |
| 686 | * Whether the current request targets the My Jetpack admin page. |
| 687 | * |
| 688 | * @since 6.3.0 |
| 689 | * |
| 690 | * @return bool |
| 691 | */ |
| 692 | public static function is_my_jetpack_admin_request() { |
| 693 | if ( ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 694 | return false; |
| 695 | } |
| 696 | |
| 697 | return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === 'my-jetpack'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 698 | } |
| 699 | |
| 700 | /** |
| 701 | * Whether the current request is the full-viewport onboarding takeover. |
| 702 | * |
| 703 | * Onboarding hides all wp-admin chrome and never renders through wp-build. The page |
| 704 | * is part of the question: `in_admin_header` and the JITM filter run everywhere. |
| 705 | * |
| 706 | * @since 6.3.0 |
| 707 | * |
| 708 | * @return bool |
| 709 | */ |
| 710 | public static function is_onboarding_request() { |
| 711 | if ( ! self::is_my_jetpack_admin_request() ) { |
| 712 | return false; |
| 713 | } |
| 714 | |
| 715 | if ( ! isset( $_GET['step'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 716 | return false; |
| 717 | } |
| 718 | |
| 719 | return sanitize_text_field( wp_unslash( $_GET['step'] ) ) === 'onboarding'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 720 | } |
| 721 | |
| 722 | /** |
| 723 | * Whether this request is the onboarding takeover rather than the dashboard. |
| 724 | * |
| 725 | * Loading wp-build, enqueueing scripts and rendering the page must all agree, |
| 726 | * so they share this one expression. |
| 727 | * |
| 728 | * @since 6.3.0 |
| 729 | * |
| 730 | * @return bool |
| 731 | */ |
| 732 | public static function is_onboarding_takeover() { |
| 733 | return self::is_onboarding_request() && self::is_onboarding_available(); |
| 734 | } |
| 735 | |
| 736 | /** |
| 737 | * Alias the screen ID to satisfy wp-build's generated enqueue check. |
| 738 | * |
| 739 | * @since 6.3.0 |
| 740 | * |
| 741 | * @return void |
| 742 | */ |
| 743 | public static function alias_screen_id_for_wp_build() { |
| 744 | $screen = get_current_screen(); |
| 745 | |
| 746 | if ( ! $screen ) { |
| 747 | return; |
| 748 | } |
| 749 | |
| 750 | self::$wp_build_original_screen_id = $screen->id; |
| 751 | $screen->id = self::WP_BUILD_PAGE_ID; |
| 752 | } |
| 753 | |
| 754 | /** |
| 755 | * Undo alias_screen_id_for_wp_build(), since JITM builds its message path from the screen ID. |
| 756 | * |
| 757 | * @since 6.3.0 |
| 758 | * |
| 759 | * @return void |
| 760 | */ |
| 761 | public static function restore_screen_id_after_wp_build() { |
| 762 | $screen = get_current_screen(); |
| 763 | |
| 764 | if ( ! $screen || null === self::$wp_build_original_screen_id ) { |
| 765 | return; |
| 766 | } |
| 767 | |
| 768 | $screen->id = self::$wp_build_original_screen_id; |
| 769 | self::$wp_build_original_screen_id = null; |
| 770 | } |
| 771 | |
| 772 | /** |
| 773 | * Whether this request should load wp-build at all. |
| 774 | * |
| 775 | * Also what keeps WP_Build_Polyfills from replacing core scripts on every other admin page. |
| 776 | * |
| 777 | * @since 6.3.0 |
| 778 | * |
| 779 | * @return bool |
| 780 | */ |
| 781 | public static function should_load_wp_build() { |
| 782 | return self::is_my_jetpack_admin_request() && ! self::is_onboarding_takeover(); |
| 783 | } |
| 784 | |
| 785 | /** |
| 786 | * Whether this request renders the dashboard through wp-build. |
| 787 | * |
| 788 | * The generated render function is missing where the package was never built. |
| 789 | * |
| 790 | * @since 6.3.0 |
| 791 | * |
| 792 | * @return bool |
| 793 | */ |
| 794 | public static function should_render_wp_build() { |
| 795 | return ! self::is_onboarding_takeover() |
| 796 | && function_exists( 'jetpack_my_jetpack_my_jetpack_dashboard_wp_admin_render_page' ); |
| 797 | } |
| 798 | |
| 799 | /** |
| 800 | * Load wp-build for the My Jetpack dashboard. |
| 801 | * |
| 802 | * @since 6.3.0 |
| 803 | * |
| 804 | * @return void |
| 805 | */ |
| 806 | public static function maybe_load_wp_build() { |
| 807 | if ( ! self::should_load_wp_build() ) { |
| 808 | return; |
| 809 | } |
| 810 | |
| 811 | $build_index = dirname( __DIR__ ) . '/build/build.php'; |
| 812 | |
| 813 | if ( file_exists( $build_index ) ) { |
| 814 | self::load_wp_build( $build_index ); |
| 815 | } |
| 816 | } |
| 817 | |
| 818 | /** |
| 819 | * Require the generated wp-build index and wire it into this request. |
| 820 | * |
| 821 | * @since 6.3.0 |
| 822 | * |
| 823 | * @param string $build_index Path to the generated `build.php`. |
| 824 | * @return void |
| 825 | */ |
| 826 | public static function load_wp_build( $build_index ) { |
| 827 | self::require_wp_build_with_screen_alias( $build_index ); |
| 828 | |
| 829 | // wp-build hooks module registration to wp_default_scripts, which has |
| 830 | // already fired by admin_menu — call it directly or the init module |
| 831 | // never reaches the import map. |
| 832 | if ( function_exists( 'jetpack_my_jetpack_register_script_modules' ) ) { |
| 833 | jetpack_my_jetpack_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes. |
| 834 | } |
| 835 | |
| 836 | WP_Build_Polyfills::register( |
| 837 | 'my-jetpack', |
| 838 | array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS ) |
| 839 | ); |
| 840 | } |
| 841 | |
| 842 | /** |
| 843 | * Require the generated build file with the screen ID aliased across its enqueue check. |
| 844 | * |
| 845 | * @see WP_Build_Screen_Id::load_with_alias() |
| 846 | * @param string $build_index Path to the generated `build.php`. |
| 847 | * @return void |
| 848 | */ |
| 849 | private static function require_wp_build_with_screen_alias( $build_index ) { |
| 850 | // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias(). |
| 851 | if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) { |
| 852 | WP_Build_Screen_Id::load_with_alias( |
| 853 | array( __CLASS__, 'alias_screen_id_for_wp_build' ), |
| 854 | array( __CLASS__, 'restore_screen_id_after_wp_build' ), |
| 855 | function () use ( $build_index ) { |
| 856 | require_once $build_index; |
| 857 | } |
| 858 | ); |
| 859 | return; |
| 860 | } |
| 861 | |
| 862 | add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) ); |
| 863 | require_once $build_index; |
| 864 | add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) ); |
| 865 | } |
| 866 | |
| 867 | /** |
| 868 | * Enqueue admin page assets. |
| 869 | * |
| 870 | * @return void |
| 871 | */ |
| 872 | public static function enqueue_scripts() { |
| 873 | if ( self::is_offline_features_enabled() ) { |
| 874 | $data_handle = self::DATA_SCRIPT_HANDLE; |
| 875 | wp_register_script( $data_handle, false, array(), self::PACKAGE_VERSION, true ); |
| 876 | wp_enqueue_script( $data_handle ); |
| 877 | add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) ); |
| 878 | wp_localize_script( |
| 879 | $data_handle, |
| 880 | 'myJetpackInitialState', |
| 881 | array( |
| 882 | 'isOfflineFeatures' => true, |
| 883 | 'products' => array( 'items' => array() ), |
| 884 | 'adminUrl' => esc_url( admin_url() ), |
| 885 | 'assetsUrl' => self::get_assets_url(), |
| 886 | 'isJetpackPluginActive' => class_exists( 'Jetpack' ), |
| 887 | 'hiddenFeatures' => Feature_Visibility::get_hidden(), |
| 888 | 'deprecatedModules' => Feature_Visibility::get_deprecated_modules(), |
| 889 | 'myJetpackFlags' => self::get_my_jetpack_flags(), |
| 890 | ) |
| 891 | ); |
| 892 | wp_localize_script( |
| 893 | $data_handle, |
| 894 | 'myJetpackRest', |
| 895 | array( |
| 896 | 'apiRoot' => esc_url_raw( rest_url() ), |
| 897 | 'apiNonce' => wp_create_nonce( 'wp_rest' ), |
| 898 | ) |
| 899 | ); |
| 900 | if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) { |
| 901 | wp_enqueue_script( 'wp-jp-i18n-loader' ); |
| 902 | } |
| 903 | self::maybe_enqueue_header_controls_script(); |
| 904 | return; |
| 905 | } |
| 906 | |
| 907 | /** |
| 908 | * Fires after the My Jetpack page is initialized. |
| 909 | * Allows for enqueuing additional scripts only on the My Jetpack page. |
| 910 | * |
| 911 | * @since 4.35.7 |
| 912 | */ |
| 913 | do_action( 'myjetpack_enqueue_scripts' ); |
| 914 | add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) ); |
| 915 | |
| 916 | // Script-less: it only prints the state below, before the dashboard's boot runs on DOMContentLoaded. |
| 917 | $data_handle = self::DATA_SCRIPT_HANDLE; |
| 918 | wp_register_script( $data_handle, false, array(), self::PACKAGE_VERSION, true ); |
| 919 | wp_enqueue_script( $data_handle ); |
| 920 | self::maybe_enqueue_header_controls_script(); |
| 921 | |
| 922 | if ( self::is_my_jetpack_admin_request() && self::is_onboarding_takeover() ) { |
| 923 | Assets::register_script( |
| 924 | self::ONBOARDING_SCRIPT_HANDLE, |
| 925 | '../build/onboarding.js', |
| 926 | __FILE__, |
| 927 | array( |
| 928 | 'dependencies' => array( $data_handle ), |
| 929 | 'enqueue' => true, |
| 930 | 'in_footer' => true, |
| 931 | 'textdomain' => 'jetpack-my-jetpack', |
| 932 | ) |
| 933 | ); |
| 934 | } elseif ( self::should_render_wp_build() && wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) { |
| 935 | // Registered on every admin page but only enqueued when depended on; the esbuild bundles don't pull it in. |
| 936 | wp_enqueue_script( 'wp-jp-i18n-loader' ); |
| 937 | } |
| 938 | |
| 939 | $modules = new Modules(); |
| 940 | $connection = new Connection_Manager(); |
| 941 | $speed_score_history = new Speed_Score_History( get_site_url() ); |
| 942 | $latest_score = $speed_score_history->latest(); |
| 943 | $previous_score = array(); |
| 944 | if ( $speed_score_history->count() > 1 ) { |
| 945 | $previous_score = $speed_score_history->latest( 1 ); |
| 946 | } |
| 947 | $latest_score['previousScores'] = $previous_score['scores'] ?? array(); |
| 948 | |
| 949 | $sandboxed_domain = ''; |
| 950 | $is_dev_version = false; |
| 951 | if ( class_exists( 'Jetpack' ) ) { |
| 952 | $is_dev_version = Jetpack::is_development_version(); |
| 953 | $sandboxed_domain = defined( 'JETPACK__SANDBOX_DOMAIN' ) ? JETPACK__SANDBOX_DOMAIN : ''; |
| 954 | } |
| 955 | |
| 956 | wp_localize_script( |
| 957 | $data_handle, |
| 958 | 'myJetpackInitialState', |
| 959 | array( |
| 960 | 'products' => array( |
| 961 | 'items' => Products::get_products(), |
| 962 | ), |
| 963 | 'mainFeatures' => Main_Features::get_state(), |
| 964 | 'featuresBanner' => array( 'isDismissed' => REST_Main_Features::is_banner_dismissed() ), |
| 965 | 'onboardingWizard' => self::get_onboarding_wizard_state(), |
| 966 | 'header' => self::get_header_state(), |
| 967 | 'plugins' => Plugins_Installer::get_plugins(), |
| 968 | 'themes' => Sync_Functions::get_themes(), |
| 969 | 'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ), |
| 970 | 'myJetpackCheckoutUri' => admin_url( 'admin.php?page=my-jetpack' ), |
| 971 | 'topJetpackMenuItemUrl' => Admin_Menu::get_top_level_menu_item_url(), |
| 972 | 'siteSuffix' => ( new Status() )->get_site_suffix(), |
| 973 | 'siteUrl' => esc_url( get_site_url() ), |
| 974 | 'blogID' => Connection_Manager::get_site_id( true ), |
| 975 | 'myJetpackVersion' => self::PACKAGE_VERSION, |
| 976 | 'myJetpackFlags' => self::get_my_jetpack_flags(), |
| 977 | 'fileSystemWriteAccess' => self::has_file_system_write_access(), |
| 978 | 'loadAddLicenseScreen' => self::is_licensing_ui_enabled(), |
| 979 | 'adminUrl' => esc_url( admin_url() ), |
| 980 | 'assetsUrl' => self::get_assets_url(), |
| 981 | 'IDCContainerID' => static::get_idc_container_id(), |
| 982 | 'userIsAdmin' => current_user_can( 'manage_options' ), |
| 983 | // wp_localize_script() decodes the HTML entities WordPress stores names with. |
| 984 | 'userFirstName' => (string) wp_get_current_user()->first_name, |
| 985 | 'lifecycleStats' => array( |
| 986 | 'jetpackPlugins' => self::get_installed_jetpack_plugins(), |
| 987 | 'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ), |
| 988 | 'brokenModules' => Red_Bubble_Notifications::check_for_broken_modules(), |
| 989 | 'isSiteConnected' => $connection->is_connected(), |
| 990 | 'isUserConnected' => $connection->is_user_connected(), |
| 991 | 'modules' => self::get_active_modules(), |
| 992 | ), |
| 993 | 'recommendedModules' => array( |
| 994 | 'modules' => self::get_recommended_modules(), |
| 995 | 'isFirstRun' => \Jetpack_Options::get_option( 'recommendations_first_run', true ), |
| 996 | 'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ), |
| 997 | ), |
| 998 | 'isStatsModuleActive' => $modules->is_active( 'stats' ), |
| 999 | 'canUserViewStats' => current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ), |
| 1000 | 'hiddenFeatures' => Feature_Visibility::get_hidden(), |
| 1001 | 'deprecatedModules' => Feature_Visibility::get_deprecated_modules(), |
| 1002 | 'sandboxedDomain' => $sandboxed_domain, |
| 1003 | 'isDevVersion' => $is_dev_version, |
| 1004 | 'isAtomic' => ( new Status_Host() )->is_woa_site(), |
| 1005 | 'isJetpackPluginActive' => class_exists( 'Jetpack' ), |
| 1006 | 'latestBoostSpeedScores' => $latest_score, |
| 1007 | 'seoOptIn' => self::get_seo_opt_in_state(), |
| 1008 | 'protectPageModuleUrls' => (object) Products\Protect::get_protect_page_module_urls(), |
| 1009 | 'partnerCoupon' => self::get_partner_coupon_screen(), |
| 1010 | ) |
| 1011 | ); |
| 1012 | |
| 1013 | wp_localize_script( |
| 1014 | $data_handle, |
| 1015 | 'myJetpackRest', |
| 1016 | array( |
| 1017 | 'apiRoot' => esc_url_raw( rest_url() ), |
| 1018 | 'apiNonce' => wp_create_nonce( 'wp_rest' ), |
| 1019 | ) |
| 1020 | ); |
| 1021 | |
| 1022 | // Connection Initial State. |
| 1023 | Connection_Initial_State::render_script( $data_handle ); |
| 1024 | |
| 1025 | // Required for Analytics. |
| 1026 | if ( self::can_use_analytics() ) { |
| 1027 | Tracking::register_tracks_functions_scripts( true ); |
| 1028 | } |
| 1029 | } |
| 1030 | |
| 1031 | /** |
| 1032 | * What the shared header needs beyond the rest of the state, so it renders without a request. |
| 1033 | * |
| 1034 | * @since 6.10.0 |
| 1035 | * |
| 1036 | * @return array{connectorsUrl: string|null} The state. |
| 1037 | */ |
| 1038 | public static function get_header_state() { |
| 1039 | return array( |
| 1040 | // The same probe the connection package uses: the core screen ships with WordPress 7.0. |
| 1041 | // `connector` names the connection package's card, which opens its details for it. |
| 1042 | 'connectorsUrl' => file_exists( ABSPATH . 'wp-admin/options-connectors.php' ) ? admin_url( 'options-connectors.php?connector=wordpress_com' ) : null, |
| 1043 | ); |
| 1044 | } |
| 1045 | |
| 1046 | /** |
| 1047 | * Add My Jetpack data to the unified script data object. |
| 1048 | * |
| 1049 | * @param array $data The script data. |
| 1050 | * @return array |
| 1051 | */ |
| 1052 | public static function add_script_data( $data ) { |
| 1053 | $block_availability = ! self::is_offline_features_enabled() && class_exists( '\Jetpack_Gutenberg' ) |
| 1054 | ? \Jetpack_Gutenberg::get_cached_availability() |
| 1055 | : array(); |
| 1056 | |
| 1057 | $data['myJetpack']['siteEditor'] = array( |
| 1058 | 'isBlockTheme' => function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(), |
| 1059 | 'isSharingBlockAvailable' => isset( $block_availability['sharing-buttons'] ) |
| 1060 | && $block_availability['sharing-buttons']['available'], |
| 1061 | 'isLikeBlockAvailable' => isset( $block_availability['like'] ) |
| 1062 | && $block_availability['like']['available'], |
| 1063 | 'activeThemeStylesheet' => get_stylesheet(), |
| 1064 | ); |
| 1065 | |
| 1066 | return $data; |
| 1067 | } |
| 1068 | |
| 1069 | /** |
| 1070 | * Whether module management can use the REST API. |
| 1071 | * |
| 1072 | * @since 6.11.0 |
| 1073 | * @return bool |
| 1074 | */ |
| 1075 | public static function is_modules_rest_api_enabled() { |
| 1076 | /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ |
| 1077 | $enabled = apply_filters( 'rest_enabled', true ); |
| 1078 | $user = wp_get_current_user(); |
| 1079 | $auth = false; |
| 1080 | // Passing true skips WordPress's nonce check, which would sign the user out; restore the user if another filter clears it. |
| 1081 | try { |
| 1082 | /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ |
| 1083 | $auth = apply_filters( 'rest_authentication_errors', true ); |
| 1084 | } finally { |
| 1085 | wp_set_current_user( $user->ID ); |
| 1086 | } |
| 1087 | return $enabled && ! is_wp_error( $enabled ) && false !== $auth && ! is_wp_error( $auth ); |
| 1088 | } |
| 1089 | |
| 1090 | /** |
| 1091 | * Get a Features management link with a classic Modules fallback. |
| 1092 | * |
| 1093 | * @since 6.11.0 |
| 1094 | * @param array $args Query arguments from a classic Modules screen link; only `s` and `module_tag` can carry over to Features. |
| 1095 | * @param bool|null $eligible Set to true when the URL points at the Features tab, false when it is the classic Modules screen. |
| 1096 | * @return string The management URL. |
| 1097 | */ |
| 1098 | public static function get_modules_management_url( $args = array(), &$eligible = null ) { |
| 1099 | $eligible = false; |
| 1100 | /** |
| 1101 | * Filters whether links to the classic Modules screen may send people to the Features tab in My Jetpack instead. |
| 1102 | * |
| 1103 | * @since 6.11.0 |
| 1104 | * @param bool $available True to allow the Features tab; false keeps the classic Modules screen, for a host where My Jetpack is not shown. |
| 1105 | */ |
| 1106 | $available = apply_filters( 'jetpack_my_jetpack_modules_management_available', true ); |
| 1107 | $available = $available && self::is_modules_to_features_enabled(); |
| 1108 | $classic = add_query_arg( array_merge( urlencode_deep( $args ), array( 'page' => 'jetpack_modules' ) ), admin_url( 'admin.php' ) ); |
| 1109 | if ( |
| 1110 | ! $available |
| 1111 | || array_diff( array_keys( $args ), array( 's', 'module_tag' ) ) |
| 1112 | || array_filter( $args, 'is_string' ) !== $args |
| 1113 | // Keep links on Modules unless Features can show controls for this installation. |
| 1114 | || Main_Features::PLUGIN_ACTIVE !== Main_Features::get_plugin_status( Product::JETPACK_PLUGIN_SLUG ) |
| 1115 | || ! current_user_can( 'jetpack_manage_modules' ) |
| 1116 | || ! current_user_can( 'manage_options' ) |
| 1117 | || ! self::current_user_can_access_page() |
| 1118 | || ( new Status() )->is_offline_mode() |
| 1119 | || ( new Status_Host() )->is_wpcom_simple() |
| 1120 | || is_network_admin() |
| 1121 | || ! self::should_initialize() |
| 1122 | || ! ( new Connection_Manager() )->is_connected() |
| 1123 | || null !== self::get_partner_coupon_screen() |
| 1124 | // Predict My Jetpack's setup redirect for a Features URL with no step; keep both decisions in sync. |
| 1125 | || null !== self::get_onboarding_redirect_args_for_step( '' ) |
| 1126 | || ! self::is_modules_rest_api_enabled() |
| 1127 | ) { |
| 1128 | return $classic; |
| 1129 | } |
| 1130 | |
| 1131 | if ( isset( $args['module_tag'] ) && '' !== $args['module_tag'] ) { |
| 1132 | if ( ! function_exists( 'jetpack_get_module_i18n_tag' ) ) { |
| 1133 | return $classic; |
| 1134 | } |
| 1135 | $known = array(); |
| 1136 | $modules = new Modules(); |
| 1137 | $hidden = Main_Features::get_hidden_modules(); |
| 1138 | $deprecated = Feature_Visibility::get_deprecated_modules(); |
| 1139 | foreach ( $modules->get_available() as $slug ) { |
| 1140 | if ( in_array( $slug, $hidden, true ) || ( in_array( $slug, $deprecated, true ) && ! $modules->is_active( $slug ) ) ) { |
| 1141 | continue; |
| 1142 | } |
| 1143 | $module = $modules->get( $slug ); |
| 1144 | $known = array_merge( $known, $module['module_tags'] ?? array() ); |
| 1145 | } |
| 1146 | $known = array_map( array( Jetpack::class, 'translate_module_tag' ), $known ); |
| 1147 | if ( ! in_array( $args['module_tag'], $known, true ) ) { |
| 1148 | return $classic; |
| 1149 | } |
| 1150 | } |
| 1151 | |
| 1152 | if ( isset( $args['s'] ) && '' !== $args['s'] ) { |
| 1153 | require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; |
| 1154 | $hidden = Main_Features::get_hidden_modules(); |
| 1155 | $deprecated = Feature_Visibility::get_deprecated_modules(); |
| 1156 | $hidden_match = false; |
| 1157 | $visible_match = false; |
| 1158 | foreach ( Jetpack::get_translated_modules( \Jetpack_Admin::init()->get_modules() ) as $slug => $module ) { |
| 1159 | if ( ! empty( $args['module_tag'] ) && ! in_array( $args['module_tag'], $module['module_tags'], true ) ) { |
| 1160 | continue; |
| 1161 | } |
| 1162 | $text = $module['name'] . ' ' . $module['description'] . ' ' . $module['long_description'] . ' ' . $module['search_terms'] . ' ' . implode( ',', $module['module_tags'] ); |
| 1163 | $match = function_exists( 'mb_stripos' ) ? mb_stripos( $text, $args['s'] ) : stripos( $text, $args['s'] ); |
| 1164 | if ( false === $match ) { |
| 1165 | continue; |
| 1166 | } |
| 1167 | if ( in_array( $slug, $hidden, true ) || ( in_array( $slug, $deprecated, true ) && ! $module['activated'] ) ) { |
| 1168 | $hidden_match = true; |
| 1169 | } else { |
| 1170 | $visible_match = true; |
| 1171 | break; |
| 1172 | } |
| 1173 | } |
| 1174 | if ( $hidden_match && ! $visible_match ) { |
| 1175 | return $classic; |
| 1176 | } |
| 1177 | } |
| 1178 | |
| 1179 | $eligible = true; |
| 1180 | $hash = array(); |
| 1181 | if ( isset( $args['s'] ) && '' !== $args['s'] ) { |
| 1182 | $hash['search'] = $args['s']; |
| 1183 | } |
| 1184 | if ( isset( $args['module_tag'] ) && '' !== $args['module_tag'] ) { |
| 1185 | $hash['module_tag'] = $args['module_tag']; |
| 1186 | } |
| 1187 | return add_query_arg( |
| 1188 | array_merge( |
| 1189 | urlencode_deep( $args ), |
| 1190 | array( |
| 1191 | 'page' => 'my-jetpack', |
| 1192 | 'modules_fallback' => '1', |
| 1193 | ) |
| 1194 | ), |
| 1195 | admin_url( 'admin.php' ) |
| 1196 | ) |
| 1197 | . '#/features' . ( $hash ? '?' . http_build_query( $hash, '', '&', PHP_QUERY_RFC3986 ) : '' ); |
| 1198 | } |
| 1199 | |
| 1200 | /** |
| 1201 | * With `my-jetpack-modules-to-features` on, send My Jetpack requests with `modules_fallback=1` back to classic Modules: |
| 1202 | * always without JavaScript, and for everyone when REST is unavailable. |
| 1203 | * |
| 1204 | * @since 6.11.0 |
| 1205 | * @return void |
| 1206 | */ |
| 1207 | public static function print_modules_fallback() { |
| 1208 | // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only navigation fallback. |
| 1209 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Only the exact allowlisted marker is accepted. |
| 1210 | if ( ! self::is_modules_to_features_enabled() || 'my-jetpack' !== ( $_GET['page'] ?? null ) || '1' !== ( $_GET['modules_fallback'] ?? null ) || Main_Features::PLUGIN_ACTIVE !== Main_Features::get_plugin_status( Product::JETPACK_PLUGIN_SLUG ) || ! current_user_can( 'jetpack_manage_modules' ) ) { |
| 1211 | return; |
| 1212 | } |
| 1213 | $args = array( 'page' => 'jetpack_modules' ); |
| 1214 | foreach ( array( 's', 'module_tag' ) as $key ) { |
| 1215 | if ( isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ) { |
| 1216 | $args[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $key ] ) ); |
| 1217 | } |
| 1218 | } |
| 1219 | // phpcs:enable WordPress.Security.NonceVerification.Recommended |
| 1220 | $meta = '<meta http-equiv="refresh" content="0; url=' . esc_url( add_query_arg( urlencode_deep( $args ), admin_url( 'admin.php' ) ) ) . '">'; |
| 1221 | echo '<noscript>' . $meta . '</noscript>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- URL escaped above. |
| 1222 | if ( ! self::is_modules_rest_api_enabled() ) { |
| 1223 | echo $meta; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- URL escaped above. |
| 1224 | } |
| 1225 | } |
| 1226 | |
| 1227 | /** |
| 1228 | * Get the slug and label of the Features tab, for footer links to it. |
| 1229 | * |
| 1230 | * Kept for footers built before the Products tab was removed: Jetpack 16.3 calls this, and |
| 1231 | * older JS footers read it from script data. Without it they link to "Products". |
| 1232 | * |
| 1233 | * @since 6.4.0 |
| 1234 | * |
| 1235 | * @return array{slug: string, label: string} |
| 1236 | */ |
| 1237 | public static function get_products_section() { |
| 1238 | return array( |
| 1239 | 'slug' => 'features', |
| 1240 | 'label' => _x( 'Features', 'Navigation item', 'jetpack-my-jetpack' ), |
| 1241 | ); |
| 1242 | } |
| 1243 | |
| 1244 | /** |
| 1245 | * Add My Jetpack's availability, image base URL and link targets to admin script data. |
| 1246 | * |
| 1247 | * Printed on every admin page by Script_Data, so the connection screen can resolve its |
| 1248 | * illustrations and Jetpack footers know whether to link to My Jetpack. |
| 1249 | * |
| 1250 | * @since 6.3.0 |
| 1251 | * |
| 1252 | * @param array $data Script data. |
| 1253 | * @return array |
| 1254 | */ |
| 1255 | public static function add_admin_script_data( $data ) { |
| 1256 | $data['myJetpack']['isAvailable'] = self::is_admin_page_available(); |
| 1257 | $data['myJetpack']['assetsUrl'] = self::get_assets_url(); |
| 1258 | $data['myJetpack']['productsSection'] = self::get_products_section(); |
| 1259 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only selects which navigation data to seed. |
| 1260 | if ( in_array( $_GET['page'] ?? '', array( 'my-jetpack', 'jetpack-settings', 'jetpack' ), true ) ) { |
| 1261 | $data['myJetpack']['modulesManagementUrl'] = self::get_modules_management_url(); |
| 1262 | } |
| 1263 | |
| 1264 | // Only administrators can open the pages this points at, so spare everyone else the lookup. |
| 1265 | if ( current_user_can( 'manage_options' ) ) { |
| 1266 | $data['myJetpack']['aiManageUrl'] = esc_url_raw( Products\Jetpack_Ai::get_manage_url() ); |
| 1267 | } |
| 1268 | |
| 1269 | if ( |
| 1270 | self::is_my_jetpack_admin_request() |
| 1271 | && self::is_offline_features_enabled() |
| 1272 | && self::should_initialize() |
| 1273 | && REST_Main_Features::permissions_callback() |
| 1274 | ) { |
| 1275 | $data['myJetpack']['offlineFeatures'] = array( |
| 1276 | 'mainFeatures' => Main_Features::get_state( true ), |
| 1277 | 'plugins' => Plugins_Installer::get_plugins(), |
| 1278 | ); |
| 1279 | $data['myJetpack']['offlineFeatures']['mainFeatures']['available_admin_pages'] = self::get_registered_admin_page_slugs(); |
| 1280 | } |
| 1281 | |
| 1282 | return $data; |
| 1283 | } |
| 1284 | |
| 1285 | /** |
| 1286 | * Get the slugs of registered admin pages. |
| 1287 | * |
| 1288 | * Skip hooks containing `?`: they are `edit.php?post_type=...` style pages, not page slugs. |
| 1289 | * |
| 1290 | * @return string[] |
| 1291 | */ |
| 1292 | private static function get_registered_admin_page_slugs() { |
| 1293 | global $_registered_pages; |
| 1294 | |
| 1295 | $hooks = array_filter( |
| 1296 | array_keys( $_registered_pages ?? array() ), |
| 1297 | static fn( $hook ) => false !== strpos( $hook, '_page_' ) && false === strpos( $hook, '?' ) |
| 1298 | ); |
| 1299 | |
| 1300 | return array_values( |
| 1301 | array_unique( |
| 1302 | array_map( static fn( $hook ) => substr( $hook, strpos( $hook, '_page_' ) + 6 ), $hooks ) |
| 1303 | ) |
| 1304 | ); |
| 1305 | } |
| 1306 | |
| 1307 | /** |
| 1308 | * Whether My Jetpack's admin page is available to the current user. |
| 1309 | * |
| 1310 | * Meaningful only after admin_menu has registered the page. |
| 1311 | * |
| 1312 | * @since 6.5.0 |
| 1313 | * |
| 1314 | * @return bool |
| 1315 | */ |
| 1316 | public static function is_admin_page_available() { |
| 1317 | global $_registered_pages; |
| 1318 | |
| 1319 | return did_action( 'my_jetpack_init' ) > 0 |
| 1320 | && isset( $_registered_pages[ get_plugin_page_hookname( 'my-jetpack', 'jetpack' ) ] ) |
| 1321 | && current_user_can( 'edit_posts' ); |
| 1322 | } |
| 1323 | |
| 1324 | /** |
| 1325 | * Get the base URL of the package's built images, with a trailing slash. |
| 1326 | * |
| 1327 | * @since 6.3.0 |
| 1328 | * |
| 1329 | * @return string |
| 1330 | */ |
| 1331 | public static function get_assets_url() { |
| 1332 | return trailingslashit( Assets::normalize_path( plugins_url( '../build/images/', __FILE__ ) ) ); |
| 1333 | } |
| 1334 | |
| 1335 | /** |
| 1336 | * Get installed Jetpack plugins |
| 1337 | * |
| 1338 | * @return array |
| 1339 | */ |
| 1340 | public static function get_installed_jetpack_plugins() { |
| 1341 | $plugin_slugs = array_keys( Plugins_Installer::get_plugins() ); |
| 1342 | $plugin_slugs = array_map( |
| 1343 | static function ( $slug ) { |
| 1344 | $parts = explode( '/', $slug ); |
| 1345 | // Return the last segment of the filepath without the PHP extension |
| 1346 | return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] ); |
| 1347 | }, |
| 1348 | $plugin_slugs |
| 1349 | ); |
| 1350 | |
| 1351 | return array_values( array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs ) ); |
| 1352 | } |
| 1353 | |
| 1354 | /** |
| 1355 | * Get active modules (except ones enabled by default) |
| 1356 | * |
| 1357 | * @return array |
| 1358 | */ |
| 1359 | public static function get_active_modules() { |
| 1360 | $modules = new Modules(); |
| 1361 | $active_modules = $modules->get_active(); |
| 1362 | |
| 1363 | // if the Jetpack plugin is active, filter out the modules that are active by default |
| 1364 | if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) { |
| 1365 | $active_modules = array_diff( $active_modules, Jetpack::get_default_modules() ); |
| 1366 | } |
| 1367 | return array_values( $active_modules ); |
| 1368 | } |
| 1369 | |
| 1370 | /** |
| 1371 | * Determine if the current user is "new" to Jetpack |
| 1372 | * This is used to vary some messaging in My Jetpack |
| 1373 | * |
| 1374 | * On the front-end, purchases are also taken into account |
| 1375 | * |
| 1376 | * @return bool |
| 1377 | */ |
| 1378 | public static function is_jetpack_user_new() { |
| 1379 | // is the user connected? |
| 1380 | $connection = new Connection_Manager(); |
| 1381 | if ( $connection->is_user_connected() ) { |
| 1382 | return false; |
| 1383 | } |
| 1384 | |
| 1385 | // TODO: add a data point for the last known connection/ disconnection time |
| 1386 | |
| 1387 | // are any modules active? |
| 1388 | $active_modules = self::get_active_modules(); |
| 1389 | if ( ! empty( $active_modules ) ) { |
| 1390 | return false; |
| 1391 | } |
| 1392 | |
| 1393 | // check for other Jetpack plugins that are installed on the site (active or not) |
| 1394 | // If there's more than one Jetpack plugin active, this user is not "new" |
| 1395 | $plugin_slugs = array_keys( Plugins_Installer::get_plugins() ); |
| 1396 | $plugin_slugs = array_map( |
| 1397 | static function ( $slug ) { |
| 1398 | $parts = explode( '/', $slug ); |
| 1399 | // Return the last segment of the filepath without the PHP extension |
| 1400 | return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] ); |
| 1401 | }, |
| 1402 | $plugin_slugs |
| 1403 | ); |
| 1404 | $installed_jetpack_plugins = array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs ); |
| 1405 | if ( is_countable( $installed_jetpack_plugins ) && count( $installed_jetpack_plugins ) >= 2 ) { |
| 1406 | return false; |
| 1407 | } |
| 1408 | |
| 1409 | // Does the site have any purchases? |
| 1410 | $purchases = Wpcom_Products::get_site_current_purchases(); |
| 1411 | if ( ! empty( $purchases ) && ! is_wp_error( $purchases ) ) { |
| 1412 | return false; |
| 1413 | } |
| 1414 | |
| 1415 | return true; |
| 1416 | } |
| 1417 | |
| 1418 | /** |
| 1419 | * Register My Jetpack's feature flags. |
| 1420 | * |
| 1421 | * @since 6.11.0 |
| 1422 | * |
| 1423 | * @return void |
| 1424 | */ |
| 1425 | public static function register_feature_flags() { |
| 1426 | Feature_Flags::register( |
| 1427 | self::ACCOUNT_CONNECT_FLAG, |
| 1428 | array( |
| 1429 | 'default' => true, |
| 1430 | 'description' => 'Connect the account from the Install or Connect button of Features cards that need one, hide Active and Inactive where the control already shows the state, exclude features from Available when their only action is an install the user cannot make, and show host restrictions below card descriptions.', |
| 1431 | 'owner' => 'my-jetpack', |
| 1432 | ) |
| 1433 | ); |
| 1434 | Feature_Flags::register( |
| 1435 | self::HEADER_CONTROLS_FLAG, |
| 1436 | array( |
| 1437 | 'default' => true, |
| 1438 | 'description' => 'Show the global controls in the shared header of My Jetpack and Boost.', |
| 1439 | 'owner' => 'my-jetpack', |
| 1440 | ) |
| 1441 | ); |
| 1442 | Feature_Flags::register( |
| 1443 | self::OVERVIEW_V1_FLAG, |
| 1444 | array( |
| 1445 | 'default' => false, |
| 1446 | 'description' => 'Show Overview v1 in My Jetpack.', |
| 1447 | 'owner' => 'my-jetpack', |
| 1448 | ) |
| 1449 | ); |
| 1450 | Feature_Flags::register( |
| 1451 | self::MODULES_TO_FEATURES_FLAG, |
| 1452 | array( |
| 1453 | 'default' => true, |
| 1454 | 'description' => 'Open supported classic Modules links in Features.', |
| 1455 | 'owner' => 'my-jetpack', |
| 1456 | ) |
| 1457 | ); |
| 1458 | } |
| 1459 | |
| 1460 | /** |
| 1461 | * Whether the shared header shows its controls: the flag is on, My Jetpack runs, and not in offline Features. |
| 1462 | * |
| 1463 | * Boost's dashboard gets My Jetpack's page state even where My Jetpack is off, hence the second check. |
| 1464 | * Offline Features prints no connection state for the controls to read, and has no connection to show. |
| 1465 | * |
| 1466 | * @return bool |
| 1467 | */ |
| 1468 | private static function is_header_controls_enabled() { |
| 1469 | return Feature_Flags::is_enabled( self::HEADER_CONTROLS_FLAG ) |
| 1470 | && self::should_initialize() |
| 1471 | && ! self::is_offline_features_enabled(); |
| 1472 | } |
| 1473 | |
| 1474 | /** |
| 1475 | * Give dashboards outside My Jetpack, such as Boost's, the header's controls as a shared script. |
| 1476 | * |
| 1477 | * Bundling them instead would ship a second copy of the connection package next to the page's own. |
| 1478 | * |
| 1479 | * @return void |
| 1480 | */ |
| 1481 | private static function maybe_enqueue_header_controls_script() { |
| 1482 | if ( self::is_my_jetpack_admin_request() || ! self::is_header_controls_enabled() ) { |
| 1483 | return; |
| 1484 | } |
| 1485 | |
| 1486 | Assets::register_script( |
| 1487 | self::HEADER_CONTROLS_SCRIPT_HANDLE, |
| 1488 | '../build/header-controls.js', |
| 1489 | __FILE__, |
| 1490 | array( |
| 1491 | 'dependencies' => array( self::DATA_SCRIPT_HANDLE ), |
| 1492 | 'enqueue' => true, |
| 1493 | 'in_footer' => true, |
| 1494 | 'textdomain' => 'jetpack-my-jetpack', |
| 1495 | ) |
| 1496 | ); |
| 1497 | } |
| 1498 | |
| 1499 | /** |
| 1500 | * Build flags for My Jetpack UI |
| 1501 | * |
| 1502 | * @return array |
| 1503 | */ |
| 1504 | public static function get_my_jetpack_flags() { |
| 1505 | $flags = array( |
| 1506 | 'videoPressStats' => Jetpack_Constants::is_true( 'JETPACK_MY_JETPACK_VIDEOPRESS_STATS_ENABLED' ), |
| 1507 | 'showFullJetpackStatsCard' => class_exists( 'Jetpack' ) || Products\Stats::is_standalone_plugin_active(), |
| 1508 | // Only says which destination `manage_url` is: the legacy Stats page |
| 1509 | // caches its report and wants a `force_refresh` hint the dashboard does not. |
| 1510 | 'premiumAnalyticsEnabled' => Products\Stats::is_premium_analytics_enabled(), |
| 1511 | 'showAiModuleToggle' => Products\Jetpack_Ai::is_feature_ui_enabled(), |
| 1512 | 'headerControls' => self::is_header_controls_enabled(), |
| 1513 | 'accountConnect' => Feature_Flags::is_enabled( self::ACCOUNT_CONNECT_FLAG ), |
| 1514 | 'overviewV1' => self::is_overview_v1_enabled(), |
| 1515 | ); |
| 1516 | |
| 1517 | if ( $flags['accountConnect'] ) { |
| 1518 | $flags['showAiModuleToggleOnceConnected'] = Products\Jetpack_Ai::is_feature_ui_waiting_for_owner(); |
| 1519 | } |
| 1520 | |
| 1521 | return $flags; |
| 1522 | } |
| 1523 | |
| 1524 | /** |
| 1525 | * Whether My Jetpack shows Overview v1. |
| 1526 | * |
| 1527 | * @since 6.10.0 |
| 1528 | * @return bool |
| 1529 | */ |
| 1530 | public static function is_overview_v1_enabled() { |
| 1531 | return Feature_Flags::is_enabled( self::OVERVIEW_V1_FLAG ); |
| 1532 | } |
| 1533 | |
| 1534 | /** |
| 1535 | * Whether Modules links may use Features. |
| 1536 | * |
| 1537 | * @return bool |
| 1538 | */ |
| 1539 | private static function is_modules_to_features_enabled() { |
| 1540 | return Feature_Flags::is_enabled( self::MODULES_TO_FEATURES_FLAG ); |
| 1541 | } |
| 1542 | |
| 1543 | /** |
| 1544 | * Build the state the My Jetpack "try the new SEO experience" opt-in card hydrates from. |
| 1545 | * |
| 1546 | * The card invites an existing self-hosted install to switch over to the new Jetpack SEO |
| 1547 | * dashboard (JETPACK-1700). Gating lives server-side, where the signals actually are. The |
| 1548 | * card shows only when all of: |
| 1549 | * |
| 1550 | * - the new SEO product is available — the `rsm_jetpack_seo` feature filter is on (the SEO |
| 1551 | * package autoloads regardless, so `class_exists()` alone isn't enough; the filter is the |
| 1552 | * real availability switch and the same one the SEO package gates its own surface behind); |
| 1553 | * - the site is self-hosted — WordPress.com (Simple + Atomic) decides its own SEO surface, so |
| 1554 | * the opt-in card is for self-hosted installs only; |
| 1555 | * - the install hasn't opted in yet — the SEO surface isn't visible. On wpcom |
| 1556 | * the SEO package's `is_seo_surface_visible()` short-circuits to `true`, so the |
| 1557 | * "not visible yet" check also doubles as the self-hosted guard, but we check the platform |
| 1558 | * explicitly for clarity. |
| 1559 | * |
| 1560 | * Referenced through `class_exists()` rather than a hard composer dependency: both packages |
| 1561 | * ship inside the Jetpack plugin and the SEO surface is feature-flagged, so a guarded read of |
| 1562 | * its public API keeps this from adding plumbing to consumers that don't load SEO. |
| 1563 | * |
| 1564 | * The on-success destination is computed by the opt-in endpoint itself; we only seed the card |
| 1565 | * with the same admin URL so the button has a sensible fallback before the request resolves. |
| 1566 | * |
| 1567 | * @return array{showCard: bool, redirect: string} |
| 1568 | */ |
| 1569 | public static function get_seo_opt_in_state() { |
| 1570 | // Guard with method_exists rather than class_exists: an older bundled SEO package can ship |
| 1571 | // the Initializer class without this method, and class_exists alone would still fatal. |
| 1572 | $seo_initializer = 'Automattic\Jetpack\SEO\Initializer'; |
| 1573 | // @phan-suppress-next-line PhanUndeclaredClassReference -- optional SEO package, guarded by method_exists. |
| 1574 | $show_card = method_exists( $seo_initializer, 'is_optin_available' ) && $seo_initializer::is_optin_available(); |
| 1575 | |
| 1576 | return array( |
| 1577 | 'showCard' => $show_card, |
| 1578 | 'redirect' => admin_url( 'admin.php?page=jetpack-seo' ), |
| 1579 | ); |
| 1580 | } |
| 1581 | |
| 1582 | /** |
| 1583 | * Echoes the admin page content. |
| 1584 | * |
| 1585 | * @return void |
| 1586 | */ |
| 1587 | public static function admin_page() { |
| 1588 | // Asked again here rather than inferred from the redirect: with the wizard on a |
| 1589 | // connected user is deliberately left on this screen to finish setup, and anyone |
| 1590 | // who cannot connect never reaches it. |
| 1591 | if ( self::is_onboarding_takeover() ) { |
| 1592 | echo '<div id="my-jetpack-container"></div>'; |
| 1593 | return; |
| 1594 | } |
| 1595 | |
| 1596 | if ( self::should_render_wp_build() ) { |
| 1597 | jetpack_my_jetpack_my_jetpack_dashboard_wp_admin_render_page(); // @phan-suppress-current-line PhanUndeclaredFunction -- should_render_wp_build() checks function_exists(); defined in the generated build/pages/, which Phan excludes. |
| 1598 | } |
| 1599 | } |
| 1600 | |
| 1601 | /** |
| 1602 | * Register the REST API routes. |
| 1603 | * |
| 1604 | * @return void |
| 1605 | */ |
| 1606 | public static function register_rest_endpoints() { |
| 1607 | if ( self::is_offline_features_enabled() ) { |
| 1608 | ( new REST_Main_Features() )->register_rest_routes(); |
| 1609 | return; |
| 1610 | } |
| 1611 | |
| 1612 | new REST_Products(); |
| 1613 | new REST_Purchases(); |
| 1614 | ( new REST_Jetpack_AI_JWT() )->register_rest_route(); |
| 1615 | new REST_Recommendations_Evaluation(); |
| 1616 | |
| 1617 | ( new REST_Onboarding() )->register_rest_routes(); |
| 1618 | ( new REST_Main_Features() )->register_rest_routes(); |
| 1619 | |
| 1620 | Products::register_product_endpoints(); |
| 1621 | Historically_Active_Modules::register_rest_endpoints(); |
| 1622 | Jetpack_Manage::register_rest_endpoints(); |
| 1623 | Red_Bubble_Notifications::register_rest_endpoints(); |
| 1624 | |
| 1625 | if ( self::is_overview_v1_enabled() ) { |
| 1626 | REST_Blog_Posts::register_rest_endpoints(); |
| 1627 | } |
| 1628 | |
| 1629 | register_rest_route( |
| 1630 | 'my-jetpack/v1', |
| 1631 | 'site', |
| 1632 | array( |
| 1633 | 'methods' => \WP_REST_Server::READABLE, |
| 1634 | 'callback' => __CLASS__ . '::get_site', |
| 1635 | 'permission_callback' => __CLASS__ . '::permissions_callback', |
| 1636 | ) |
| 1637 | ); |
| 1638 | |
| 1639 | register_rest_route( |
| 1640 | 'my-jetpack/v1', |
| 1641 | 'site/dismiss-welcome-banner', |
| 1642 | array( |
| 1643 | 'methods' => \WP_REST_Server::EDITABLE, |
| 1644 | 'callback' => __CLASS__ . '::dismiss_welcome_banner', |
| 1645 | 'permission_callback' => __CLASS__ . '::permissions_callback', |
| 1646 | ) |
| 1647 | ); |
| 1648 | } |
| 1649 | |
| 1650 | /** |
| 1651 | * Check user capability to access the endpoint. |
| 1652 | * |
| 1653 | * @access public |
| 1654 | * @static |
| 1655 | * |
| 1656 | * @return true|WP_Error |
| 1657 | */ |
| 1658 | public static function permissions_callback() { |
| 1659 | return current_user_can( 'manage_options' ); |
| 1660 | } |
| 1661 | |
| 1662 | /** |
| 1663 | * Return true if we should initialize the My Jetpack admin page. |
| 1664 | */ |
| 1665 | public static function should_initialize() { |
| 1666 | $should = true; |
| 1667 | |
| 1668 | if ( ( new Status() )->is_offline_mode() ) { |
| 1669 | $should = self::is_offline_features_enabled(); |
| 1670 | } |
| 1671 | |
| 1672 | /** |
| 1673 | * Allows filtering whether My Jetpack should be initialized. |
| 1674 | * Outside REST requests, once the user is set, this cannot enable offline Features for a user without access. |
| 1675 | * |
| 1676 | * @since 0.5.0-alpha |
| 1677 | * |
| 1678 | * @param bool $shoud_initialize Should we initialize My Jetpack? |
| 1679 | */ |
| 1680 | $should = apply_filters( 'jetpack_my_jetpack_should_initialize', $should ); |
| 1681 | |
| 1682 | // REST authenticates after route registration; its permission callbacks check access. |
| 1683 | if ( self::is_offline_features_enabled() && ! Jetpack_Constants::is_true( 'REST_REQUEST' ) && function_exists( 'wp_get_current_user' ) && did_action( 'set_current_user' ) && ! self::current_user_can_access_page() ) { |
| 1684 | return false; |
| 1685 | } |
| 1686 | |
| 1687 | return $should; |
| 1688 | } |
| 1689 | |
| 1690 | /** |
| 1691 | * Whether the site uses the local Features entry instead of the connected dashboard. |
| 1692 | * |
| 1693 | * @since 6.10.0 |
| 1694 | * @return bool |
| 1695 | */ |
| 1696 | public static function is_offline_features_enabled() { |
| 1697 | if ( ! ( new Status() )->is_offline_mode() ) { |
| 1698 | return false; |
| 1699 | } |
| 1700 | |
| 1701 | /** |
| 1702 | * Enable local Features data and entry in offline mode. |
| 1703 | * |
| 1704 | * @since 6.10.0 |
| 1705 | * @since $$next-version$$ Enabled by default. |
| 1706 | * @param bool $enabled Whether to enable offline Features. Default true. |
| 1707 | */ |
| 1708 | return (bool) apply_filters( 'jetpack_my_jetpack_offline_features', true ); |
| 1709 | } |
| 1710 | |
| 1711 | /** |
| 1712 | * Hook into several connection-based actions to update the historically active Jetpack modules |
| 1713 | * If the transient that indicates the list needs to be synced, update it and delete the transient |
| 1714 | * |
| 1715 | * @return void |
| 1716 | */ |
| 1717 | public static function setup_historically_active_jetpack_modules_sync() { |
| 1718 | // yummmm. ham. |
| 1719 | $ham = new Historically_Active_Modules(); |
| 1720 | if ( get_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) { |
| 1721 | $ham::update_historically_active_jetpack_modules(); |
| 1722 | delete_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ); |
| 1723 | } |
| 1724 | |
| 1725 | $actions = array( |
| 1726 | 'jetpack_site_registered', |
| 1727 | 'jetpack_user_authorized', |
| 1728 | 'activated_plugin', |
| 1729 | ); |
| 1730 | |
| 1731 | foreach ( $actions as $action ) { |
| 1732 | add_action( $action, array( $ham, 'queue_historically_active_jetpack_modules_update' ), 5 ); |
| 1733 | } |
| 1734 | |
| 1735 | // Modules are often updated async, so we need to update them right away as there will sometimes be no page reload. |
| 1736 | add_action( 'jetpack_activate_module', array( $ham, 'update_historically_active_jetpack_modules' ), 5 ); |
| 1737 | } |
| 1738 | |
| 1739 | /** |
| 1740 | * Site full-data endpoint. |
| 1741 | * |
| 1742 | * @return object Site data. |
| 1743 | */ |
| 1744 | public static function get_site() { |
| 1745 | $site_id = \Jetpack_Options::get_option( 'id' ); |
| 1746 | $wpcom_endpoint = sprintf( '/sites/%d?force=wpcom', $site_id ); |
| 1747 | $wpcom_api_version = '1.1'; |
| 1748 | $response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version ); |
| 1749 | $response_code = wp_remote_retrieve_response_code( $response ); |
| 1750 | $body = json_decode( wp_remote_retrieve_body( $response ) ); |
| 1751 | |
| 1752 | if ( is_wp_error( $response ) || empty( $response['body'] ) ) { |
| 1753 | return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) ); |
| 1754 | } |
| 1755 | |
| 1756 | return rest_ensure_response( $body ); |
| 1757 | } |
| 1758 | |
| 1759 | /** |
| 1760 | * Populates the self::$site_info var with site data from the /sites/%d endpoint |
| 1761 | * |
| 1762 | * @return object|WP_Error |
| 1763 | */ |
| 1764 | public static function get_site_info() { |
| 1765 | static $site_info = null; |
| 1766 | |
| 1767 | if ( $site_info !== null ) { |
| 1768 | return $site_info; |
| 1769 | } |
| 1770 | |
| 1771 | // Check for a cached value before doing lookup |
| 1772 | $stored_site_info = get_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY ); |
| 1773 | if ( $stored_site_info !== false ) { |
| 1774 | return $stored_site_info; |
| 1775 | } |
| 1776 | |
| 1777 | $response = self::get_site(); |
| 1778 | if ( is_wp_error( $response ) ) { |
| 1779 | return $response; |
| 1780 | } |
| 1781 | $site_info = $response->data; |
| 1782 | set_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY, $site_info, DAY_IN_SECONDS ); |
| 1783 | |
| 1784 | return $site_info; |
| 1785 | } |
| 1786 | |
| 1787 | /** |
| 1788 | * Returns whether a site has been determined "commercial" or not. |
| 1789 | * |
| 1790 | * @return bool|null |
| 1791 | */ |
| 1792 | public static function is_commercial_site() { |
| 1793 | if ( is_wp_error( self::$site_info ) ) { |
| 1794 | return null; |
| 1795 | } |
| 1796 | |
| 1797 | return empty( self::$site_info->options->is_commercial ) ? false : self::$site_info->options->is_commercial; |
| 1798 | } |
| 1799 | |
| 1800 | /** |
| 1801 | * Check if site is registered (has been connected before). |
| 1802 | * |
| 1803 | * @return bool |
| 1804 | */ |
| 1805 | public static function is_registered() { |
| 1806 | return (bool) \Jetpack_Options::get_option( 'id' ); |
| 1807 | } |
| 1808 | |
| 1809 | /** |
| 1810 | * Dismiss the welcome banner. |
| 1811 | * |
| 1812 | * @return \WP_REST_Response |
| 1813 | */ |
| 1814 | public static function dismiss_welcome_banner() { |
| 1815 | \Jetpack_Options::update_option( 'dismissed_welcome_banner', true ); |
| 1816 | return rest_ensure_response( array( 'success' => true ) ); |
| 1817 | } |
| 1818 | |
| 1819 | /** |
| 1820 | * Returns "yes" if the site has file write access to the plugins folder, "no" otherwise. |
| 1821 | * |
| 1822 | * @return string |
| 1823 | **/ |
| 1824 | public static function has_file_system_write_access() { |
| 1825 | |
| 1826 | $cache = get_transient( 'my_jetpack_write_access' ); |
| 1827 | |
| 1828 | if ( false !== $cache ) { |
| 1829 | return $cache; |
| 1830 | } |
| 1831 | |
| 1832 | if ( ! function_exists( 'get_filesystem_method' ) ) { |
| 1833 | require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 1834 | } |
| 1835 | |
| 1836 | require_once ABSPATH . 'wp-admin/includes/template.php'; |
| 1837 | |
| 1838 | $write_access = 'no'; |
| 1839 | |
| 1840 | $filesystem_method = get_filesystem_method( array(), WP_PLUGIN_DIR ); |
| 1841 | if ( 'direct' === $filesystem_method ) { |
| 1842 | $write_access = 'yes'; |
| 1843 | } |
| 1844 | |
| 1845 | if ( 'no' === $write_access ) { |
| 1846 | ob_start(); |
| 1847 | $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); |
| 1848 | ob_end_clean(); |
| 1849 | |
| 1850 | if ( $filesystem_credentials_are_stored ) { |
| 1851 | $write_access = 'yes'; |
| 1852 | } |
| 1853 | } |
| 1854 | |
| 1855 | set_transient( 'my_jetpack_write_access', $write_access, 30 * MINUTE_IN_SECONDS ); |
| 1856 | |
| 1857 | return $write_access; |
| 1858 | } |
| 1859 | |
| 1860 | /** |
| 1861 | * Get container IDC for the IDC screen. |
| 1862 | * |
| 1863 | * @return string |
| 1864 | */ |
| 1865 | public static function get_idc_container_id() { |
| 1866 | return static::IDC_CONTAINER_ID; |
| 1867 | } |
| 1868 | |
| 1869 | /** |
| 1870 | * Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show. |
| 1871 | * |
| 1872 | * On My Jetpack page: Uses blocking behavior to fetch fresh data. |
| 1873 | * On other admin pages: Uses cached data only to avoid blocking, with async JS fetch if cache is empty. |
| 1874 | * |
| 1875 | * @return void |
| 1876 | */ |
| 1877 | public static function maybe_show_red_bubble() { |
| 1878 | global $pagenow; |
| 1879 | |
| 1880 | // Don't show red bubble alerts for non-admin users |
| 1881 | // These alerts are generally only actionable for admins |
| 1882 | if ( ! current_user_can( 'manage_options' ) ) { |
| 1883 | return; |
| 1884 | } |
| 1885 | |
| 1886 | // Don't show any red bubbles when Jetpack is disconnected |
| 1887 | // Users can't act on most alerts without a connection |
| 1888 | $connection = new Connection_Manager(); |
| 1889 | if ( ! $connection->is_connected() ) { |
| 1890 | return; |
| 1891 | } |
| 1892 | |
| 1893 | // Check if we're on the My Jetpack page. |
| 1894 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 1895 | $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; |
| 1896 | $is_my_jetpack_page = $pagenow === 'admin.php' && $page === 'my-jetpack'; |
| 1897 | |
| 1898 | if ( $is_my_jetpack_page ) { |
| 1899 | // On My Jetpack page: use blocking behavior for fresh data. |
| 1900 | add_filter( 'my_jetpack_red_bubble_notification_slugs', array( Red_Bubble_Notifications::class, 'add_red_bubble_alerts' ) ); |
| 1901 | $red_bubble_alerts = Red_Bubble_Notifications::get_red_bubble_alerts(); |
| 1902 | } else { |
| 1903 | // On other pages: use cached data only to avoid blocking. |
| 1904 | $cached_alerts = Red_Bubble_Notifications::get_cached_alerts(); |
| 1905 | |
| 1906 | if ( false === $cached_alerts ) { |
| 1907 | // No cache: warm it asynchronously via JS. Register a hidden zero-count |
| 1908 | // placeholder so Menu_Renderer emits a `my-jetpack` badge element the |
| 1909 | // warmer can reveal (see async-notification-bubble.ts) without a reload. |
| 1910 | Menu_Badges::init(); // idempotent; wires the renderer. |
| 1911 | Notification_Counts::register( |
| 1912 | 'my-jetpack', |
| 1913 | array( |
| 1914 | 'menu_slug' => 'my-jetpack', |
| 1915 | 'count' => 0, |
| 1916 | 'type' => 'count', |
| 1917 | ) |
| 1918 | ); |
| 1919 | add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_red_bubble_script' ) ); |
| 1920 | return; |
| 1921 | } |
| 1922 | |
| 1923 | $red_bubble_alerts = $cached_alerts; |
| 1924 | } |
| 1925 | |
| 1926 | // Filter out silent alerts. |
| 1927 | $red_bubble_alerts = array_filter( |
| 1928 | $red_bubble_alerts, |
| 1929 | function ( $alert ) { |
| 1930 | return empty( $alert['is_silent'] ); |
| 1931 | } |
| 1932 | ); |
| 1933 | |
| 1934 | // Report each non-silent alert to the central menu-badges registry as an |
| 1935 | // attention entry (count 1). The registry + renderer own the badge. |
| 1936 | Menu_Badges::init(); // idempotent; wires the renderer. |
| 1937 | |
| 1938 | // Connection errors are owned by the Error Handler (the single source of truth): |
| 1939 | // surface any it reports for this viewer as their own attention entry. Read live |
| 1940 | // rather than from the red-bubble transient, since that cache is not viewer-keyed. |
| 1941 | $has_connection_error = self::has_connection_error(); |
| 1942 | if ( $has_connection_error ) { |
| 1943 | Notification_Counts::register( |
| 1944 | 'my-jetpack-connection-error', |
| 1945 | array( |
| 1946 | 'menu_slug' => 'my-jetpack', |
| 1947 | 'type' => 'attention', |
| 1948 | ) |
| 1949 | ); |
| 1950 | } |
| 1951 | |
| 1952 | $counted_elsewhere = self::get_alerts_counted_elsewhere(); |
| 1953 | foreach ( array_keys( $red_bubble_alerts ) as $slug ) { |
| 1954 | if ( in_array( $slug, $counted_elsewhere, true ) ) { |
| 1955 | continue; |
| 1956 | } |
| 1957 | // The missing-connection slug and a connection error describe the same broken |
| 1958 | // connection; count it once (the error, above, is the more specific signal). |
| 1959 | if ( $has_connection_error && 'missing-connection' === $slug ) { |
| 1960 | continue; |
| 1961 | } |
| 1962 | Notification_Counts::register( |
| 1963 | 'my-jetpack-' . $slug, |
| 1964 | array( |
| 1965 | 'menu_slug' => 'my-jetpack', |
| 1966 | 'type' => 'attention', |
| 1967 | ) |
| 1968 | ); |
| 1969 | } |
| 1970 | } |
| 1971 | |
| 1972 | /** |
| 1973 | * Whether the Connection Error Handler reports a displayable connection error |
| 1974 | * for the current viewer. |
| 1975 | * |
| 1976 | * Read live (not via the red-bubble transient): get_displayable_errors() is a |
| 1977 | * cached option read that the Error Handler already scopes and caches per viewer. |
| 1978 | * Guarded for the mid-plugin-update window, where a stale connection package |
| 1979 | * predating the method can be loaded. |
| 1980 | * |
| 1981 | * @return bool |
| 1982 | */ |
| 1983 | private static function has_connection_error() { |
| 1984 | if ( ! class_exists( Connection_Error_Handler::class ) || ! method_exists( Connection_Error_Handler::class, 'get_displayable_errors' ) ) { |
| 1985 | return false; |
| 1986 | } |
| 1987 | return ! empty( Connection_Error_Handler::get_instance()->get_displayable_errors() ); |
| 1988 | } |
| 1989 | |
| 1990 | /** |
| 1991 | * Alerts left out of the My Jetpack badge because another menu item already counts them. |
| 1992 | * |
| 1993 | * The Protect page reports its own count, from the standalone plugin (class-jetpack-protect.php::admin_page_init()) |
| 1994 | * or the Protect package's dashboard; without either, nobody else counts the threats, so they must stay. |
| 1995 | * |
| 1996 | * @return string[] Alert slugs. |
| 1997 | */ |
| 1998 | private static function get_alerts_counted_elsewhere() { |
| 1999 | // Without has_protect_page(), use the standalone plugin check this relied on before that method existed. |
| 2000 | $has_protect_page = method_exists( Products\Protect::class, 'has_protect_page' ) ? Products\Protect::has_protect_page() : Products\Protect::is_standalone_plugin_active(); |
| 2001 | return $has_protect_page ? array( 'protect_has_threats' ) : array(); |
| 2002 | } |
| 2003 | |
| 2004 | /** |
| 2005 | * Enqueue the notification bubble script. |
| 2006 | * Fetches fresh alert data via REST API without blocking page load. |
| 2007 | * |
| 2008 | * @return void |
| 2009 | */ |
| 2010 | public static function enqueue_red_bubble_script() { |
| 2011 | Assets::register_script( |
| 2012 | 'my-jetpack-notification-bubble', |
| 2013 | '../build/async-notification-bubble.js', |
| 2014 | __FILE__, |
| 2015 | array( |
| 2016 | 'enqueue' => true, |
| 2017 | 'in_footer' => true, |
| 2018 | ) |
| 2019 | ); |
| 2020 | wp_localize_script( |
| 2021 | 'my-jetpack-notification-bubble', |
| 2022 | 'myJetpackNotificationBubble', |
| 2023 | array( 'excludedAlerts' => self::get_alerts_counted_elsewhere() ) |
| 2024 | ); |
| 2025 | } |
| 2026 | |
| 2027 | /** |
| 2028 | * Get list of module names sorted by their recommendation score |
| 2029 | * |
| 2030 | * @return array|null |
| 2031 | */ |
| 2032 | public static function get_recommended_modules() { |
| 2033 | $recommendations_evaluation = \Jetpack_Options::get_option( 'recommendations_evaluation', null ); |
| 2034 | |
| 2035 | if ( empty( $recommendations_evaluation ) || ! is_array( $recommendations_evaluation ) ) { |
| 2036 | return null; |
| 2037 | } |
| 2038 | |
| 2039 | arsort( $recommendations_evaluation ); // Sort by scores in descending order |
| 2040 | |
| 2041 | return array_keys( $recommendations_evaluation ); // Get only module names |
| 2042 | } |
| 2043 | } |