Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.47% covered (warning)
64.47%
1686 / 2615
17.86% covered (danger)
17.86%
15 / 84
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Core_Json_Api_Endpoints
64.62% covered (warning)
64.62%
1686 / 2609
17.86% covered (danger)
17.86%
15 / 84
6431.59
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%
845 / 845
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
 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\' and \'subscribe_modal_heading\'.', 'jetpack' ),
2604                'type'              => 'object',
2605                'default'           => array(
2606                    'invitation'              => '',
2607                    'welcome'                 => '',
2608                    'comment_follow'          => '',
2609                    'subscribe_modal_heading' => '',
2610                ),
2611                'validate_callback' => __CLASS__ . '::validate_subscription_options',
2612                'jp_group'          => 'subscriptions',
2613            ),
2614
2615            // Related Posts.
2616            'show_headline'                             => array(
2617                'description'       => esc_html__( 'Highlight related content with a heading', 'jetpack' ),
2618                'type'              => 'boolean',
2619                'default'           => 1,
2620                'validate_callback' => __CLASS__ . '::validate_boolean',
2621                'jp_group'          => 'related-posts',
2622            ),
2623            'show_thumbnails'                           => array(
2624                'description'       => esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
2625                'type'              => 'boolean',
2626                'default'           => 0,
2627                'validate_callback' => __CLASS__ . '::validate_boolean',
2628                'jp_group'          => 'related-posts',
2629            ),
2630
2631            // Search.
2632            'instant_search_enabled'                    => array(
2633                'description'       => esc_html__( 'Enable Instant Search', 'jetpack' ),
2634                'type'              => 'boolean',
2635                'default'           => 0,
2636                'validate_callback' => __CLASS__ . '::validate_boolean',
2637                'jp_group'          => 'search',
2638            ),
2639
2640            'has_jetpack_search_product'                => array(
2641                'description'       => esc_html__( 'Has an active Jetpack Search product purchase', 'jetpack' ),
2642                'type'              => 'boolean',
2643                'default'           => 0,
2644                'validate_callback' => __CLASS__ . '::validate_boolean',
2645                'jp_group'          => 'settings',
2646            ),
2647
2648            'search_auto_config'                        => array(
2649                'description'       => esc_html__( 'Trigger an auto config of instant search', 'jetpack' ),
2650                'type'              => 'boolean',
2651                'default'           => 0,
2652                'validate_callback' => __CLASS__ . '::validate_boolean',
2653                'jp_group'          => 'search',
2654            ),
2655
2656            // Verification Tools.
2657            'google'                                    => array(
2658                'description'       => esc_html__( 'Google Search Console', 'jetpack' ),
2659                'type'              => 'string',
2660                'default'           => '',
2661                'validate_callback' => __CLASS__ . '::validate_verification_service',
2662                'jp_group'          => 'verification-tools',
2663            ),
2664            'bing'                                      => array(
2665                'description'       => esc_html__( 'Bing Webmaster Center', 'jetpack' ),
2666                'type'              => 'string',
2667                'default'           => '',
2668                'validate_callback' => __CLASS__ . '::validate_verification_service',
2669                'jp_group'          => 'verification-tools',
2670            ),
2671            'pinterest'                                 => array(
2672                'description'       => esc_html__( 'Pinterest Site Verification', 'jetpack' ),
2673                'type'              => 'string',
2674                'default'           => '',
2675                'validate_callback' => __CLASS__ . '::validate_verification_service',
2676                'jp_group'          => 'verification-tools',
2677            ),
2678            'yandex'                                    => array(
2679                'description'       => esc_html__( 'Yandex Site Verification', 'jetpack' ),
2680                'type'              => 'string',
2681                'default'           => '',
2682                'validate_callback' => __CLASS__ . '::validate_verification_service',
2683                'jp_group'          => 'verification-tools',
2684            ),
2685            'facebook'                                  => array(
2686                'description'       => esc_html__( 'Facebook Domain Verification', 'jetpack' ),
2687                'type'              => 'string',
2688                'default'           => '',
2689                'validate_callback' => __CLASS__ . '::validate_verification_service',
2690                'jp_group'          => 'verification-tools',
2691            ),
2692
2693            // WordAds.
2694            'enable_header_ad'                          => array(
2695                'description'       => esc_html__( 'Display an ad unit at the top of each page.', 'jetpack' ),
2696                'type'              => 'boolean',
2697                'default'           => 1,
2698                'validate_callback' => __CLASS__ . '::validate_boolean',
2699                'jp_group'          => 'wordads',
2700            ),
2701            'wordads_approved'                          => array(
2702                'description'       => esc_html__( 'Is site approved for WordAds?', 'jetpack' ),
2703                'type'              => 'boolean',
2704                'default'           => 0,
2705                'validate_callback' => __CLASS__ . '::validate_boolean',
2706                'jp_group'          => 'wordads',
2707            ),
2708            'wordads_second_belowpost'                  => array(
2709                'description'       => esc_html__( 'Display second ad below post?', 'jetpack' ),
2710                'type'              => 'boolean',
2711                'default'           => 1,
2712                'validate_callback' => __CLASS__ . '::validate_boolean',
2713                'jp_group'          => 'wordads',
2714            ),
2715            'wordads_inline_enabled'                    => array(
2716                'description'       => esc_html__( 'Display inline ad within post content?', 'jetpack' ),
2717                'type'              => 'boolean',
2718                'default'           => 1,
2719                'validate_callback' => __CLASS__ . '::validate_boolean',
2720                'jp_group'          => 'wordads',
2721            ),
2722            'wordads_display_front_page'                => array(
2723                'description'       => esc_html__( 'Display ads on the front page?', 'jetpack' ),
2724                'type'              => 'boolean',
2725                'default'           => 1,
2726                'validate_callback' => __CLASS__ . '::validate_boolean',
2727                'jp_group'          => 'wordads',
2728            ),
2729            'wordads_display_post'                      => array(
2730                'description'       => esc_html__( 'Display ads on posts?', 'jetpack' ),
2731                'type'              => 'boolean',
2732                'default'           => 1,
2733                'validate_callback' => __CLASS__ . '::validate_boolean',
2734                'jp_group'          => 'wordads',
2735            ),
2736            'wordads_display_page'                      => array(
2737                'description'       => esc_html__( 'Display ads on pages?', 'jetpack' ),
2738                'type'              => 'boolean',
2739                'default'           => 1,
2740                'validate_callback' => __CLASS__ . '::validate_boolean',
2741                'jp_group'          => 'wordads',
2742            ),
2743            'wordads_display_archive'                   => array(
2744                'description'       => esc_html__( 'Display ads on archive pages?', 'jetpack' ),
2745                'type'              => 'boolean',
2746                'default'           => 1,
2747                'validate_callback' => __CLASS__ . '::validate_boolean',
2748                'jp_group'          => 'wordads',
2749            ),
2750            'wordads_custom_adstxt_enabled'             => array(
2751                'description'       => esc_html__( 'Custom ads.txt', 'jetpack' ),
2752                'type'              => 'boolean',
2753                'default'           => 0,
2754                'validate_callback' => __CLASS__ . '::validate_boolean',
2755                'jp_group'          => 'wordads',
2756            ),
2757            'wordads_custom_adstxt'                     => array(
2758                'description'       => esc_html__( 'Custom ads.txt entries', 'jetpack' ),
2759                'type'              => 'string',
2760                'default'           => '',
2761                'validate_callback' => __CLASS__ . '::validate_string',
2762                'sanitize_callback' => 'sanitize_textarea_field',
2763                'jp_group'          => 'wordads',
2764            ),
2765            'wordads_ccpa_enabled'                      => array(
2766                'description'       => esc_html__( 'Enable support for California Consumer Privacy Act', 'jetpack' ),
2767                'type'              => 'boolean',
2768                'default'           => 0,
2769                'validate_callback' => __CLASS__ . '::validate_boolean',
2770                'jp_group'          => 'wordads',
2771            ),
2772            'wordads_ccpa_privacy_policy_url'           => array(
2773                'description'       => esc_html__( 'Privacy Policy URL', 'jetpack' ),
2774                'type'              => 'string',
2775                'default'           => '',
2776                'validate_callback' => __CLASS__ . '::validate_string',
2777                'sanitize_callback' => 'sanitize_text_field',
2778                'jp_group'          => 'wordads',
2779            ),
2780            'wordads_cmp_enabled'                       => array(
2781                'description'       => esc_html__( 'Enable GDPR Consent Management Banner for WordAds', 'jetpack' ),
2782                'type'              => 'boolean',
2783                'default'           => 0,
2784                'validate_callback' => __CLASS__ . '::validate_boolean',
2785                'jp_group'          => 'wordads',
2786            ),
2787
2788            // Google Analytics.
2789            'google_analytics_tracking_id'              => array(
2790                'description'       => esc_html__( 'Google Analytics', 'jetpack' ),
2791                'type'              => 'string',
2792                'default'           => '',
2793                'validate_callback' => __CLASS__ . '::validate_alphanum',
2794                'jp_group'          => 'google-analytics',
2795            ),
2796            'jetpack_wga'                               => array(
2797                'description' => esc_html__( 'Google Analytics', 'jetpack' ),
2798                'type'        => 'object',
2799                'jp_group'    => 'settings',
2800            ),
2801
2802            // Stats.
2803            'admin_bar'                                 => array(
2804                'description'       => esc_html__( 'Include a small chart in your admin bar with a 48-hour traffic snapshot.', 'jetpack' ),
2805                'type'              => 'boolean',
2806                'default'           => 1,
2807                'validate_callback' => __CLASS__ . '::validate_boolean',
2808                'jp_group'          => 'stats',
2809            ),
2810            'enable_odyssey_stats'                      => array(
2811                'description'       => esc_html__( 'Preview the new Jetpack Stats experience (Experimental).', 'jetpack' ),
2812                'type'              => 'boolean',
2813                'default'           => 1,
2814                'validate_callback' => __CLASS__ . '::validate_boolean',
2815                'jp_group'          => 'stats',
2816            ),
2817            'roles'                                     => array(
2818                'description'       => esc_html__( 'Select the roles that will be able to view stats reports.', 'jetpack' ),
2819                'type'              => 'array',
2820                'items'             => array(
2821                    'type' => 'string',
2822                ),
2823                'default'           => array( 'administrator' ),
2824                'validate_callback' => __CLASS__ . '::validate_stats_roles',
2825                'sanitize_callback' => __CLASS__ . '::sanitize_stats_allowed_roles',
2826                'jp_group'          => 'stats',
2827            ),
2828            'count_roles'                               => array(
2829                'description'       => esc_html__( 'Count the page views of registered users who are logged in.', 'jetpack' ),
2830                'type'              => 'array',
2831                'items'             => array(
2832                    'type' => 'string',
2833                ),
2834                'default'           => array( 'administrator' ),
2835                'validate_callback' => __CLASS__ . '::validate_stats_roles',
2836                'jp_group'          => 'stats',
2837            ),
2838            'blog_id'                                   => array(
2839                'description'       => esc_html__( 'Blog ID.', 'jetpack' ),
2840                'type'              => 'boolean',
2841                'default'           => 0,
2842                'validate_callback' => __CLASS__ . '::validate_boolean',
2843                'jp_group'          => 'stats',
2844            ),
2845            'do_not_track'                              => array(
2846                'description'       => esc_html__( 'Do not track.', 'jetpack' ),
2847                'type'              => 'boolean',
2848                'default'           => 1,
2849                'validate_callback' => __CLASS__ . '::validate_boolean',
2850                'jp_group'          => 'stats',
2851            ),
2852            'version'                                   => array(
2853                'description'       => esc_html__( 'Version.', 'jetpack' ),
2854                'type'              => 'integer',
2855                'default'           => 9,
2856                'validate_callback' => __CLASS__ . '::validate_posint',
2857                'jp_group'          => 'stats',
2858            ),
2859            'collapse_nudges'                           => array(
2860                'description'       => esc_html__( 'Collapse upgrade nudges', 'jetpack' ),
2861                'type'              => 'boolean',
2862                'default'           => 0,
2863                'validate_callback' => __CLASS__ . '::validate_boolean',
2864                'jp_group'          => 'stats',
2865            ),
2866
2867            // Whether to share stats views with WordPress.com Reader.
2868            'wpcom_reader_views_enabled'                => array(
2869                'description'       => esc_html__( 'Show post views in the WordPress.com Reader.', 'jetpack' ),
2870                'type'              => 'boolean',
2871                'default'           => 1,
2872                'validate_callback' => __CLASS__ . '::validate_boolean',
2873                'jp_group'          => 'settings',
2874            ),
2875
2876            // Akismet - Not a module, but a plugin. The options can be passed and handled differently.
2877            'akismet_show_user_comments_approved'       => array(
2878                'description'       => '',
2879                'type'              => 'boolean',
2880                'default'           => 0,
2881                'validate_callback' => __CLASS__ . '::validate_boolean',
2882                'jp_group'          => 'settings',
2883            ),
2884
2885            'wordpress_api_key'                         => array(
2886                'description'       => '',
2887                'type'              => 'string',
2888                'default'           => '',
2889                'validate_callback' => __CLASS__ . '::validate_alphanum',
2890                'jp_group'          => 'settings',
2891            ),
2892
2893            // Empty stats card dismiss.
2894            'dismiss_empty_stats_card'                  => array(
2895                'description'       => '',
2896                'type'              => 'boolean',
2897                'default'           => 0,
2898                'validate_callback' => __CLASS__ . '::validate_boolean',
2899                'jp_group'          => 'settings',
2900            ),
2901
2902            // Backup Getting Started card on dashboard.
2903            'dismiss_dash_backup_getting_started'       => array(
2904                'description'       => '',
2905                'type'              => 'boolean',
2906                'default'           => 0,
2907                'validate_callback' => __CLASS__ . '::validate_boolean',
2908                'jp_group'          => 'settings',
2909            ),
2910
2911            // Agencies Learn More card on dashboard.
2912            'dismiss_dash_agencies_learn_more'          => array(
2913                'description'       => '',
2914                'type'              => 'boolean',
2915                'default'           => 0,
2916                'validate_callback' => __CLASS__ . '::validate_boolean',
2917                'jp_group'          => 'settings',
2918            ),
2919
2920            'lang_id'                                   => array(
2921                'description' => esc_html__( 'Primary language for the site.', 'jetpack' ),
2922                'type'        => 'string',
2923                'default'     => 'en_US',
2924                'jp_group'    => 'settings',
2925            ),
2926
2927            // SEO Tools.
2928            'advanced_seo_front_page_description'       => array(
2929                'description'       => esc_html__( 'Front page meta description.', 'jetpack' ),
2930                'type'              => 'string',
2931                'default'           => '',
2932                'sanitize_callback' => 'Jetpack_SEO_Utils::sanitize_front_page_meta_description',
2933                'jp_group'          => 'seo-tools',
2934            ),
2935
2936            'advanced_seo_title_formats'                => array(
2937                'description'       => esc_html__( 'SEO page title structures.', 'jetpack' ),
2938                'type'              => 'object',
2939                'default'           => array(
2940                    'archives'   => array(),
2941                    'front_page' => array(),
2942                    'groups'     => array(),
2943                    'pages'      => array(),
2944                    'posts'      => array(),
2945                ),
2946                'jp_group'          => 'seo-tools',
2947                'validate_callback' => 'Jetpack_SEO_Titles::are_valid_title_formats',
2948                'sanitize_callback' => 'Jetpack_SEO_Titles::sanitize_title_formats',
2949            ),
2950
2951            // VideoPress.
2952            'videopress_private_enabled_for_site'       => array(
2953                'description'       => esc_html__( 'Video Privacy: Restrict views to members of this site', 'jetpack' ),
2954                'type'              => 'boolean',
2955                'default'           => 0,
2956                'validate_callback' => __CLASS__ . '::validate_boolean',
2957                'jp_group'          => 'videopress',
2958            ),
2959        );
2960
2961        // SEO Tools - SEO Enhancer.
2962        // TODO: move this to the main options array? The filter was there while developing the feature.
2963        // It might come in handy to hold its availability behind the filter since it still depends on AI to be available.
2964        if ( apply_filters( 'ai_seo_enhancer_enabled', true ) ) {
2965            $options['ai_seo_enhancer_enabled'] = array(
2966                'description'       => esc_html__( 'Automatically generate SEO title, SEO description, and image alt text for new posts.', 'jetpack' ),
2967                'type'              => 'boolean',
2968                'default'           => 0,
2969                'validate_callback' => __CLASS__ . '::validate_boolean',
2970                'jp_group'          => 'seo-tools',
2971            );
2972        }
2973
2974        // Add modules to list so they can be toggled.
2975        $modules = Jetpack::get_available_modules();
2976        if ( is_array( $modules ) && ! empty( $modules ) ) {
2977            $module_args = array(
2978                'description'       => '',
2979                'type'              => 'boolean',
2980                'default'           => 0,
2981                'validate_callback' => __CLASS__ . '::validate_boolean',
2982                'jp_group'          => 'modules',
2983            );
2984            foreach ( $modules as $module ) {
2985                $options[ $module ] = $module_args;
2986            }
2987        }
2988
2989        if ( is_array( $selector ) ) {
2990
2991            // Return only those options whose keys match $selector keys.
2992            return array_intersect_key( $options, $selector );
2993        }
2994
2995        if ( 'any' === $selector ) {
2996
2997            // Toggle module or update any module option or any general setting.
2998            return $options;
2999        }
3000
3001        // We're updating the options for a single module.
3002        if ( empty( $selector ) ) {
3003            $selector = self::get_module_requested();
3004        }
3005        $selected = array();
3006        foreach ( $options as $option => $attributes ) {
3007
3008            // Not adding an isset( $attributes['jp_group'] ) because if it's not set, it must be fixed, otherwise options will fail.
3009            if ( $selector === $attributes['jp_group'] ) {
3010                $selected[ $option ] = $attributes;
3011            }
3012        }
3013        return $selected;
3014    }
3015
3016    /**
3017     * Validates that the parameters are proper values that can be set during Jetpack onboarding.
3018     *
3019     * @since 5.4.0
3020     *
3021     * @deprecated since 13.9
3022     *
3023     * @param array           $onboarding_data Values to check.
3024     * @param WP_REST_Request $request         The request sent to the WP REST API.
3025     * @param string          $param           Name of the parameter passed to endpoint holding $value.
3026     *
3027     * @return bool|WP_Error
3028     */
3029    public static function validate_onboarding( $onboarding_data, $request, $param ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
3030        _deprecated_function( __METHOD__, '13.9' );
3031        return true;
3032    }
3033
3034    /**
3035     * Validates that the parameter is either a pure boolean or a numeric string that can be mapped to a boolean.
3036     *
3037     * @since 4.3.0
3038     *
3039     * @param string|bool     $value Value to check.
3040     * @param WP_REST_Request $request The request sent to the WP REST API.
3041     * @param string          $param Name of the parameter passed to endpoint holding $value.
3042     *
3043     * @return bool|WP_Error
3044     */
3045    public static function validate_boolean( $value, $request, $param ) {
3046        // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- Other code depends on loose comparison here.
3047        if ( ! is_bool( $value ) && ! ( ctype_digit( (string) $value ) && in_array( $value, array( 0, 1 ) ) ) ) {
3048            return new WP_Error(
3049                'invalid_param',
3050                sprintf(
3051                    /* Translators: Placeholder is a parameter name. */
3052                    esc_html__( '%s must be true, false, 0 or 1.', 'jetpack' ),
3053                    $param
3054                )
3055            );
3056        }
3057        return true;
3058    }
3059
3060    /**
3061     * Validates that the parameter is a positive integer.
3062     *
3063     * @since 4.3.0
3064     *
3065     * @param int             $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_posint( $value, $request, $param ) {
3072        if ( ! is_numeric( $value ) || $value <= 0 ) {
3073            return new WP_Error(
3074                'invalid_param',
3075                sprintf(
3076                    /* Translators: Placeholder is a parameter name. */
3077                    esc_html__( '%s must be a positive integer.', 'jetpack' ),
3078                    $param
3079                )
3080            );
3081        }
3082        return true;
3083    }
3084
3085    /**
3086     * Validates that the parameter is a non-negative integer (includes 0).
3087     *
3088     * @since 10.4.0
3089     *
3090     * @param int             $value Value to check.
3091     * @param WP_REST_Request $request The request sent to the WP REST API.
3092     * @param string          $param Name of the parameter passed to endpoint holding $value.
3093     *
3094     * @return bool|WP_Error
3095     */
3096    public static function validate_non_neg_int( $value, $request, $param ) {
3097        if ( ! is_numeric( $value ) || $value < 0 ) {
3098            return new WP_Error(
3099                'invalid_param',
3100                /* translators: %s: The literal parameter name. Should not be translated. */
3101                sprintf( esc_html__( '%s must be a non-negative integer.', 'jetpack' ), $param )
3102            );
3103        }
3104        return true;
3105    }
3106
3107    /**
3108     * Validates that the parameter belongs to a list of admitted values.
3109     *
3110     * @since 4.3.0
3111     *
3112     * @param string          $value Value to check.
3113     * @param WP_REST_Request $request The request sent to the WP REST API.
3114     * @param string          $param Name of the parameter passed to endpoint holding $value.
3115     *
3116     * @return bool|WP_Error
3117     */
3118    public static function validate_list_item( $value, $request, $param ) {
3119        $attributes = $request->get_attributes();
3120        if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
3121            return new WP_Error(
3122                'invalid_param',
3123                sprintf(
3124                    /* Translators: Placeholder is a parameter name. */
3125                    esc_html__( '%s not recognized', 'jetpack' ),
3126                    $param
3127                )
3128            );
3129        }
3130        $args = $attributes['args'][ $param ];
3131        if ( ! empty( $args['enum'] ) ) {
3132            // If it's an associative array, use the keys to check that the value is among those admitted.
3133            $enum = ( count( array_filter( array_keys( $args['enum'] ), 'is_string' ) ) > 0 )
3134                ? array_keys( $args['enum'] )
3135                : $args['enum'];
3136            $enum = array_map( 'strval', $enum );
3137            if ( ! in_array( $value, $enum, true ) ) {
3138                return new WP_Error(
3139                    'invalid_param_value',
3140                    sprintf(
3141                    /* Translators: first variable is the parameter passed to endpoint that holds the list item, the second is a list of admitted values. */
3142                        esc_html__( '%1$s must be one of %2$s', 'jetpack' ),
3143                        $param,
3144                        implode( ', ', $enum )
3145                    )
3146                );
3147            }
3148        }
3149        return true;
3150    }
3151
3152    /**
3153     * Validates that the parameter belongs to a list of admitted values.
3154     *
3155     * @since 4.3.0
3156     *
3157     * @param string          $value Value to check.
3158     * @param WP_REST_Request $request The request sent to the WP REST API.
3159     * @param string          $param Name of the parameter passed to endpoint holding $value.
3160     *
3161     * @return bool|WP_Error
3162     */
3163    public static function validate_module_list( $value, $request, $param ) {
3164        if ( ! is_array( $value ) ) {
3165            return new WP_Error(
3166                'invalid_param_value',
3167                sprintf(
3168                    /* Translators: Placeholder is a parameter name. */
3169                    esc_html__( '%s must be an array', 'jetpack' ),
3170                    $param
3171                )
3172            );
3173        }
3174
3175        $modules = Jetpack::get_available_modules();
3176
3177        if ( count( array_intersect( $value, $modules ) ) !== count( $value ) ) {
3178            return new WP_Error(
3179                'invalid_param_value',
3180                sprintf(
3181                    /* Translators: Placeholder is a parameter name. */
3182                    esc_html__( '%s must be a list of valid modules', 'jetpack' ),
3183                    $param
3184                )
3185            );
3186        }
3187
3188        return true;
3189    }
3190
3191    /**
3192     * Validates that the parameter is an alphanumeric or empty string (to be able to clear the field).
3193     *
3194     * @since 4.3.0
3195     *
3196     * @param string          $value Value to check.
3197     * @param WP_REST_Request $request The request sent to the WP REST API.
3198     * @param string          $param Name of the parameter passed to endpoint holding $value.
3199     *
3200     * @return bool|WP_Error
3201     */
3202    public static function validate_alphanum( $value, $request, $param ) {
3203        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^[a-z0-9]+$/i', $value ) ) ) {
3204            return new WP_Error(
3205                'invalid_param',
3206                sprintf(
3207                    /* Translators: Placeholder is a parameter name. */
3208                    esc_html__( '%s must be an alphanumeric string.', 'jetpack' ),
3209                    $param
3210                )
3211            );
3212        }
3213        return true;
3214    }
3215
3216    /**
3217     * Validates that the parameter is a tag or id for a verification service, or an empty string (to be able to clear the field).
3218     *
3219     * @since 4.6.0
3220     *
3221     * @param string          $value   Value to check.
3222     * @param WP_REST_Request $request The request sent to the WP REST API.
3223     * @param string          $param   Name of the parameter passed to endpoint holding $value.
3224     *
3225     * @return bool|WP_Error
3226     */
3227    public static function validate_verification_service( $value, $request, $param ) {
3228        if ( ! empty( $value ) && ! ( is_string( $value ) && ( preg_match( '/^[a-z0-9_-]+$/i', $value ) || jetpack_verification_get_code( $value ) !== false ) ) ) {
3229            return new WP_Error(
3230                'invalid_param',
3231                sprintf(
3232                    /* Translators: Placeholder is a verification string used to verify a service like Google Webmaster Console. */
3233                    esc_html__( '%s must be an alphanumeric string or a verification tag.', 'jetpack' ),
3234                    $param
3235                )
3236            );
3237        }
3238        return true;
3239    }
3240
3241    /**
3242     * Validates that the parameter is among the roles allowed for Stats.
3243     *
3244     * @since 4.3.0
3245     *
3246     * @param string|bool     $value Value to check.
3247     * @param WP_REST_Request $request The request sent to the WP REST API.
3248     * @param string          $param Name of the parameter passed to endpoint holding $value.
3249     *
3250     * @return bool|WP_Error
3251     */
3252    public static function validate_stats_roles( $value, $request, $param ) {
3253        if ( ! function_exists( 'get_editable_roles' ) ) {
3254            require_once ABSPATH . 'wp-admin/includes/user.php';
3255        }
3256        $editable_roles = array_keys( get_editable_roles() );
3257        if ( ! empty( $value ) && ! array_intersect( $editable_roles, $value ) ) {
3258            return new WP_Error(
3259                'invalid_param',
3260                sprintf(
3261                    /* 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. */
3262                    esc_html__( '%1$s must be %2$s.', 'jetpack' ),
3263                    $param,
3264                    implode( ', ', $editable_roles )
3265                )
3266            );
3267        }
3268        return true;
3269    }
3270
3271    /**
3272     * Validates that the parameter is among the views where the Sharing can be displayed.
3273     *
3274     * @since 4.3.0
3275     *
3276     * @param string|bool     $value Value to check.
3277     * @param WP_REST_Request $request The request sent to the WP REST API.
3278     * @param string          $param Name of the parameter passed to endpoint holding $value.
3279     *
3280     * @return bool|WP_Error
3281     */
3282    public static function validate_sharing_show( $value, $request, $param ) {
3283        $views = array( 'index', 'post', 'page', 'attachment', 'jetpack-portfolio' );
3284        if ( ! is_array( $value ) ) {
3285            return new WP_Error(
3286                'invalid_param',
3287                sprintf(
3288                    /* Translators: Placeholder is a parameter name. */
3289                    esc_html__( '%s must be an array of post types.', 'jetpack' ),
3290                    $param
3291                )
3292            );
3293        }
3294        if ( ! array_intersect( $views, $value ) ) {
3295            return new WP_Error(
3296                'invalid_param',
3297                sprintf(
3298                    /* 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 */
3299                    esc_html__( '%1$s must be %2$s.', 'jetpack' ),
3300                    $param,
3301                    implode( ', ', $views )
3302                )
3303            );
3304        }
3305        return true;
3306    }
3307
3308    /**
3309     * Validates that the parameter is among the valid reply-to types for subscriptions.
3310     *
3311     * @since 4.3.0
3312     *
3313     * @param string|bool     $value Value to check.
3314     * @param WP_REST_Request $request The request sent to the WP REST API.
3315     * @param string          $param Name of the parameter passed to endpoint holding $value.
3316     *
3317     * @return bool|WP_Error
3318     */
3319    public static function validate_subscriptions_reply_to( $value, $request, $param ) {
3320        require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
3321        if ( ! empty( $value ) && ! Automattic\Jetpack\Modules\Subscriptions\Settings::is_valid_reply_to( $value ) ) {
3322            return new WP_Error(
3323                'invalid_param',
3324                sprintf(
3325                    /* Translators: Placeholder is a parameter name. */
3326                    esc_html__( '%s must be a valid type.', 'jetpack' ),
3327                    $param
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_name( $value, $request, $param ) {
3346        if ( ! empty( $value ) && ! is_string( $value ) ) {
3347            return new WP_Error(
3348                'invalid_param',
3349                sprintf(
3350                    /* Translators: Placeholder is a parameter name. */
3351                    esc_html__( '%s must be a valid type.', 'jetpack' ),
3352                    $param
3353                )
3354            );
3355        }
3356        return true;
3357    }
3358
3359    /**
3360     * Validates that the parameter is among the views where the Sharing can be displayed.
3361     *
3362     * @since 4.3.0
3363     *
3364     * @param string|bool     $value {
3365     *         Value to check received by request.
3366     *
3367     *     @type array $visible List of slug of services to share to that are displayed directly in the page.
3368     *     @type array $hidden  List of slug of services to share to that are concealed in a folding menu.
3369     * }
3370     * @param WP_REST_Request $request The request sent to the WP REST API.
3371     * @param string          $param Name of the parameter passed to endpoint holding $value.
3372     *
3373     * @return bool|WP_Error
3374     */
3375    public static function validate_services( $value, $request, $param ) {
3376        if ( ! is_array( $value ) || ! isset( $value['visible'] ) || ! isset( $value['hidden'] ) ) {
3377            return new WP_Error(
3378                'invalid_param',
3379                sprintf(
3380                    /* Translators: Placeholder is a parameter name. */
3381                    esc_html__( '%s must be an array with visible and hidden items.', 'jetpack' ),
3382                    $param
3383                )
3384            );
3385        }
3386
3387        // Allow to clear everything.
3388        if ( empty( $value['visible'] ) && empty( $value['hidden'] ) ) {
3389            return true;
3390        }
3391
3392        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3393            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3394        }
3395        $sharer   = new Sharing_Service();
3396        $services = array_keys( $sharer->get_all_services() );
3397
3398        if (
3399            ( ! empty( $value['visible'] ) && ! array_intersect( $value['visible'], $services ) )
3400            ||
3401            ( ! empty( $value['hidden'] ) && ! array_intersect( $value['hidden'], $services ) ) ) {
3402            return new WP_Error(
3403                'invalid_param',
3404                sprintf(
3405                    /* Translators: placeholder 1 is a parameter holding the services passed to endpoint, placeholder 2 is a list of all Jetpack Sharing services */
3406                    esc_html__( '%1$s visible and hidden items must be a list of %2$s.', 'jetpack' ),
3407                    $param,
3408                    implode( ', ', $services )
3409                )
3410            );
3411        }
3412        return true;
3413    }
3414
3415    /**
3416     * Validates that the parameter has enough information to build a custom sharing button.
3417     *
3418     * @since 4.3.0
3419     *
3420     * @param string|bool     $value Value to check.
3421     * @param WP_REST_Request $request The request sent to the WP REST API.
3422     * @param string          $param Name of the parameter passed to endpoint holding $value.
3423     *
3424     * @return bool|WP_Error
3425     */
3426    public static function validate_custom_service( $value, $request, $param ) {
3427        if ( ! is_array( $value ) || ! isset( $value['sharing_name'] ) || ! isset( $value['sharing_url'] ) || ! isset( $value['sharing_icon'] ) ) {
3428            return new WP_Error(
3429                'invalid_param',
3430                sprintf(
3431                    /* Translators: Placeholder is a parameter name. */
3432                    esc_html__( '%s must be an array with sharing name, url and icon.', 'jetpack' ),
3433                    $param
3434                )
3435            );
3436        }
3437
3438        // Allow to clear everything.
3439        if ( empty( $value['sharing_name'] ) && empty( $value['sharing_url'] ) && empty( $value['sharing_icon'] ) ) {
3440            return true;
3441        }
3442
3443        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3444            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3445        }
3446
3447        if ( ( ! empty( $value['sharing_name'] ) && ! is_string( $value['sharing_name'] ) )
3448        || ( ! empty( $value['sharing_url'] ) && ! is_string( $value['sharing_url'] ) )
3449        || ( ! empty( $value['sharing_icon'] ) && ! is_string( $value['sharing_icon'] ) ) ) {
3450            return new WP_Error(
3451                'invalid_param',
3452                sprintf(
3453                    /* Translators: Placeholder is a parameter name. */
3454                    esc_html__( '%s needs sharing name, url and icon.', 'jetpack' ),
3455                    $param
3456                )
3457            );
3458        }
3459        return true;
3460    }
3461
3462    /**
3463     * Validates that the parameter is a custom sharing service ID like 'custom-1461976264'.
3464     *
3465     * @since 4.3.0
3466     *
3467     * @param string          $value Value to check.
3468     * @param WP_REST_Request $request The request sent to the WP REST API.
3469     * @param string          $param Name of the parameter passed to endpoint holding $value.
3470     *
3471     * @return bool|WP_Error
3472     */
3473    public static function validate_custom_service_id( $value, $request, $param ) {
3474        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/custom\-[0-1]+/i', $value ) ) ) {
3475            return new WP_Error(
3476                'invalid_param',
3477                sprintf(
3478                    /* Translators: Placeholder is a parameter name. */
3479                    esc_html__( "%s must be a string prefixed with 'custom-' and followed by a numeric ID.", 'jetpack' ),
3480                    $param
3481                )
3482            );
3483        }
3484
3485        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3486            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3487        }
3488        $sharer   = new Sharing_Service();
3489        $services = $sharer->get_all_services();
3490
3491        if ( ! empty( $value ) && ! isset( $services[ $value ] ) ) {
3492            return new WP_Error(
3493                'invalid_param',
3494                sprintf(
3495                    /* Translators: Placeholder is a parameter name. */
3496                    esc_html__( '%s is not a registered custom sharing service.', 'jetpack' ),
3497                    $param
3498                )
3499            );
3500        }
3501
3502        return true;
3503    }
3504
3505    /**
3506     * Validates that the parameter is a Twitter username or empty string (to be able to clear the field).
3507     *
3508     * @since 4.3.0
3509     *
3510     * @param string          $value   Value to check.
3511     * @param WP_REST_Request $request The request sent to the WP REST API.
3512     * @param string          $param   Name of the parameter passed to endpoint holding $value.
3513     *
3514     * @return bool|WP_Error
3515     */
3516    public static function validate_twitter_username( $value, $request, $param ) {
3517        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^@?\w{1,15}$/i', $value ) ) ) {
3518            return new WP_Error(
3519                'invalid_param',
3520                sprintf(
3521                    /* Translators: Placeholder is a twitter name. */
3522                    esc_html__( '%s must be a Twitter username.', 'jetpack' ),
3523                    $param
3524                )
3525            );
3526        }
3527        return true;
3528    }
3529
3530    /**
3531     * Validates that the parameter is a string.
3532     *
3533     * @since 4.3.0
3534     *
3535     * @param string          $value Value to check.
3536     * @param WP_REST_Request $request The request sent to the WP REST API.
3537     * @param string          $param Name of the parameter passed to endpoint holding $value.
3538     *
3539     * @return bool|WP_Error
3540     */
3541    public static function validate_string( $value, $request, $param ) {
3542        if ( ! is_string( $value ) ) {
3543            return new WP_Error(
3544                'invalid_param',
3545                sprintf(
3546                    /* Translators: Placeholder is a parameter name. */
3547                    esc_html__( '%s must be a string.', 'jetpack' ),
3548                    $param
3549                )
3550            );
3551        }
3552        return true;
3553    }
3554
3555    /**
3556     * Validates that the parameter is an array of strings.
3557     *
3558     * @param array           $value Value to check.
3559     * @param WP_REST_Request $request The request sent to the WP REST API.
3560     * @param string          $param Name of the parameter passed to the endpoint holding $value.
3561     *
3562     * @return bool|WP_Error
3563     */
3564    public static function validate_array_of_strings( $value, $request, $param ) {
3565        foreach ( $value as $array_item ) {
3566            $validate = self::validate_string( $array_item, $request, $param );
3567            if ( is_wp_error( $validate ) ) {
3568                return $validate;
3569            }
3570        }
3571
3572        return true;
3573    }
3574
3575    /**
3576     * Validates the subscription_options parameter.
3577     *
3578     * @param array $values Value to check.
3579     *
3580     * @return bool|WP_Error
3581     */
3582    public static function validate_subscription_options( $values ) {
3583        if ( is_object( $values ) ) {
3584            return new WP_Error(
3585                'invalid_param',
3586                /* Translators: subscription_options is a variable name, and shouldn't be translated. */
3587                esc_html__( 'subscription_options must be an object.', 'jetpack' )
3588            );
3589        }
3590        foreach ( array_keys( $values ) as $key ) {
3591            if ( ! in_array( $key, array( 'welcome', 'invitation', 'comment_follow', 'subscribe_modal_heading' ), true ) ) {
3592                return new WP_Error(
3593                    'invalid_param',
3594                    sprintf(
3595                        /* Translators: Placeholder is the invalid param being sent. */
3596                        esc_html__( '%s is not one of the allowed members of subscription_options.', 'jetpack' ),
3597                        $key
3598                    )
3599                );
3600            }
3601        }
3602        return true;
3603    }
3604
3605    /**
3606     * Validates that the parameter is an array.
3607     *
3608     * @param array           $values Value to check.
3609     * @param WP_REST_Request $request The request sent to the WP REST API.
3610     * @param string          $param Name of the parameter passed to the endpoint holding $value.
3611     *
3612     * @return bool|WP_Error
3613     */
3614    public static function validate_array( $values, $request, $param ) {
3615        if ( ! is_array( $values ) ) {
3616            return new WP_Error(
3617                'invalid_param',
3618                sprintf(
3619                    /* Translators: Placeholder is a parameter name. */
3620                    esc_html__( '%s must be an object.', 'jetpack' ),
3621                    $param
3622                )
3623            );
3624        }
3625        return true;
3626    }
3627
3628    /**
3629     * If for some reason the roles allowed to see Stats are empty (for example, user tampering with checkboxes),
3630     * return an array with only 'administrator' as the allowed role and save it for 'roles' option.
3631     *
3632     * @since 4.3.0
3633     *
3634     * @param string|bool $value Value to check.
3635     *
3636     * @return bool|array
3637     */
3638    public static function sanitize_stats_allowed_roles( $value ) {
3639        if ( empty( $value ) ) {
3640            return array( 'administrator' );
3641        }
3642        return $value;
3643    }
3644
3645    /**
3646     * Get the currently accessed route and return the module slug in it.
3647     *
3648     * @since 4.3.0
3649     *
3650     * @param string $route Regular expression for the endpoint with the module slug to return.
3651     *
3652     * @return array|string
3653     */
3654    public static function get_module_requested( $route = '/module/(?P<slug>[a-z\-]+)' ) {
3655
3656        if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) || ! is_string( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
3657            return '';
3658        }
3659
3660        preg_match( "#$route#", $GLOBALS['wp']->query_vars['rest_route'], $module );
3661
3662        if ( empty( $module['slug'] ) ) {
3663            return '';
3664        }
3665
3666        return $module['slug'];
3667    }
3668
3669    /**
3670     * Adds extra information for modules.
3671     *
3672     * @since 4.3.0
3673     *
3674     * @param string|array $modules Can be a single module or a list of modules.
3675     * @param null|string  $slug    Slug of the module in the first parameter.
3676     *
3677     * @return array|string
3678     */
3679    public static function prepare_modules_for_response( $modules = '', $slug = null ) {
3680        global $wp_rewrite;
3681
3682        /** This filter is documented in modules/sitemaps/sitemaps.php */
3683        $location = apply_filters( 'jetpack_sitemap_location', '' );
3684
3685        if ( $wp_rewrite->using_index_permalinks() ) {
3686            $sitemap_url      = home_url( '/index.php' . $location . '/sitemap.xml' );
3687            $news_sitemap_url = home_url( '/index.php' . $location . '/news-sitemap.xml' );
3688        } elseif ( $wp_rewrite->using_permalinks() ) {
3689            $sitemap_url      = home_url( $location . '/sitemap.xml' );
3690            $news_sitemap_url = home_url( $location . '/news-sitemap.xml' );
3691        } else {
3692            $sitemap_url      = home_url( $location . '/?jetpack-sitemap=sitemap.xml' );
3693            $news_sitemap_url = home_url( $location . '/?jetpack-sitemap=news-sitemap.xml' );
3694        }
3695
3696        if ( $slug === null && isset( $modules['sitemaps'] ) ) {
3697            // Is a list of modules.
3698            $modules['sitemaps']['extra']['sitemap_url']      = $sitemap_url;
3699            $modules['sitemaps']['extra']['news_sitemap_url'] = $news_sitemap_url;
3700        } elseif ( 'sitemaps' === $slug ) {
3701            // It's a single module.
3702            $modules['extra']['sitemap_url']      = $sitemap_url;
3703            $modules['extra']['news_sitemap_url'] = $news_sitemap_url;
3704        }
3705        return $modules;
3706    }
3707
3708    /**
3709     * Remove 'validate_callback' item from options available for module.
3710     * Fetch current option value and add to array of module options.
3711     * Prepare values of module options that need special handling, like those saved in wpcom.
3712     *
3713     * @since 4.3.0
3714     *
3715     * @param string $module Module slug.
3716     * @return array
3717     */
3718    public static function prepare_options_for_response( $module = '' ) {
3719        $options = self::get_updateable_data_list( $module );
3720
3721        if ( ! is_array( $options ) || empty( $options ) ) {
3722            return $options;
3723        }
3724
3725        // Some modules need special treatment.
3726        switch ( $module ) {
3727
3728            case 'monitor':
3729                // Status of user notifications.
3730                $options['monitor_receive_notifications']['current_value'] = self::cast_value( self::get_remote_value( 'monitor', 'monitor_receive_notifications' ), $options['monitor_receive_notifications'] );
3731                break;
3732
3733            case 'post-by-email':
3734                // Email address.
3735                $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'] );
3736                break;
3737
3738            case 'protect':
3739                // Protect.
3740                $options['jetpack_protect_key']['current_value']              = get_site_option( 'jetpack_protect_key', false );
3741                $options['jetpack_protect_global_whitelist']['current_value'] = Brute_Force_Protection_Shared_Functions::format_allow_list();
3742                break;
3743
3744            case 'related-posts':
3745                // It's local, but it must be broken apart since it's saved as an array.
3746                $options = self::split_options( $options, Jetpack_Options::get_option( 'relatedposts' ) );
3747                break;
3748
3749            case 'verification-tools':
3750                // It's local, but it must be broken apart since it's saved as an array.
3751                $options = self::split_options( $options, get_option( 'verification_services_codes' ) );
3752                break;
3753
3754            case 'google-analytics':
3755                $wga  = get_option( 'jetpack_wga' );
3756                $code = '';
3757                if ( is_array( $wga ) && array_key_exists( 'code', $wga ) ) {
3758                    $code = $wga['code'];
3759                }
3760                $options['google_analytics_tracking_id']['current_value'] = $code;
3761                break;
3762
3763            case 'sharedaddy':
3764                // It's local, but it must be broken apart since it's saved as an array.
3765                if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3766                    break;
3767                }
3768                $sharer                                       = new Sharing_Service();
3769                $options                                      = self::split_options( $options, $sharer->get_global_options() );
3770                $options['sharing_services']['current_value'] = $sharer->get_blog_services();
3771                $other_sharedaddy_options                     = array( 'jetpack-twitter-cards-site-tag', 'sharedaddy_disable_resources', 'sharing_delete_service' );
3772                foreach ( $other_sharedaddy_options as $key ) {
3773                    $default_value                    = $options[ $key ]['default'] ?? '';
3774                    $current_value                    = get_option( $key, $default_value );
3775                    $options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
3776                }
3777                break;
3778
3779            case 'stats':
3780                // It's local, but it must be broken apart since it's saved as an array.
3781                $options = self::split_options( $options, Stats_Options::get_options() );
3782                break;
3783            default:
3784                // These option are just stored as plain WordPress options.
3785                foreach ( $options as $key => $value ) {
3786                    $default_value                    = $options[ $key ]['default'] ?? '';
3787                    $current_value                    = get_option( $key, $default_value );
3788                    $options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
3789                }
3790        }
3791        // At this point some options have current_value not set because they're options
3792        // that only get written on update, so we set current_value to the default one.
3793        foreach ( $options as $key => $value ) {
3794            // We don't need validate_callback in the response.
3795            if ( isset( $options[ $key ]['validate_callback'] ) ) {
3796                unset( $options[ $key ]['validate_callback'] );
3797            }
3798            $default_value = $options[ $key ]['default'] ?? '';
3799            if ( ! array_key_exists( 'current_value', $options[ $key ] ) ) {
3800                $options[ $key ]['current_value'] = self::cast_value( $default_value, $options[ $key ] );
3801            }
3802        }
3803        return $options;
3804    }
3805
3806    /**
3807     * Splits module options saved as arrays like relatedposts or verification_services_codes into separate options to be returned in the response.
3808     *
3809     * @since 4.3.0
3810     *
3811     * @param array  $separate_options Array of options admitted by the module.
3812     * @param array  $grouped_options Option saved as array to be splitted.
3813     * @param string $prefix Optional prefix for the separate option keys.
3814     *
3815     * @return array
3816     */
3817    public static function split_options( $separate_options, $grouped_options, $prefix = '' ) {
3818        if ( is_array( $grouped_options ) ) {
3819            foreach ( $grouped_options as $key => $value ) {
3820                $option_key = $prefix . $key;
3821                if ( isset( $separate_options[ $option_key ] ) ) {
3822                    $separate_options[ $option_key ]['current_value'] = self::cast_value( $grouped_options[ $key ], $separate_options[ $option_key ] );
3823                }
3824            }
3825        }
3826        return $separate_options;
3827    }
3828
3829    /**
3830     * Perform a casting to the value specified in the option definition.
3831     *
3832     * @since 4.3.0
3833     *
3834     * @param mixed $value Value to cast to the proper type.
3835     * @param array $definition Type to cast the value to.
3836     *
3837     * @return bool|float|int|string
3838     */
3839    public static function cast_value( $value, $definition ) {
3840        if ( 'NULL' === $value ) {
3841            return null;
3842        }
3843
3844        if ( isset( $definition['type'] ) ) {
3845            switch ( $definition['type'] ) {
3846                case 'boolean':
3847                    if ( 'true' === $value || 'on' === $value ) {
3848                        return true;
3849                    } elseif ( 'false' === $value || 'off' === $value ) {
3850                        return false;
3851                    }
3852                    $value = (bool) $value;
3853                    break;
3854
3855                case 'integer':
3856                    $value = (int) $value;
3857                    break;
3858
3859                case 'float':
3860                    $value = (float) $value;
3861                    break;
3862
3863                case 'string':
3864                    $value = (string) $value;
3865                    break;
3866            }
3867        }
3868        return $value;
3869    }
3870
3871    /**
3872     * Get a value not saved locally.
3873     *
3874     * @since 4.3.0
3875     *
3876     * @param string $module Module slug.
3877     * @param string $option Option name.
3878     *
3879     * @return bool Whether user is receiving notifications or not.
3880     */
3881    public static function get_remote_value( $module, $option ) {
3882
3883        if ( in_array( $module, array( 'post-by-email' ), true ) ) {
3884            $option .= get_current_user_id();
3885        }
3886
3887        // If option doesn't exist, 'does_not_exist' will be returned.
3888        $value = get_option( $option, 'does_not_exist' );
3889
3890        // If option exists, just return it.
3891        if ( 'does_not_exist' !== $value ) {
3892            return $value;
3893        }
3894
3895        // Only check a remote option if Jetpack is connected.
3896        if ( ! Jetpack::is_connection_ready() ) {
3897            return false;
3898        }
3899
3900        // Do what is necessary for each module.
3901        switch ( $module ) {
3902            case 'monitor':
3903                // Load the class to use the method. If class can't be found, do nothing.
3904                if ( ! class_exists( 'Jetpack_Monitor' ) && ! include_once Jetpack::get_module_path( $module ) ) {
3905                    return false;
3906                }
3907                $value = Jetpack_Monitor::user_receives_notifications( false );
3908                break;
3909
3910            case 'post-by-email':
3911                // Load the class to use the method. If class can't be found, do nothing.
3912                if ( ! class_exists( 'Jetpack_Post_By_Email' ) && ! include_once Jetpack::get_module_path( $module ) ) {
3913                    return false;
3914                }
3915                $value = Jetpack_Post_By_Email::init()->get_post_by_email_address();
3916                if ( null === $value ) {
3917                    $value = 'NULL'; // sentinel value so it actually gets set.
3918                }
3919                break;
3920        }
3921
3922        // Normalize value to boolean.
3923        if ( is_wp_error( $value ) || $value === null ) {
3924            $value = false;
3925        }
3926
3927        // Save option to use it next time.
3928        update_option( $option, $value );
3929
3930        return $value;
3931    }
3932
3933    /**
3934     * Get number of plugin updates available.
3935     *
3936     * @since 4.3.0
3937     *
3938     * @return mixed|WP_Error Number of plugin updates available. Otherwise, a WP_Error instance with the corresponding error.
3939     */
3940    public static function get_plugin_update_count() {
3941        $updates = wp_get_update_data();
3942        if ( isset( $updates['counts'] ) && isset( $updates['counts']['plugins'] ) ) {
3943            $count = $updates['counts']['plugins'];
3944            if ( 0 === $count ) {
3945                $response = array(
3946                    'code'    => 'success',
3947                    'message' => esc_html__( 'All plugins are up-to-date. Keep up the good work!', 'jetpack' ),
3948                    'count'   => 0,
3949                );
3950            } else {
3951                $response = array(
3952                    'code'    => 'updates-available',
3953                    'message' => esc_html(
3954                        sprintf(
3955                            /* Translators: placeholders are numbers. */
3956                            _n( '%s plugin needs updating.', '%s plugins need updating.', $count, 'jetpack' ),
3957                            $count
3958                        )
3959                    ),
3960                    'count'   => $count,
3961                );
3962            }
3963            return rest_ensure_response( $response );
3964        }
3965
3966        return new WP_Error( 'not_found', esc_html__( 'Could not check updates for plugins on this site.', 'jetpack' ), array( 'status' => 404 ) );
3967    }
3968
3969    /**
3970     * Get plugins data in site.
3971     *
3972     * @since 4.2.0
3973     *
3974     * @return WP_REST_Response|WP_Error List of plugins in the site. Otherwise, a WP_Error instance with the corresponding error.
3975     */
3976    public static function get_plugins() {
3977        $plugins = Plugins_Installer::get_plugins();
3978
3979        if ( ! empty( $plugins ) ) {
3980            return rest_ensure_response( $plugins );
3981        }
3982
3983        return new WP_Error( 'not_found', esc_html__( 'Unable to list plugins.', 'jetpack' ), array( 'status' => 404 ) );
3984    }
3985
3986    /**
3987     * Install a specific plugin and optionally activates it.
3988     *
3989     * @since 8.9.0
3990     *
3991     * @param WP_REST_Request $request {
3992     *     Array of parameters received by request.
3993     *
3994     *     @type string $slug   Plugin slug.
3995     *     @type string $status Plugin status.
3996     *     @type string $source Where did the plugin installation request originate.
3997     * }
3998     *
3999     * @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.
4000     */
4001    public static function install_plugin( $request ) {
4002        $plugin = stripslashes( $request['slug'] );
4003
4004        // Let's make sure the plugin isn't already installed.
4005        $plugin_id = Plugins_Installer::get_plugin_id_by_slug( $plugin );
4006
4007        // If not installed, let's install now.
4008        if ( ! $plugin_id ) {
4009            $result = Plugins_Installer::install_plugin( $plugin );
4010
4011            if ( is_wp_error( $result ) ) {
4012                return new WP_Error(
4013                    'install_plugin_failed',
4014                    sprintf(
4015                        /* translators: %1$s: plugin name. -- %2$s: error message. */
4016                        __( 'Unable to install %1$s: %2$s ', 'jetpack' ),
4017                        $plugin,
4018                        $result->get_error_message()
4019                    ),
4020                    array( 'status' => 500 )
4021                );
4022            }
4023        }
4024
4025        /*
4026         * We may want to activate the plugin as well.
4027         * Let's check for the status parameter in the request to find out.
4028         * If none was passed (or something other than active), let's return now.
4029         */
4030        if ( empty( $request['status'] ) || 'active' !== $request['status'] ) {
4031            return rest_ensure_response(
4032                array(
4033                    'code'    => 'success',
4034                    'message' => esc_html(
4035                        sprintf(
4036                            /* translators: placeholder is a plugin name. */
4037                            __( 'Installed %s', 'jetpack' ),
4038                            $plugin
4039                        )
4040                    ),
4041                )
4042            );
4043        }
4044
4045        /*
4046         * Proceed with plugin activation.
4047         * Let's check again for the plugin's ID if we don't already have it.
4048         */
4049        if ( ! $plugin_id ) {
4050            $plugin_id = Plugins_Installer::get_plugin_id_by_slug( $plugin );
4051            if ( ! $plugin_id ) {
4052                return new WP_Error(
4053                    'unable_to_determine_installed_plugin',
4054                    __( 'Unable to determine what plugin was installed.', 'jetpack' ),
4055                    array( 'status' => 500 )
4056                );
4057            }
4058        }
4059
4060        $source      = ! empty( $request['source'] ) ? stripslashes( $request['source'] ) : 'rest_api';
4061        $plugin_args = array(
4062            'plugin' => substr( $plugin_id, 0, - 4 ),
4063            'status' => 'active',
4064            'source' => $source,
4065        );
4066        return self::activate_plugin( $plugin_args );
4067    }
4068
4069    /**
4070     * Activate a specific plugin.
4071     *
4072     * @since 8.9.0
4073     *
4074     * @param WP_REST_Request $request {
4075     *     Array of parameters received by request.
4076     *
4077     *     @type string $plugin Plugin long slug (slug/index-file)
4078     *     @type string $status Plugin status. We only support active in Jetpack.
4079     *     @type string $source Where did the plugin installation request originate.
4080     * }
4081     *
4082     * @return WP_REST_Response|WP_Error A response object if the activation was successful, or a WP_Error object if the activation failed.
4083     */
4084    public static function activate_plugin( $request ) {
4085        /*
4086         * We need an "active" status parameter to be passed to the request
4087         * just like the core plugins endpoind we'll eventually switch to.
4088         */
4089        if ( empty( $request['status'] ) || 'active' !== $request['status'] ) {
4090            return new WP_Error(
4091                'missing_status_parameter',
4092                esc_html__( 'Status parameter missing.', 'jetpack' ),
4093                array( 'status' => 403 )
4094            );
4095        }
4096
4097        $plugins = Plugins_Installer::get_plugins();
4098
4099        if ( empty( $plugins ) ) {
4100            return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
4101        }
4102
4103        if ( empty( $request['plugin'] ) ) {
4104            return new WP_Error( 'no_plugin_specified', esc_html__( 'You did not specify a plugin.', 'jetpack' ), array( 'status' => 404 ) );
4105        }
4106
4107        $plugin = $request['plugin'] . '.php';
4108
4109        // Is the plugin installed?
4110        if ( ! array_key_exists( $plugin, $plugins ) ) {
4111            return new WP_Error(
4112                'plugin_not_found',
4113                esc_html(
4114                    sprintf(
4115                        /* translators: placeholder is a plugin slug. */
4116                        __( 'Plugin %s is not installed.', 'jetpack' ),
4117                        $plugin
4118                    )
4119                ),
4120                array( 'status' => 404 )
4121            );
4122        }
4123
4124        // Is the plugin active already?
4125        $status = Plugins_Installer::get_plugin_status( $plugin );
4126        if ( in_array( $status, array( 'active', 'network-active' ), true ) ) {
4127            return new WP_Error(
4128                'plugin_already_active',
4129                esc_html(
4130                    sprintf(
4131                        /* translators: placeholder is a plugin slug. */
4132                        __( 'Plugin %s is already active.', 'jetpack' ),
4133                        $plugin
4134                    )
4135                ),
4136                array( 'status' => 404 )
4137            );
4138        }
4139
4140        // Now try to activate the plugin.
4141        $activated = activate_plugin( $plugin );
4142
4143        if ( is_wp_error( $activated ) ) {
4144            return $activated;
4145        } else {
4146            $source = ! empty( $request['source'] ) ? stripslashes( $request['source'] ) : 'rest_api';
4147            /**
4148             * Fires when Jetpack installs a plugin for you.
4149             *
4150             * @since 8.9.0
4151             *
4152             * @param string $plugin_file Plugin file.
4153             * @param string $source      Where did the plugin installation originate.
4154             */
4155            do_action( 'jetpack_activated_plugin', $plugin, $source );
4156            return rest_ensure_response(
4157                array(
4158                    'code'    => 'success',
4159                    'message' => sprintf(
4160                        /* translators: placeholder is a plugin name. */
4161                        esc_html__( 'Activated %s', 'jetpack' ),
4162                        $plugin
4163                    ),
4164                )
4165            );
4166        }
4167    }
4168
4169    /**
4170     * Check if a plugin can be activated.
4171     *
4172     * @since 8.9.0
4173     *
4174     * @param string|bool     $value   Value to check.
4175     * @param WP_REST_Request $request The request sent to the WP REST API.
4176     * @param string          $param   Name of the parameter passed to endpoint holding $value.
4177     */
4178    public static function validate_activate_plugin( $value, $request, $param ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
4179        return 'active' === $value;
4180    }
4181
4182    /**
4183     * Get data about the queried plugin. Currently it only returns whether the plugin is active or not.
4184     *
4185     * @since 4.2.0
4186     *
4187     * @param WP_REST_Request $request {
4188     *     Array of parameters received by request.
4189     *
4190     *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
4191     * }
4192     *
4193     * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
4194     */
4195    public static function get_plugin( $request ) {
4196        $plugins = Plugins_Installer::get_plugins();
4197
4198        if ( empty( $plugins ) ) {
4199            return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
4200        }
4201
4202        $plugin = stripslashes( $request['plugin'] );
4203
4204        if ( ! array_key_exists( $plugin, $plugins ) ) {
4205            return new WP_Error(
4206                'plugin_not_found',
4207                esc_html(
4208                    sprintf(
4209                        /* Translators: placeholder is a plugin name. */
4210                        __( 'Plugin %s is not installed.', 'jetpack' ),
4211                        $plugin
4212                    )
4213                ),
4214                array( 'status' => 404 )
4215            );
4216        }
4217
4218        $plugin_data = $plugins[ $plugin ];
4219
4220        $plugin_data['active'] = in_array( Plugins_Installer::get_plugin_status( $plugin ), array( 'active', 'network-active' ), true );
4221
4222        return rest_ensure_response(
4223            array(
4224                'code'    => 'success',
4225                'message' => esc_html__( 'Plugin found.', 'jetpack' ),
4226                'data'    => $plugin_data,
4227            )
4228        );
4229    }
4230
4231    /**
4232     * Returns the Jetpack CRM data.
4233     *
4234     * @return WP_REST_Response A response object containing the Jetpack CRM data.
4235     */
4236    public static function get_jetpack_crm_data() {
4237        $jetpack_crm_data = ( new Jetpack_CRM_Data() )->get_crm_data();
4238        return rest_ensure_response( $jetpack_crm_data );
4239    }
4240
4241    /**
4242     * Activates Jetpack CRM's Jetpack Forms extension.
4243     *
4244     * @param WP_REST_Request $request The request sent to the WP REST API.
4245     * @return WP_REST_Response|WP_Error A response object if the extension activation was successful, or a WP_Error object if it failed.
4246     */
4247    public static function activate_crm_jetpack_forms_extension( $request ) {
4248        if ( ! isset( $request['extension'] ) || 'jetpackforms' !== $request['extension'] ) {
4249            return new WP_Error( 'invalid_param', esc_html__( 'Missing or invalid extension parameter.', 'jetpack' ), array( 'status' => 404 ) );
4250        }
4251
4252        $result = ( new Jetpack_CRM_Data() )->activate_crm_jetpackforms_extension();
4253
4254        if ( is_wp_error( $result ) ) {
4255            return $result;
4256        }
4257
4258        return rest_ensure_response( array( 'code' => 'success' ) );
4259    }
4260
4261    /**
4262     * Verifies that the current user has the required permission for accessing the CRM data.
4263     *
4264     * @return true|WP_Error Returns true if the user has the required capability, else a WP_Error object.
4265     */
4266    public static function jetpack_crm_data_permission_check() {
4267        if ( current_user_can( 'publish_posts' ) ) {
4268            return true;
4269        }
4270
4271        return new WP_Error(
4272            'invalid_user_permission_jetpack_crm_data',
4273            REST_Connector::get_user_permissions_error_msg(),
4274            array( 'status' => rest_authorization_required_code() )
4275        );
4276    }
4277
4278    /**
4279     * Verifies that the current user has the required capability for activating Jetpack CRM extensions.
4280     *
4281     * @return true|WP_Error Returns true if the user has the required capability, else a WP_Error object.
4282     */
4283    public static function activate_crm_extensions_permission_check() {
4284        // phpcs:ignore WordPress.WP.Capabilities.Unknown
4285        if ( current_user_can( 'admin_zerobs_manage_options' ) ) {
4286            return true;
4287        }
4288
4289        return new WP_Error(
4290            'invalid_user_permission_activate_jetpack_crm_ext',
4291            REST_Connector::get_user_permissions_error_msg(),
4292            array( 'status' => rest_authorization_required_code() )
4293        );
4294    }
4295
4296    /**
4297     * Set hasSeenWCConnectionModal to true when the site has displayed it
4298     *
4299     * @since 10.4.0
4300     *
4301     * @return bool
4302     */
4303    public static function set_has_seen_wc_connection_modal() {
4304        $updated_option = Jetpack_Options::update_option( 'has_seen_wc_connection_modal', true );
4305
4306        return rest_ensure_response( array( 'success' => $updated_option ) );
4307    }
4308
4309    /**
4310     * Fetch introdution offers.
4311     *
4312     * @since 10.9
4313     *
4314     * @return array|WP_Error
4315     */
4316    public static function get_intro_offers() {
4317        $site_id = Jetpack_Options::get_option( 'id' );
4318
4319        if ( ! $site_id ) {
4320            return new WP_Error(
4321                'site_id_missing',
4322                esc_html__( 'Site ID is missing.', 'jetpack' ),
4323                array( 'status' => 400 )
4324            );
4325        }
4326
4327        $response = Client::wpcom_json_api_request_as_user(
4328            '/introductory-offers',
4329            '2',
4330            array(
4331                'method'  => 'GET',
4332                'headers' => array(
4333                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
4334                ),
4335            )
4336        );
4337
4338        $response_code = wp_remote_retrieve_response_code( $response );
4339
4340        if ( 200 !== $response_code ) {
4341            return new WP_Error(
4342                'intro_offers_fetch_failed',
4343                esc_html__( 'Could not retrieve intro offers.', 'jetpack' ),
4344                array( 'status' => $response_code )
4345            );
4346        }
4347
4348        $data = json_decode( wp_remote_retrieve_body( $response ) );
4349
4350        if ( ! isset( $data ) ) {
4351            return new WP_Error(
4352                'intro_offers_error',
4353                esc_html__( 'Could not parse intro offers.', 'jetpack' ),
4354                array( 'status' => 204 ) // no content.
4355            );
4356        }
4357
4358        return rest_ensure_response(
4359            array(
4360                'code' => 'success',
4361                'data' => $data,
4362            )
4363        );
4364    }
4365
4366    /**
4367     * Return the list of available features.
4368     *
4369     * @return array
4370     */
4371    public static function get_features_available() {
4372        $raw_modules = Jetpack::get_available_modules();
4373        $modules     = array();
4374        foreach ( $raw_modules as $module ) {
4375            $modules[] = Jetpack::get_module_slug( $module );
4376        }
4377
4378        return $modules;
4379    }
4380
4381    /**
4382     * Returns what features are enabled. Uses the slug of the modules files.
4383     *
4384     * @return array
4385     */
4386    public static function get_features_enabled() {
4387        $raw_modules = Jetpack::get_active_modules();
4388        $modules     = array();
4389        foreach ( $raw_modules as $module ) {
4390            $modules[] = Jetpack::get_module_slug( $module );
4391        }
4392
4393        return $modules;
4394    }
4395
4396    /**
4397     * Verify that the API client is allowed to replace user token.
4398     *
4399     * @since 1.29.0
4400     *
4401     * @return bool|WP_Error
4402     */
4403    public static function get_features_permission_check() {
4404        if ( ! Rest_Authentication::is_signed_with_blog_token() ) {
4405            $message = esc_html__(
4406                'You do not have the correct user permissions to perform this action. Please contact your site admin if you think this is a mistake.',
4407                'jetpack'
4408            );
4409            return new WP_Error( 'invalid_permission_fetch_features', $message, array( 'status' => rest_authorization_required_code() ) );
4410        }
4411
4412        return true;
4413    }
4414} // class end