Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.13% covered (warning)
67.13%
1732 / 2580
22.89% covered (danger)
22.89%
19 / 83
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Core_Json_Api_Endpoints
67.29% covered (warning)
67.29%
1732 / 2574
22.89% covered (danger)
22.89%
19 / 83
5001.81
0.00% covered (danger)
0.00%
0 / 1
 register_endpoints
100.00% covered (success)
100.00%
595 / 595
100.00% covered (success)
100.00%
1 / 1
2
 get_openai_jwt
n/a
0 / 0
n/a
0 / 0
2
 set_subscriber_cookie_and_redirect
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 get_recommendations_data
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update_recommendations_data
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get_recommendations_step
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update_recommendations_step
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get_recommendations_product_suggestions
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 get_recommendations_upsell
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
20
 get_conditional_recommendations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validate_recommendations_data
53.85% covered (warning)
53.85%
7 / 13
0.00% covered (danger)
0.00%
0 / 1
11.82
 get_purchase_token
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 delete_purchase_token
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_plans
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 get_products
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 submit_survey
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 is_site_verified_and_token
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
342
 verify_site
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 dismiss_notice
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 disconnect_site_permission_callback
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 connect_url_permission_callback
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 unlink_user_permission_callback
n/a
0 / 0
n/a
0 / 0
1
 manage_modules_permission_check
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 configure_modules_permission_check
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 view_admin_page_permission_check
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 update_settings_permission_check
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
3.46
 activate_plugins_permission_check
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 edit_others_posts_check
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 purchase_token_permission_check
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 rewind_data
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 get_rewind_data
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 scan_state
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 increase_timeout_30
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_scan_state
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 disconnect_site
n/a
0 / 0
n/a
0 / 0
4
 build_connect_url
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 get_user_connection_data
n/a
0 / 0
n/a
0 / 0
2
 unlink_user
n/a
0 / 0
n/a
0 / 0
1
 get_user_tracking_settings
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 update_user_tracking_settings
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 site_data
n/a
0 / 0
n/a
0 / 0
1
 get_site_data
n/a
0 / 0
n/a
0 / 0
1
 get_site_activity
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
20
 get_site_discount
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
42
 reset_jetpack_options
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
90
 get_updateable_parameters
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 get_updateable_data_list
100.00% covered (success)
100.00%
861 / 861
100.00% covered (success)
100.00%
1 / 1
10
 validate_onboarding
n/a
0 / 0
n/a
0 / 0
1
 validate_boolean
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
11.53
 validate_posint
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
7.23
 validate_non_neg_int
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 validate_list_item
36.00% covered (danger)
36.00%
9 / 25
0.00% covered (danger)
0.00%
0 / 1
15.44
 validate_module_list
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 validate_alphanum
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
11.53
 validate_verification_service
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
16.76
 validate_stats_roles
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
6
 validate_sharing_show
57.89% covered (warning)
57.89%
11 / 19
0.00% covered (danger)
0.00%
0 / 1
3.67
 validate_subscriptions_reply_to
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
6.09
 validate_subscriptions_reply_to_name
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
7.23
 validate_services
32.00% covered (danger)
32.00%
8 / 25
0.00% covered (danger)
0.00%
0 / 1
57.28
 validate_custom_service
13.04% covered (danger)
13.04%
3 / 23
0.00% covered (danger)
0.00%
0 / 1
184.32
 validate_custom_service_id
28.57% covered (danger)
28.57%
6 / 21
0.00% covered (danger)
0.00%
0 / 1
31.32
 validate_twitter_username
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
11.53
 validate_string
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
3.88
 validate_array_of_strings
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 validate_subscription_options
26.67% covered (danger)
26.67%
4 / 15
0.00% covered (danger)
0.00%
0 / 1
10.31
 validate_array
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
3.88
 sanitize_stats_allowed_roles
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 sanitize_ai_crawler_overrides
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 get_module_requested
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
8.74
 prepare_modules_for_response
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 filter_options_for_response
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 prepare_options_for_response
86.54% covered (warning)
86.54%
45 / 52
0.00% covered (danger)
0.00%
0 / 1
22.08
 split_options
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 cast_value
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
12.63
 get_remote_value
