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