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