26.09% covered (danger)
26.09%
6 / 23
0.00% covered (danger)
0.00%
0 / 1
81.24
 get_plugin_update_count
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 get_plugins
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 install_plugin
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
72
 activate_plugin
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
90
 validate_activate_plugin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_plugin
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 get_jetpack_crm_data
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 activate_crm_jetpack_forms_extension
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 jetpack_crm_data_permission_check
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 activate_crm_extensions_permission_check
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 set_has_seen_wc_connection_modal
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_intro_offers
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
20
 get_features_available
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_features_enabled
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_features_permission_check
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Register WP REST API endpoints for Jetpack.
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Connection\Client;
9use Automattic\Jetpack\Connection\Manager as Connection_Manager;
10use Automattic\Jetpack\Connection\Rest_Authentication;
11use Automattic\Jetpack\Connection\REST_Connector;
12use Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT;
13use Automattic\Jetpack\Connection\SSO;
14use Automattic\Jetpack\Jetpack_CRM_Data;
15use Automattic\Jetpack\Plugins_Installer;
16use Automattic\Jetpack\Stats\Options as Stats_Options;
17use Automattic\Jetpack\Status\Host;
18use Automattic\Jetpack\Status\Visitor;
19use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection_Shared_Functions;
20use Automattic\Jetpack\Waf\Waf_Compatibility;
21
22// Disable direct access.
23if ( ! defined( 'ABSPATH' ) ) {
24    exit( 0 );
25}
26
27// Load WP_Error for error messages.
28require_once ABSPATH . '/wp-includes/class-wp-error.php';
29
30// Register endpoints when WP REST API is initialized.
31add_action( 'rest_api_init', array( 'Jetpack_Core_Json_Api_Endpoints', 'register_endpoints' ) );
32// Load API endpoints that are synced with WP.com
33// Each of these is a class that will register its own routes on 'rest_api_init'.
34require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/load-wpcom-endpoints.php';
35
36require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
37
38/**
39 * Class Jetpack_Core_Json_Api_Endpoints
40 *
41 * @since 4.3.0
42 */
43class Jetpack_Core_Json_Api_Endpoints {
44    /**
45     * Roles that can access Stats once they're granted access.
46     *
47     * @var array
48     */
49    public static $stats_roles;
50
51    /**
52     * Declare the Jetpack REST API endpoints.
53     *
54     * @since 4.3.0
55     */
56    public static function register_endpoints() {
57
58        // Load API endpoint base classes.
59        require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php';
60
61        // Load API endpoints.
62        require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php';
63        require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php';
64        require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php';
65
66        self::$stats_roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
67
68        $ixr_client             = new Jetpack_IXR_Client( array( 'user_id' => get_current_user_id() ) );
69        $core_api_endpoint      = new Jetpack_Core_API_Data( $ixr_client );
70        $module_list_endpoint   = new Jetpack_Core_API_Module_List_Endpoint();
71        $module_data_endpoint   = new Jetpack_Core_API_Module_Data_Endpoint();
72        $module_toggle_endpoint = new Jetpack_Core_API_Module_Toggle_Endpoint( new Jetpack_IXR_Client() );
73        $site_endpoint          = new Jetpack_Core_API_Site_Endpoint();
74        $widget_endpoint        = new Jetpack_Core_API_Widget_Endpoint();
75
76        // My Jetpack and Agents Manager register the same controller; its guard keeps the route registered once.
77        ( new REST_Jetpack_AI_JWT() )->register_rest_route();
78
79        register_rest_route(
80            'jetpack/v4',
81            'plans',
82            array(
83                'methods'             => WP_REST_Server::READABLE,
84                'callback'            => __CLASS__ . '::get_plans',
85                'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
86            )
87        );
88
89        register_rest_route(
90            'jetpack/v4',
91            'products',
92            array(
93                'methods'             => WP_REST_Server::READABLE,
94                'callback'            => __CLASS__ . '::get_products',
95                'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
96            )
97        );
98
99        register_rest_route(
100            'jetpack/v4',
101            'marketing/survey',
102            array(
103                'methods'             => WP_REST_Server::CREATABLE,
104                'callback'            => __CLASS__ . '::submit_survey',
105                'permission_callback' => __CLASS__ . '::disconnect_site_permission_callback',
106            )
107        );
108
109        register_rest_route(
110            'jetpack/v4',
111            '/rewind',
112            array(
113                'methods'             => WP_REST_Server::READABLE,
114                'callback'            => __CLASS__ . '::get_rewind_data',
115                'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
116            )
117        );
118
119        register_rest_route(
120            'jetpack/v4',
121            '/scan',
122            array(
123                'methods'             => WP_REST_Server::READABLE,
124                'callback'            => __CLASS__ . '::get_scan_state',
125                'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
126            )
127        );
128
129        // Fetches a fresh connect URL.
130        register_rest_route(
131            'jetpack/v4',
132            '/connection/url',
133            array(
134                'methods'             => WP_REST_Server::READABLE,
135                'callback'            => __CLASS__ . '::build_connect_url',
136                'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
137                'args'                => array(
138                    'from'     => array( 'type' => 'string' ),
139                    'redirect' => array( 'type' => 'string' ),
140                ),
141            )
142        );
143
144        // Current user: get or set tracking settings.
145        register_rest_route(
146            'jetpack/v4',
147            '/tracking/settings',
148            array(
149                array(
150                    'methods'             => WP_REST_Server::READABLE,
151                    'callback'            => __CLASS__ . '::get_user_tracking_settings',
152                    'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
153                ),
154                array(
155                    'methods'             => WP_REST_Server::EDITABLE,
156                    'callback'            => __CLASS__ . '::update_user_tracking_settings',
157                    'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
158                    'args'                => array(
159                        'tracks_opt_out' => array( 'type' => 'boolean' ),
160                    ),
161                ),
162            )
163        );
164
165        // Get current site features.
166        register_rest_route(
167            'jetpack/v4',
168            '/site/features',
169            array(
170                'methods'             => WP_REST_Server::READABLE,
171                'callback'            => array( $site_endpoint, 'get_features' ),
172                'permission_callback' => array( $site_endpoint, 'can_request' ),
173            )
174        );
175
176        register_rest_route(
177            'jetpack/v4',
178            '/site/products',
179            array(
180                'methods'             => WP_REST_Server::READABLE,
181                'callback'            => array( $site_endpoint, 'get_products' ),
182                'permission_callback' => array( $site_endpoint, 'can_request' ),
183            )
184        );
185
186        // Get current site purchases.
187        register_rest_route(
188            'jetpack/v4',
189            '/site/purchases',
190            array(
191                'methods'             => WP_REST_Server::READABLE,
192                'callback'            => array( $site_endpoint, 'get_purchases' ),
193                'permission_callback' => array( $site_endpoint, 'can_request' ),
194            )
195        );
196
197        // Get current site benefits.
198        register_rest_route(
199            'jetpack/v4',
200            '/site/benefits',
201            array(
202                'methods'             => WP_REST_Server::READABLE,
203                'callback'            => array( $site_endpoint, 'get_benefits' ),
204                'permission_callback' => array( $site_endpoint, 'can_request' ),
205            )
206        );
207
208        // Get Activity Log data for this site.
209        register_rest_route(
210            'jetpack/v4',
211            '/site/activity',
212            array(
213                'methods'             => WP_REST_Server::READABLE,
214                'callback'            => __CLASS__ . '::get_site_activity',
215                'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
216            )
217        );
218
219        // Return all modules.
220        register_rest_route(
221            'jetpack/v4',
222            '/module/all',
223            array(
224                'methods'             => WP_REST_Server::READABLE,
225                'callback'            => array( $module_list_endpoint, 'process' ),
226                'permission_callback' => array( $module_list_endpoint, 'can_request' ),
227            )
228        );
229
230        // Activate many modules.
231        register_rest_route(
232            'jetpack/v4',
233            '/module/all/active',
234            array(
235                'methods'             => WP_REST_Server::EDITABLE,
236                'callback'            => array( $module_list_endpoint, 'process' ),
237                'permission_callback' => array( $module_list_endpoint, 'can_request' ),
238                'args'                => array(
239                    'modules' => array(
240                        'default'           => '',
241                        'type'              => 'array',
242                        'items'             => array(
243                            'type' => 'string',
244                        ),
245                        'required'          => true,
246                        'validate_callback' => __CLASS__ . '::validate_module_list',
247                    ),
248                    'active'  => array(
249                        'default'           => true,
250                        'type'              => 'boolean',
251                        'required'          => false,
252                        'validate_callback' => __CLASS__ . '::validate_boolean',
253                    ),
254                ),
255            )
256        );
257
258        // Return a single module and update it when needed.
259        register_rest_route(
260            'jetpack/v4',
261            '/module/(?P<slug>[a-z\-]+)',
262            array(
263                'methods'             => WP_REST_Server::READABLE,
264                'callback'            => array( $core_api_endpoint, 'process' ),
265                'permission_callback' => array( $core_api_endpoint, 'can_request' ),
266            )
267        );
268
269        // Activate and deactivate a module.
270        register_rest_route(
271            'jetpack/v4',
272            '/module/(?P<slug>[a-z\-]+)/active',
273            array(
274                'methods'             => WP_REST_Server::EDITABLE,
275                'callback'            => array( $module_toggle_endpoint, 'process' ),
276                'permission_callback' => array( $module_toggle_endpoint, 'can_request' ),
277                'args'                => array(
278                    'active' => array(
279                        'default'           => true,
280                        'type'              => 'boolean',
281                        'required'          => true,
282                        'validate_callback' => __CLASS__ . '::validate_boolean',
283                    ),
284                ),
285            )
286        );
287
288        // Update a module.
289        register_rest_route(
290            'jetpack/v4',
291            '/module/(?P<slug>[a-z\-]+)',
292            array(
293                'methods'             => WP_REST_Server::EDITABLE,
294                'callback'            => array( $core_api_endpoint, 'process' ),
295                'permission_callback' => array( $core_api_endpoint, 'can_request' ),
296                'args'                => self::get_updateable_parameters( 'any' ),
297            )
298        );
299
300        // Get data for a specific module, i.e. Protect block count, WPCOM stats,
301        // Akismet spam count, etc.
302        register_rest_route(
303            'jetpack/v4',
304            '/module/(?P<slug>[a-z\-]+)/data',
305            array(
306                'methods'             => WP_REST_Server::READABLE,
307                'callback'            => array( $module_data_endpoint, 'process' ),
308                'permission_callback' => array( $module_data_endpoint, 'can_request' ),
309                'args'                => array(
310                    'range' => array(
311                        'default'           => 'day',
312                        'type'              => 'string',
313                        'required'          => false,
314                        'validate_callback' => __CLASS__ . '::validate_string',
315                    ),
316                ),
317            )
318        );
319
320        // Check if the API key for a specific service is valid or not.
321        register_rest_route(
322            'jetpack/v4',
323            '/module/(?P<service>[a-z\-]+)/key/check',
324            array(
325                'methods'             => WP_REST_Server::READABLE,
326                'callback'            => array( $module_data_endpoint, 'key_check' ),
327                'permission_callback' => __CLASS__ . '::update_settings_permission_check',
328                'sanitize_callback'   => 'sanitize_text_field',
329            )
330        );
331
332        register_rest_route(
333            'jetpack/v4',
334            '/module/(?P<service>[a-z\-]+)/key/check',
335            array(
336                'methods'             => WP_REST_Server::EDITABLE,
337                'callback'            => array( $module_data_endpoint, 'key_check' ),
338                'permission_callback' => __CLASS__ . '::update_settings_permission_check',
339                'sanitize_callback'   => 'sanitize_text_field',
340                'args'                => array(
341                    'api_key' => array(
342                        'default'           => '',
343                        'type'              => 'string',
344                        'validate_callback' => __CLASS__ . '::validate_alphanum',
345                    ),
346                ),
347            )
348        );
349
350        // Update any Jetpack module option or setting.
351        register_rest_route(
352            'jetpack/v4',
353            '/settings',
354            array(
355                'methods'             => WP_REST_Server::EDITABLE,
356                'callback'            => array( $core_api_endpoint, 'process' ),
357                'permission_callback' => array( $core_api_endpoint, 'can_request' ),
358                'args'                => self::get_updateable_parameters( 'any' ),
359            )
360        );
361
362        // Update a module.
363        register_rest_route(
364            'jetpack/v4',
365            '/settings/(?P<slug>[a-z\-]+)',
366            array(
367                'methods'             => WP_REST_Server::EDITABLE,
368                'callback'            => array( $core_api_endpoint, 'process' ),
369                'permission_callback' => array( $core_api_endpoint, 'can_request' ),
370                'args'                => self::get_updateable_parameters(),
371            )
372        );
373
374        // Return all module settings.
375        register_rest_route(
376            'jetpack/v4',
377            '/settings/',
378            array(
379                'methods'             => WP_REST_Server::READABLE,
380                'callback'            => array( $core_api_endpoint, 'process' ),
381                'permission_callback' => array( $core_api_endpoint, 'can_request' ),
382            )
383        );
384
385        // Reset all Jetpack options.
386        register_rest_route(
387            'jetpack/v4',
388            '/options/(?P<options>[a-z\-]+)',
389            array(
390                'methods'             => WP_REST_Server::EDITABLE,
391                'callback'            => __CLASS__ . '::reset_jetpack_options',
392                'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
393            )
394        );
395
396        // Updates: get number of plugin updates available.
397        register_rest_route(
398            'jetpack/v4',
399            '/updates/plugins',
400            array(
401                'methods'             => WP_REST_Server::READABLE,
402                'callback'            => __CLASS__ . '::get_plugin_update_count',
403                'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
404            )
405        );
406
407        // Dismiss Jetpack Notices.
408        register_rest_route(
409            'jetpack/v4',
410            '/notice/(?P<notice>[a-z\-_]+)',
411            array(
412                'methods'             => WP_REST_Server::EDITABLE,
413                'callback'            => __CLASS__ . '::dismiss_notice',
414                'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
415            )
416        );
417
418        /*
419         * Plugins: manage plugins on your site.
420         *
421         * @since 8.9.0
422         *
423         * @to-do: deprecate and switch to /wp/v2/plugins when WordPress 5.5 is the minimum required version.
424         * Noting that the `source` parameter is Jetpack-specific (not implemented in Core).
425         */
426        register_rest_route(
427            'jetpack/v4',
428            '/plugins',
429            array(
430                array(
431                    'methods'             => WP_REST_Server::READABLE,
432                    'callback'            => __CLASS__ . '::get_plugins',
433                    'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
434                ),
435                array(
436                    'methods'             => WP_REST_Server::CREATABLE,
437                    'callback'            => __CLASS__ . '::install_plugin',
438                    'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
439                    'args'                => array(
440                        'slug'   => array(
441                            'type'        => 'string',
442                            'required'    => true,
443                            'description' => __( 'WordPress.org plugin directory slug.', 'jetpack' ),
444                            'pattern'     => '[\w\-]+',
445                        ),
446                        'status' => array(
447                            'description' => __( 'The plugin activation status.', 'jetpack' ),
448                            'type'        => 'string',
449                            'enum'        => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
450                            'default'     => 'inactive',
451                        ),
452                        'source' => array(
453                            'required'          => false,
454                            'type'              => 'string',
455                            'validate_callback' => __CLASS__ . '::validate_string',
456                        ),
457                    ),
458                ),
459            )
460        );
461
462        /*
463         * Plugins: activate a specific plugin.
464         *
465         * @since 8.9.0
466         *
467         * @to-do: deprecate and switch to /wp/v2/plugins when WordPress 5.5 is the minimum required version.
468         * Noting that the `source` parameter is Jetpack-specific (not implemented in Core).
469         */
470        register_rest_route(
471            'jetpack/v4',
472            '/plugins/(?P<plugin>[^.\/]+(?:\/[^.\/]+)?)',
473            array(
474                'methods'             => WP_REST_Server::EDITABLE,
475                'callback'            => __CLASS__ . '::activate_plugin',
476                'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
477                'args'                => array(
478                    'status' => array(
479                        'required'          => true,
480                        'type'              => 'string',
481                        'validate_callback' => __CLASS__ . '::validate_activate_plugin',
482                    ),
483                    'source' => array(
484                        'required'          => false,
485                        'type'              => 'string',
486                        'validate_callback' => __CLASS__ . '::validate_string',
487                    ),
488                ),
489            )
490        );
491
492        // Plugins: check if the plugin is active.
493        register_rest_route(
494            'jetpack/v4',
495            '/plugin/(?P<plugin>[a-z\/\.\-_]+)',
496            array(
497                'methods'             => WP_REST_Server::READABLE,
498                'callback'            => __CLASS__ . '::get_plugin',
499                'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
500            )
501        );
502
503        // Widgets: get information about a widget that supports it.
504        register_rest_route(
505            'jetpack/v4',
506            '/widgets/(?P<id>[0-9a-z\-_]+)',
507            array(
508                'methods'             => WP_REST_Server::READABLE,
509                'callback'            => array( $widget_endpoint, 'process' ),
510                'permission_callback' => array( $widget_endpoint, 'can_request' ),
511            )
512        );
513
514        // Site Verify: check if the site is verified, and a get verification token if not.
515        register_rest_route(
516            'jetpack/v4',
517            '/verify-site/(?P<service>[a-z\-_]+)',
518            array(
519                'methods'             => WP_REST_Server::READABLE,
520                'callback'            => __CLASS__ . '::is_site_verified_and_token',
521                'permission_callback' => __CLASS__ . '::update_settings_permission_check',
522            )
523        );
524
525        register_rest_route(
526            'jetpack/v4',
527            '/verify-site/(?P<service>[a-z\-_]+)/(?<keyring_id>[0-9]+)',
528            array(
529                'methods'             => WP_REST_Server::READABLE,
530                'callback'            => __CLASS__ . '::is_site_verified_and_token',
531                'permission_callback' => __CLASS__ . '::update_settings_permission_check',
532            )
533        );
534
535        // Site Verify: tell a service to verify the site.
536        register_rest_route(
537            'jetpack/v4',
538            '/verify-site/(?P<service>[a-z\-_]+)',
539            array(
540                'methods'             => WP_REST_Server::EDITABLE,
541                'callback'            => __CLASS__ . '::verify_site',
542                'permission_callback' => __CLASS__ . '::update_settings_permission_check',
543                'args'                => array(
544                    'keyring_id' => array(
545                        'required'          => true,
546                        'type'              => 'integer',
547                        'validate_callback' => __CLASS__ . '::validate_posint',
548                    ),
549                ),
550            )
551        );
552
553        register_rest_route(
554            'jetpack/v4',
555            '/recommendations/data',
556            array(
557                array(
558                    'methods'             => WP_REST_Server::READABLE,
559                    'callback'            => __CLASS__ . '::get_recommendations_data',
560                    'permission_callback' => __CLASS__ . '::update_settings_permission_check',
561                ),
562                array(
563                    'methods'             => WP_REST_Server::EDITABLE,
564                    'callback'            => __CLASS__ . '::update_recommendations_data',
565                    'permission_callback' => __CLASS__ . '::update_settings_permission_check',
566                    'args'                => array(
567                        'data' => array(
568                            'required'          => true,
569                            'type'              => 'object',
570                            'validate_callback' => __CLASS__ . '::validate_recommendations_data',
571                        ),
572                    ),
573                ),
574            )
575        );
576
577        register_rest_route(
578            'jetpack/v4',
579            '/recommendations/step',
580            array(
581                array(
582                    'methods'             => WP_REST_Server::READABLE,
583                    'callback'            => __CLASS__ . '::get_recommendations_step',
584                    'permission_callback' => __CLASS__ . '::update_settings_permission_check',
585                ),
586                array(
587                    'methods'             => WP_REST_Server::EDITABLE,
588                    'callback'            => __CLASS__ . '::update_recommendations_step',
589                    'permission_callback' => __CLASS__ . '::update_settings_permission_check',
590                    'args'                => array(
591                        'step' => array(
592                            'required'          => true,
593                            'type'              => 'string',
594                            'validate_callback' => __CLASS__ . '::validate_string',
595                        ),
596                    ),
597                ),
598            )
599        );
600
601        register_rest_route(
602            'jetpack/v4',
603            '/recommendations/product-suggestions',
604            array(
605                array(
606                    'methods'             => WP_REST_Server::READABLE,
607                    'callback'            => __CLASS__ . '::get_recommendations_product_suggestions',
608                    'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
609                ),
610            )
611        );
612
613        register_rest_route(
614            'jetpack/v4',
615            '/recommendations/upsell',
616            array(
617                array(
618                    'methods'             => WP_REST_Server::READABLE,
619                    'callback'            => __CLASS__ . '::get_recommendations_upsell',
620                    'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
621                ),
622            )
623        );
624
625        register_rest_route(
626            'jetpack/v4',
627            '/recommendations/conditional',
628            array(
629                array(
630                    'methods'             => WP_REST_Server::READABLE,
631                    'callback'            => __CLASS__ . '::get_conditional_recommendations',
632                    'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
633                ),
634            )
635        );
636
637        // Get site discount.
638        register_rest_route(
639            'jetpack/v4',
640            '/site/discount',
641            array(
642                'methods'             => WP_REST_Server::READABLE,
643                'callback'            => __CLASS__ . '::get_site_discount',
644                'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
645            )
646        );
647
648        /*
649         * Manage the Jetpack CRM plugin's integration with Jetpack contact forms.
650         */
651        register_rest_route(
652            'jetpack/v4',
653            'jetpack_crm',
654            array(
655                array(
656                    'methods'             => WP_REST_Server::READABLE,
657                    'callback'            => __CLASS__ . '::get_jetpack_crm_data',
658                    'permission_callback' => __CLASS__ . '::jetpack_crm_data_permission_check',
659                ),
660                array(
661                    'methods'             => WP_REST_Server::EDITABLE,
662                    'callback'            => __CLASS__ . '::activate_crm_jetpack_forms_extension',
663                    'permission_callback' => __CLASS__ . '::activate_crm_extensions_permission_check',
664                    'args'                => array(
665                        'extension' => array(
666                            'required' => true,
667                            'type'     => 'text',
668                        ),
669                    ),
670                ),
671            )
672        );
673
674        register_rest_route(
675            'jetpack/v4',
676            'purchase-token',
677            array(
678                array(
679                    'methods'             => WP_REST_Server::READABLE,
680                    'callback'            => __CLASS__ . '::get_purchase_token',
681                    'permission_callback' => __CLASS__ . '::purchase_token_permission_check',
682                ),
683                array(
684                    'methods'             => WP_REST_Server::CREATABLE,
685                    'callback'            => __CLASS__ . '::delete_purchase_token',
686                    'permission_callback' => __CLASS__ . '::purchase_token_permission_check',
687                ),
688            )
689        );
690
691        /*
692         * Set the Jetpack Option `has_see_wc_connection_modal` to true
693         */
694        register_rest_route(
695            'jetpack/v4',
696            'seen-wc-connection-modal',
697            array(
698                'methods'             => WP_REST_Server::EDITABLE,
699                'callback'            => __CLASS__ . '::set_has_seen_wc_connection_modal',
700                'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
701            )
702        );
703
704        // Get Jetpack introduction offers
705        register_rest_route(
706            'jetpack/v4',
707            '/intro-offers',
708            array(
709                'methods'             => WP_REST_Server::READABLE,
710                'callback'            => __CLASS__ . '::get_intro_offers',
711                'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
712            )
713        );
714
715        // Save subscriber token and redirect
716        register_rest_route(
717            'jetpack/v4',
718            '/subscribers/auth',
719            array(
720                'methods'             => WP_REST_Server::READABLE,
721                'callback'            => __CLASS__ . '::set_subscriber_cookie_and_redirect',
722                'permission_callback' => '__return_true',
723                'args'                => array(
724                    'redirect_url' => array(
725                        'required'          => true,
726                        'description'       => __( 'The URL to redirect to.', 'jetpack' ),
727                        'validate_callback' => 'wp_http_validate_url',
728                        'sanitize_callback' => 'sanitize_url',
729                        'type'              => 'string',
730                        'format'            => 'uri',
731                    ),
732                ),
733            )
734        );
735
736        /**
737         * Get the list of available Jetpack features.
738         *
739         * @since 13.9
740         */
741        register_rest_route(
742            'jetpack/v4',
743            '/features/available',
744            array(
745                'methods'             => WP_REST_Server::READABLE,
746                'callback'            => array( static::class, 'get_features_available' ),
747                'permission_callback' => array( static::class, 'get_features_permission_check' ),
748            )
749        );
750
751        /**
752         * Get the list of enabled Jetpack features.
753         *
754         * @since 13.9
755         */
756        register_rest_route(
757            'jetpack/v4',
758            '/features/enabled',
759            array(
760                'methods'             => WP_REST_Server::READABLE,
761                'callback'            => array( static::class, 'get_features_enabled' ),
762                'permission_callback' => array( static::class, 'get_features_permission_check' ),
763            )
764        );
765    }
766
767    /**
768     * Ask WPCOM for a JWT token to use for OpenAI conversations.
769     *
770     * @deprecated since 16.2
771     * @see Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT::get_jwt()
772     *
773     * @return array|WP_Error The token and blog ID, or the error from WPCOM.
774     */
775    public static function get_openai_jwt() {
776        _deprecated_function( __METHOD__, 'jetpack-16.2', '\Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT::get_jwt' );
777
778        $response = ( new REST_Jetpack_AI_JWT() )->get_jwt();
779
780        if ( is_wp_error( $response ) ) {
781            return $response;
782        }
783
784        // Pre-deprecation callers expect the raw array, not a WP_REST_Response.
785        return $response->get_data();
786    }
787
788    /**
789     * Set subscriber cookie and redirect
790     *
791     * @param \WP_Rest_Request $request The URL to redirect to.
792     *
793     * @return WP_Error|WP_REST_Response
794     */
795    public static function set_subscriber_cookie_and_redirect( $request ) {
796        require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
797        $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
798        // Note: get_and_set_token_from_request() sets the subscriber cookie as a side effect.
799        // The cookie is set regardless of the redirect target below; only the redirect is gated.
800        $token          = $subscription_service->get_and_set_token_from_request();
801        $payload        = $subscription_service->decode_token( $token );
802        $is_valid_token = ! empty( $payload );
803        if ( ! $is_valid_token ) {
804            return new WP_Error( 'invalid-token', 'Invalid Token', array( 'status' => 403 ) );
805        }
806
807        // Only redirect to the current site, not to an arbitrary host.
808        $redirect_url = wp_validate_redirect( $request['redirect_url'], '' );
809        if ( ! $redirect_url ) {
810            return new WP_Error( 'invalid-redirect', 'Invalid Redirect URL', array( 'status' => 400 ) );
811        }
812
813        return new WP_REST_Response( null, 302, array( 'location' => $redirect_url ) );
814    }
815
816    /**
817     * Get the data for the recommendations
818     *
819     * @return array Recommendations data
820     */
821    public static function get_recommendations_data() {
822        return Jetpack_Recommendations::get_recommendations_data();
823    }
824
825    /**
826     * Update the data for the recommendations
827     *
828     * @param WP_REST_Request $request The request.
829     *
830     * @return bool true
831     */
832    public static function update_recommendations_data( $request ) {
833        $data = $request['data'];
834        Jetpack_Recommendations::update_recommendations_data( $data );
835
836        return true;
837    }
838
839    /**
840     * Get the data for the recommendations
841     *
842     * @return array Recommendations data
843     */
844    public static function get_recommendations_step() {
845        return Jetpack_Recommendations::get_recommendations_step();
846    }
847
848    /**
849     * Update the step for the recommendations
850     *
851     * @param WP_REST_Request $request The request.
852     *
853     * @return bool true
854     */
855    public static function update_recommendations_step( $request ) {
856        $step = $request['step'];
857        Jetpack_Recommendations::update_recommendations_step( $step );
858
859        return true;
860    }
861
862    /**
863     * Get product suggestions for the recommendations
864     *
865     * @return string|WP_Error The response from the wpcom product suggestions endpoint as a JSON object.
866     */
867    public static function get_recommendations_product_suggestions() {
868        $blog_id = Jetpack_Options::get_option( 'id' );
869        if ( ! $blog_id ) {
870            return new WP_Error( 'site_not_registered', esc_html__( 'Site not registered.', 'jetpack' ) );
871        }
872
873        $user_connected = ( new Connection_Manager( 'jetpack' ) )->is_user_connected( get_current_user_id() );
874        if ( ! $user_connected ) {
875            return wp_json_encode( array(), JSON_UNESCAPED_SLASHES );
876        }
877
878        $request_path  = sprintf( '/sites/%s/jetpack-recommendations/product-suggestions?locale=' . get_user_locale(), $blog_id );
879        $wpcom_request = Client::wpcom_json_api_request_as_user(
880            $request_path,
881            '2',
882            array(
883                'method'  => 'GET',
884                'headers' => array(
885                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
886                ),
887            )
888        );
889
890        $response_code = wp_remote_retrieve_response_code( $wpcom_request );
891        if ( 200 === $response_code ) {
892            return json_decode( wp_remote_retrieve_body( $wpcom_request ) );
893        } else {
894            return new WP_Error(
895                'failed_to_fetch_data',
896                esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
897                array( 'status' => $response_code )
898            );
899        }
900    }
901
902    /**
903     * Get the upsell for the recommendations
904     *
905     * @return string The response from the wpcom upsell endpoint as a JSON object
906     */
907    public static function get_recommendations_upsell() {
908        $blog_id = Jetpack_Options::get_option( 'id' );
909        if ( ! $blog_id ) {
910            return new WP_Error( 'site_not_registered', esc_html__( 'Site not registered.', 'jetpack' ) );
911        }
912
913        $user_connected = ( new Connection_Manager( 'jetpack' ) )->is_user_connected( get_current_user_id() );
914        if ( ! $user_connected ) {
915            $response = array(
916                'hide_upsell' => true,
917            );
918
919            return $response;
920        }
921
922        $request_path  = sprintf( '/sites/%s/jetpack-recommendations/upsell?locale=' . get_user_locale(), $blog_id );
923        $wpcom_request = Client::wpcom_json_api_request_as_user(
924            $request_path,
925            '2',
926            array(
927                'method'  => 'GET',
928                'headers' => array(
929                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
930                ),
931            )
932        );
933
934        $response_code = wp_remote_retrieve_response_code( $wpcom_request );
935        if ( 200 === $response_code ) {
936            return json_decode( wp_remote_retrieve_body( $wpcom_request ) );
937        } else {
938            return new WP_Error(
939                'failed_to_fetch_data',
940                esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
941                array( 'status' => $response_code )
942            );
943        }
944    }
945
946    /**
947     * Get conditional recommendations data.
948     *
949     * @return array Conditional recommendations data.
950     */
951    public static function get_conditional_recommendations() {
952        return Jetpack_Recommendations::get_conditional_recommendations();
953    }
954
955    /**
956     * Validate the recommendations data
957     *
958     * @param array           $value Value to check received by request.
959     * @param WP_REST_Request $request The request sent to the WP REST API.
960     * @param string          $param Name of the parameter passed to endpoint holding $value.
961     *
962     * @return bool|WP_Error
963     */
964    public static function validate_recommendations_data( $value, $request, $param ) {
965        if ( ! is_array( $value ) ) {
966            /* translators: Name of a parameter that must be an object */
967            return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an object.', 'jetpack' ), $param ) );
968        }
969
970        foreach ( $value as $answer ) {
971            if ( is_array( $answer ) ) {
972                $validate = self::validate_array_of_strings( $answer, $request, $param );
973            } elseif ( is_string( $answer ) ) {
974                $validate = self::validate_string( $answer, $request, $param );
975            } elseif ( $answer === null ) {
976                $validate = true;
977            } else {
978                $validate = self::validate_boolean( $answer, $request, $param );
979            }
980
981            if ( is_wp_error( $validate ) ) {
982                return $validate;
983            }
984        }
985
986        return true;
987    }
988
989    /**
990     * Return a purchase token used for site-connected (non user-authenticated) checkout.
991     *
992     * @return string|WP_Error The current purchase token or WP_Error with error details.
993     */
994    public static function get_purchase_token() {
995        $blog_id = Jetpack_Options::get_option( 'id' );
996        if ( ! $blog_id ) {
997            return new WP_Error( 'site_not_registered', esc_html__( 'Site not registered.', 'jetpack' ) );
998        }
999
1000        return Jetpack_Options::get_option( 'purchase_token', '' );
1001    }
1002
1003    /**
1004     * Delete the current purchase token.
1005     *
1006     * @return boolean|WP_Error Whether the token was deleted or WP_Error with error details.
1007     */
1008    public static function delete_purchase_token() {
1009        $blog_id = Jetpack_Options::get_option( 'id' );
1010        if ( ! $blog_id ) {
1011            return new WP_Error( 'site_not_registered', esc_html__( 'Site not registered.', 'jetpack' ) );
1012        }
1013
1014        return Jetpack_Options::delete_option( 'purchase_token' );
1015    }
1016
1017    /**
1018     * Get list of Jetpack Plans.
1019     *
1020     * @param WP_REST_Request $request The request.
1021     */
1022    public static function get_plans( $request ) {
1023        $request = Client::wpcom_json_api_request_as_user(
1024            '/plans?_locale=' . get_user_locale(),
1025            '2',
1026            array(
1027                'method'  => 'GET',
1028                'headers' => array(
1029                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1030                ),
1031            )
1032        );
1033
1034        $body = json_decode( wp_remote_retrieve_body( $request ) );
1035        if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
1036            $data = $body;
1037        } else {
1038            // something went wrong so we'll just return the response without caching.
1039            return $body;
1040        }
1041
1042        return $data;
1043    }
1044
1045    /**
1046     * Gets the WP.com products that are in use on wpcom.
1047     * Similar to the WP.com plans that we currently in user on WPCOM.
1048     *
1049     * @param WP_REST_Request $request The request.
1050     *
1051     * @return string|WP_Error A JSON object of wpcom products if the request was successful, or a WP_Error otherwise.
1052     */
1053    public static function get_products( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1054        $wpcom_request = Client::wpcom_json_api_request_as_user(
1055            '/products?_locale=' . get_user_locale() . '&type=jetpack',
1056            '2',
1057            array(
1058                'method'  => 'GET',
1059                'headers' => array(
1060                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1061                ),
1062            )
1063        );
1064
1065        $response_code = wp_remote_retrieve_response_code( $wpcom_request );
1066        if ( 200 === $response_code ) {
1067            return json_decode( wp_remote_retrieve_body( $wpcom_request ) );
1068        } else {
1069            // Something went wrong so we'll just return the response without caching.
1070            return new WP_Error(
1071                'failed_to_fetch_data',
1072                esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
1073                array( 'status' => $response_code )
1074            );
1075        }
1076    }
1077
1078    /**
1079     * Send Survey details to WordPress.com.
1080     *
1081     * @param WP_REST_Request $request The request.
1082     */
1083    public static function submit_survey( $request ) {
1084        $wpcom_request = Client::wpcom_json_api_request_as_user(
1085            '/marketing/survey',
1086            'v2',
1087            array(
1088                'method'  => 'POST',
1089                'headers' => array(
1090                    'Content-Type'    => 'application/json',
1091                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1092                ),
1093            ),
1094            $request->get_json_params()
1095        );
1096
1097        $wpcom_request_body = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
1098        if ( 200 === wp_remote_retrieve_response_code( $wpcom_request ) ) {
1099            $data = $wpcom_request_body;
1100        } else {
1101            // something went wrong so we'll just return the response without caching.
1102            return $wpcom_request_body;
1103        }
1104
1105        return $data;
1106    }
1107
1108    /**
1109     * Checks if this site has been verified using a service - only 'google' supported at present - and a specfic
1110     *  keyring to use to get the token if it is not
1111     *
1112     * Returns 'verified' = true/false, and a token if 'verified' is false and site is ready for verification
1113     *
1114     * @since 6.6.0
1115     *
1116     * @param WP_REST_Request $request The request sent to the WP REST API.
1117     *
1118     * @return array|WP_Error
1119     */
1120    public static function is_site_verified_and_token( $request ) {
1121        /**
1122         * Return an error if the site uses a Maintenance / Coming Soon plugin
1123         * and if the plugin is configured to make the site private.
1124         *
1125         * We currently handle the following plugins:
1126         * - https://github.com/mojoness/mojo-marketplace-wp-plugin (used by bluehost)
1127         * - https://wordpress.org/plugins/mojo-under-construction
1128         * - https://wordpress.org/plugins/under-construction-page
1129         * - https://wordpress.org/plugins/ultimate-under-construction
1130         * - https://wordpress.org/plugins/coming-soon
1131         *
1132         * You can handle this in your own plugin thanks to the `jetpack_is_under_construction_plugin` filter.
1133         * If the filter returns true, we will consider the site as under construction.
1134         */
1135        $mm_coming_soon                       = get_option( 'mm_coming_soon', null );
1136        $under_construction_activation_status = get_option( 'underConstructionActivationStatus', null );
1137        $ucp_options                          = get_option( 'ucp_options', array() );
1138        $uuc_settings                         = get_option( 'uuc_settings', array() );
1139        $csp4                                 = get_option( 'seed_csp4_settings_content', array() );
1140        if (
1141            ( Jetpack::is_plugin_active( 'mojo-marketplace-wp-plugin/mojo-marketplace.php' ) && 'true' === $mm_coming_soon )
1142            || Jetpack::is_plugin_active( 'mojo-under-construction/mojo-contruction.php' ) && 1 == $under_construction_activation_status // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1143            || ( Jetpack::is_plugin_active( 'under-construction-page/under-construction.php' ) && isset( $ucp_options['status'] ) && 1 == $ucp_options['status'] ) // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1144            || ( Jetpack::is_plugin_active( 'ultimate-under-construction/ultimate-under-construction.php' ) && isset( $uuc_settings['enable'] ) && 1 == $uuc_settings['enable'] ) // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1145            || ( Jetpack::is_plugin_active( 'coming-soon/coming-soon.php' ) && isset( $csp4['status'] ) && ( 1 == $csp4['status'] || 2 == $csp4['status'] ) ) // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1146            ||
1147            /**
1148             * Allow plugins to mark a site as "under construction".
1149             *
1150             * @since 6.7.0
1151             *
1152             * @param false bool Is the site under construction? Default to false.
1153             */
1154            true === apply_filters( 'jetpack_is_under_construction_plugin', false )
1155        ) {
1156            return new WP_Error( 'forbidden', __( 'Site is under construction and cannot be verified', 'jetpack' ) );
1157        }
1158
1159        $xml = new Jetpack_IXR_Client(
1160            array(
1161                'user_id' => get_current_user_id(),
1162            )
1163        );
1164
1165        $args = array(
1166            'user_id' => get_current_user_id(),
1167            'service' => $request['service'],
1168        );
1169
1170        if ( isset( $request['keyring_id'] ) ) {
1171            $args['keyring_id'] = $request['keyring_id'];
1172        }
1173
1174        $xml->query( 'jetpack.isSiteVerified', $args );
1175
1176        if ( $xml->isError() ) {
1177            return new WP_Error( 'error_checking_if_site_verified_google', sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
1178        } else {
1179            return $xml->getResponse();
1180        }
1181    }
1182
1183    /**
1184     * Verify site with external service.
1185     *
1186     * @param WP_REST_Request $request The request.
1187     */
1188    public static function verify_site( $request ) {
1189        $xml = new Jetpack_IXR_Client(
1190            array(
1191                'user_id' => get_current_user_id(),
1192            )
1193        );
1194
1195        $params = $request->get_json_params();
1196
1197        $xml->query(
1198            'jetpack.verifySite',
1199            array(
1200                'user_id'    => get_current_user_id(),
1201                'service'    => $request['service'],
1202                'keyring_id' => $params['keyring_id'],
1203            )
1204        );
1205
1206        if ( $xml->isError() ) {
1207            return new WP_Error( 'error_verifying_site_google', sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
1208        } else {
1209            $response = $xml->getResponse();
1210
1211            if ( ! empty( $response['errors'] ) ) {
1212                $error         = new WP_Error();
1213                $error->errors = $response['errors'];
1214                return $error;
1215            }
1216
1217            return $response;
1218        }
1219    }
1220
1221    /**
1222     * Handles dismissing of Jetpack Notices
1223     *
1224     * @since 4.3.0
1225     *
1226     * @param WP_REST_Request $request The request sent to the WP REST API.
1227     *
1228     * @return array|WP_Error
1229     */
1230    public static function dismiss_notice( $request ) {
1231        $notice = $request['notice'];
1232
1233        if ( ! isset( $request['dismissed'] ) || true !== $request['dismissed'] ) {
1234            return new WP_Error( 'invalid_param', esc_html__( 'Invalid parameter "dismissed".', 'jetpack' ), array( 'status' => 404 ) );
1235        }
1236
1237        if ( isset( $notice ) && ! empty( $notice ) ) {
1238            switch ( $notice ) {
1239                case 'feedback_dash_request':
1240                case 'welcome':
1241                    $notices            = get_option( 'jetpack_dismissed_notices', array() );
1242                    $notices[ $notice ] = true;
1243                    update_option( 'jetpack_dismissed_notices', $notices );
1244                    return rest_ensure_response( get_option( 'jetpack_dismissed_notices', array() ) );
1245
1246                default:
1247                    return new WP_Error( 'invalid_param', esc_html__( 'Invalid parameter "notice".', 'jetpack' ), array( 'status' => 404 ) );
1248            }
1249        }
1250
1251        return new WP_Error( 'required_param', esc_html__( 'Missing parameter "notice".', 'jetpack' ), array( 'status' => 404 ) );
1252    }
1253
1254    /**
1255     * Verify that the user can disconnect the site.
1256     *
1257     * @since 4.3.0
1258     *
1259     * @return bool|WP_Error True if user is able to disconnect the site.
1260     */
1261    public static function disconnect_site_permission_callback() {
1262        if ( current_user_can( 'jetpack_disconnect' ) ) {
1263            return true;
1264        }
1265
1266        return new WP_Error(
1267            'invalid_user_permission_jetpack_disconnect',
1268            REST_Connector::get_user_permissions_error_msg(),
1269            array( 'status' => rest_authorization_required_code() )
1270        );
1271    }
1272
1273    /**
1274     * Verify that the user can get a connect/link URL
1275     *
1276     * @since 4.3.0
1277     *
1278     * @return bool|WP_Error True if user is able to disconnect the site.
1279     */
1280    public static function connect_url_permission_callback() {
1281        if ( current_user_can( 'jetpack_connect_user' ) ) {
1282            return true;
1283        }
1284
1285        return new WP_Error(
1286            'invalid_user_permission_jetpack_connect',
1287            REST_Connector::get_user_permissions_error_msg(),
1288            array( 'status' => rest_authorization_required_code() )
1289        );
1290    }
1291
1292    /**
1293     * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
1294     *
1295     * @uses Automattic\Jetpack\Connection\Manager::is_user_connected();)
1296     *
1297     * @deprecated since Jetpack 14.4.0
1298     * @see Automattic\Jetpack\Connection\REST_Connector::unlink_user_permission_callback()
1299     *
1300     * @since 4.3.0
1301     *
1302     * @return bool|WP_Error True if user is able to unlink.
1303     */
1304    public static function unlink_user_permission_callback() {
1305        _deprecated_function( __METHOD__, 'jetpack-14.4.0', 'Automattic\Jetpack\Connection\REST_Connector::unlink_user_permission_callback()' );
1306        return REST_Connector::unlink_user_permission_callback();
1307    }
1308
1309    /**
1310     * Verify that user can manage Jetpack modules.
1311     *
1312     * @since 4.3.0
1313     *
1314     * @return bool Whether user has the capability 'jetpack_manage_modules'.
1315     */
1316    public static function manage_modules_permission_check() {
1317        if ( current_user_can( 'jetpack_manage_modules' ) ) {
1318            return true;
1319        }
1320
1321        return new WP_Error(
1322            'invalid_user_permission_manage_modules',
1323            REST_Connector::get_user_permissions_error_msg(),
1324            array( 'status' => rest_authorization_required_code() )
1325        );
1326    }
1327
1328    /**
1329     * Verify that user can update Jetpack modules.
1330     *
1331     * @since 4.3.0
1332     *
1333     * @return bool Whether user has the capability 'jetpack_configure_modules'.
1334     */
1335    public static function configure_modules_permission_check() {
1336        if ( current_user_can( 'jetpack_configure_modules' ) ) {
1337            return true;
1338        }
1339
1340        return new WP_Error(
1341            'invalid_user_permission_configure_modules',
1342            REST_Connector::get_user_permissions_error_msg(),
1343            array( 'status' => rest_authorization_required_code() )
1344        );
1345    }
1346
1347    /**
1348     * Verify that user can view Jetpack admin page.
1349     *
1350     * @since 4.3.0
1351     *
1352     * @return bool Whether user has the capability 'jetpack_admin_page'.
1353     */
1354    public static function view_admin_page_permission_check() {
1355        if ( current_user_can( 'jetpack_admin_page' ) ) {
1356            return true;
1357        }
1358
1359        return new WP_Error(
1360            'invalid_user_permission_view_admin',
1361            REST_Connector::get_user_permissions_error_msg(),
1362            array( 'status' => rest_authorization_required_code() )
1363        );
1364    }
1365
1366    /**
1367     * Verify that user can update Jetpack general settings.
1368     *
1369     * @since 4.3.0
1370     *
1371     * @return bool Whether user has the capability 'update_settings_permission_check'.
1372     */
1373    public static function update_settings_permission_check() {
1374        if ( current_user_can( 'jetpack_configure_modules' ) ) {
1375            return true;
1376        }
1377
1378        return new WP_Error(
1379            'invalid_user_permission_manage_settings',
1380            REST_Connector::get_user_permissions_error_msg(),
1381            array( 'status' => rest_authorization_required_code() )
1382        );
1383    }
1384
1385    /**
1386     * Verify that user can view Jetpack admin page and can activate plugins.
1387     *
1388     * @since 4.3.0
1389     *
1390     * @return bool Whether user has the capability 'jetpack_admin_page' and 'activate_plugins'.
1391     */
1392    public static function activate_plugins_permission_check() {
1393        if ( current_user_can( 'jetpack_admin_page' ) && current_user_can( 'activate_plugins' ) ) {
1394            return true;
1395        }
1396
1397        return new WP_Error(
1398            'invalid_user_permission_activate_plugins',
1399            REST_Connector::get_user_permissions_error_msg(),
1400            array( 'status' => rest_authorization_required_code() )
1401        );
1402    }
1403
1404    /**
1405     * Verify that user can edit other's posts (Editors and Administrators).
1406     *
1407     * @return bool Whether user has the capability 'edit_others_posts'.
1408     */
1409    public static function edit_others_posts_check() {
1410        if ( current_user_can( 'edit_others_posts' ) ) {
1411            return true;
1412        }
1413
1414        return new WP_Error(
1415            'invalid_user_permission_edit_others_posts',
1416            REST_Connector::get_user_permissions_error_msg(),
1417            array( 'status' => rest_authorization_required_code() )
1418        );
1419    }
1420
1421    /**
1422     * Verify that site can view and delete the site's purchase token.
1423     *
1424     * @return bool Whether site has level-site auth or user has the capability 'manage_options'.
1425     */
1426    public static function purchase_token_permission_check() {
1427        if ( Rest_Authentication::is_signed_with_blog_token() ) {
1428            return true;
1429        }
1430
1431        if ( current_user_can( 'manage_options' ) ) {
1432            return true;
1433        }
1434
1435        return new WP_Error(
1436            'invalid_permission_manage_purchase_token',
1437            REST_Connector::get_user_permissions_error_msg(),
1438            array( 'status' => rest_authorization_required_code() )
1439        );
1440    }
1441
1442    /**
1443     * Fetch information about the Rewind status of the site.
1444     */
1445    public static function rewind_data() {
1446        $site_id = Jetpack_Options::get_option( 'id' );
1447
1448        if ( ! $site_id ) {
1449            return new WP_Error( 'site_id_missing' );
1450        }
1451
1452        if ( ! isset( $_GET['_cacheBuster'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1453            $rewind_state = get_transient( 'jetpack_rewind_state' );
1454            if ( $rewind_state ) {
1455                return $rewind_state;
1456            }
1457        }
1458
1459        $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/rewind', $site_id ) . '?force=wpcom', '2', array(), null, 'wpcom' );
1460
1461        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1462            return new WP_Error( 'rewind_data_fetch_failed' );
1463        }
1464
1465        $body   = wp_remote_retrieve_body( $response );
1466        $result = json_decode( $body );
1467        set_transient( 'jetpack_rewind_state', $result, 30 * MINUTE_IN_SECONDS );
1468
1469        return $result;
1470    }
1471
1472    /**
1473     * Get rewind data
1474     *
1475     * @since 5.7.0
1476     *
1477     * @return array Array of rewind properties.
1478     */
1479    public static function get_rewind_data() {
1480        $rewind_data = self::rewind_data();
1481
1482        if ( ! is_wp_error( $rewind_data ) ) {
1483            return rest_ensure_response(
1484                array(
1485                    'code'    => 'success',
1486                    'message' => esc_html__( 'Backup & Scan data correctly received.', 'jetpack' ),
1487                    'data'    => wp_json_encode( $rewind_data, JSON_UNESCAPED_SLASHES ),
1488                )
1489            );
1490        }
1491
1492        if ( $rewind_data->get_error_code() === 'rewind_data_fetch_failed' ) {
1493            return new WP_Error( 'rewind_data_fetch_failed', esc_html__( 'Failed fetching rewind data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
1494        }
1495
1496        if ( $rewind_data->get_error_code() === 'site_id_missing' ) {
1497            return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
1498        }
1499
1500        return new WP_Error(
1501            'error_get_rewind_data',
1502            esc_html__( 'Could not retrieve Backup & Scan data.', 'jetpack' ),
1503            array( 'status' => 500 )
1504        );
1505    }
1506
1507    /**
1508     * Gets Scan state data.
1509     *
1510     * @since 8.5.0
1511     *
1512     * @return array|WP_Error Result from WPCOM API or error.
1513     */
1514    public static function scan_state() {
1515
1516        if ( ! isset( $_GET['_cacheBuster'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1517            $scan_state = get_transient( 'jetpack_scan_state' );
1518            if ( ! empty( $scan_state ) ) {
1519                return $scan_state;
1520            }
1521        }
1522        $site_id = Jetpack_Options::get_option( 'id' );
1523
1524        if ( ! $site_id ) {
1525            return new WP_Error( 'site_id_missing' );
1526        }
1527        // The default timeout was too short in come cases.
1528        add_filter( 'http_request_timeout', array( __CLASS__, 'increase_timeout_30' ), PHP_INT_MAX - 1 );
1529        $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/scan', $site_id ) . '?force=wpcom', '2', array(), null, 'wpcom' );
1530        remove_filter( 'http_request_timeout', array( __CLASS__, 'increase_timeout_30' ), PHP_INT_MAX - 1 );
1531
1532        if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
1533            return new WP_Error( 'scan_state_fetch_failed' );
1534        }
1535
1536        $body   = wp_remote_retrieve_body( $response );
1537        $result = json_decode( $body );
1538        set_transient( 'jetpack_scan_state', $result, 30 * MINUTE_IN_SECONDS );
1539
1540        return $result;
1541    }
1542
1543    /**
1544     * Increases the request timeout value to 30 seconds.
1545     *
1546     * @return int Always returns 30.
1547     */
1548    public static function increase_timeout_30() {
1549        return 30; // 30 Seconds
1550    }
1551
1552    /**
1553     * Get Scan state for API.
1554     *
1555     * @since 8.5.0
1556     *
1557     * @return WP_REST_Response|WP_Error REST response or error state.
1558     */
1559    public static function get_scan_state() {
1560        $scan_state = self::scan_state();
1561
1562        if ( ! is_wp_error( $scan_state ) ) {
1563            if ( ( new Host() )->is_woa_site() && ! empty( $scan_state->threats ) ) {
1564                $scan_state->threats = array();
1565            }
1566            return rest_ensure_response(
1567                array(
1568                    'code'    => 'success',
1569                    'message' => esc_html__( 'Scan state correctly received.', 'jetpack' ),
1570                    'data'    => wp_json_encode( $scan_state, JSON_UNESCAPED_SLASHES ),
1571                )
1572            );
1573        }
1574
1575        if ( $scan_state->get_error_code() === 'scan_state_fetch_failed' ) {
1576            return new WP_Error( 'scan_state_fetch_failed', esc_html__( 'Failed fetching rewind data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
1577        }
1578
1579        if ( $scan_state->get_error_code() === 'site_id_missing' ) {
1580            return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
1581        }
1582
1583        return new WP_Error(
1584            'error_get_rewind_data',
1585            esc_html__( 'Could not retrieve Scan state.', 'jetpack' ),
1586            array( 'status' => 500 )
1587        );
1588    }
1589
1590    /**
1591     * Disconnects Jetpack from the WordPress.com Servers
1592     *
1593     * @deprecated since Jetpack 10.0.0
1594     * @see Automattic\Jetpack\Connection\REST_Connector::disconnect_site()
1595     *
1596     * @uses Jetpack::disconnect();
1597     * @since 4.3.0
1598     *
1599     * @param WP_REST_Request $request The request sent to the WP REST API.
1600     *
1601     * @return bool|WP_Error True if Jetpack successfully disconnected.
1602     */
1603    public static function disconnect_site( $request ) {
1604        _deprecated_function( __METHOD__, 'jetpack-10.0.0', '\Automattic\Jetpack\Connection\REST_Connector::disconnect_site' );
1605
1606        if ( ! isset( $request['isActive'] ) || false !== $request['isActive'] ) {
1607            return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
1608        }
1609
1610        if ( Jetpack::is_connection_ready() ) {
1611            Jetpack::disconnect();
1612            return rest_ensure_response( array( 'code' => 'success' ) );
1613        }
1614
1615        return new WP_Error( 'disconnect_failed', esc_html__( 'Was not able to disconnect the site. Please try again.', 'jetpack' ), array( 'status' => 400 ) );
1616    }
1617
1618    /**
1619     * Gets a new connect raw URL with fresh nonce.
1620     *
1621     * @uses Jetpack::disconnect();
1622     * @since 4.3.0
1623     *
1624     * @param WP_REST_Request $request The request sent to the WP REST API.
1625     *
1626     * @return string|WP_Error A raw URL if the connection URL could be built; error message otherwise.
1627     */
1628    public static function build_connect_url( $request = array() ) {
1629        $from     = $request['from'] ?? false;
1630        $redirect = $request['redirect'] ?? false;
1631
1632        $url = Jetpack::init()->build_connect_url( true, $redirect, $from );
1633        if ( $url ) {
1634            return rest_ensure_response( $url );
1635        }
1636
1637        return new WP_Error( 'build_connect_url_failed', esc_html__( 'Unable to build the connect URL. Please reload the page and try again.', 'jetpack' ), array( 'status' => 400 ) );
1638    }
1639
1640    /**
1641     * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
1642     * Information about the master/primary user.
1643     * Information about the current user.
1644     *
1645     * @deprecated since Jetpack 10.0.0
1646     * @see Automattic\Jetpack\Connection\REST_Connector::get_user_connection_data()
1647     *
1648     * @since 4.3.0
1649     *
1650     * @return object
1651     */
1652    public static function get_user_connection_data() {
1653        _deprecated_function( __METHOD__, 'jetpack-10.0.0', '\Automattic\Jetpack\Connection\REST_Connector::get_user_connection_data' );
1654
1655        require_once JETPACK__PLUGIN_DIR . '_inc/lib/admin-pages/class.jetpack-react-page.php';
1656
1657        $connection_owner   = ( new Connection_Manager() )->get_connection_owner();
1658        $owner_display_name = false === $connection_owner ? null : $connection_owner->data->display_name;
1659
1660        $response = array(
1661            'currentUser'     => jetpack_current_user_data(),
1662            'connectionOwner' => $owner_display_name,
1663        );
1664        return rest_ensure_response( $response );
1665    }
1666
1667    /**
1668     * Unlinks current user from the WordPress.com Servers.
1669     *
1670     * @param WP_REST_Request $request The request sent to the WP REST API.
1671     * @uses  Automattic\Jetpack\Connection\Manager->disconnect_user
1672     *
1673     * @deprecated since Jetpack 14.4.0
1674     * @see Automattic\Jetpack\Connection\REST_Connector::unlink_user()
1675     *
1676     * @since 4.3.0
1677     *
1678     * @return bool|WP_Error True if user successfully unlinked.
1679     */
1680    public static function unlink_user( $request ) {
1681        _deprecated_function( __METHOD__, 'jetpack-14.4.0', 'Automattic\Jetpack\Connection\REST_Connector::unlink_user()' );
1682        return REST_Connector::unlink_user( $request );
1683    }
1684
1685    /**
1686     * Gets current user's tracking settings.
1687     *
1688     * @since 6.0.0
1689     *
1690     * @param  WP_REST_Request $request The request sent to the WP REST API.
1691     *
1692     * @return WP_REST_Response|WP_Error Response, else error.
1693     */
1694    public static function get_user_tracking_settings( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1695        if ( ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
1696            $response = array(
1697                'tracks_opt_out' => true, // Default to opt-out if not connected to wp.com.
1698            );
1699        } else {
1700            $response = Client::wpcom_json_api_request_as_user(
1701                '/jetpack-user-tracking',
1702                'v2',
1703                array(
1704                    'method'  => 'GET',
1705                    'headers' => array(
1706                        'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1707                    ),
1708                )
1709            );
1710            if ( ! is_wp_error( $response ) ) {
1711                $response = json_decode( wp_remote_retrieve_body( $response ), true );
1712            }
1713        }
1714
1715        return rest_ensure_response( $response );
1716    }
1717
1718    /**
1719     * Updates current user's tracking settings.
1720     *
1721     * @since 6.0.0
1722     *
1723     * @param  WP_REST_Request $request The request sent to the WP REST API.
1724     *
1725     * @return WP_REST_Response|WP_Error Response, else error.
1726     */
1727    public static function update_user_tracking_settings( $request ) {
1728        if ( ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
1729            $response = array(
1730                'tracks_opt_out' => true, // Default to opt-out if not connected to wp.com.
1731            );
1732        } else {
1733            $response = Client::wpcom_json_api_request_as_user(
1734                '/jetpack-user-tracking',
1735                'v2',
1736                array(
1737                    'method'  => 'PUT',
1738                    'headers' => array(
1739                        'Content-Type'    => 'application/json',
1740                        'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1741                    ),
1742                ),
1743                wp_json_encode( $request->get_params(), JSON_UNESCAPED_SLASHES )
1744            );
1745            if ( ! is_wp_error( $response ) ) {
1746                $response = json_decode( wp_remote_retrieve_body( $response ), true );
1747            }
1748        }
1749
1750        return rest_ensure_response( $response );
1751    }
1752
1753    /**
1754     * Fetch site data from .com including the site's current plan and the site's products.
1755     *
1756     * @since 5.5.0
1757     * @deprecated 16.2 Use Automattic\Jetpack\Connection\Manager::get_connected_site_data().
1758     *
1759     * @return stdClass|WP_Error
1760     */
1761    public static function site_data() {
1762        _deprecated_function( __METHOD__, 'jetpack-16.2', 'Automattic\Jetpack\Connection\Manager::get_connected_site_data' );
1763
1764        return ( new Connection_Manager() )->get_connected_site_data();
1765    }
1766
1767    /**
1768     * Get site data, including for example, the site's current plan.
1769     *
1770     * @since 4.3.0
1771     * @deprecated 16.2 Use Automattic\Jetpack\Connection\REST_Connector::site_data_response().
1772     *
1773     * @return WP_Error|WP_HTTP_Response|WP_REST_Response
1774     */
1775    public static function get_site_data() {
1776        _deprecated_function( __METHOD__, 'jetpack-16.2', 'Automattic\Jetpack\Connection\REST_Connector::site_data_response' );
1777
1778        return REST_Connector::site_data_response();
1779    }
1780
1781    /**
1782     * Fetch AL data for this site and return it.
1783     *
1784     * @since 7.4
1785     *
1786     * @return array|WP_Error
1787     */
1788    public static function get_site_activity() {
1789        $site_id = Jetpack_Options::get_option( 'id' );
1790
1791        if ( ! $site_id ) {
1792            return new WP_Error(
1793                'site_id_missing',
1794                esc_html__( 'Site ID is missing.', 'jetpack' ),
1795                array( 'status' => 400 )
1796            );
1797        }
1798
1799        $response      = Client::wpcom_json_api_request_as_user(
1800            "/sites/$site_id/activity",
1801            '2',
1802            array(
1803                'method'  => 'GET',
1804                'headers' => array(
1805                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1806                ),
1807            ),
1808            null,
1809            'wpcom'
1810        );
1811        $response_code = wp_remote_retrieve_response_code( $response );
1812
1813        if ( 200 !== $response_code ) {
1814            return new WP_Error(
1815                'activity_fetch_failed',
1816                esc_html__( 'Could not retrieve site activity.', 'jetpack' ),
1817                array( 'status' => $response_code )
1818            );
1819        }
1820
1821        $data = json_decode( wp_remote_retrieve_body( $response ) );
1822
1823        if ( ! isset( $data->current->orderedItems ) ) {
1824            return new WP_Error(
1825                'activity_not_found',
1826                esc_html__( 'No activity found', 'jetpack' ),
1827                array( 'status' => 204 ) // no content.
1828            );
1829        }
1830
1831        return rest_ensure_response(
1832            array(
1833                'code' => 'success',
1834                'data' => $data->current->orderedItems,
1835            )
1836        );
1837    }
1838
1839    /**
1840     * Fetch the discount for this site and return it.
1841     *
1842     * @since 10.8
1843     *
1844     * @return array|WP_Error
1845     */
1846    public static function get_site_discount() {
1847        $site_id = Jetpack_Options::get_option( 'id' );
1848
1849        if ( ! $site_id ) {
1850            return new WP_Error(
1851                'site_id_missing',
1852                esc_html__( 'Site ID is missing.', 'jetpack' ),
1853                array( 'status' => 400 )
1854            );
1855        }
1856
1857        $response = Client::wpcom_json_api_request_as_user(
1858            "/sites/$site_id/discount",
1859            '2',
1860            array(
1861                'method'  => 'GET',
1862                'headers' => array(
1863                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1864                ),
1865            )
1866        );
1867
1868        $response_code = wp_remote_retrieve_response_code( $response );
1869        $data          = json_decode( wp_remote_retrieve_body( $response ) );
1870
1871        if ( 200 !== $response_code ) {
1872            return new WP_Error(
1873                'discount_fetch_failed',
1874                is_object( $data ) && property_exists( $data, 'error' ) ? $data->error : esc_html__( 'Could not retrieve site discount.', 'jetpack' ),
1875                array( 'status' => $response_code )
1876            );
1877        }
1878
1879        if ( ! isset( $data ) ) {
1880            return new WP_Error(
1881                'discount_parse_error',
1882                esc_html__( 'Could not parse discount', 'jetpack' ),
1883                array( 'status' => 204 ) // no content.
1884            );
1885        }
1886
1887        return rest_ensure_response(
1888            array(
1889                'code' => 'success',
1890                'data' => $data,
1891            )
1892        );
1893    }
1894
1895    /**
1896     * Reset Jetpack options
1897     *
1898     * @since 4.3.0
1899     *
1900     * @param WP_REST_Request $request {
1901     *     Array of parameters received by request.
1902     *
1903     *     @type string $options Available options to reset are options|modules
1904     * }
1905     *
1906     * @return bool|WP_Error True if options were reset. Otherwise, a WP_Error instance with the corresponding error.
1907     */
1908    public static function reset_jetpack_options( $request ) {
1909
1910        if ( ! isset( $request['reset'] ) || true !== $request['reset'] ) {
1911            return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
1912        }
1913
1914        if ( isset( $request['options'] ) ) {
1915            $data    = $request['options'];
1916            $message = '';
1917
1918            switch ( $data ) {
1919                case ( 'options' ):
1920                    $options_to_reset = Jetpack::get_jetpack_options_for_reset();
1921
1922                    // Reset the Jetpack options.
1923                    foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
1924                        Jetpack_Options::delete_option( $option_to_reset );
1925                    }
1926
1927                    foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
1928                        delete_option( $option_to_reset );
1929                    }
1930
1931                    // Reset to default modules.
1932                    $default_modules = Jetpack::get_default_modules();
1933                    Jetpack::update_active_modules( $default_modules );
1934                    $message = esc_html__( 'Jetpack options reset.', 'jetpack' );
1935
1936                    break;
1937                case 'modules':
1938                    $default_modules = Jetpack::get_default_modules();
1939                    Jetpack::update_active_modules( $default_modules );
1940                    $message = esc_html__( 'Modules reset to default.', 'jetpack' );
1941
1942                    break;
1943                default:
1944                    return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
1945            }
1946
1947            return rest_ensure_response(
1948                array(
1949                    'code'    => 'success',
1950                    'message' => $message,
1951                )
1952            );
1953        }
1954
1955        return new WP_Error( 'required_param', esc_html__( 'Missing parameter "type".', 'jetpack' ), array( 'status' => 404 ) );
1956    }
1957
1958    /**
1959     * Get the query parameters to update module options or general settings.
1960     *
1961     * @since 4.3.0
1962     * @since 4.4.0 Accepts a $selector parameter.
1963     *
1964     * @param string $selector Selects a set of options to update, Can be empty, a module slug or 'any'.
1965     *
1966     * @return array
1967     */
1968    public static function get_updateable_parameters( $selector = '' ) {
1969        $parameters = array(
1970            'context' => array(
1971                'default' => 'edit',
1972            ),
1973        );
1974
1975        return array_merge( $parameters, self::get_updateable_data_list( $selector ) );
1976    }
1977
1978    /**
1979     * Returns a list of module options or general settings that can be updated.
1980     *
1981     * @since 4.3.0
1982     * @since 4.4.0 Accepts 'any' as a parameter which will make it return the entire list.
1983     *
1984     * @param string|array $selector Module slug, 'any', or an array of parameters.
1985     *                               If empty, it's assumed we're updating a module and we'll try to get its slug.
1986     *                               If 'any' the full list is returned.
1987     *                               If it's an array of parameters, includes the elements by matching keys.
1988     *
1989     * @return array
1990     */
1991    public static function get_updateable_data_list( $selector = '' ) {
1992
1993        $options = array(
1994            // Blocks.
1995            'jetpack_blocks_disabled'                   => array(
1996                'description'       => esc_html__( 'Jetpack Blocks disabled.', 'jetpack' ),
1997                'type'              => 'boolean',
1998                'default'           => false,
1999                'validate_callback' => __CLASS__ . '::validate_boolean',
2000                'jp_group'          => 'settings',
2001            ),
2002
2003            // Carousel
2004            'carousel_background_color'                 => array(
2005                'description'       => esc_html__( 'Color scheme.', 'jetpack' ),
2006                'type'              => 'string',
2007                'default'           => 'black',
2008                'enum'              => array(
2009                    'black',
2010                    'white',
2011                ),
2012                'enum_labels'       => array(
2013                    'black' => esc_html__( 'Black', 'jetpack' ),
2014                    'white' => esc_html__( 'White', 'jetpack' ),
2015                ),
2016                'validate_callback' => __CLASS__ . '::validate_list_item',
2017                'jp_group'          => 'carousel',
2018            ),
2019            'carousel_display_exif'                     => array(
2020                'description'       => wp_kses(
2021                    sprintf( __( 'Show photo metadata (<a href="https://en.wikipedia.org/wiki/Exchangeable_image_file_format" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) ),
2022                    array(
2023                        'a' => array(
2024                            'href'   => true,
2025                            'target' => true,
2026                        ),
2027                    )
2028                ),
2029                'type'              => 'boolean',
2030                'default'           => 0,
2031                'validate_callback' => __CLASS__ . '::validate_boolean',
2032                'jp_group'          => 'carousel',
2033            ),
2034            'carousel_display_comments'                 => array(
2035                'description'       => esc_html__( 'Show comments area in carousel', 'jetpack' ),
2036                'type'              => 'boolean',
2037                'default'           => 1,
2038                'validate_callback' => __CLASS__ . '::validate_boolean',
2039                'jp_group'          => 'carousel',
2040            ),
2041
2042            // Comments.
2043            'highlander_comment_form_prompt'            => array(
2044                'description'       => esc_html__( 'Greeting Text', 'jetpack' ),
2045                'type'              => 'string',
2046                'default'           => esc_html__( 'Leave a Reply', 'jetpack' ),
2047                'sanitize_callback' => 'sanitize_text_field',
2048                'jp_group'          => 'comments',
2049            ),
2050            'jetpack_comment_form_color_scheme'         => array(
2051                'description'       => esc_html__( 'Color scheme', 'jetpack' ),
2052                'type'              => 'string',
2053                'default'           => 'light',
2054                'enum'              => array(
2055                    'light',
2056                    'dark',
2057                    'transparent',
2058                ),
2059                'enum_labels'       => array(
2060                    'light'       => esc_html__( 'Light', 'jetpack' ),
2061                    'dark'        => esc_html__( 'Dark', 'jetpack' ),
2062                    'transparent' => esc_html__( 'Transparent', 'jetpack' ),
2063                ),
2064                'validate_callback' => __CLASS__ . '::validate_list_item',
2065                'jp_group'          => 'comments',
2066            ),
2067
2068            // Custom Content Types.
2069            'jetpack_portfolio'                         => array(
2070                'description'       => esc_html__( 'Enable or disable Jetpack portfolio post type.', 'jetpack' ),
2071                'type'              => 'boolean',
2072                'default'           => 0,
2073                'validate_callback' => __CLASS__ . '::validate_boolean',
2074                'jp_group'          => 'settings',
2075            ),
2076            'jetpack_portfolio_posts_per_page'          => array(
2077                'description'       => esc_html__( 'Number of entries to show at most in Portfolio pages.', 'jetpack' ),
2078                'type'              => 'integer',
2079                'default'           => 10,
2080                'validate_callback' => __CLASS__ . '::validate_posint',
2081                'jp_group'          => 'settings',
2082            ),
2083            'jetpack_testimonial'                       => array(
2084                'description'       => esc_html__( 'Enable or disable Jetpack testimonial post type.', 'jetpack' ),
2085                'type'              => 'boolean',
2086                'default'           => 0,
2087                'validate_callback' => __CLASS__ . '::validate_boolean',
2088                'jp_group'          => 'settings',
2089            ),
2090            'jetpack_testimonial_posts_per_page'        => array(
2091                'description'       => esc_html__( 'Number of entries to show at most in Testimonial pages.', 'jetpack' ),
2092                'type'              => 'integer',
2093                'default'           => 10,
2094                'validate_callback' => __CLASS__ . '::validate_posint',
2095                'jp_group'          => 'settings',
2096            ),
2097            // WAF.
2098            'jetpack_waf_automatic_rules'               => array(
2099                'description'       => esc_html__( 'Enable automatic rules - Protect your site against untrusted traffic sources with automatic security rules.', 'jetpack' ),
2100                'type'              => 'boolean',
2101                'default'           => Waf_Compatibility::get_default_automatic_rules_option(),
2102                'validate_callback' => __CLASS__ . '::validate_boolean',
2103                'jp_group'          => 'waf',
2104            ),
2105            'jetpack_waf_ip_block_list_enabled'         => array(
2106                'description'       => esc_html__( 'Block list - Block a specific request IP.', 'jetpack' ),
2107                'type'              => 'boolean',
2108                'default'           => 0,
2109                'validate_callback' => __CLASS__ . '::validate_boolean',
2110                'jp_group'          => 'waf',
2111            ),
2112            'jetpack_waf_ip_block_list'                 => array(
2113                'description'       => esc_html__( 'Blocked IP addresses', 'jetpack' ),
2114                'type'              => 'string',
2115                'default'           => '',
2116                'validate_callback' => __CLASS__ . '::validate_string',
2117                'sanitize_callback' => 'esc_textarea',
2118                'jp_group'          => 'waf',
2119            ),
2120            'jetpack_waf_ip_allow_list_enabled'         => array(
2121                'description'       => esc_html__( 'Allow list - Allow a specific request IP.', 'jetpack' ),
2122                'type'              => 'boolean',
2123                'default'           => 0,
2124                'validate_callback' => __CLASS__ . '::validate_boolean',
2125                'jp_group'          => 'settings',
2126            ),
2127            'jetpack_waf_ip_allow_list'                 => array(
2128                'description'       => esc_html__( 'Always allowed IP addresses', 'jetpack' ),
2129                'type'              => 'string',
2130                'default'           => '',
2131                'validate_callback' => __CLASS__ . '::validate_string',
2132                'sanitize_callback' => 'esc_textarea',
2133                'jp_group'          => 'settings',
2134            ),
2135            'jetpack_waf_share_data'                    => array(
2136                'description'       => esc_html__( 'Share basic data with Jetpack.', 'jetpack' ),
2137                'type'              => 'boolean',
2138                'default'           => 0,
2139                'validate_callback' => __CLASS__ . '::validate_boolean',
2140                'jp_group'          => 'waf',
2141            ),
2142            'jetpack_waf_share_debug_data'              => array(
2143                'description'       => esc_html__( 'Share detailed data with Jetpack.', 'jetpack' ),
2144                'type'              => 'boolean',
2145                'default'           => 0,
2146                'validate_callback' => __CLASS__ . '::validate_boolean',
2147                'jp_group'          => 'waf',
2148            ),
2149            // Galleries.
2150            'tiled_galleries'                           => array(
2151                'description'       => esc_html__( 'Display all your gallery pictures in a cool mosaic.', 'jetpack' ),
2152                'type'              => 'boolean',
2153                'default'           => 0,
2154                'validate_callback' => __CLASS__ . '::validate_boolean',
2155                'jp_group'          => 'tiled-gallery',
2156            ),
2157
2158            'gravatar_disable_hovercards'               => array(
2159                'description'       => esc_html__( "View people's profiles when you mouse over their Gravatars", 'jetpack' ),
2160                'type'              => 'string',
2161                'default'           => 'enabled',
2162                // Not visible. This is used as the checkbox value.
2163                'enum'              => array(
2164                    'enabled',
2165                    'disabled',
2166                ),
2167                'enum_labels'       => array(
2168                    'enabled'  => esc_html__( 'Enabled', 'jetpack' ),
2169                    'disabled' => esc_html__( 'Disabled', 'jetpack' ),
2170                ),
2171                'validate_callback' => __CLASS__ . '::validate_list_item',
2172                'jp_group'          => 'gravatar-hovercards',
2173            ),
2174
2175            // Infinite Scroll.
2176            'infinite_scroll'                           => array(
2177                'description'       => esc_html__( 'To infinity and beyond', 'jetpack' ),
2178                'type'              => 'boolean',
2179                'default'           => 1,
2180                'validate_callback' => __CLASS__ . '::validate_boolean',
2181                'jp_group'          => 'infinite-scroll',
2182            ),
2183            'infinite_scroll_google_analytics'          => array(
2184                'description'       => esc_html__( 'Use Google Analytics with Infinite Scroll', 'jetpack' ),
2185                'type'              => 'boolean',
2186                'default'           => 0,
2187                'validate_callback' => __CLASS__ . '::validate_boolean',
2188                'jp_group'          => 'infinite-scroll',
2189            ),
2190
2191            // Likes.
2192            'wpl_default'                               => array(
2193                'description'       => esc_html__( 'WordPress.com Likes are', 'jetpack' ),
2194                'type'              => 'string',
2195                'default'           => 'on',
2196                'enum'              => array(
2197                    'on',
2198                    'off',
2199                ),
2200                'enum_labels'       => array(
2201                    'on'  => esc_html__( 'On for all posts', 'jetpack' ),
2202                    'off' => esc_html__( 'Turned on per post', 'jetpack' ),
2203                ),
2204                'validate_callback' => __CLASS__ . '::validate_list_item',
2205                'jp_group'          => 'likes',
2206            ),
2207            'social_notifications_like'                 => array(
2208                'description'       => esc_html__( 'Send email notification when someone likes a post', 'jetpack' ),
2209                'type'              => 'boolean',
2210                'default'           => 1,
2211                'validate_callback' => __CLASS__ . '::validate_boolean',
2212                'jp_group'          => 'likes',
2213            ),
2214
2215            // Markdown.
2216            'wpcom_publish_comments_with_markdown'      => array(
2217                'description'       => esc_html__( 'Use Markdown for comments.', 'jetpack' ),
2218                'type'              => 'boolean',
2219                'default'           => 0,
2220                'validate_callback' => __CLASS__ . '::validate_boolean',
2221                'jp_group'          => 'markdown',
2222            ),
2223            'wpcom_publish_posts_with_markdown'         => array(
2224                'description'       => esc_html__( 'Use Markdown for posts.', 'jetpack' ),
2225                'type'              => 'boolean',
2226                'default'           => 0,
2227                'validate_callback' => __CLASS__ . '::validate_boolean',
2228                'jp_group'          => 'markdown',
2229            ),
2230
2231            // Monitor.
2232            'monitor_receive_notifications'             => array(
2233                'description'       => esc_html__( 'Receive Monitor Email Notifications.', 'jetpack' ),
2234                'type'              => 'boolean',
2235                'default'           => 0,
2236                'validate_callback' => __CLASS__ . '::validate_boolean',
2237                'jp_group'          => 'monitor',
2238            ),
2239
2240            // Post by Email.
2241            'post_by_email_address'                     => array(
2242                'description'       => esc_html__( 'Email Address', 'jetpack' ),
2243                'type'              => 'string',
2244                'default'           => 'noop',
2245                'enum'              => array(
2246                    'noop',
2247                    'create',
2248                    'regenerate',
2249                    'delete',
2250                ),
2251                'enum_labels'       => array(
2252                    'noop'       => '',
2253                    'create'     => esc_html__( 'Create Post by Email address', 'jetpack' ),
2254                    'regenerate' => esc_html__( 'Regenerate Post by Email address', 'jetpack' ),
2255                    'delete'     => esc_html__( 'Delete Post by Email address', 'jetpack' ),
2256                ),
2257                'validate_callback' => __CLASS__ . '::validate_list_item',
2258                'jp_group'          => 'post-by-email',
2259            ),
2260
2261            // Protect.
2262            'jetpack_protect_key'                       => array(
2263                'description'       => esc_html__( 'Protect API key', 'jetpack' ),
2264                'type'              => 'string',
2265                'default'           => '',
2266                'validate_callback' => __CLASS__ . '::validate_alphanum',
2267                'jp_group'          => 'protect',
2268            ),
2269            'jetpack_protect_global_whitelist'          => array(
2270                'description'       => esc_html__( 'Protect global IP allow list', 'jetpack' ),
2271                'type'              => 'string',
2272                'default'           => '',
2273                'validate_callback' => __CLASS__ . '::validate_string',
2274                'sanitize_callback' => 'esc_textarea',
2275                'jp_group'          => 'protect',
2276            ),
2277
2278            // Sharing.
2279            'sharing_services'                          => array(
2280                'description'       => esc_html__( 'Enabled Services and those hidden behind a button', 'jetpack' ),
2281                'type'              => 'object',
2282                'default'           => array(
2283                    'visible' => array( 'facebook', 'x' ),
2284                    'hidden'  => array(),
2285                ),
2286                'validate_callback' => __CLASS__ . '::validate_services',
2287                'jp_group'          => 'sharedaddy',
2288            ),
2289            'button_style'                              => array(
2290                'description'       => esc_html__( 'Button Style', 'jetpack' ),
2291                'type'              => 'string',
2292                'default'           => 'icon',
2293                'enum'              => array(
2294                    'icon-text',
2295                    'icon',
2296                    'text',
2297                    'official',
2298                ),
2299                'enum_labels'       => array(
2300                    'icon-text' => esc_html__( 'Icon + text', 'jetpack' ),
2301                    'icon'      => esc_html__( 'Icon only', 'jetpack' ),
2302                    'text'      => esc_html__( 'Text only', 'jetpack' ),
2303                    'official'  => esc_html__( 'Official buttons', 'jetpack' ),
2304                ),
2305                'validate_callback' => __CLASS__ . '::validate_list_item',
2306                'jp_group'          => 'sharedaddy',
2307            ),
2308            'sharing_label'                             => array(
2309                'description'       => esc_html__( 'Sharing Label', 'jetpack' ),
2310                'type'              => 'string',
2311                'default'           => '',
2312                'validate_callback' => __CLASS__ . '::validate_string',
2313                'sanitize_callback' => 'esc_html',
2314                'jp_group'          => 'sharedaddy',
2315            ),
2316            'show'                                      => array(
2317                'description'       => esc_html__( 'Views where buttons are shown', 'jetpack' ),
2318                'type'              => 'array',
2319                'items'             => array(
2320                    'type' => 'string',
2321                ),
2322                'default'           => array( 'post' ),
2323                'validate_callback' => __CLASS__ . '::validate_sharing_show',
2324                'jp_group'          => 'sharedaddy',
2325            ),
2326            'jetpack-twitter-cards-site-tag'            => array(
2327                'description'       => esc_html__( "The Twitter username of the owner of this site's domain.", 'jetpack' ),
2328                'type'              => 'string',
2329                'default'           => '',
2330                'validate_callback' => __CLASS__ . '::validate_twitter_username',
2331                'sanitize_callback' => 'esc_html',
2332                'jp_group'          => 'sharedaddy',
2333            ),
2334            'sharedaddy_disable_resources'              => array(
2335                'description'       => esc_html__( 'Disable CSS and JS', 'jetpack' ),
2336                'type'              => 'boolean',
2337                'default'           => 0,
2338                'validate_callback' => __CLASS__ . '::validate_boolean',
2339                'jp_group'          => 'sharedaddy',
2340            ),
2341            'custom'                                    => array(
2342                'description'       => esc_html__( 'Custom sharing services added by user.', 'jetpack' ),
2343                'type'              => 'object',
2344                'default'           => array(
2345                    'sharing_name' => '',
2346                    'sharing_url'  => '',
2347                    'sharing_icon' => '',
2348                ),
2349                'validate_callback' => __CLASS__ . '::validate_custom_service',
2350                'jp_group'          => 'sharedaddy',
2351            ),
2352            // Not an option, but an action that can be performed on the list of custom services passing the service ID.
2353            'sharing_delete_service'                    => array(
2354                'description'       => esc_html__( 'Delete custom sharing service.', 'jetpack' ),
2355                'type'              => 'string',
2356                'default'           => '',
2357                'validate_callback' => __CLASS__ . '::validate_custom_service_id',
2358                'jp_group'          => 'sharedaddy',
2359            ),
2360
2361            // SSO.
2362            'jetpack_sso_require_two_step'              => array(
2363                'description'       => esc_html__( 'Require Two-Step Authentication', 'jetpack' ),
2364                'type'              => 'boolean',
2365                'default'           => SSO\Helpers::is_require_two_step_checkbox_disabled(),
2366                'validate_callback' => __CLASS__ . '::validate_boolean',
2367                'jp_group'          => 'sso',
2368            ),
2369            'jetpack_sso_match_by_email'                => array(
2370                'description'       => esc_html__( 'Match by Email', 'jetpack' ),
2371                'type'              => 'boolean',
2372                'default'           => 1,
2373                'validate_callback' => __CLASS__ . '::validate_boolean',
2374                'jp_group'          => 'sso',
2375            ),
2376
2377            // Subscriptions.
2378            'stb_enabled'                               => array(
2379                'description'       => esc_html__( "Show a <em>'follow blog'</em> option in the comment form", 'jetpack' ),
2380                'type'              => 'boolean',
2381                'default'           => 1,
2382                'validate_callback' => __CLASS__ . '::validate_boolean',
2383                'jp_group'          => 'subscriptions',
2384            ),
2385            'stc_enabled'                               => array(
2386                'description'       => esc_html__( "Show a <em>'follow comments'</em> option in the comment form", 'jetpack' ),
2387                'type'              => 'boolean',
2388                'default'           => 1,
2389                'validate_callback' => __CLASS__ . '::validate_boolean',
2390                'jp_group'          => 'subscriptions',
2391            ),
2392            'wpcom_newsletter_categories'               => array(
2393                'description'       => esc_html__( 'Array of post category ids that are marked as newsletter categories', 'jetpack' ),
2394                'type'              => 'array',
2395                'default'           => array(),
2396                'validate_callback' => __CLASS__ . '::validate_array',
2397                'jp_group'          => 'subscriptions',
2398            ),
2399            'wpcom_newsletter_categories_enabled'       => array(
2400                'description'       => esc_html__( 'Whether the newsletter categories are enabled or not', 'jetpack' ),
2401                'type'              => 'boolean',
2402                'default'           => 0,
2403                'validate_callback' => __CLASS__ . '::validate_boolean',
2404                'jp_group'          => 'subscriptions',
2405            ),
2406            'wpcom_newsletter_send_default'             => array(
2407                'description'       => esc_html__( 'Whether to send newsletter emails by default when publishing a post', 'jetpack' ),
2408                'type'              => 'boolean',
2409                'default'           => 1,
2410                'validate_callback' => __CLASS__ . '::validate_boolean',
2411                'jp_group'          => 'subscriptions',
2412            ),
2413            'wpcom_featured_image_in_email'             => array(
2414                'description'       => esc_html__( 'Whether to include the featured image in the email or not', 'jetpack' ),
2415                'type'              => 'boolean',
2416                'default'           => 0,
2417                'validate_callback' => __CLASS__ . '::validate_boolean',
2418                'jp_group'          => 'subscriptions',
2419            ),
2420            'jetpack_gravatar_in_email'                 => array(
2421                'description'       => esc_html__( 'Whether to show author avatar in the email byline', 'jetpack' ),
2422                'type'              => 'boolean',
2423                'default'           => 1,
2424                'validate_callback' => __CLASS__ . '::validate_boolean',
2425                'jp_group'          => 'subscriptions',
2426            ),
2427            'jetpack_author_in_email'                   => array(
2428                'description'       => esc_html__( 'Whether to show author display name in the email byline', 'jetpack' ),
2429                'type'              => 'boolean',
2430                'default'           => 1,
2431                'validate_callback' => __CLASS__ . '::validate_boolean',
2432                'jp_group'          => 'subscriptions',
2433            ),
2434            'jetpack_post_date_in_email'                => array(
2435                'description'       => esc_html__( 'Whether to show date in the email byline', 'jetpack' ),
2436                'type'              => 'boolean',
2437                'default'           => 1,
2438                'validate_callback' => __CLASS__ . '::validate_boolean',
2439                'jp_group'          => 'subscriptions',
2440            ),
2441            'wpcom_subscription_emails_use_excerpt'     => array(
2442                'description'       => esc_html__( 'Whether to use the excerpt in the email or not', 'jetpack' ),
2443                'type'              => 'boolean',
2444                'default'           => 0,
2445                'validate_callback' => __CLASS__ . '::validate_boolean',
2446                'jp_group'          => 'subscriptions',
2447            ),
2448            'jetpack_subscriptions_reply_to'            => array(
2449                'description'       => esc_html__( 'Reply to email behaviour for newsletters emails', 'jetpack' ),
2450                'type'              => 'string',
2451                'default'           => Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to,
2452                'validate_callback' => __CLASS__ . '::validate_subscriptions_reply_to',
2453                'jp_group'          => 'subscriptions',
2454            ),
2455            'jetpack_subscriptions_from_name'           => array(
2456                'description'       => esc_html__( 'From name for newsletters emails', 'jetpack' ),
2457                'type'              => 'string',
2458                'default'           => '',
2459                'validate_callback' => __CLASS__ . '::validate_subscriptions_reply_to_name',
2460                'jp_group'          => 'subscriptions',
2461            ),
2462            'sm_enabled'                                => array(
2463                'description'       => esc_html__( 'Show popup Subscribe modal to readers.', 'jetpack' ),
2464                'type'              => 'boolean',
2465                'default'           => 0,
2466                'validate_callback' => __CLASS__ . '::validate_boolean',
2467                'jp_group'          => 'subscriptions',
2468            ),
2469            'jetpack_subscribe_overlay_enabled'         => array(
2470                'description'       => esc_html__( 'Show subscribe overlay on homepage.', 'jetpack' ),
2471                'type'              => 'boolean',
2472                'default'           => 0,
2473                'validate_callback' => __CLASS__ . '::validate_boolean',
2474                'jp_group'          => 'subscriptions',
2475            ),
2476            'jetpack_subscribe_floating_button_enabled' => array(
2477                'description'       => esc_html__( 'Show a floating subscribe button.', 'jetpack' ),
2478                'type'              => 'boolean',
2479                'default'           => 0,
2480                'validate_callback' => __CLASS__ . '::validate_boolean',
2481                'jp_group'          => 'subscriptions',
2482            ),
2483            'jetpack_subscriptions_subscribe_post_end_enabled' => array(
2484                'description'       => esc_html__( 'Add Subscribe block at the end of each post.', 'jetpack' ),
2485                'type'              => 'boolean',
2486                'default'           => 0,
2487                'validate_callback' => __CLASS__ . '::validate_boolean',
2488                'jp_group'          => 'subscriptions',
2489            ),
2490            'jetpack_subscriptions_login_navigation_enabled' => array(
2491                'description'       => esc_html__( 'Add Subscriber Login block to the navigation.', 'jetpack' ),
2492                'type'              => 'boolean',
2493                'default'           => 0,
2494                'validate_callback' => __CLASS__ . '::validate_boolean',
2495                'jp_group'          => 'subscriptions',
2496            ),
2497            'jetpack_subscriptions_subscribe_navigation_enabled' => array(
2498                'description'       => esc_html__( 'Add Subscribe block to the navigation.', 'jetpack' ),
2499                'type'              => 'boolean',
2500                'default'           => 0,
2501                'validate_callback' => __CLASS__ . '::validate_boolean',
2502                'jp_group'          => 'subscriptions',
2503            ),
2504            'social_notifications_subscribe'            => array(
2505                'description'       => esc_html__( 'Send email notification when someone subscribes to my blog', 'jetpack' ),
2506                'type'              => 'boolean',
2507                'default'           => 0,
2508                'validate_callback' => __CLASS__ . '::validate_boolean',
2509                'jp_group'          => 'subscriptions',
2510            ),
2511            'subscription_options'                      => array(
2512                'description'       => esc_html__( 'Options used in subscription email templates and the Subscribe block: \'invitation\', \'welcome\', \'comment_follow\', \'subscribe_modal_heading\', \'free_tier_description\' and \'hide_free_tier\'.', 'jetpack' ),
2513                'type'              => 'object',
2514                'default'           => array(
2515                    'invitation'              => '',
2516                    'welcome'                 => '',
2517                    'comment_follow'          => '',
2518                    'subscribe_modal_heading' => '',
2519                    'free_tier_description'   => '',
2520                    'hide_free_tier'          => false,
2521                ),
2522                'validate_callback' => __CLASS__ . '::validate_subscription_options',
2523                'jp_group'          => 'subscriptions',
2524            ),
2525
2526            // Related Posts.
2527            'show_headline'                             => array(
2528                'description'       => esc_html__( 'Highlight related content with a heading', 'jetpack' ),
2529                'type'              => 'boolean',
2530                'default'           => 1,
2531                'validate_callback' => __CLASS__ . '::validate_boolean',
2532                'jp_group'          => 'related-posts',
2533            ),
2534            'show_thumbnails'                           => array(
2535                'description'       => esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
2536                'type'              => 'boolean',
2537                'default'           => 0,
2538                'validate_callback' => __CLASS__ . '::validate_boolean',
2539                'jp_group'          => 'related-posts',
2540            ),
2541
2542            // Search.
2543            'instant_search_enabled'                    => array(
2544                'description'       => esc_html__( 'Enable Instant Search', 'jetpack' ),
2545                'type'              => 'boolean',
2546                'default'           => 0,
2547                'validate_callback' => __CLASS__ . '::validate_boolean',
2548                'jp_group'          => 'search',
2549            ),
2550
2551            'has_jetpack_search_product'                => array(
2552                'description'       => esc_html__( 'Has an active Jetpack Search product purchase', 'jetpack' ),
2553                'type'              => 'boolean',
2554                'default'           => 0,
2555                'validate_callback' => __CLASS__ . '::validate_boolean',
2556                'jp_group'          => 'settings',
2557            ),
2558
2559            'search_auto_config'                        => array(
2560                'description'       => esc_html__( 'Trigger an auto config of instant search', 'jetpack' ),
2561                'type'              => 'boolean',
2562                'default'           => 0,
2563                'validate_callback' => __CLASS__ . '::validate_boolean',
2564                'jp_group'          => 'search',
2565            ),
2566
2567            // Verification Tools.
2568            'google'                                    => array(
2569                'description'       => esc_html__( 'Google Search Console', 'jetpack' ),
2570                'type'              => 'string',
2571                'default'           => '',
2572                'validate_callback' => __CLASS__ . '::validate_verification_service',
2573                'jp_group'          => 'verification-tools',
2574            ),
2575            'bing'                                      => array(
2576                'description'       => esc_html__( 'Bing Webmaster Center', 'jetpack' ),
2577                'type'              => 'string',
2578                'default'           => '',
2579                'validate_callback' => __CLASS__ . '::validate_verification_service',
2580                'jp_group'          => 'verification-tools',
2581            ),
2582            'pinterest'                                 => array(
2583                'description'       => esc_html__( 'Pinterest Site Verification', 'jetpack' ),
2584                'type'              => 'string',
2585                'default'           => '',
2586                'validate_callback' => __CLASS__ . '::validate_verification_service',
2587                'jp_group'          => 'verification-tools',
2588            ),
2589            'yandex'                                    => array(
2590                'description'       => esc_html__( 'Yandex Site Verification', 'jetpack' ),
2591                'type'              => 'string',
2592                'default'           => '',
2593                'validate_callback' => __CLASS__ . '::validate_verification_service',
2594                'jp_group'          => 'verification-tools',
2595            ),
2596            'facebook'                                  => array(
2597                'description'       => esc_html__( 'Facebook Domain Verification', 'jetpack' ),
2598                'type'              => 'string',
2599                'default'           => '',
2600                'validate_callback' => __CLASS__ . '::validate_verification_service',
2601                'jp_group'          => 'verification-tools',
2602            ),
2603
2604            // WordAds.
2605            'enable_header_ad'                          => array(
2606                'description'       => esc_html__( 'Display an ad unit at the top of each page.', 'jetpack' ),
2607                'type'              => 'boolean',
2608                'default'           => 1,
2609                'validate_callback' => __CLASS__ . '::validate_boolean',
2610                'jp_group'          => 'wordads',
2611            ),
2612            'wordads_approved'                          => array(
2613                'description'       => esc_html__( 'Is site approved for WordAds?', 'jetpack' ),
2614                'type'              => 'boolean',
2615                'default'           => 0,
2616                'validate_callback' => __CLASS__ . '::validate_boolean',
2617                'jp_group'          => 'wordads',
2618            ),
2619            'wordads_second_belowpost'                  => array(
2620                'description'       => esc_html__( 'Display second ad below post?', 'jetpack' ),
2621                'type'              => 'boolean',
2622                'default'           => 1,
2623                'validate_callback' => __CLASS__ . '::validate_boolean',
2624                'jp_group'          => 'wordads',
2625            ),
2626            'wordads_inline_enabled'                    => array(
2627                'description'       => esc_html__( 'Display inline ad within post content?', 'jetpack' ),
2628                'type'              => 'boolean',
2629                'default'           => 1,
2630                'validate_callback' => __CLASS__ . '::validate_boolean',
2631                'jp_group'          => 'wordads',
2632            ),
2633            'wordads_display_front_page'                => array(
2634                'description'       => esc_html__( 'Display ads on the front page?', 'jetpack' ),
2635                'type'              => 'boolean',
2636                'default'           => 1,
2637                'validate_callback' => __CLASS__ . '::validate_boolean',
2638                'jp_group'          => 'wordads',
2639            ),
2640            'wordads_display_post'                      => array(
2641                'description'       => esc_html__( 'Display ads on posts?', 'jetpack' ),
2642                'type'              => 'boolean',
2643                'default'           => 1,
2644                'validate_callback' => __CLASS__ . '::validate_boolean',
2645                'jp_group'          => 'wordads',
2646            ),
2647            'wordads_display_page'                      => array(
2648                'description'       => esc_html__( 'Display ads on pages?', 'jetpack' ),
2649                'type'              => 'boolean',
2650                'default'           => 1,
2651                'validate_callback' => __CLASS__ . '::validate_boolean',
2652                'jp_group'          => 'wordads',
2653            ),
2654            'wordads_display_archive'                   => array(
2655                'description'       => esc_html__( 'Display ads on archive pages?', 'jetpack' ),
2656                'type'              => 'boolean',
2657                'default'           => 1,
2658                'validate_callback' => __CLASS__ . '::validate_boolean',
2659                'jp_group'          => 'wordads',
2660            ),
2661            'wordads_custom_adstxt_enabled'             => array(
2662                'description'       => esc_html__( 'Custom ads.txt', 'jetpack' ),
2663                'type'              => 'boolean',
2664                'default'           => 0,
2665                'validate_callback' => __CLASS__ . '::validate_boolean',
2666                'jp_group'          => 'wordads',
2667            ),
2668            'wordads_custom_adstxt'                     => array(
2669                'description'       => esc_html__( 'Custom ads.txt entries', 'jetpack' ),
2670                'type'              => 'string',
2671                'default'           => '',
2672                'validate_callback' => __CLASS__ . '::validate_string',
2673                'sanitize_callback' => 'sanitize_textarea_field',
2674                'jp_group'          => 'wordads',
2675            ),
2676            'wordads_ccpa_enabled'                      => array(
2677                'description'       => esc_html__( 'Enable support for California Consumer Privacy Act', 'jetpack' ),
2678                'type'              => 'boolean',
2679                'default'           => 0,
2680                'validate_callback' => __CLASS__ . '::validate_boolean',
2681                'jp_group'          => 'wordads',
2682            ),
2683            'wordads_ccpa_privacy_policy_url'           => array(
2684                'description'       => esc_html__( 'Privacy Policy URL', 'jetpack' ),
2685                'type'              => 'string',
2686                'default'           => '',
2687                'validate_callback' => __CLASS__ . '::validate_string',
2688                'sanitize_callback' => 'sanitize_text_field',
2689                'jp_group'          => 'wordads',
2690            ),
2691            'wordads_cmp_enabled'                       => array(
2692                'description'       => esc_html__( 'Enable GDPR Consent Management Banner for WordAds', 'jetpack' ),
2693                'type'              => 'boolean',
2694                'default'           => 0,
2695                'validate_callback' => __CLASS__ . '::validate_boolean',
2696                'jp_group'          => 'wordads',
2697            ),
2698
2699            // Google Analytics.
2700            'google_analytics_tracking_id'              => array(
2701                'description'       => esc_html__( 'Google Analytics', 'jetpack' ),
2702                'type'              => 'string',
2703                'default'           => '',
2704                'validate_callback' => __CLASS__ . '::validate_alphanum',
2705                'jp_group'          => 'google-analytics',
2706            ),
2707            'jetpack_wga'                               => array(
2708                'description' => esc_html__( 'Google Analytics', 'jetpack' ),
2709                'type'        => 'object',
2710                'jp_group'    => 'settings',
2711            ),
2712
2713            // Stats.
2714            'admin_bar'                                 => array(
2715                'description'       => esc_html__( 'Include a small chart in your admin bar with a 48-hour traffic snapshot.', 'jetpack' ),
2716                'type'              => 'boolean',
2717                'default'           => 1,
2718                'validate_callback' => __CLASS__ . '::validate_boolean',
2719                'jp_group'          => 'stats',
2720            ),
2721            'enable_odyssey_stats'                      => array(
2722                'description'       => esc_html__( 'Preview the new Jetpack Stats experience (Experimental).', 'jetpack' ),
2723                'type'              => 'boolean',
2724                'default'           => 1,
2725                'validate_callback' => __CLASS__ . '::validate_boolean',
2726                'jp_group'          => 'stats',
2727            ),
2728            'roles'                                     => array(
2729                'description'       => esc_html__( 'Select the roles that will be able to view stats reports.', 'jetpack' ),
2730                'type'              => 'array',
2731                'items'             => array(
2732                    'type' => 'string',
2733                ),
2734                'default'           => array( 'administrator' ),
2735                'validate_callback' => __CLASS__ . '::validate_stats_roles',
2736                'sanitize_callback' => __CLASS__ . '::sanitize_stats_allowed_roles',
2737                'jp_group'          => 'stats',
2738            ),
2739            'count_roles'                               => array(
2740                'description'       => esc_html__( 'Count the page views of registered users who are logged in.', 'jetpack' ),
2741                'type'              => 'array',
2742                'items'             => array(
2743                    'type' => 'string',
2744                ),
2745                'default'           => array( 'administrator' ),
2746                'validate_callback' => __CLASS__ . '::validate_stats_roles',
2747                'jp_group'          => 'stats',
2748            ),
2749            'blog_id'                                   => array(
2750                'description'       => esc_html__( 'Blog ID.', 'jetpack' ),
2751                'type'              => 'boolean',
2752                'default'           => 0,
2753                'validate_callback' => __CLASS__ . '::validate_boolean',
2754                'jp_group'          => 'stats',
2755            ),
2756            'do_not_track'                              => array(
2757                'description'       => esc_html__( 'Do not track.', 'jetpack' ),
2758                'type'              => 'boolean',
2759                'default'           => 1,
2760                'validate_callback' => __CLASS__ . '::validate_boolean',
2761                'jp_group'          => 'stats',
2762            ),
2763            'version'                                   => array(
2764                'description'       => esc_html__( 'Version.', 'jetpack' ),
2765                'type'              => 'integer',
2766                'default'           => 9,
2767                'validate_callback' => __CLASS__ . '::validate_posint',
2768                'jp_group'          => 'stats',
2769            ),
2770            'collapse_nudges'                           => array(
2771                'description'       => esc_html__( 'Collapse upgrade nudges', 'jetpack' ),
2772                'type'              => 'boolean',
2773                'default'           => 0,
2774                'validate_callback' => __CLASS__ . '::validate_boolean',
2775                'jp_group'          => 'stats',
2776            ),
2777
2778            // Whether to share stats views with WordPress.com Reader.
2779            'wpcom_reader_views_enabled'                => array(
2780                'description'       => esc_html__( 'Show post views in the WordPress.com Reader.', 'jetpack' ),
2781                'type'              => 'boolean',
2782                'default'           => 1,
2783                'validate_callback' => __CLASS__ . '::validate_boolean',
2784                'jp_group'          => 'settings',
2785            ),
2786
2787            // Akismet - Not a module, but a plugin. The options can be passed and handled differently.
2788            'akismet_show_user_comments_approved'       => array(
2789                'description'       => '',
2790                'type'              => 'boolean',
2791                'default'           => 0,
2792                'validate_callback' => __CLASS__ . '::validate_boolean',
2793                'jp_group'          => 'settings',
2794            ),
2795
2796            'wordpress_api_key'                         => array(
2797                'description'       => '',
2798                'type'              => 'string',
2799                'default'           => '',
2800                'validate_callback' => __CLASS__ . '::validate_alphanum',
2801                'jp_group'          => 'settings',
2802            ),
2803
2804            // Empty stats card dismiss.
2805            'dismiss_empty_stats_card'                  => array(
2806                'description'       => '',
2807                'type'              => 'boolean',
2808                'default'           => 0,
2809                'validate_callback' => __CLASS__ . '::validate_boolean',
2810                'jp_group'          => 'settings',
2811            ),
2812
2813            // Backup Getting Started card on dashboard.
2814            'dismiss_dash_backup_getting_started'       => array(
2815                'description'       => '',
2816                'type'              => 'boolean',
2817                'default'           => 0,
2818                'validate_callback' => __CLASS__ . '::validate_boolean',
2819                'jp_group'          => 'settings',
2820            ),
2821
2822            // Agencies Learn More card on dashboard.
2823            'dismiss_dash_agencies_learn_more'          => array(
2824                'description'       => '',
2825                'type'              => 'boolean',
2826                'default'           => 0,
2827                'validate_callback' => __CLASS__ . '::validate_boolean',
2828                'jp_group'          => 'settings',
2829            ),
2830
2831            'lang_id'                                   => array(
2832                'description' => esc_html__( 'Primary language for the site.', 'jetpack' ),
2833                'type'        => 'string',
2834                'default'     => 'en_US',
2835                'jp_group'    => 'settings',
2836            ),
2837
2838            // SEO Tools.
2839            'advanced_seo_front_page_description'       => array(
2840                'description'       => esc_html__( 'Front page meta description.', 'jetpack' ),
2841                'type'              => 'string',
2842                'default'           => '',
2843                'sanitize_callback' => 'Jetpack_SEO_Utils::sanitize_front_page_meta_description',
2844                'jp_group'          => 'seo-tools',
2845            ),
2846
2847            'advanced_seo_title_formats'                => array(
2848                'description'       => esc_html__( 'SEO page title structures.', 'jetpack' ),
2849                'type'              => 'object',
2850                'default'           => array(
2851                    'archives'   => array(),
2852                    'front_page' => array(),
2853                    'groups'     => array(),
2854                    'pages'      => array(),
2855                    'posts'      => array(),
2856                ),
2857                'jp_group'          => 'seo-tools',
2858                'validate_callback' => 'Jetpack_SEO_Titles::are_valid_title_formats',
2859                'sanitize_callback' => 'Jetpack_SEO_Titles::sanitize_title_formats',
2860            ),
2861
2862            // AI tab (Jetpack > SEO). Plain option the SEO package reads to serve
2863            // /llms.txt. The front-end behavior is gated inside the package; this
2864            // only round-trips the persisted state alongside the other seo-tools
2865            // settings.
2866            'jetpack_seo_llms_txt_enabled'              => array(
2867                'description'       => esc_html__( 'Generate an llms.txt file to guide AI assistants around your content.', 'jetpack' ),
2868                'type'              => 'boolean',
2869                'default'           => 0,
2870                'validate_callback' => __CLASS__ . '::validate_boolean',
2871                'jp_group'          => 'seo-tools',
2872            ),
2873
2874            // AI tab (Jetpack > SEO). Sparse per-crawler override map the SEO
2875            // package reads to emit robots.txt directives for blocked AI crawlers.
2876            // Stored as `slug => bool` (true = blocked); catalog validation and
2877            // default-pruning happen in `Ai_Crawlers::get_overrides()`.
2878            'jetpack_seo_ai_crawler_overrides'          => array(
2879                'description'       => esc_html__( 'AI crawler allow/block overrides.', 'jetpack' ),
2880                'type'              => 'object',
2881                'default'           => array(),
2882                'jp_group'          => 'seo-tools',
2883                'sanitize_callback' => __CLASS__ . '::sanitize_ai_crawler_overrides',
2884            ),
2885
2886            // VideoPress.
2887            'videopress_private_enabled_for_site'       => array(
2888                'description'       => esc_html__( 'Video Privacy: Restrict views to members of this site', 'jetpack' ),
2889                'type'              => 'boolean',
2890                'default'           => 0,
2891                'validate_callback' => __CLASS__ . '::validate_boolean',
2892                'jp_group'          => 'videopress',
2893            ),
2894        );
2895
2896        // SEO Tools - SEO Enhancer.
2897        // TODO: move this to the main options array? The filter was there while developing the feature.
2898        // It might come in handy to hold its availability behind the filter since it still depends on AI to be available.
2899        if ( apply_filters( 'ai_seo_enhancer_enabled', true ) ) {
2900            $options['ai_seo_enhancer_enabled'] = array(
2901                'description'       => esc_html__( 'Automatically generate SEO title, SEO description, and image alt text for new posts.', 'jetpack' ),
2902                'type'              => 'boolean',
2903                'default'           => 0,
2904                'validate_callback' => __CLASS__ . '::validate_boolean',
2905                'jp_group'          => 'seo-tools',
2906            );
2907        }
2908
2909        // Add modules to list so they can be toggled.
2910        $modules = Jetpack::get_available_modules();
2911        if ( is_array( $modules ) && ! empty( $modules ) ) {
2912            $module_args = array(
2913                'description'       => '',
2914                'type'              => 'boolean',
2915                'default'           => 0,
2916                'validate_callback' => __CLASS__ . '::validate_boolean',
2917                'jp_group'          => 'modules',
2918            );
2919            foreach ( $modules as $module ) {
2920                $options[ $module ] = $module_args;
2921            }
2922        }
2923
2924        if ( is_array( $selector ) ) {
2925
2926            // Return only those options whose keys match $selector keys.
2927            return array_intersect_key( $options, $selector );
2928        }
2929
2930        if ( 'any' === $selector ) {
2931
2932            // Toggle module or update any module option or any general setting.
2933            return $options;
2934        }
2935
2936        // We're updating the options for a single module.
2937        if ( empty( $selector ) ) {
2938            $selector = self::get_module_requested();
2939        }
2940        $selected = array();
2941        foreach ( $options as $option => $attributes ) {
2942
2943            // Not adding an isset( $attributes['jp_group'] ) because if it's not set, it must be fixed, otherwise options will fail.
2944            if ( $selector === $attributes['jp_group'] ) {
2945                $selected[ $option ] = $attributes;
2946            }
2947        }
2948        return $selected;
2949    }
2950
2951    /**
2952     * Validates that the parameters are proper values that can be set during Jetpack onboarding.
2953     *
2954     * @since 5.4.0
2955     *
2956     * @deprecated since 13.9
2957     *
2958     * @param array           $onboarding_data Values to check.
2959     * @param WP_REST_Request $request         The request sent to the WP REST API.
2960     * @param string          $param           Name of the parameter passed to endpoint holding $value.
2961     *
2962     * @return bool|WP_Error
2963     */
2964    public static function validate_onboarding( $onboarding_data, $request, $param ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
2965        _deprecated_function( __METHOD__, '13.9' );
2966        return true;
2967    }
2968
2969    /**
2970     * Validates that the parameter is either a pure boolean or a numeric string that can be mapped to a boolean.
2971     *
2972     * @since 4.3.0
2973     *
2974     * @param string|bool     $value Value to check.
2975     * @param WP_REST_Request $request The request sent to the WP REST API.
2976     * @param string          $param Name of the parameter passed to endpoint holding $value.
2977     *
2978     * @return bool|WP_Error
2979     */
2980    public static function validate_boolean( $value, $request, $param ) {
2981        // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- Other code depends on loose comparison here.
2982        if ( ! is_bool( $value ) && ! ( ctype_digit( (string) $value ) && in_array( $value, array( 0, 1 ) ) ) ) {
2983            return new WP_Error(
2984                'invalid_param',
2985                sprintf(
2986                    /* Translators: Placeholder is a parameter name. */
2987                    esc_html__( '%s must be true, false, 0 or 1.', 'jetpack' ),
2988                    $param
2989                )
2990            );
2991        }
2992        return true;
2993    }
2994
2995    /**
2996     * Validates that the parameter is a positive integer.
2997     *
2998     * @since 4.3.0
2999     *
3000     * @param int             $value Value to check.
3001     * @param WP_REST_Request $request The request sent to the WP REST API.
3002     * @param string          $param Name of the parameter passed to endpoint holding $value.
3003     *
3004     * @return bool|WP_Error
3005     */
3006    public static function validate_posint( $value, $request, $param ) {
3007        if ( ! is_numeric( $value ) || $value <= 0 ) {
3008            return new WP_Error(
3009                'invalid_param',
3010                sprintf(
3011                    /* Translators: Placeholder is a parameter name. */
3012                    esc_html__( '%s must be a positive integer.', 'jetpack' ),
3013                    $param
3014                )
3015            );
3016        }
3017        return true;
3018    }
3019
3020    /**
3021     * Validates that the parameter is a non-negative integer (includes 0).
3022     *
3023     * @since 10.4.0
3024     *
3025     * @param int             $value Value to check.
3026     * @param WP_REST_Request $request The request sent to the WP REST API.
3027     * @param string          $param Name of the parameter passed to endpoint holding $value.
3028     *
3029     * @return bool|WP_Error
3030     */
3031    public static function validate_non_neg_int( $value, $request, $param ) {
3032        if ( ! is_numeric( $value ) || $value < 0 ) {
3033            return new WP_Error(
3034                'invalid_param',
3035                /* translators: %s: The literal parameter name. Should not be translated. */
3036                sprintf( esc_html__( '%s must be a non-negative integer.', 'jetpack' ), $param )
3037            );
3038        }
3039        return true;
3040    }
3041
3042    /**
3043     * Validates that the parameter belongs to a list of admitted values.
3044     *
3045     * @since 4.3.0
3046     *
3047     * @param string          $value Value to check.
3048     * @param WP_REST_Request $request The request sent to the WP REST API.
3049     * @param string          $param Name of the parameter passed to endpoint holding $value.
3050     *
3051     * @return bool|WP_Error
3052     */
3053    public static function validate_list_item( $value, $request, $param ) {
3054        $attributes = $request->get_attributes();
3055        if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
3056            return new WP_Error(
3057                'invalid_param',
3058                sprintf(
3059                    /* Translators: Placeholder is a parameter name. */
3060                    esc_html__( '%s not recognized', 'jetpack' ),
3061                    $param
3062                )
3063            );
3064        }
3065        $args = $attributes['args'][ $param ];
3066        if ( ! empty( $args['enum'] ) ) {
3067            // If it's an associative array, use the keys to check that the value is among those admitted.
3068            $enum = ( count( array_filter( array_keys( $args['enum'] ), 'is_string' ) ) > 0 )
3069                ? array_keys( $args['enum'] )
3070                : $args['enum'];
3071            $enum = array_map( 'strval', $enum );
3072            if ( ! in_array( $value, $enum, true ) ) {
3073                return new WP_Error(
3074                    'invalid_param_value',
3075                    sprintf(
3076                    /* Translators: first variable is the parameter passed to endpoint that holds the list item, the second is a list of admitted values. */
3077                        esc_html__( '%1$s must be one of %2$s', 'jetpack' ),
3078                        $param,
3079                        implode( ', ', $enum )
3080                    )
3081                );
3082            }
3083        }
3084        return true;
3085    }
3086
3087    /**
3088     * Validates that the parameter belongs to a list of admitted values.
3089     *
3090     * @since 4.3.0
3091     *
3092     * @param string          $value Value to check.
3093     * @param WP_REST_Request $request The request sent to the WP REST API.
3094     * @param string          $param Name of the parameter passed to endpoint holding $value.
3095     *
3096     * @return bool|WP_Error
3097     */
3098    public static function validate_module_list( $value, $request, $param ) {
3099        if ( ! is_array( $value ) ) {
3100            return new WP_Error(
3101                'invalid_param_value',
3102                sprintf(
3103                    /* Translators: Placeholder is a parameter name. */
3104                    esc_html__( '%s must be an array', 'jetpack' ),
3105                    $param
3106                )
3107            );
3108        }
3109
3110        $modules = Jetpack::get_available_modules();
3111
3112        if ( count( array_intersect( $value, $modules ) ) !== count( $value ) ) {
3113            return new WP_Error(
3114                'invalid_param_value',
3115                sprintf(
3116                    /* Translators: Placeholder is a parameter name. */
3117                    esc_html__( '%s must be a list of valid modules', 'jetpack' ),
3118                    $param
3119                )
3120            );
3121        }
3122
3123        return true;
3124    }
3125
3126    /**
3127     * Validates that the parameter is an alphanumeric or empty string (to be able to clear the field).
3128     *
3129     * @since 4.3.0
3130     *
3131     * @param string          $value Value to check.
3132     * @param WP_REST_Request $request The request sent to the WP REST API.
3133     * @param string          $param Name of the parameter passed to endpoint holding $value.
3134     *
3135     * @return bool|WP_Error
3136     */
3137    public static function validate_alphanum( $value, $request, $param ) {
3138        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^[a-z0-9]+$/i', $value ) ) ) {
3139            return new WP_Error(
3140                'invalid_param',
3141                sprintf(
3142                    /* Translators: Placeholder is a parameter name. */
3143                    esc_html__( '%s must be an alphanumeric string.', 'jetpack' ),
3144                    $param
3145                )
3146            );
3147        }
3148        return true;
3149    }
3150
3151    /**
3152     * Validates that the parameter is a tag or id for a verification service, or an empty string (to be able to clear the field).
3153     *
3154     * @since 4.6.0
3155     *
3156     * @param string          $value   Value to check.
3157     * @param WP_REST_Request $request The request sent to the WP REST API.
3158     * @param string          $param   Name of the parameter passed to endpoint holding $value.
3159     *
3160     * @return bool|WP_Error
3161     */
3162    public static function validate_verification_service( $value, $request, $param ) {
3163        if ( ! empty( $value ) && ! ( is_string( $value ) && ( preg_match( '/^[a-z0-9_-]+$/i', $value ) || jetpack_verification_get_code( $value ) !== false ) ) ) {
3164            return new WP_Error(
3165                'invalid_param',
3166                sprintf(
3167                    /* Translators: Placeholder is a verification string used to verify a service like Google Webmaster Console. */
3168                    esc_html__( '%s must be an alphanumeric string or a verification tag.', 'jetpack' ),
3169                    $param
3170                )
3171            );
3172        }
3173        return true;
3174    }
3175
3176    /**
3177     * Validates that the parameter is among the roles allowed for Stats.
3178     *
3179     * @since 4.3.0
3180     *
3181     * @param mixed           $value Value to check.
3182     * @param WP_REST_Request $request The request sent to the WP REST API.
3183     * @param string          $param Name of the parameter passed to endpoint holding $value.
3184     *
3185     * @return bool|WP_Error
3186     */
3187    public static function validate_stats_roles( $value, $request, $param ) {
3188        // An empty value clears the setting; sanitize_stats_allowed_roles() falls back to 'administrator'.
3189        if ( empty( $value ) ) {
3190            return true;
3191        }
3192
3193        // Enforce the schema's list-of-strings contract before array_intersect() below sees the value.
3194        if ( ! is_array( $value ) || count( array_filter( $value, 'is_string' ) ) !== count( $value ) ) {
3195            return new WP_Error(
3196                'invalid_param',
3197                sprintf(
3198                    /* Translators: Placeholder is a parameter name. */
3199                    esc_html__( '%s must be an array of user roles.', 'jetpack' ),
3200                    $param
3201                )
3202            );
3203        }
3204
3205        if ( ! function_exists( 'get_editable_roles' ) ) {
3206            require_once ABSPATH . 'wp-admin/includes/user.php';
3207        }
3208        $editable_roles = array_keys( get_editable_roles() );
3209        if ( ! array_intersect( $editable_roles, $value ) ) {
3210            return new WP_Error(
3211                'invalid_param',
3212                sprintf(
3213                    /* Translators: first variable is the name of a parameter passed to endpoint holding the role that will be checked, the second is a list of roles allowed to see stats. The parameter is checked against this list. */
3214                    esc_html__( '%1$s must be %2$s.', 'jetpack' ),
3215                    $param,
3216                    implode( ', ', $editable_roles )
3217                )
3218            );
3219        }
3220        return true;
3221    }
3222
3223    /**
3224     * Validates that the parameter is among the views where the Sharing can be displayed.
3225     *
3226     * @since 4.3.0
3227     *
3228     * @param string|bool     $value Value to check.
3229     * @param WP_REST_Request $request The request sent to the WP REST API.
3230     * @param string          $param Name of the parameter passed to endpoint holding $value.
3231     *
3232     * @return bool|WP_Error
3233     */
3234    public static function validate_sharing_show( $value, $request, $param ) {
3235        $views = array( 'index', 'post', 'page', 'attachment', 'jetpack-portfolio' );
3236        if ( ! is_array( $value ) ) {
3237            return new WP_Error(
3238                'invalid_param',
3239                sprintf(
3240                    /* Translators: Placeholder is a parameter name. */
3241                    esc_html__( '%s must be an array of post types.', 'jetpack' ),
3242                    $param
3243                )
3244            );
3245        }
3246        if ( ! array_intersect( $views, $value ) ) {
3247            return new WP_Error(
3248                'invalid_param',
3249                sprintf(
3250                    /* Translators: first variable is the name of a parameter passed to endpoint holding the post type where Sharing will be displayed, the second is a list of post types where Sharing can be displayed */
3251                    esc_html__( '%1$s must be %2$s.', 'jetpack' ),
3252                    $param,
3253                    implode( ', ', $views )
3254                )
3255            );
3256        }
3257        return true;
3258    }
3259
3260    /**
3261     * Validates that the parameter is among the valid reply-to types for subscriptions.
3262     *
3263     * @since 4.3.0
3264     *
3265     * @param string|bool     $value Value to check.
3266     * @param WP_REST_Request $request The request sent to the WP REST API.
3267     * @param string          $param Name of the parameter passed to endpoint holding $value.
3268     *
3269     * @return bool|WP_Error
3270     */
3271    public static function validate_subscriptions_reply_to( $value, $request, $param ) {
3272        require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
3273        if ( ! empty( $value ) && ! Automattic\Jetpack\Modules\Subscriptions\Settings::is_valid_reply_to( $value ) ) {
3274            return new WP_Error(
3275                'invalid_param',
3276                sprintf(
3277                    /* Translators: Placeholder is a parameter name. */
3278                    esc_html__( '%s must be a valid type.', 'jetpack' ),
3279                    $param
3280                )
3281            );
3282        }
3283        return true;
3284    }
3285
3286    /**
3287     * Validates that the parameter is among the valid reply-to types for subscriptions.
3288     *
3289     * @since 4.3.0
3290     *
3291     * @param string|bool     $value Value to check.
3292     * @param WP_REST_Request $request The request sent to the WP REST API.
3293     * @param string          $param Name of the parameter passed to endpoint holding $value.
3294     *
3295     * @return bool|WP_Error
3296     */
3297    public static function validate_subscriptions_reply_to_name( $value, $request, $param ) {
3298        if ( ! empty( $value ) && ! is_string( $value ) ) {
3299            return new WP_Error(
3300                'invalid_param',
3301                sprintf(
3302                    /* Translators: Placeholder is a parameter name. */
3303                    esc_html__( '%s must be a valid type.', 'jetpack' ),
3304                    $param
3305                )
3306            );
3307        }
3308        return true;
3309    }
3310
3311    /**
3312     * Validates that the parameter is among the views where the Sharing can be displayed.
3313     *
3314     * @since 4.3.0
3315     *
3316     * @param string|bool     $value {
3317     *         Value to check received by request.
3318     *
3319     *     @type array $visible List of slug of services to share to that are displayed directly in the page.
3320     *     @type array $hidden  List of slug of services to share to that are concealed in a folding menu.
3321     * }
3322     * @param WP_REST_Request $request The request sent to the WP REST API.
3323     * @param string          $param Name of the parameter passed to endpoint holding $value.
3324     *
3325     * @return bool|WP_Error
3326     */
3327    public static function validate_services( $value, $request, $param ) {
3328        if ( ! is_array( $value ) || ! isset( $value['visible'] ) || ! isset( $value['hidden'] ) ) {
3329            return new WP_Error(
3330                'invalid_param',
3331                sprintf(
3332                    /* Translators: Placeholder is a parameter name. */
3333                    esc_html__( '%s must be an array with visible and hidden items.', 'jetpack' ),
3334                    $param
3335                )
3336            );
3337        }
3338
3339        // Allow to clear everything.
3340        if ( empty( $value['visible'] ) && empty( $value['hidden'] ) ) {
3341            return true;
3342        }
3343
3344        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3345            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3346        }
3347        $sharer   = new Sharing_Service();
3348        $services = array_keys( $sharer->get_all_services() );
3349
3350        if (
3351            ( ! empty( $value['visible'] ) && ! array_intersect( $value['visible'], $services ) )
3352            ||
3353            ( ! empty( $value['hidden'] ) && ! array_intersect( $value['hidden'], $services ) ) ) {
3354            return new WP_Error(
3355                'invalid_param',
3356                sprintf(
3357                    /* Translators: placeholder 1 is a parameter holding the services passed to endpoint, placeholder 2 is a list of all Jetpack Sharing services */
3358                    esc_html__( '%1$s visible and hidden items must be a list of %2$s.', 'jetpack' ),
3359                    $param,
3360                    implode( ', ', $services )
3361                )
3362            );
3363        }
3364        return true;
3365    }
3366
3367    /**
3368     * Validates that the parameter has enough information to build a custom sharing button.
3369     *
3370     * @since 4.3.0
3371     *
3372     * @param string|bool     $value Value to check.
3373     * @param WP_REST_Request $request The request sent to the WP REST API.
3374     * @param string          $param Name of the parameter passed to endpoint holding $value.
3375     *
3376     * @return bool|WP_Error
3377     */
3378    public static function validate_custom_service( $value, $request, $param ) {
3379        if ( ! is_array( $value ) || ! isset( $value['sharing_name'] ) || ! isset( $value['sharing_url'] ) || ! isset( $value['sharing_icon'] ) ) {
3380            return new WP_Error(
3381                'invalid_param',
3382                sprintf(
3383                    /* Translators: Placeholder is a parameter name. */
3384                    esc_html__( '%s must be an array with sharing name, url and icon.', 'jetpack' ),
3385                    $param
3386                )
3387            );
3388        }
3389
3390        // Allow to clear everything.
3391        if ( empty( $value['sharing_name'] ) && empty( $value['sharing_url'] ) && empty( $value['sharing_icon'] ) ) {
3392            return true;
3393        }
3394
3395        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3396            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3397        }
3398
3399        if ( ( ! empty( $value['sharing_name'] ) && ! is_string( $value['sharing_name'] ) )
3400        || ( ! empty( $value['sharing_url'] ) && ! is_string( $value['sharing_url'] ) )
3401        || ( ! empty( $value['sharing_icon'] ) && ! is_string( $value['sharing_icon'] ) ) ) {
3402            return new WP_Error(
3403                'invalid_param',
3404                sprintf(
3405                    /* Translators: Placeholder is a parameter name. */
3406                    esc_html__( '%s needs sharing name, url and icon.', 'jetpack' ),
3407                    $param
3408                )
3409            );
3410        }
3411        return true;
3412    }
3413
3414    /**
3415     * Validates that the parameter is a custom sharing service ID like 'custom-1461976264'.
3416     *
3417     * @since 4.3.0
3418     *
3419     * @param string          $value Value to check.
3420     * @param WP_REST_Request $request The request sent to the WP REST API.
3421     * @param string          $param Name of the parameter passed to endpoint holding $value.
3422     *
3423     * @return bool|WP_Error
3424     */
3425    public static function validate_custom_service_id( $value, $request, $param ) {
3426        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/custom\-[0-1]+/i', $value ) ) ) {
3427            return new WP_Error(
3428                'invalid_param',
3429                sprintf(
3430                    /* Translators: Placeholder is a parameter name. */
3431                    esc_html__( "%s must be a string prefixed with 'custom-' and followed by a numeric ID.", 'jetpack' ),
3432                    $param
3433                )
3434            );
3435        }
3436
3437        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3438            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3439        }
3440        $sharer   = new Sharing_Service();
3441        $services = $sharer->get_all_services();
3442
3443        if ( ! empty( $value ) && ! isset( $services[ $value ] ) ) {
3444            return new WP_Error(
3445                'invalid_param',
3446                sprintf(
3447                    /* Translators: Placeholder is a parameter name. */
3448                    esc_html__( '%s is not a registered custom sharing service.', 'jetpack' ),
3449                    $param
3450                )
3451            );
3452        }
3453
3454        return true;
3455    }
3456
3457    /**
3458     * Validates that the parameter is a Twitter username or empty string (to be able to clear the field).
3459     *
3460     * @since 4.3.0
3461     *
3462     * @param string          $value   Value to check.
3463     * @param WP_REST_Request $request The request sent to the WP REST API.
3464     * @param string          $param   Name of the parameter passed to endpoint holding $value.
3465     *
3466     * @return bool|WP_Error
3467     */
3468    public static function validate_twitter_username( $value, $request, $param ) {
3469        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^@?\w{1,15}$/i', $value ) ) ) {
3470            return new WP_Error(
3471                'invalid_param',
3472                sprintf(
3473                    /* Translators: Placeholder is a twitter name. */
3474                    esc_html__( '%s must be a Twitter username.', 'jetpack' ),
3475                    $param
3476                )
3477            );
3478        }
3479        return true;
3480    }
3481
3482    /**
3483     * Validates that the parameter is a string.
3484     *
3485     * @since 4.3.0
3486     *
3487     * @param string          $value Value to check.
3488     * @param WP_REST_Request $request The request sent to the WP REST API.
3489     * @param string          $param Name of the parameter passed to endpoint holding $value.
3490     *
3491     * @return bool|WP_Error
3492     */
3493    public static function validate_string( $value, $request, $param ) {
3494        if ( ! is_string( $value ) ) {
3495            return new WP_Error(
3496                'invalid_param',
3497                sprintf(
3498                    /* Translators: Placeholder is a parameter name. */
3499                    esc_html__( '%s must be a string.', 'jetpack' ),
3500                    $param
3501                )
3502            );
3503        }
3504        return true;
3505    }
3506
3507    /**
3508     * Validates that the parameter is an array of strings.
3509     *
3510     * @param array           $value Value to check.
3511     * @param WP_REST_Request $request The request sent to the WP REST API.
3512     * @param string          $param Name of the parameter passed to the endpoint holding $value.
3513     *
3514     * @return bool|WP_Error
3515     */
3516    public static function validate_array_of_strings( $value, $request, $param ) {
3517        foreach ( $value as $array_item ) {
3518            $validate = self::validate_string( $array_item, $request, $param );
3519            if ( is_wp_error( $validate ) ) {
3520                return $validate;
3521            }
3522        }
3523
3524        return true;
3525    }
3526
3527    /**
3528     * Validates the subscription_options parameter.
3529     *
3530     * @param array $values Value to check.
3531     *
3532     * @return bool|WP_Error
3533     */
3534    public static function validate_subscription_options( $values ) {
3535        // A REST "object" decodes to a PHP associative array. Reject any other
3536        // type (object, string, int, null, ...) up front so the array_keys()
3537        // loop below never runs against a non-array and triggers a PHP warning.
3538        if ( ! is_array( $values ) ) {
3539            return new WP_Error(
3540                'invalid_param',
3541                /* Translators: subscription_options is a variable name, and shouldn't be translated. */
3542                esc_html__( 'subscription_options must be an object.', 'jetpack' )
3543            );
3544        }
3545        foreach ( array_keys( $values ) as $key ) {
3546            if ( ! in_array( $key, array( 'welcome', 'invitation', 'comment_follow', 'subscribe_modal_heading', 'free_tier_description', 'hide_free_tier' ), true ) ) {
3547                return new WP_Error(
3548                    'invalid_param',
3549                    sprintf(
3550                        /* Translators: Placeholder is the invalid param being sent. */
3551                        esc_html__( '%s is not one of the allowed members of subscription_options.', 'jetpack' ),
3552                        $key
3553                    )
3554                );
3555            }
3556        }
3557        return true;
3558    }
3559
3560    /**
3561     * Validates that the parameter is an array.
3562     *
3563     * @param array           $values Value to check.
3564     * @param WP_REST_Request $request The request sent to the WP REST API.
3565     * @param string          $param Name of the parameter passed to the endpoint holding $value.
3566     *
3567     * @return bool|WP_Error
3568     */
3569    public static function validate_array( $values, $request, $param ) {
3570        if ( ! is_array( $values ) ) {
3571            return new WP_Error(
3572                'invalid_param',
3573                sprintf(
3574                    /* Translators: Placeholder is a parameter name. */
3575                    esc_html__( '%s must be an object.', 'jetpack' ),
3576                    $param
3577                )
3578            );
3579        }
3580        return true;
3581    }
3582
3583    /**
3584     * If for some reason the roles allowed to see Stats are empty (for example, user tampering with checkboxes),
3585     * return an array with only 'administrator' as the allowed role and save it for 'roles' option.
3586     *
3587     * @since 4.3.0
3588     *
3589     * @param mixed $value Value to check.
3590     *
3591     * @return mixed The value as submitted, or an array holding only 'administrator' when it is empty.
3592     */
3593    public static function sanitize_stats_allowed_roles( $value ) {
3594        if ( empty( $value ) ) {
3595            return array( 'administrator' );
3596        }
3597        return $value;
3598    }
3599
3600    /**
3601     * Sanitize the AI crawler override map.
3602     *
3603     * Keeps the value package-agnostic: each key is normalized with sanitize_key()
3604     * and each value cast to bool. Catalog validation and default-pruning happen in
3605     * `Automattic\Jetpack\SEO\Ai_Crawlers::get_overrides()`.
3606     *
3607     * @param mixed $value The submitted override map.
3608     *
3609     * @return array<string, bool> Sanitized `slug => bool` map.
3610     */
3611    public static function sanitize_ai_crawler_overrides( $value ) {
3612        if ( ! is_array( $value ) ) {
3613            return array();
3614        }
3615
3616        $sanitized = array();
3617        foreach ( $value as $k => $v ) {
3618            $sanitized[ sanitize_key( $k ) ] = (bool) $v;
3619        }
3620        return $sanitized;
3621    }
3622
3623    /**
3624     * Get the currently accessed route and return the module slug in it.
3625     *
3626     * @since 4.3.0
3627     *
3628     * @param string $route Regular expression for the endpoint with the module slug to return.
3629     *
3630     * @return array|string
3631     */
3632    public static function get_module_requested( $route = '/module/(?P<slug>[a-z\-]+)' ) {
3633
3634        if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) || ! is_string( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
3635            return '';
3636        }
3637
3638        preg_match( "#$route#", $GLOBALS['wp']->query_vars['rest_route'], $module );
3639
3640        if ( empty( $module['slug'] ) ) {
3641            return '';
3642        }
3643
3644        return $module['slug'];
3645    }
3646
3647    /**
3648     * Adds extra information for modules.
3649     *
3650     * @since 4.3.0
3651     *
3652     * @param string|array $modules Can be a single module or a list of modules.
3653     * @param null|string  $slug    Slug of the module in the first parameter.
3654     *
3655     * @return array|string
3656     */
3657    public static function prepare_modules_for_response( $modules = '', $slug = null ) {
3658        global $wp_rewrite;
3659
3660        /** This filter is documented in modules/sitemaps/sitemaps.php */
3661        $location = apply_filters( 'jetpack_sitemap_location', '' );
3662
3663        if ( $wp_rewrite->using_index_permalinks() ) {
3664            $sitemap_url      = home_url( '/index.php' . $location . '/sitemap.xml' );
3665            $news_sitemap_url = home_url( '/index.php' . $location . '/news-sitemap.xml' );
3666        } elseif ( $wp_rewrite->using_permalinks() ) {
3667            $sitemap_url      = home_url( $location . '/sitemap.xml' );
3668            $news_sitemap_url = home_url( $location . '/news-sitemap.xml' );
3669        } else {
3670            $sitemap_url      = home_url( $location . '/?jetpack-sitemap=sitemap.xml' );
3671            $news_sitemap_url = home_url( $location . '/?jetpack-sitemap=news-sitemap.xml' );
3672        }
3673
3674        if ( $slug === null && isset( $modules['sitemaps'] ) ) {
3675            // Is a list of modules.
3676            $modules['sitemaps']['extra']['sitemap_url']      = $sitemap_url;
3677            $modules['sitemaps']['extra']['news_sitemap_url'] = $news_sitemap_url;
3678        } elseif ( 'sitemaps' === $slug ) {
3679            // It's a single module.
3680            $modules['extra']['sitemap_url']      = $sitemap_url;
3681            $modules['extra']['news_sitemap_url'] = $news_sitemap_url;
3682        }
3683        return $modules;
3684    }
3685
3686    /**
3687     * Remove options the current user cannot read.
3688     *
3689     * Covers every `jetpack_waf_*` option, plus the two Protect options that expose the
3690     * same data under a different name: `jetpack_protect_global_whitelist` is populated
3691     * from `jetpack_waf_ip_allow_list`, and `jetpack_protect_key` is a shared secret.
3692     *
3693     * @since 16.2
3694     *
3695     * @param array $options Option definitions keyed by option name.
3696     * @return array
3697     */
3698    public static function filter_options_for_response( $options ) {
3699        if ( current_user_can( 'manage_options' ) ) {
3700            return $options;
3701        }
3702
3703        $restricted = array(
3704            'jetpack_protect_key',
3705            'jetpack_protect_global_whitelist',
3706        );
3707
3708        return array_filter(
3709            $options,
3710            static function ( $option_name ) use ( $restricted ) {
3711                return 0 !== strpos( $option_name, 'jetpack_waf_' )
3712                    && ! in_array( $option_name, $restricted, true );
3713            },
3714            ARRAY_FILTER_USE_KEY
3715        );
3716    }
3717
3718    /**
3719     * Remove 'validate_callback' item from options available for module.
3720     * Fetch current option value and add to array of module options.
3721     * Prepare values of module options that need special handling, like those saved in wpcom.
3722     *
3723     * @since 4.3.0
3724     *
3725     * @param string $module Module slug.
3726     * @return array
3727     */
3728    public static function prepare_options_for_response( $module = '' ) {
3729        $options = self::get_updateable_data_list( $module );
3730
3731        if ( ! is_array( $options ) || empty( $options ) ) {
3732            return $options;
3733        }
3734
3735        // Some modules need special treatment.
3736        switch ( $module ) {
3737
3738            case 'monitor':
3739                // Status of user notifications.
3740                $options['monitor_receive_notifications']['current_value'] = self::cast_value( self::get_remote_value( 'monitor', 'monitor_receive_notifications' ), $options['monitor_receive_notifications'] );
3741                break;
3742
3743            case 'post-by-email':
3744                // Email address.
3745                $options['post_by_email_address']['current_value'] = self::cast_value( self::get_remote_value( 'post-by-email', 'post_by_email_address' ), $options['post_by_email_address'] );
3746                break;
3747
3748            case 'protect':
3749                // Protect.
3750                $options['jetpack_protect_key']['current_value']              = get_site_option( 'jetpack_protect_key', false );
3751                $options['jetpack_protect_global_whitelist']['current_value'] = Brute_Force_Protection_Shared_Functions::format_allow_list();
3752                break;
3753
3754            case 'related-posts':
3755                // It's local, but it must be broken apart since it's saved as an array.
3756                $options = self::split_options( $options, Jetpack_Options::get_option( 'relatedposts' ) );
3757                break;
3758
3759            case 'verification-tools':
3760                // It's local, but it must be broken apart since it's saved as an array.
3761                $options = self::split_options( $options, get_option( 'verification_services_codes' ) );
3762                break;
3763
3764            case 'google-analytics':
3765                $wga  = get_option( 'jetpack_wga' );
3766                $code = '';
3767                if ( is_array( $wga ) && array_key_exists( 'code', $wga ) ) {
3768                    $code = $wga['code'];
3769                }
3770                $options['google_analytics_tracking_id']['current_value'] = $code;
3771                break;
3772
3773            case 'sharedaddy':
3774                // It's local, but it must be broken apart since it's saved as an array.
3775                if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3776                    break;
3777                }
3778                $sharer                                       = new Sharing_Service();
3779                $options                                      = self::split_options( $options, $sharer->get_global_options() );
3780                $options['sharing_services']['current_value'] = $sharer->get_blog_services();
3781                $other_sharedaddy_options                     = array( 'jetpack-twitter-cards-site-tag', 'sharedaddy_disable_resources', 'sharing_delete_service' );
3782                foreach ( $other_sharedaddy_options as $key ) {
3783                    $default_value                    = $options[ $key ]['default'] ?? '';
3784                    $current_value                    = get_option( $key, $default_value );
3785                    $options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
3786                }
3787                break;
3788
3789            case 'stats':
3790                // It's local, but it must be broken apart since it's saved as an array.
3791                $options = self::split_options( $options, Stats_Options::get_options() );
3792                break;
3793            default:
3794                // These option are just stored as plain WordPress options.
3795                foreach ( $options as $key => $value ) {
3796                    $default_value                    = $options[ $key ]['default'] ?? '';
3797                    $current_value                    = get_option( $key, $default_value );
3798                    $options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
3799                }
3800        }
3801        // At this point some options have current_value not set because they're options
3802        // that only get written on update, so we set current_value to the default one.
3803        foreach ( $options as $key => $value ) {
3804            // We don't need validate_callback in the response.
3805            if ( isset( $options[ $key ]['validate_callback'] ) ) {
3806                unset( $options[ $key ]['validate_callback'] );
3807            }
3808            $default_value = $options[ $key ]['default'] ?? '';
3809            if ( ! array_key_exists( 'current_value', $options[ $key ] ) ) {
3810                $options[ $key ]['current_value'] = self::cast_value( $default_value, $options[ $key ] );
3811            }
3812        }
3813
3814        // Filter last: the switch above assigns current_value by key without isset(),
3815        // so filtering earlier would let those assignments re-add a removed option.
3816        return self::filter_options_for_response( $options );
3817    }
3818
3819    /**
3820     * Splits module options saved as arrays like relatedposts or verification_services_codes into separate options to be returned in the response.
3821     *
3822     * @since 4.3.0
3823     *
3824     * @param array  $separate_options Array of options admitted by the module.
3825     * @param array  $grouped_options Option saved as array to be splitted.
3826     * @param string $prefix Optional prefix for the separate option keys.
3827     *
3828     * @return array
3829     */
3830    public static function split_options( $separate_options, $grouped_options, $prefix = '' ) {
3831        if ( is_array( $grouped_options ) ) {
3832            foreach ( $grouped_options as $key => $value ) {
3833                $option_key = $prefix . $key;
3834                if ( isset( $separate_options[ $option_key ] ) ) {
3835                    $separate_options[ $option_key ]['current_value'] = self::cast_value( $grouped_options[ $key ], $separate_options[ $option_key ] );
3836                }
3837            }
3838        }
3839        return $separate_options;
3840    }
3841
3842    /**
3843     * Perform a casting to the value specified in the option definition.
3844     *
3845     * @since 4.3.0
3846     *
3847     * @param mixed $value Value to cast to the proper type.
3848     * @param array $definition Type to cast the value to.
3849     *
3850     * @return bool|float|int|string
3851     */
3852    public static function cast_value( $value, $definition ) {
3853        if ( 'NULL' === $value ) {
3854            return null;
3855        }
3856
3857        if ( isset( $definition['type'] ) ) {
3858            switch ( $definition['type'] ) {
3859                case 'boolean':
3860                    if ( 'true' === $value || 'on' === $value ) {
3861                        return true;
3862                    } elseif ( 'false' === $value || 'off' === $value ) {
3863                        return false;
3864                    }
3865                    $value = (bool) $value;
3866                    break;
3867
3868                case 'integer':
3869                    $value = (int) $value;
3870                    break;
3871
3872                case 'float':
3873                    $value = (float) $value;
3874                    break;
3875
3876                case 'string':
3877                    $value = (string) $value;
3878                    break;
3879            }
3880        }
3881        return $value;
3882    }
3883
3884    /**
3885     * Get a value not saved locally.
3886     *
3887     * @since 4.3.0
3888     *
3889     * @param string $module Module slug.
3890     * @param string $option Option name.
3891     *
3892     * @return bool Whether user is receiving notifications or not.
3893     */
3894    public static function get_remote_value( $module, $option ) {
3895
3896        if ( in_array( $module, array( 'post-by-email' ), true ) ) {
3897            $option .= get_current_user_id();
3898        }
3899
3900        // If option doesn't exist, 'does_not_exist' will be returned.
3901        $value = get_option( $option, 'does_not_exist' );
3902
3903        // If option exists, just return it.
3904        if ( 'does_not_exist' !== $value ) {
3905            return $value;
3906        }
3907
3908        // Only check a remote option if Jetpack is connected.
3909        if ( ! Jetpack::is_connection_ready() ) {
3910            return false;
3911        }
3912
3913        // Do what is necessary for each module.
3914        switch ( $module ) {
3915            case 'monitor':
3916                // Load the class to use the method. If class can't be found, do nothing.
3917                if ( ! class_exists( 'Jetpack_Monitor' ) && ! include_once Jetpack::get_module_path( $module ) ) {
3918                    return false;
3919                }
3920                $value = Jetpack_Monitor::user_receives_notifications( false );
3921                break;
3922
3923            case 'post-by-email':
3924                // Load the class to use the method. If class can't be found, do nothing.
3925                if ( ! class_exists( 'Jetpack_Post_By_Email' ) && ! include_once Jetpack::get_module_path( $module ) ) {
3926                    return false;
3927                }
3928                $value = Jetpack_Post_By_Email::init()->get_post_by_email_address();
3929                if ( null === $value ) {
3930                    $value = 'NULL'; // sentinel value so it actually gets set.
3931                }
3932                break;
3933        }
3934
3935        // Normalize value to boolean.
3936        if ( is_wp_error( $value ) || $value === null ) {
3937            $value = false;
3938        }
3939
3940        // Save option to use it next time.
3941        update_option( $option, $value );
3942
3943        return $value;
3944    }
3945
3946    /**
3947     * Get number of plugin updates available.
3948     *
3949     * @since 4.3.0
3950     *
3951     * @return mixed|WP_Error Number of plugin updates available. Otherwise, a WP_Error instance with the corresponding error.
3952     */
3953    public static function get_plugin_update_count() {
3954        $updates = wp_get_update_data();
3955        if ( isset( $updates['counts'] ) && isset( $updates['counts']['plugins'] ) ) {
3956            $count = $updates['counts']['plugins'];
3957            if ( 0 === $count ) {
3958                $response = array(
3959                    'code'    => 'success',
3960                    'message' => esc_html__( 'All plugins are up-to-date. Keep up the good work!', 'jetpack' ),
3961                    'count'   => 0,
3962                );
3963            } else {
3964                $response = array(
3965                    'code'    => 'updates-available',
3966                    'message' => esc_html(
3967                        sprintf(
3968                            /* Translators: placeholders are numbers. */
3969                            _n( '%s plugin needs updating.', '%s plugins need updating.', $count, 'jetpack' ),
3970                            $count
3971                        )
3972                    ),
3973                    'count'   => $count,
3974                );
3975            }
3976            return rest_ensure_response( $response );
3977        }
3978
3979        return new WP_Error( 'not_found', esc_html__( 'Could not check updates for plugins on this site.', 'jetpack' ), array( 'status' => 404 ) );
3980    }
3981
3982    /**
3983     * Get plugins data in site.
3984     *
3985     * @since 4.2.0
3986     *
3987     * @return WP_REST_Response|WP_Error List of plugins in the site. Otherwise, a WP_Error instance with the corresponding error.
3988     */
3989    public static function get_plugins() {
3990        $plugins = Plugins_Installer::get_plugins();
3991
3992        if ( ! empty( $plugins ) ) {
3993            return rest_ensure_response( $plugins );
3994        }
3995
3996        return new WP_Error( 'not_found', esc_html__( 'Unable to list plugins.', 'jetpack' ), array( 'status' => 404 ) );
3997    }
3998
3999    /**
4000     * Install a specific plugin and optionally activates it.
4001     *
4002     * @since 8.9.0
4003     *
4004     * @param WP_REST_Request $request {
4005     *     Array of parameters received by request.
4006     *
4007     *     @type string $slug   Plugin slug.
4008     *     @type string $status Plugin status.
4009     *     @type string $source Where did the plugin installation request originate.
4010     * }
4011     *
4012     * @return WP_REST_Response|WP_Error A response object if the installation and / or activation was successful, or a WP_Error object if it failed.
4013     */
4014    public static function install_plugin( $request ) {
4015        $plugin = stripslashes( $request['slug'] );
4016
4017        // Let's make sure the plugin isn't already installed.
4018        $plugin_id = Plugins_Installer::get_plugin_id_by_slug( $plugin );
4019
4020        // If not installed, let's install now.
4021        if ( ! $plugin_id ) {
4022            $result = Plugins_Installer::install_plugin( $plugin );
4023
4024            if ( is_wp_error( $result ) ) {
4025                return new WP_Error(
4026                    'install_plugin_failed',
4027                    sprintf(
4028                        /* translators: %1$s: plugin name. -- %2$s: error message. */
4029                        __( 'Unable to install %1$s: %2$s ', 'jetpack' ),
4030                        $plugin,
4031                        $result->get_error_message()
4032                    ),
4033                    array( 'status' => 500 )
4034                );
4035            }
4036        }
4037
4038        /*
4039         * We may want to activate the plugin as well.
4040         * Let's check for the status parameter in the request to find out.
4041         * If none was passed (or something other than active), let's return now.
4042         */
4043        if ( empty( $request['status'] ) || 'active' !== $request['status'] ) {
4044            return rest_ensure_response(
4045                array(
4046                    'code'    => 'success',
4047                    'message' => esc_html(
4048                        sprintf(
4049                            /* translators: placeholder is a plugin name. */
4050                            __( 'Installed %s', 'jetpack' ),
4051                            $plugin
4052                        )
4053                    ),
4054                )
4055            );
4056        }
4057
4058        /*
4059         * Proceed with plugin activation.
4060         * Let's check again for the plugin's ID if we don't already have it.
4061         */
4062        if ( ! $plugin_id ) {
4063            $plugin_id = Plugins_Installer::get_plugin_id_by_slug( $plugin );
4064            if ( ! $plugin_id ) {
4065                return new WP_Error(
4066                    'unable_to_determine_installed_plugin',
4067                    __( 'Unable to determine what plugin was installed.', 'jetpack' ),
4068                    array( 'status' => 500 )
4069                );
4070            }
4071        }
4072
4073        $source      = ! empty( $request['source'] ) ? stripslashes( $request['source'] ) : 'rest_api';
4074        $plugin_args = array(
4075            'plugin' => substr( $plugin_id, 0, - 4 ),
4076            'status' => 'active',
4077            'source' => $source,
4078        );
4079        return self::activate_plugin( $plugin_args );
4080    }
4081
4082    /**
4083     * Activate a specific plugin.
4084     *
4085     * @since 8.9.0
4086     *
4087     * @param WP_REST_Request $request {
4088     *     Array of parameters received by request.
4089     *
4090     *     @type string $plugin Plugin long slug (slug/index-file)
4091     *     @type string $status Plugin status. We only support active in Jetpack.
4092     *     @type string $source Where did the plugin installation request originate.
4093     * }
4094     *
4095     * @return WP_REST_Response|WP_Error A response object if the activation was successful, or a WP_Error object if the activation failed.
4096     */
4097    public static function activate_plugin( $request ) {
4098        /*
4099         * We need an "active" status parameter to be passed to the request
4100         * just like the core plugins endpoind we'll eventually switch to.
4101         */
4102        if ( empty( $request['status'] ) || 'active' !== $request['status'] ) {
4103            return new WP_Error(
4104                'missing_status_parameter',
4105                esc_html__( 'Status parameter missing.', 'jetpack' ),
4106                array( 'status' => 403 )
4107            );
4108        }
4109
4110        $plugins = Plugins_Installer::get_plugins();
4111
4112        if ( empty( $plugins ) ) {
4113            return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
4114        }
4115
4116        if ( empty( $request['plugin'] ) ) {
4117            return new WP_Error( 'no_plugin_specified', esc_html__( 'You did not specify a plugin.', 'jetpack' ), array( 'status' => 404 ) );
4118        }
4119
4120        $plugin = $request['plugin'] . '.php';
4121
4122        // Is the plugin installed?
4123        if ( ! array_key_exists( $plugin, $plugins ) ) {
4124            return new WP_Error(
4125                'plugin_not_found',
4126                esc_html(
4127                    sprintf(
4128                        /* translators: placeholder is a plugin slug. */
4129                        __( 'Plugin %s is not installed.', 'jetpack' ),
4130                        $plugin
4131                    )
4132                ),
4133                array( 'status' => 404 )
4134            );
4135        }
4136
4137        // Is the plugin active already?
4138        $status = Plugins_Installer::get_plugin_status( $plugin );
4139        if ( in_array( $status, array( 'active', 'network-active' ), true ) ) {
4140            return new WP_Error(
4141                'plugin_already_active',
4142                esc_html(
4143                    sprintf(
4144                        /* translators: placeholder is a plugin slug. */
4145                        __( 'Plugin %s is already active.', 'jetpack' ),
4146                        $plugin
4147                    )
4148                ),
4149                array( 'status' => 404 )
4150            );
4151        }
4152
4153        // Now try to activate the plugin.
4154        $activated = activate_plugin( $plugin );
4155
4156        if ( is_wp_error( $activated ) ) {
4157            return $activated;
4158        } else {
4159            $source = ! empty( $request['source'] ) ? stripslashes( $request['source'] ) : 'rest_api';
4160            /**
4161             * Fires when Jetpack installs a plugin for you.
4162             *
4163             * @since 8.9.0
4164             *
4165             * @param string $plugin_file Plugin file.
4166             * @param string $source      Where did the plugin installation originate.
4167             */
4168            do_action( 'jetpack_activated_plugin', $plugin, $source );
4169            return rest_ensure_response(
4170                array(
4171                    'code'    => 'success',
4172                    'message' => sprintf(
4173                        /* translators: placeholder is a plugin name. */
4174                        esc_html__( 'Activated %s', 'jetpack' ),
4175                        $plugin
4176                    ),
4177                )
4178            );
4179        }
4180    }
4181
4182    /**
4183     * Check if a plugin can be activated.
4184     *
4185     * @since 8.9.0
4186     *
4187     * @param string|bool     $value   Value to check.
4188     * @param WP_REST_Request $request The request sent to the WP REST API.
4189     * @param string          $param   Name of the parameter passed to endpoint holding $value.
4190     */
4191    public static function validate_activate_plugin( $value, $request, $param ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
4192        return 'active' === $value;
4193    }
4194
4195    /**
4196     * Get data about the queried plugin. Currently it only returns whether the plugin is active or not.
4197     *
4198     * @since 4.2.0
4199     *
4200     * @param WP_REST_Request $request {
4201     *     Array of parameters received by request.
4202     *
4203     *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
4204     * }
4205     *
4206     * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
4207     */
4208    public static function get_plugin( $request ) {
4209        $plugins = Plugins_Installer::get_plugins();
4210
4211        if ( empty( $plugins ) ) {
4212            return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
4213        }
4214
4215        $plugin = stripslashes( $request['plugin'] );
4216
4217        if ( ! array_key_exists( $plugin, $plugins ) ) {
4218            return new WP_Error(
4219                'plugin_not_found',
4220                esc_html(
4221                    sprintf(
4222                        /* Translators: placeholder is a plugin name. */
4223                        __( 'Plugin %s is not installed.', 'jetpack' ),
4224                        $plugin
4225                    )
4226                ),
4227                array( 'status' => 404 )
4228            );
4229        }
4230
4231        $plugin_data = $plugins[ $plugin ];
4232
4233        $plugin_data['active'] = in_array( Plugins_Installer::get_plugin_status( $plugin ), array( 'active', 'network-active' ), true );
4234
4235        return rest_ensure_response(
4236            array(
4237                'code'    => 'success',
4238                'message' => esc_html__( 'Plugin found.', 'jetpack' ),
4239                'data'    => $plugin_data,
4240            )
4241        );
4242    }
4243
4244    /**
4245     * Returns the Jetpack CRM data.
4246     *
4247     * @return WP_REST_Response A response object containing the Jetpack CRM data.
4248     */
4249    public static function get_jetpack_crm_data() {
4250        $jetpack_crm_data = ( new Jetpack_CRM_Data() )->get_crm_data();
4251        return rest_ensure_response( $jetpack_crm_data );
4252    }
4253
4254    /**
4255     * Activates Jetpack CRM's Jetpack Forms extension.
4256     *
4257     * @param WP_REST_Request $request The request sent to the WP REST API.
4258     * @return WP_REST_Response|WP_Error A response object if the extension activation was successful, or a WP_Error object if it failed.
4259     */
4260    public static function activate_crm_jetpack_forms_extension( $request ) {
4261        if ( ! isset( $request['extension'] ) || 'jetpackforms' !== $request['extension'] ) {
4262            return new WP_Error( 'invalid_param', esc_html__( 'Missing or invalid extension parameter.', 'jetpack' ), array( 'status' => 404 ) );
4263        }
4264
4265        $result = ( new Jetpack_CRM_Data() )->activate_crm_jetpackforms_extension();
4266
4267        if ( is_wp_error( $result ) ) {
4268            return $result;
4269        }
4270
4271        return rest_ensure_response( array( 'code' => 'success' ) );
4272    }
4273
4274    /**
4275     * Verifies that the current user has the required permission for accessing the CRM data.
4276     *
4277     * @return true|WP_Error Returns true if the user has the required capability, else a WP_Error object.
4278     */
4279    public static function jetpack_crm_data_permission_check() {
4280        if ( current_user_can( 'publish_posts' ) ) {
4281            return true;
4282        }
4283
4284        return new WP_Error(
4285            'invalid_user_permission_jetpack_crm_data',
4286            REST_Connector::get_user_permissions_error_msg(),
4287            array( 'status' => rest_authorization_required_code() )
4288        );
4289    }
4290
4291    /**
4292     * Verifies that the current user has the required capability for activating Jetpack CRM extensions.
4293     *
4294     * @return true|WP_Error Returns true if the user has the required capability, else a WP_Error object.
4295     */
4296    public static function activate_crm_extensions_permission_check() {
4297        // phpcs:ignore WordPress.WP.Capabilities.Unknown
4298        if ( current_user_can( 'admin_zerobs_manage_options' ) ) {
4299            return true;
4300        }
4301
4302        return new WP_Error(
4303            'invalid_user_permission_activate_jetpack_crm_ext',
4304            REST_Connector::get_user_permissions_error_msg(),
4305            array( 'status' => rest_authorization_required_code() )
4306        );
4307    }
4308
4309    /**
4310     * Set hasSeenWCConnectionModal to true when the site has displayed it
4311     *
4312     * @since 10.4.0
4313     *
4314     * @return bool
4315     */
4316    public static function set_has_seen_wc_connection_modal() {
4317        $updated_option = Jetpack_Options::update_option( 'has_seen_wc_connection_modal', true );
4318
4319        return rest_ensure_response( array( 'success' => $updated_option ) );
4320    }
4321
4322    /**
4323     * Fetch introdution offers.
4324     *
4325     * @since 10.9
4326     *
4327     * @return array|WP_Error
4328     */
4329    public static function get_intro_offers() {
4330        $site_id = Jetpack_Options::get_option( 'id' );
4331
4332        if ( ! $site_id ) {
4333            return new WP_Error(
4334                'site_id_missing',
4335                esc_html__( 'Site ID is missing.', 'jetpack' ),
4336                array( 'status' => 400 )
4337            );
4338        }
4339
4340        $response = Client::wpcom_json_api_request_as_user(
4341            '/introductory-offers',
4342            '2',
4343            array(
4344                'method'  => 'GET',
4345                'headers' => array(
4346                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
4347                ),
4348            )
4349        );
4350
4351        $response_code = wp_remote_retrieve_response_code( $response );
4352
4353        if ( 200 !== $response_code ) {
4354            return new WP_Error(
4355                'intro_offers_fetch_failed',
4356                esc_html__( 'Could not retrieve intro offers.', 'jetpack' ),
4357                array( 'status' => $response_code )
4358            );
4359        }
4360
4361        $data = json_decode( wp_remote_retrieve_body( $response ) );
4362
4363        if ( ! isset( $data ) ) {
4364            return new WP_Error(
4365                'intro_offers_error',
4366                esc_html__( 'Could not parse intro offers.', 'jetpack' ),
4367                array( 'status' => 204 ) // no content.
4368            );
4369        }
4370
4371        return rest_ensure_response(
4372            array(
4373                'code' => 'success',
4374                'data' => $data,
4375            )
4376        );
4377    }
4378
4379    /**
4380     * Return the list of available features.
4381     *
4382     * @return array
4383     */
4384    public static function get_features_available() {
4385        $raw_modules = Jetpack::get_available_modules();
4386        $modules     = array();
4387        foreach ( $raw_modules as $module ) {
4388            $modules[] = Jetpack::get_module_slug( $module );
4389        }
4390
4391        return $modules;
4392    }
4393
4394    /**
4395     * Returns what features are enabled. Uses the slug of the modules files.
4396     *
4397     * @return array
4398     */
4399    public static function get_features_enabled() {
4400        $raw_modules = Jetpack::get_active_modules();
4401        $modules     = array();
4402        foreach ( $raw_modules as $module ) {
4403            $modules[] = Jetpack::get_module_slug( $module );
4404        }
4405
4406        return $modules;
4407    }
4408
4409    /**
4410     * Verify that the API client is allowed to replace user token.
4411     *
4412     * @since 1.29.0
4413     *
4414     * @return bool|WP_Error
4415     */
4416    public static function get_features_permission_check() {
4417        if ( ! Rest_Authentication::is_signed_with_blog_token() ) {
4418            $message = esc_html__(
4419                'You do not have the correct user permissions to perform this action. Please contact your site admin if you think this is a mistake.',
4420                'jetpack'
4421            );
4422            return new WP_Error( 'invalid_permission_fetch_features', $message, array( 'status' => rest_authorization_required_code() ) );
4423        }
4424
4425        return true;
4426    }
4427} // class end