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