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