Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.02% covered (warning)
63.02%
1696 / 2691
17.24% covered (danger)
17.24%
15 / 87
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Core_Json_Api_Endpoints
63.17% covered (warning)
63.17%
1696 / 2685
17.24% covered (danger)
17.24%
15 / 87
8111.65
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 / 20
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%
837 / 837
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() ),
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() );
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        );
1536
1537        if (
1538            ! function_exists( 'openssl_verify' )
1539            || 1 !== openssl_verify(
1540                $signature_data,
1541                $signature,
1542                JETPACK__DEBUGGER_PUBLIC_KEY
1543            )
1544        ) {
1545            return false;
1546        }
1547
1548        // signature timestamp must be within 5min of current time.
1549        if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
1550            return false;
1551        }
1552
1553        // phpcs:enable WordPress.Security.NonceVerification.Recommended
1554
1555        return true;
1556    }
1557
1558    /**
1559     * Test connection status for this Jetpack site, encrypt the results for decryption by a third-party.
1560     *
1561     * @since 7.1.0
1562     *
1563     * @return array|mixed|object|WP_Error
1564     */
1565    public static function jetpack_connection_test_for_external() {
1566        // 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.
1567        add_filter( 'jetpack_debugger_run_self_test', '__return_false' );
1568        require_once JETPACK__PLUGIN_DIR . '_inc/lib/debugger.php';
1569        $cxntests = new Jetpack_Cxn_Tests();
1570
1571        if ( $cxntests->pass() ) {
1572            $result = array(
1573                'code'    => 'success',
1574                'message' => __( 'All connection tests passed.', 'jetpack' ),
1575            );
1576        } else {
1577            $error  = $cxntests->output_fails_as_wp_error(); // Using this so the output is similar both ways.
1578            $errors = array();
1579
1580            // Borrowed from WP_REST_Server::error_to_response().
1581            foreach ( (array) $error->errors as $code => $messages ) {
1582                foreach ( (array) $messages as $message ) {
1583                    $errors[] = array(
1584                        'code'    => $code,
1585                        'message' => $message,
1586                        'data'    => $error->get_error_data( $code ),
1587                    );
1588                }
1589            }
1590
1591            $result = ( ! empty( $errors ) ) ? $errors[0] : null;
1592            if ( count( $errors ) > 1 ) {
1593                // Remove the primary error.
1594                array_shift( $errors );
1595                $result['additional_errors'] = $errors;
1596            }
1597        }
1598
1599        $result = wp_json_encode( $result );
1600
1601        $encrypted = $cxntests->encrypt_string_for_wpcom( $result );
1602
1603        if ( ! $encrypted || ! is_array( $encrypted ) ) {
1604            return rest_ensure_response(
1605                array(
1606                    'code'    => 'action_required',
1607                    'message' => 'Please request results from the in-plugin debugger',
1608                )
1609            );
1610        }
1611
1612        return rest_ensure_response(
1613            array(
1614                'code'  => 'response',
1615                'debug' => $encrypted,
1616            )
1617        );
1618    }
1619
1620    /**
1621     * Fetch information about the Rewind status of the site.
1622     */
1623    public static function rewind_data() {
1624        $site_id = Jetpack_Options::get_option( 'id' );
1625
1626        if ( ! $site_id ) {
1627            return new WP_Error( 'site_id_missing' );
1628        }
1629
1630        if ( ! isset( $_GET['_cacheBuster'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1631            $rewind_state = get_transient( 'jetpack_rewind_state' );
1632            if ( $rewind_state ) {
1633                return $rewind_state;
1634            }
1635        }
1636
1637        $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/rewind', $site_id ) . '?force=wpcom', '2', array(), null, 'wpcom' );
1638
1639        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1640            return new WP_Error( 'rewind_data_fetch_failed' );
1641        }
1642
1643        $body   = wp_remote_retrieve_body( $response );
1644        $result = json_decode( $body );
1645        set_transient( 'jetpack_rewind_state', $result, 30 * MINUTE_IN_SECONDS );
1646
1647        return $result;
1648    }
1649
1650    /**
1651     * Get rewind data
1652     *
1653     * @since 5.7.0
1654     *
1655     * @return array Array of rewind properties.
1656     */
1657    public static function get_rewind_data() {
1658        $rewind_data = self::rewind_data();
1659
1660        if ( ! is_wp_error( $rewind_data ) ) {
1661            return rest_ensure_response(
1662                array(
1663                    'code'    => 'success',
1664                    'message' => esc_html__( 'Backup & Scan data correctly received.', 'jetpack' ),
1665                    'data'    => wp_json_encode( $rewind_data ),
1666                )
1667            );
1668        }
1669
1670        if ( $rewind_data->get_error_code() === 'rewind_data_fetch_failed' ) {
1671            return new WP_Error( 'rewind_data_fetch_failed', esc_html__( 'Failed fetching rewind data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
1672        }
1673
1674        if ( $rewind_data->get_error_code() === 'site_id_missing' ) {
1675            return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
1676        }
1677
1678        return new WP_Error(
1679            'error_get_rewind_data',
1680            esc_html__( 'Could not retrieve Backup & Scan data.', 'jetpack' ),
1681            array( 'status' => 500 )
1682        );
1683    }
1684
1685    /**
1686     * Gets Scan state data.
1687     *
1688     * @since 8.5.0
1689     *
1690     * @return array|WP_Error Result from WPCOM API or error.
1691     */
1692    public static function scan_state() {
1693
1694        if ( ! isset( $_GET['_cacheBuster'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1695            $scan_state = get_transient( 'jetpack_scan_state' );
1696            if ( ! empty( $scan_state ) ) {
1697                return $scan_state;
1698            }
1699        }
1700        $site_id = Jetpack_Options::get_option( 'id' );
1701
1702        if ( ! $site_id ) {
1703            return new WP_Error( 'site_id_missing' );
1704        }
1705        // The default timeout was too short in come cases.
1706        add_filter( 'http_request_timeout', array( __CLASS__, 'increase_timeout_30' ), PHP_INT_MAX - 1 );
1707        $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/scan', $site_id ) . '?force=wpcom', '2', array(), null, 'wpcom' );
1708        remove_filter( 'http_request_timeout', array( __CLASS__, 'increase_timeout_30' ), PHP_INT_MAX - 1 );
1709
1710        if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
1711            return new WP_Error( 'scan_state_fetch_failed' );
1712        }
1713
1714        $body   = wp_remote_retrieve_body( $response );
1715        $result = json_decode( $body );
1716        set_transient( 'jetpack_scan_state', $result, 30 * MINUTE_IN_SECONDS );
1717
1718        return $result;
1719    }
1720
1721    /**
1722     * Increases the request timeout value to 30 seconds.
1723     *
1724     * @return int Always returns 30.
1725     */
1726    public static function increase_timeout_30() {
1727        return 30; // 30 Seconds
1728    }
1729
1730    /**
1731     * Get Scan state for API.
1732     *
1733     * @since 8.5.0
1734     *
1735     * @return WP_REST_Response|WP_Error REST response or error state.
1736     */
1737    public static function get_scan_state() {
1738        $scan_state = self::scan_state();
1739
1740        if ( ! is_wp_error( $scan_state ) ) {
1741            if ( ( new Host() )->is_woa_site() && ! empty( $scan_state->threats ) ) {
1742                $scan_state->threats = array();
1743            }
1744            return rest_ensure_response(
1745                array(
1746                    'code'    => 'success',
1747                    'message' => esc_html__( 'Scan state correctly received.', 'jetpack' ),
1748                    'data'    => wp_json_encode( $scan_state ),
1749                )
1750            );
1751        }
1752
1753        if ( $scan_state->get_error_code() === 'scan_state_fetch_failed' ) {
1754            return new WP_Error( 'scan_state_fetch_failed', esc_html__( 'Failed fetching rewind data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
1755        }
1756
1757        if ( $scan_state->get_error_code() === 'site_id_missing' ) {
1758            return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
1759        }
1760
1761        return new WP_Error(
1762            'error_get_rewind_data',
1763            esc_html__( 'Could not retrieve Scan state.', 'jetpack' ),
1764            array( 'status' => 500 )
1765        );
1766    }
1767
1768    /**
1769     * Disconnects Jetpack from the WordPress.com Servers
1770     *
1771     * @deprecated since Jetpack 10.0.0
1772     * @see Automattic\Jetpack\Connection\REST_Connector::disconnect_site()
1773     *
1774     * @uses Jetpack::disconnect();
1775     * @since 4.3.0
1776     *
1777     * @param WP_REST_Request $request The request sent to the WP REST API.
1778     *
1779     * @return bool|WP_Error True if Jetpack successfully disconnected.
1780     */
1781    public static function disconnect_site( $request ) {
1782        _deprecated_function( __METHOD__, 'jetpack-10.0.0', '\Automattic\Jetpack\Connection\REST_Connector::disconnect_site' );
1783
1784        if ( ! isset( $request['isActive'] ) || false !== $request['isActive'] ) {
1785            return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
1786        }
1787
1788        if ( Jetpack::is_connection_ready() ) {
1789            Jetpack::disconnect();
1790            return rest_ensure_response( array( 'code' => 'success' ) );
1791        }
1792
1793        return new WP_Error( 'disconnect_failed', esc_html__( 'Was not able to disconnect the site. Please try again.', 'jetpack' ), array( 'status' => 400 ) );
1794    }
1795
1796    /**
1797     * Gets a new connect raw URL with fresh nonce.
1798     *
1799     * @uses Jetpack::disconnect();
1800     * @since 4.3.0
1801     *
1802     * @param WP_REST_Request $request The request sent to the WP REST API.
1803     *
1804     * @return string|WP_Error A raw URL if the connection URL could be built; error message otherwise.
1805     */
1806    public static function build_connect_url( $request = array() ) {
1807        $from     = isset( $request['from'] ) ? $request['from'] : false;
1808        $redirect = isset( $request['redirect'] ) ? $request['redirect'] : false;
1809
1810        $url = Jetpack::init()->build_connect_url( true, $redirect, $from );
1811        if ( $url ) {
1812            return rest_ensure_response( $url );
1813        }
1814
1815        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 ) );
1816    }
1817
1818    /**
1819     * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
1820     * Information about the master/primary user.
1821     * Information about the current user.
1822     *
1823     * @deprecated since Jetpack 10.0.0
1824     * @see Automattic\Jetpack\Connection\REST_Connector::get_user_connection_data()
1825     *
1826     * @since 4.3.0
1827     *
1828     * @return object
1829     */
1830    public static function get_user_connection_data() {
1831        _deprecated_function( __METHOD__, 'jetpack-10.0.0', '\Automattic\Jetpack\Connection\REST_Connector::get_user_connection_data' );
1832
1833        require_once JETPACK__PLUGIN_DIR . '_inc/lib/admin-pages/class.jetpack-react-page.php';
1834
1835        $connection_owner   = ( new Connection_Manager() )->get_connection_owner();
1836        $owner_display_name = false === $connection_owner ? null : $connection_owner->data->display_name;
1837
1838        $response = array(
1839            'currentUser'     => jetpack_current_user_data(),
1840            'connectionOwner' => $owner_display_name,
1841        );
1842        return rest_ensure_response( $response );
1843    }
1844
1845    /**
1846     * Unlinks current user from the WordPress.com Servers.
1847     *
1848     * @param WP_REST_Request $request The request sent to the WP REST API.
1849     * @uses  Automattic\Jetpack\Connection\Manager->disconnect_user
1850     *
1851     * @deprecated since Jetpack 14.4.0
1852     * @see Automattic\Jetpack\Connection\REST_Connector::unlink_user()
1853     *
1854     * @since 4.3.0
1855     *
1856     * @return bool|WP_Error True if user successfully unlinked.
1857     */
1858    public static function unlink_user( $request ) {
1859        _deprecated_function( __METHOD__, 'jetpack-14.4.0', 'Automattic\Jetpack\Connection\REST_Connector::unlink_user()' );
1860        return REST_Connector::unlink_user( $request );
1861    }
1862
1863    /**
1864     * Gets current user's tracking settings.
1865     *
1866     * @since 6.0.0
1867     *
1868     * @param  WP_REST_Request $request The request sent to the WP REST API.
1869     *
1870     * @return WP_REST_Response|WP_Error Response, else error.
1871     */
1872    public static function get_user_tracking_settings( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1873        if ( ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
1874            $response = array(
1875                'tracks_opt_out' => true, // Default to opt-out if not connected to wp.com.
1876            );
1877        } else {
1878            $response = Client::wpcom_json_api_request_as_user(
1879                '/jetpack-user-tracking',
1880                'v2',
1881                array(
1882                    'method'  => 'GET',
1883                    'headers' => array(
1884                        'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1885                    ),
1886                )
1887            );
1888            if ( ! is_wp_error( $response ) ) {
1889                $response = json_decode( wp_remote_retrieve_body( $response ), true );
1890            }
1891        }
1892
1893        return rest_ensure_response( $response );
1894    }
1895
1896    /**
1897     * Updates current user's tracking settings.
1898     *
1899     * @since 6.0.0
1900     *
1901     * @param  WP_REST_Request $request The request sent to the WP REST API.
1902     *
1903     * @return WP_REST_Response|WP_Error Response, else error.
1904     */
1905    public static function update_user_tracking_settings( $request ) {
1906        if ( ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
1907            $response = array(
1908                'tracks_opt_out' => true, // Default to opt-out if not connected to wp.com.
1909            );
1910        } else {
1911            $response = Client::wpcom_json_api_request_as_user(
1912                '/jetpack-user-tracking',
1913                'v2',
1914                array(
1915                    'method'  => 'PUT',
1916                    'headers' => array(
1917                        'Content-Type'    => 'application/json',
1918                        'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
1919                    ),
1920                ),
1921                wp_json_encode( $request->get_params() )
1922            );
1923            if ( ! is_wp_error( $response ) ) {
1924                $response = json_decode( wp_remote_retrieve_body( $response ), true );
1925            }
1926        }
1927
1928        return rest_ensure_response( $response );
1929    }
1930
1931    /**
1932     * Fetch site data from .com including the site's current plan and the site's products.
1933     *
1934     * @since 5.5.0
1935     *
1936     * @return stdClass|WP_Error
1937     */
1938    public static function site_data() {
1939        $site_id = Jetpack_Options::get_option( 'id' );
1940
1941        if ( ! $site_id ) {
1942            return new WP_Error( 'site_id_missing', '', array( 'api_error_code' => __( 'site_id_missing', 'jetpack' ) ) );
1943        }
1944
1945        $args = array( 'headers' => array() );
1946
1947        // Allow use a store sandbox. Internal ref: PCYsg-IA-p2.
1948        if ( isset( $_COOKIE ) && isset( $_COOKIE['store_sandbox'] ) ) {
1949            $secret                    = filter_var( wp_unslash( $_COOKIE['store_sandbox'] ) );
1950            $args['headers']['Cookie'] = "store_sandbox=$secret;";
1951        }
1952
1953        $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d', $site_id ) . '?force=wpcom', '1.1', $args );
1954        $body     = wp_remote_retrieve_body( $response );
1955        $data     = $body ? json_decode( $body ) : null;
1956
1957        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1958            $error_info = array(
1959                'api_error_code' => null,
1960                'api_http_code'  => wp_remote_retrieve_response_code( $response ),
1961            );
1962
1963            if ( is_wp_error( $response ) ) {
1964                $error_info['api_error_code'] = $response->get_error_code() ? wp_strip_all_tags( $response->get_error_code() ) : null;
1965            } elseif ( $data && ! empty( $data->error ) ) {
1966                $error_info['api_error_code'] = $data->error;
1967            }
1968
1969            return new WP_Error( 'site_data_fetch_failed', '', $error_info );
1970        }
1971
1972        Jetpack_Plan::update_from_sites_response( $response );
1973
1974        return $data;
1975    }
1976    /**
1977     * Get site data, including for example, the site's current plan.
1978     *
1979     * @return WP_Error|WP_HTTP_Response|WP_REST_Response
1980     * @since 4.3.0
1981     */
1982    public static function get_site_data() {
1983        $site_data = self::site_data();
1984
1985        if ( ! is_wp_error( $site_data ) ) {
1986
1987            /**
1988             * Fires when the site data was successfully returned from the /sites/%d wpcom endpoint.
1989             *
1990             * @since 8.7.0
1991             */
1992            do_action( 'jetpack_get_site_data_success' );
1993            return rest_ensure_response(
1994                array(
1995                    'code'    => 'success',
1996                    'message' => esc_html__( 'Site data correctly received.', 'jetpack' ),
1997                    'data'    => wp_json_encode( $site_data ),
1998                )
1999            );
2000        }
2001
2002        $error_data = $site_data->get_error_data();
2003
2004        if ( empty( $error_data['api_error_code'] ) ) {
2005            $error_message = esc_html__( 'Failed fetching site data from WordPress.com. If the problem persists, try reconnecting Jetpack.', 'jetpack' );
2006        } else {
2007            /* translators: %s is an error code (e.g. `token_mismatch`) */
2008            $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'] );
2009        }
2010
2011        return new WP_Error(
2012            $site_data->get_error_code(),
2013            $error_message,
2014            array(
2015                'status'         => 400,
2016                'api_error_code' => empty( $error_data['api_error_code'] ) ? null : $error_data['api_error_code'],
2017                'api_http_code'  => empty( $error_data['api_http_code'] ) ? null : $error_data['api_http_code'],
2018            )
2019        );
2020    }
2021
2022    /**
2023     * Fetch AL data for this site and return it.
2024     *
2025     * @since 7.4
2026     *
2027     * @return array|WP_Error
2028     */
2029    public static function get_site_activity() {
2030        $site_id = Jetpack_Options::get_option( 'id' );
2031
2032        if ( ! $site_id ) {
2033            return new WP_Error(
2034                'site_id_missing',
2035                esc_html__( 'Site ID is missing.', 'jetpack' ),
2036                array( 'status' => 400 )
2037            );
2038        }
2039
2040        $response      = Client::wpcom_json_api_request_as_user(
2041            "/sites/$site_id/activity",
2042            '2',
2043            array(
2044                'method'  => 'GET',
2045                'headers' => array(
2046                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
2047                ),
2048            ),
2049            null,
2050            'wpcom'
2051        );
2052        $response_code = wp_remote_retrieve_response_code( $response );
2053
2054        if ( 200 !== $response_code ) {
2055            return new WP_Error(
2056                'activity_fetch_failed',
2057                esc_html__( 'Could not retrieve site activity.', 'jetpack' ),
2058                array( 'status' => $response_code )
2059            );
2060        }
2061
2062        $data = json_decode( wp_remote_retrieve_body( $response ) );
2063
2064        if ( ! isset( $data->current->orderedItems ) ) {
2065            return new WP_Error(
2066                'activity_not_found',
2067                esc_html__( 'No activity found', 'jetpack' ),
2068                array( 'status' => 204 ) // no content.
2069            );
2070        }
2071
2072        return rest_ensure_response(
2073            array(
2074                'code' => 'success',
2075                'data' => $data->current->orderedItems,
2076            )
2077        );
2078    }
2079
2080    /**
2081     * Fetch the discount for this site and return it.
2082     *
2083     * @since 10.8
2084     *
2085     * @return array|WP_Error
2086     */
2087    public static function get_site_discount() {
2088        $site_id = Jetpack_Options::get_option( 'id' );
2089
2090        if ( ! $site_id ) {
2091            return new WP_Error(
2092                'site_id_missing',
2093                esc_html__( 'Site ID is missing.', 'jetpack' ),
2094                array( 'status' => 400 )
2095            );
2096        }
2097
2098        $response = Client::wpcom_json_api_request_as_user(
2099            "/sites/$site_id/discount",
2100            '2',
2101            array(
2102                'method'  => 'GET',
2103                'headers' => array(
2104                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
2105                ),
2106            )
2107        );
2108
2109        $response_code = wp_remote_retrieve_response_code( $response );
2110        $data          = json_decode( wp_remote_retrieve_body( $response ) );
2111
2112        if ( 200 !== $response_code ) {
2113            return new WP_Error(
2114                'discount_fetch_failed',
2115                is_object( $data ) && property_exists( $data, 'error' ) ? $data->error : esc_html__( 'Could not retrieve site discount.', 'jetpack' ),
2116                array( 'status' => $response_code )
2117            );
2118        }
2119
2120        if ( ! isset( $data ) ) {
2121            return new WP_Error(
2122                'discount_parse_error',
2123                esc_html__( 'Could not parse discount', 'jetpack' ),
2124                array( 'status' => 204 ) // no content.
2125            );
2126        }
2127
2128        return rest_ensure_response(
2129            array(
2130                'code' => 'success',
2131                'data' => $data,
2132            )
2133        );
2134    }
2135
2136    /**
2137     * Reset Jetpack options
2138     *
2139     * @since 4.3.0
2140     *
2141     * @param WP_REST_Request $request {
2142     *     Array of parameters received by request.
2143     *
2144     *     @type string $options Available options to reset are options|modules
2145     * }
2146     *
2147     * @return bool|WP_Error True if options were reset. Otherwise, a WP_Error instance with the corresponding error.
2148     */
2149    public static function reset_jetpack_options( $request ) {
2150
2151        if ( ! isset( $request['reset'] ) || true !== $request['reset'] ) {
2152            return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
2153        }
2154
2155        if ( isset( $request['options'] ) ) {
2156            $data    = $request['options'];
2157            $message = '';
2158
2159            switch ( $data ) {
2160                case ( 'options' ):
2161                    $options_to_reset = Jetpack::get_jetpack_options_for_reset();
2162
2163                    // Reset the Jetpack options.
2164                    foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
2165                        Jetpack_Options::delete_option( $option_to_reset );
2166                    }
2167
2168                    foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
2169                        delete_option( $option_to_reset );
2170                    }
2171
2172                    // Reset to default modules.
2173                    $default_modules = Jetpack::get_default_modules();
2174                    Jetpack::update_active_modules( $default_modules );
2175                    $message = esc_html__( 'Jetpack options reset.', 'jetpack' );
2176
2177                    break;
2178                case 'modules':
2179                    $default_modules = Jetpack::get_default_modules();
2180                    Jetpack::update_active_modules( $default_modules );
2181                    $message = esc_html__( 'Modules reset to default.', 'jetpack' );
2182
2183                    break;
2184                default:
2185                    return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
2186            }
2187
2188            return rest_ensure_response(
2189                array(
2190                    'code'    => 'success',
2191                    'message' => $message,
2192                )
2193            );
2194        }
2195
2196        return new WP_Error( 'required_param', esc_html__( 'Missing parameter "type".', 'jetpack' ), array( 'status' => 404 ) );
2197    }
2198
2199    /**
2200     * Get the query parameters to update module options or general settings.
2201     *
2202     * @since 4.3.0
2203     * @since 4.4.0 Accepts a $selector parameter.
2204     *
2205     * @param string $selector Selects a set of options to update, Can be empty, a module slug or 'any'.
2206     *
2207     * @return array
2208     */
2209    public static function get_updateable_parameters( $selector = '' ) {
2210        $parameters = array(
2211            'context' => array(
2212                'default' => 'edit',
2213            ),
2214        );
2215
2216        return array_merge( $parameters, self::get_updateable_data_list( $selector ) );
2217    }
2218
2219    /**
2220     * Returns a list of module options or general settings that can be updated.
2221     *
2222     * @since 4.3.0
2223     * @since 4.4.0 Accepts 'any' as a parameter which will make it return the entire list.
2224     *
2225     * @param string|array $selector Module slug, 'any', or an array of parameters.
2226     *                               If empty, it's assumed we're updating a module and we'll try to get its slug.
2227     *                               If 'any' the full list is returned.
2228     *                               If it's an array of parameters, includes the elements by matching keys.
2229     *
2230     * @return array
2231     */
2232    public static function get_updateable_data_list( $selector = '' ) {
2233
2234        $options = array(
2235            // Blocks.
2236            'jetpack_blocks_disabled'                   => array(
2237                'description'       => esc_html__( 'Jetpack Blocks disabled.', 'jetpack' ),
2238                'type'              => 'boolean',
2239                'default'           => false,
2240                'validate_callback' => __CLASS__ . '::validate_boolean',
2241                'jp_group'          => 'settings',
2242            ),
2243
2244            // Carousel
2245            'carousel_background_color'                 => array(
2246                'description'       => esc_html__( 'Color scheme.', 'jetpack' ),
2247                'type'              => 'string',
2248                'default'           => 'black',
2249                'enum'              => array(
2250                    'black',
2251                    'white',
2252                ),
2253                'enum_labels'       => array(
2254                    'black' => esc_html__( 'Black', 'jetpack' ),
2255                    'white' => esc_html__( 'White', 'jetpack' ),
2256                ),
2257                'validate_callback' => __CLASS__ . '::validate_list_item',
2258                'jp_group'          => 'carousel',
2259            ),
2260            'carousel_display_exif'                     => array(
2261                'description'       => wp_kses(
2262                    sprintf( __( 'Show photo metadata (<a href="https://en.wikipedia.org/wiki/Exchangeable_image_file_format" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) ),
2263                    array(
2264                        'a' => array(
2265                            'href'   => true,
2266                            'target' => true,
2267                        ),
2268                    )
2269                ),
2270                'type'              => 'boolean',
2271                'default'           => 0,
2272                'validate_callback' => __CLASS__ . '::validate_boolean',
2273                'jp_group'          => 'carousel',
2274            ),
2275            'carousel_display_comments'                 => array(
2276                'description'       => esc_html__( 'Show comments area in carousel', 'jetpack' ),
2277                'type'              => 'boolean',
2278                'default'           => 1,
2279                'validate_callback' => __CLASS__ . '::validate_boolean',
2280                'jp_group'          => 'carousel',
2281            ),
2282
2283            // Comments.
2284            'highlander_comment_form_prompt'            => array(
2285                'description'       => esc_html__( 'Greeting Text', 'jetpack' ),
2286                'type'              => 'string',
2287                'default'           => esc_html__( 'Leave a Reply', 'jetpack' ),
2288                'sanitize_callback' => 'sanitize_text_field',
2289                'jp_group'          => 'comments',
2290            ),
2291            'jetpack_comment_form_color_scheme'         => array(
2292                'description'       => esc_html__( 'Color scheme', 'jetpack' ),
2293                'type'              => 'string',
2294                'default'           => 'light',
2295                'enum'              => array(
2296                    'light',
2297                    'dark',
2298                    'transparent',
2299                ),
2300                'enum_labels'       => array(
2301                    'light'       => esc_html__( 'Light', 'jetpack' ),
2302                    'dark'        => esc_html__( 'Dark', 'jetpack' ),
2303                    'transparent' => esc_html__( 'Transparent', 'jetpack' ),
2304                ),
2305                'validate_callback' => __CLASS__ . '::validate_list_item',
2306                'jp_group'          => 'comments',
2307            ),
2308
2309            // Custom Content Types.
2310            'jetpack_portfolio'                         => array(
2311                'description'       => esc_html__( 'Enable or disable Jetpack portfolio post type.', 'jetpack' ),
2312                'type'              => 'boolean',
2313                'default'           => 0,
2314                'validate_callback' => __CLASS__ . '::validate_boolean',
2315                'jp_group'          => 'settings',
2316            ),
2317            'jetpack_portfolio_posts_per_page'          => array(
2318                'description'       => esc_html__( 'Number of entries to show at most in Portfolio pages.', 'jetpack' ),
2319                'type'              => 'integer',
2320                'default'           => 10,
2321                'validate_callback' => __CLASS__ . '::validate_posint',
2322                'jp_group'          => 'settings',
2323            ),
2324            'jetpack_testimonial'                       => array(
2325                'description'       => esc_html__( 'Enable or disable Jetpack testimonial post type.', 'jetpack' ),
2326                'type'              => 'boolean',
2327                'default'           => 0,
2328                'validate_callback' => __CLASS__ . '::validate_boolean',
2329                'jp_group'          => 'settings',
2330            ),
2331            'jetpack_testimonial_posts_per_page'        => array(
2332                'description'       => esc_html__( 'Number of entries to show at most in Testimonial pages.', 'jetpack' ),
2333                'type'              => 'integer',
2334                'default'           => 10,
2335                'validate_callback' => __CLASS__ . '::validate_posint',
2336                'jp_group'          => 'settings',
2337            ),
2338            // WAF.
2339            'jetpack_waf_automatic_rules'               => array(
2340                'description'       => esc_html__( 'Enable automatic rules - Protect your site against untrusted traffic sources with automatic security rules.', 'jetpack' ),
2341                'type'              => 'boolean',
2342                'default'           => Waf_Compatibility::get_default_automatic_rules_option(),
2343                'validate_callback' => __CLASS__ . '::validate_boolean',
2344                'jp_group'          => 'waf',
2345            ),
2346            'jetpack_waf_ip_block_list_enabled'         => array(
2347                'description'       => esc_html__( 'Block list - Block a specific request IP.', 'jetpack' ),
2348                'type'              => 'boolean',
2349                'default'           => 0,
2350                'validate_callback' => __CLASS__ . '::validate_boolean',
2351                'jp_group'          => 'waf',
2352            ),
2353            'jetpack_waf_ip_block_list'                 => array(
2354                'description'       => esc_html__( 'Blocked IP addresses', 'jetpack' ),
2355                'type'              => 'string',
2356                'default'           => '',
2357                'validate_callback' => __CLASS__ . '::validate_string',
2358                'sanitize_callback' => 'esc_textarea',
2359                'jp_group'          => 'waf',
2360            ),
2361            'jetpack_waf_ip_allow_list_enabled'         => array(
2362                'description'       => esc_html__( 'Allow list - Allow a specific request IP.', 'jetpack' ),
2363                'type'              => 'boolean',
2364                'default'           => 0,
2365                'validate_callback' => __CLASS__ . '::validate_boolean',
2366                'jp_group'          => 'settings',
2367            ),
2368            'jetpack_waf_ip_allow_list'                 => array(
2369                'description'       => esc_html__( 'Always allowed IP addresses', 'jetpack' ),
2370                'type'              => 'string',
2371                'default'           => '',
2372                'validate_callback' => __CLASS__ . '::validate_string',
2373                'sanitize_callback' => 'esc_textarea',
2374                'jp_group'          => 'settings',
2375            ),
2376            'jetpack_waf_share_data'                    => array(
2377                'description'       => esc_html__( 'Share basic data with Jetpack.', 'jetpack' ),
2378                'type'              => 'boolean',
2379                'default'           => 0,
2380                'validate_callback' => __CLASS__ . '::validate_boolean',
2381                'jp_group'          => 'waf',
2382            ),
2383            'jetpack_waf_share_debug_data'              => array(
2384                'description'       => esc_html__( 'Share detailed data with Jetpack.', 'jetpack' ),
2385                'type'              => 'boolean',
2386                'default'           => 0,
2387                'validate_callback' => __CLASS__ . '::validate_boolean',
2388                'jp_group'          => 'waf',
2389            ),
2390            // Galleries.
2391            'tiled_galleries'                           => array(
2392                'description'       => esc_html__( 'Display all your gallery pictures in a cool mosaic.', 'jetpack' ),
2393                'type'              => 'boolean',
2394                'default'           => 0,
2395                'validate_callback' => __CLASS__ . '::validate_boolean',
2396                'jp_group'          => 'tiled-gallery',
2397            ),
2398
2399            'gravatar_disable_hovercards'               => array(
2400                'description'       => esc_html__( "View people's profiles when you mouse over their Gravatars", 'jetpack' ),
2401                'type'              => 'string',
2402                'default'           => 'enabled',
2403                // Not visible. This is used as the checkbox value.
2404                'enum'              => array(
2405                    'enabled',
2406                    'disabled',
2407                ),
2408                'enum_labels'       => array(
2409                    'enabled'  => esc_html__( 'Enabled', 'jetpack' ),
2410                    'disabled' => esc_html__( 'Disabled', 'jetpack' ),
2411                ),
2412                'validate_callback' => __CLASS__ . '::validate_list_item',
2413                'jp_group'          => 'gravatar-hovercards',
2414            ),
2415
2416            // Infinite Scroll.
2417            'infinite_scroll'                           => array(
2418                'description'       => esc_html__( 'To infinity and beyond', 'jetpack' ),
2419                'type'              => 'boolean',
2420                'default'           => 1,
2421                'validate_callback' => __CLASS__ . '::validate_boolean',
2422                'jp_group'          => 'infinite-scroll',
2423            ),
2424            'infinite_scroll_google_analytics'          => array(
2425                'description'       => esc_html__( 'Use Google Analytics with Infinite Scroll', 'jetpack' ),
2426                'type'              => 'boolean',
2427                'default'           => 0,
2428                'validate_callback' => __CLASS__ . '::validate_boolean',
2429                'jp_group'          => 'infinite-scroll',
2430            ),
2431
2432            // Likes.
2433            'wpl_default'                               => array(
2434                'description'       => esc_html__( 'WordPress.com Likes are', 'jetpack' ),
2435                'type'              => 'string',
2436                'default'           => 'on',
2437                'enum'              => array(
2438                    'on',
2439                    'off',
2440                ),
2441                'enum_labels'       => array(
2442                    'on'  => esc_html__( 'On for all posts', 'jetpack' ),
2443                    'off' => esc_html__( 'Turned on per post', 'jetpack' ),
2444                ),
2445                'validate_callback' => __CLASS__ . '::validate_list_item',
2446                'jp_group'          => 'likes',
2447            ),
2448            'social_notifications_like'                 => array(
2449                'description'       => esc_html__( 'Send email notification when someone likes a post', 'jetpack' ),
2450                'type'              => 'boolean',
2451                'default'           => 1,
2452                'validate_callback' => __CLASS__ . '::validate_boolean',
2453                'jp_group'          => 'likes',
2454            ),
2455
2456            // Markdown.
2457            'wpcom_publish_comments_with_markdown'      => array(
2458                'description'       => esc_html__( 'Use Markdown for comments.', 'jetpack' ),
2459                'type'              => 'boolean',
2460                'default'           => 0,
2461                'validate_callback' => __CLASS__ . '::validate_boolean',
2462                'jp_group'          => 'markdown',
2463            ),
2464            'wpcom_publish_posts_with_markdown'         => array(
2465                'description'       => esc_html__( 'Use Markdown for posts.', 'jetpack' ),
2466                'type'              => 'boolean',
2467                'default'           => 0,
2468                'validate_callback' => __CLASS__ . '::validate_boolean',
2469                'jp_group'          => 'markdown',
2470            ),
2471
2472            // Monitor.
2473            'monitor_receive_notifications'             => array(
2474                'description'       => esc_html__( 'Receive Monitor Email Notifications.', 'jetpack' ),
2475                'type'              => 'boolean',
2476                'default'           => 0,
2477                'validate_callback' => __CLASS__ . '::validate_boolean',
2478                'jp_group'          => 'monitor',
2479            ),
2480
2481            // Post by Email.
2482            'post_by_email_address'                     => array(
2483                'description'       => esc_html__( 'Email Address', 'jetpack' ),
2484                'type'              => 'string',
2485                'default'           => 'noop',
2486                'enum'              => array(
2487                    'noop',
2488                    'create',
2489                    'regenerate',
2490                    'delete',
2491                ),
2492                'enum_labels'       => array(
2493                    'noop'       => '',
2494                    'create'     => esc_html__( 'Create Post by Email address', 'jetpack' ),
2495                    'regenerate' => esc_html__( 'Regenerate Post by Email address', 'jetpack' ),
2496                    'delete'     => esc_html__( 'Delete Post by Email address', 'jetpack' ),
2497                ),
2498                'validate_callback' => __CLASS__ . '::validate_list_item',
2499                'jp_group'          => 'post-by-email',
2500            ),
2501
2502            // Protect.
2503            'jetpack_protect_key'                       => array(
2504                'description'       => esc_html__( 'Protect API key', 'jetpack' ),
2505                'type'              => 'string',
2506                'default'           => '',
2507                'validate_callback' => __CLASS__ . '::validate_alphanum',
2508                'jp_group'          => 'protect',
2509            ),
2510            'jetpack_protect_global_whitelist'          => array(
2511                'description'       => esc_html__( 'Protect global IP allow list', 'jetpack' ),
2512                'type'              => 'string',
2513                'default'           => '',
2514                'validate_callback' => __CLASS__ . '::validate_string',
2515                'sanitize_callback' => 'esc_textarea',
2516                'jp_group'          => 'protect',
2517            ),
2518
2519            // Sharing.
2520            'sharing_services'                          => array(
2521                'description'       => esc_html__( 'Enabled Services and those hidden behind a button', 'jetpack' ),
2522                'type'              => 'object',
2523                'default'           => array(
2524                    'visible' => array( 'facebook', 'x' ),
2525                    'hidden'  => array(),
2526                ),
2527                'validate_callback' => __CLASS__ . '::validate_services',
2528                'jp_group'          => 'sharedaddy',
2529            ),
2530            'button_style'                              => array(
2531                'description'       => esc_html__( 'Button Style', 'jetpack' ),
2532                'type'              => 'string',
2533                'default'           => 'icon',
2534                'enum'              => array(
2535                    'icon-text',
2536                    'icon',
2537                    'text',
2538                    'official',
2539                ),
2540                'enum_labels'       => array(
2541                    'icon-text' => esc_html__( 'Icon + text', 'jetpack' ),
2542                    'icon'      => esc_html__( 'Icon only', 'jetpack' ),
2543                    'text'      => esc_html__( 'Text only', 'jetpack' ),
2544                    'official'  => esc_html__( 'Official buttons', 'jetpack' ),
2545                ),
2546                'validate_callback' => __CLASS__ . '::validate_list_item',
2547                'jp_group'          => 'sharedaddy',
2548            ),
2549            'sharing_label'                             => array(
2550                'description'       => esc_html__( 'Sharing Label', 'jetpack' ),
2551                'type'              => 'string',
2552                'default'           => '',
2553                'validate_callback' => __CLASS__ . '::validate_string',
2554                'sanitize_callback' => 'esc_html',
2555                'jp_group'          => 'sharedaddy',
2556            ),
2557            'show'                                      => array(
2558                'description'       => esc_html__( 'Views where buttons are shown', 'jetpack' ),
2559                'type'              => 'array',
2560                'items'             => array(
2561                    'type' => 'string',
2562                ),
2563                'default'           => array( 'post' ),
2564                'validate_callback' => __CLASS__ . '::validate_sharing_show',
2565                'jp_group'          => 'sharedaddy',
2566            ),
2567            'jetpack-twitter-cards-site-tag'            => array(
2568                'description'       => esc_html__( "The Twitter username of the owner of this site's domain.", 'jetpack' ),
2569                'type'              => 'string',
2570                'default'           => '',
2571                'validate_callback' => __CLASS__ . '::validate_twitter_username',
2572                'sanitize_callback' => 'esc_html',
2573                'jp_group'          => 'sharedaddy',
2574            ),
2575            'sharedaddy_disable_resources'              => array(
2576                'description'       => esc_html__( 'Disable CSS and JS', 'jetpack' ),
2577                'type'              => 'boolean',
2578                'default'           => 0,
2579                'validate_callback' => __CLASS__ . '::validate_boolean',
2580                'jp_group'          => 'sharedaddy',
2581            ),
2582            'custom'                                    => array(
2583                'description'       => esc_html__( 'Custom sharing services added by user.', 'jetpack' ),
2584                'type'              => 'object',
2585                'default'           => array(
2586                    'sharing_name' => '',
2587                    'sharing_url'  => '',
2588                    'sharing_icon' => '',
2589                ),
2590                'validate_callback' => __CLASS__ . '::validate_custom_service',
2591                'jp_group'          => 'sharedaddy',
2592            ),
2593            // Not an option, but an action that can be performed on the list of custom services passing the service ID.
2594            'sharing_delete_service'                    => array(
2595                'description'       => esc_html__( 'Delete custom sharing service.', 'jetpack' ),
2596                'type'              => 'string',
2597                'default'           => '',
2598                'validate_callback' => __CLASS__ . '::validate_custom_service_id',
2599                'jp_group'          => 'sharedaddy',
2600            ),
2601
2602            // SSO.
2603            'jetpack_sso_require_two_step'              => array(
2604                'description'       => esc_html__( 'Require Two-Step Authentication', 'jetpack' ),
2605                'type'              => 'boolean',
2606                'default'           => SSO\Helpers::is_require_two_step_checkbox_disabled(),
2607                'validate_callback' => __CLASS__ . '::validate_boolean',
2608                'jp_group'          => 'sso',
2609            ),
2610            'jetpack_sso_match_by_email'                => array(
2611                'description'       => esc_html__( 'Match by Email', 'jetpack' ),
2612                'type'              => 'boolean',
2613                'default'           => 1,
2614                'validate_callback' => __CLASS__ . '::validate_boolean',
2615                'jp_group'          => 'sso',
2616            ),
2617
2618            // Subscriptions.
2619            'stb_enabled'                               => array(
2620                'description'       => esc_html__( "Show a <em>'follow blog'</em> option in the comment form", 'jetpack' ),
2621                'type'              => 'boolean',
2622                'default'           => 1,
2623                'validate_callback' => __CLASS__ . '::validate_boolean',
2624                'jp_group'          => 'subscriptions',
2625            ),
2626            'stc_enabled'                               => array(
2627                'description'       => esc_html__( "Show a <em>'follow comments'</em> option in the comment form", 'jetpack' ),
2628                'type'              => 'boolean',
2629                'default'           => 1,
2630                'validate_callback' => __CLASS__ . '::validate_boolean',
2631                'jp_group'          => 'subscriptions',
2632            ),
2633            'wpcom_newsletter_categories'               => array(
2634                'description'       => esc_html__( 'Array of post category ids that are marked as newsletter categories', 'jetpack' ),
2635                'type'              => 'array',
2636                'default'           => array(),
2637                'validate_callback' => __CLASS__ . '::validate_array',
2638                'jp_group'          => 'subscriptions',
2639            ),
2640            'wpcom_newsletter_categories_enabled'       => array(
2641                'description'       => esc_html__( 'Whether the newsletter categories are enabled or not', 'jetpack' ),
2642                'type'              => 'boolean',
2643                'default'           => 0,
2644                'validate_callback' => __CLASS__ . '::validate_boolean',
2645                'jp_group'          => 'subscriptions',
2646            ),
2647            'wpcom_featured_image_in_email'             => array(
2648                'description'       => esc_html__( 'Whether to include the featured image in the email or not', 'jetpack' ),
2649                'type'              => 'boolean',
2650                'default'           => 0,
2651                'validate_callback' => __CLASS__ . '::validate_boolean',
2652                'jp_group'          => 'subscriptions',
2653            ),
2654            'jetpack_gravatar_in_email'                 => array(
2655                'description'       => esc_html__( 'Whether to show author avatar in the email byline', 'jetpack' ),
2656                'type'              => 'boolean',
2657                'default'           => 1,
2658                'validate_callback' => __CLASS__ . '::validate_boolean',
2659                'jp_group'          => 'subscriptions',
2660            ),
2661            'jetpack_author_in_email'                   => array(
2662                'description'       => esc_html__( 'Whether to show author display name in the email byline', 'jetpack' ),
2663                'type'              => 'boolean',
2664                'default'           => 1,
2665                'validate_callback' => __CLASS__ . '::validate_boolean',
2666                'jp_group'          => 'subscriptions',
2667            ),
2668            'jetpack_post_date_in_email'                => array(
2669                'description'       => esc_html__( 'Whether to show date in the email byline', 'jetpack' ),
2670                'type'              => 'boolean',
2671                'default'           => 1,
2672                'validate_callback' => __CLASS__ . '::validate_boolean',
2673                'jp_group'          => 'subscriptions',
2674            ),
2675            'wpcom_subscription_emails_use_excerpt'     => array(
2676                'description'       => esc_html__( 'Whether to use the excerpt in the email or not', 'jetpack' ),
2677                'type'              => 'boolean',
2678                'default'           => 0,
2679                'validate_callback' => __CLASS__ . '::validate_boolean',
2680                'jp_group'          => 'subscriptions',
2681            ),
2682            'jetpack_subscriptions_reply_to'            => array(
2683                'description'       => esc_html__( 'Reply to email behaviour for newsletters emails', 'jetpack' ),
2684                'type'              => 'string',
2685                'default'           => Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to,
2686                'validate_callback' => __CLASS__ . '::validate_subscriptions_reply_to',
2687                'jp_group'          => 'subscriptions',
2688            ),
2689            'jetpack_subscriptions_from_name'           => array(
2690                'description'       => esc_html__( 'From name for newsletters emails', 'jetpack' ),
2691                'type'              => 'string',
2692                'default'           => '',
2693                'validate_callback' => __CLASS__ . '::validate_subscriptions_reply_to_name',
2694                'jp_group'          => 'subscriptions',
2695            ),
2696            'sm_enabled'                                => array(
2697                'description'       => esc_html__( 'Show popup Subscribe modal to readers.', 'jetpack' ),
2698                'type'              => 'boolean',
2699                'default'           => 0,
2700                'validate_callback' => __CLASS__ . '::validate_boolean',
2701                'jp_group'          => 'subscriptions',
2702            ),
2703            'jetpack_subscribe_overlay_enabled'         => array(
2704                'description'       => esc_html__( 'Show subscribe overlay on homepage.', 'jetpack' ),
2705                'type'              => 'boolean',
2706                'default'           => 0,
2707                'validate_callback' => __CLASS__ . '::validate_boolean',
2708                'jp_group'          => 'subscriptions',
2709            ),
2710            'jetpack_subscribe_floating_button_enabled' => array(
2711                'description'       => esc_html__( 'Show a floating subscribe button.', 'jetpack' ),
2712                'type'              => 'boolean',
2713                'default'           => 0,
2714                'validate_callback' => __CLASS__ . '::validate_boolean',
2715                'jp_group'          => 'subscriptions',
2716            ),
2717            'jetpack_subscriptions_subscribe_post_end_enabled' => array(
2718                'description'       => esc_html__( 'Add Subscribe block at the end of each post.', 'jetpack' ),
2719                'type'              => 'boolean',
2720                'default'           => 0,
2721                'validate_callback' => __CLASS__ . '::validate_boolean',
2722                'jp_group'          => 'subscriptions',
2723            ),
2724            'jetpack_subscriptions_login_navigation_enabled' => array(
2725                'description'       => esc_html__( 'Add Subscriber Login block to the navigation.', 'jetpack' ),
2726                'type'              => 'boolean',
2727                'default'           => 0,
2728                'validate_callback' => __CLASS__ . '::validate_boolean',
2729                'jp_group'          => 'subscriptions',
2730            ),
2731            'jetpack_subscriptions_subscribe_navigation_enabled' => array(
2732                'description'       => esc_html__( 'Add Subscribe block to the navigation.', 'jetpack' ),
2733                'type'              => 'boolean',
2734                'default'           => 0,
2735                'validate_callback' => __CLASS__ . '::validate_boolean',
2736                'jp_group'          => 'subscriptions',
2737            ),
2738            'social_notifications_subscribe'            => array(
2739                'description'       => esc_html__( 'Send email notification when someone subscribes to my blog', 'jetpack' ),
2740                'type'              => 'boolean',
2741                'default'           => 0,
2742                'validate_callback' => __CLASS__ . '::validate_boolean',
2743                'jp_group'          => 'subscriptions',
2744            ),
2745            'subscription_options'                      => array(
2746                'description'       => esc_html__( 'Three options used in subscription email templates: \'invitation\', \'welcome\' and \'comment_follow\'.', 'jetpack' ),
2747                'type'              => 'object',
2748                'default'           => array(
2749                    'invitation'     => '',
2750                    'welcome'        => '',
2751                    'comment_follow' => '',
2752                ),
2753                'validate_callback' => __CLASS__ . '::validate_subscription_options',
2754                'jp_group'          => 'subscriptions',
2755            ),
2756
2757            // Related Posts.
2758            'show_headline'                             => array(
2759                'description'       => esc_html__( 'Highlight related content with a heading', 'jetpack' ),
2760                'type'              => 'boolean',
2761                'default'           => 1,
2762                'validate_callback' => __CLASS__ . '::validate_boolean',
2763                'jp_group'          => 'related-posts',
2764            ),
2765            'show_thumbnails'                           => array(
2766                'description'       => esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
2767                'type'              => 'boolean',
2768                'default'           => 0,
2769                'validate_callback' => __CLASS__ . '::validate_boolean',
2770                'jp_group'          => 'related-posts',
2771            ),
2772
2773            // Search.
2774            'instant_search_enabled'                    => array(
2775                'description'       => esc_html__( 'Enable Instant Search', 'jetpack' ),
2776                'type'              => 'boolean',
2777                'default'           => 0,
2778                'validate_callback' => __CLASS__ . '::validate_boolean',
2779                'jp_group'          => 'search',
2780            ),
2781
2782            'has_jetpack_search_product'                => array(
2783                'description'       => esc_html__( 'Has an active Jetpack Search product purchase', 'jetpack' ),
2784                'type'              => 'boolean',
2785                'default'           => 0,
2786                'validate_callback' => __CLASS__ . '::validate_boolean',
2787                'jp_group'          => 'settings',
2788            ),
2789
2790            'search_auto_config'                        => array(
2791                'description'       => esc_html__( 'Trigger an auto config of instant search', 'jetpack' ),
2792                'type'              => 'boolean',
2793                'default'           => 0,
2794                'validate_callback' => __CLASS__ . '::validate_boolean',
2795                'jp_group'          => 'search',
2796            ),
2797
2798            // Verification Tools.
2799            'google'                                    => array(
2800                'description'       => esc_html__( 'Google Search Console', 'jetpack' ),
2801                'type'              => 'string',
2802                'default'           => '',
2803                'validate_callback' => __CLASS__ . '::validate_verification_service',
2804                'jp_group'          => 'verification-tools',
2805            ),
2806            'bing'                                      => array(
2807                'description'       => esc_html__( 'Bing Webmaster Center', 'jetpack' ),
2808                'type'              => 'string',
2809                'default'           => '',
2810                'validate_callback' => __CLASS__ . '::validate_verification_service',
2811                'jp_group'          => 'verification-tools',
2812            ),
2813            'pinterest'                                 => array(
2814                'description'       => esc_html__( 'Pinterest Site Verification', 'jetpack' ),
2815                'type'              => 'string',
2816                'default'           => '',
2817                'validate_callback' => __CLASS__ . '::validate_verification_service',
2818                'jp_group'          => 'verification-tools',
2819            ),
2820            'yandex'                                    => array(
2821                'description'       => esc_html__( 'Yandex Site Verification', 'jetpack' ),
2822                'type'              => 'string',
2823                'default'           => '',
2824                'validate_callback' => __CLASS__ . '::validate_verification_service',
2825                'jp_group'          => 'verification-tools',
2826            ),
2827            'facebook'                                  => array(
2828                'description'       => esc_html__( 'Facebook Domain Verification', 'jetpack' ),
2829                'type'              => 'string',
2830                'default'           => '',
2831                'validate_callback' => __CLASS__ . '::validate_verification_service',
2832                'jp_group'          => 'verification-tools',
2833            ),
2834
2835            // WordAds.
2836            'enable_header_ad'                          => array(
2837                'description'       => esc_html__( 'Display an ad unit at the top of each page.', 'jetpack' ),
2838                'type'              => 'boolean',
2839                'default'           => 1,
2840                'validate_callback' => __CLASS__ . '::validate_boolean',
2841                'jp_group'          => 'wordads',
2842            ),
2843            'wordads_approved'                          => array(
2844                'description'       => esc_html__( 'Is site approved for WordAds?', 'jetpack' ),
2845                'type'              => 'boolean',
2846                'default'           => 0,
2847                'validate_callback' => __CLASS__ . '::validate_boolean',
2848                'jp_group'          => 'wordads',
2849            ),
2850            'wordads_second_belowpost'                  => array(
2851                'description'       => esc_html__( 'Display second ad below post?', 'jetpack' ),
2852                'type'              => 'boolean',
2853                'default'           => 1,
2854                'validate_callback' => __CLASS__ . '::validate_boolean',
2855                'jp_group'          => 'wordads',
2856            ),
2857            'wordads_inline_enabled'                    => array(
2858                'description'       => esc_html__( 'Display inline ad within post content?', 'jetpack' ),
2859                'type'              => 'boolean',
2860                'default'           => 1,
2861                'validate_callback' => __CLASS__ . '::validate_boolean',
2862                'jp_group'          => 'wordads',
2863            ),
2864            'wordads_display_front_page'                => array(
2865                'description'       => esc_html__( 'Display ads on the front page?', 'jetpack' ),
2866                'type'              => 'boolean',
2867                'default'           => 1,
2868                'validate_callback' => __CLASS__ . '::validate_boolean',
2869                'jp_group'          => 'wordads',
2870            ),
2871            'wordads_display_post'                      => array(
2872                'description'       => esc_html__( 'Display ads on posts?', 'jetpack' ),
2873                'type'              => 'boolean',
2874                'default'           => 1,
2875                'validate_callback' => __CLASS__ . '::validate_boolean',
2876                'jp_group'          => 'wordads',
2877            ),
2878            'wordads_display_page'                      => array(
2879                'description'       => esc_html__( 'Display ads on pages?', 'jetpack' ),
2880                'type'              => 'boolean',
2881                'default'           => 1,
2882                'validate_callback' => __CLASS__ . '::validate_boolean',
2883                'jp_group'          => 'wordads',
2884            ),
2885            'wordads_display_archive'                   => array(
2886                'description'       => esc_html__( 'Display ads on archive pages?', 'jetpack' ),
2887                'type'              => 'boolean',
2888                'default'           => 1,
2889                'validate_callback' => __CLASS__ . '::validate_boolean',
2890                'jp_group'          => 'wordads',
2891            ),
2892            'wordads_custom_adstxt_enabled'             => array(
2893                'description'       => esc_html__( 'Custom ads.txt', 'jetpack' ),
2894                'type'              => 'boolean',
2895                'default'           => 0,
2896                'validate_callback' => __CLASS__ . '::validate_boolean',
2897                'jp_group'          => 'wordads',
2898            ),
2899            'wordads_custom_adstxt'                     => array(
2900                'description'       => esc_html__( 'Custom ads.txt entries', 'jetpack' ),
2901                'type'              => 'string',
2902                'default'           => '',
2903                'validate_callback' => __CLASS__ . '::validate_string',
2904                'sanitize_callback' => 'sanitize_textarea_field',
2905                'jp_group'          => 'wordads',
2906            ),
2907            'wordads_ccpa_enabled'                      => array(
2908                'description'       => esc_html__( 'Enable support for California Consumer Privacy Act', 'jetpack' ),
2909                'type'              => 'boolean',
2910                'default'           => 0,
2911                'validate_callback' => __CLASS__ . '::validate_boolean',
2912                'jp_group'          => 'wordads',
2913            ),
2914            'wordads_ccpa_privacy_policy_url'           => array(
2915                'description'       => esc_html__( 'Privacy Policy URL', 'jetpack' ),
2916                'type'              => 'string',
2917                'default'           => '',
2918                'validate_callback' => __CLASS__ . '::validate_string',
2919                'sanitize_callback' => 'sanitize_text_field',
2920                'jp_group'          => 'wordads',
2921            ),
2922            'wordads_cmp_enabled'                       => array(
2923                'description'       => esc_html__( 'Enable GDPR Consent Management Banner for WordAds', 'jetpack' ),
2924                'type'              => 'boolean',
2925                'default'           => 0,
2926                'validate_callback' => __CLASS__ . '::validate_boolean',
2927                'jp_group'          => 'wordads',
2928            ),
2929
2930            // Google Analytics.
2931            'google_analytics_tracking_id'              => array(
2932                'description'       => esc_html__( 'Google Analytics', 'jetpack' ),
2933                'type'              => 'string',
2934                'default'           => '',
2935                'validate_callback' => __CLASS__ . '::validate_alphanum',
2936                'jp_group'          => 'google-analytics',
2937            ),
2938            'jetpack_wga'                               => array(
2939                'description' => esc_html__( 'Google Analytics', 'jetpack' ),
2940                'type'        => 'object',
2941                'jp_group'    => 'settings',
2942            ),
2943
2944            // Stats.
2945            'admin_bar'                                 => array(
2946                'description'       => esc_html__( 'Include a small chart in your admin bar with a 48-hour traffic snapshot.', 'jetpack' ),
2947                'type'              => 'boolean',
2948                'default'           => 1,
2949                'validate_callback' => __CLASS__ . '::validate_boolean',
2950                'jp_group'          => 'stats',
2951            ),
2952            'enable_odyssey_stats'                      => array(
2953                'description'       => esc_html__( 'Preview the new Jetpack Stats experience (Experimental).', 'jetpack' ),
2954                'type'              => 'boolean',
2955                'default'           => 1,
2956                'validate_callback' => __CLASS__ . '::validate_boolean',
2957                'jp_group'          => 'stats',
2958            ),
2959            'roles'                                     => array(
2960                'description'       => esc_html__( 'Select the roles that will be able to view stats reports.', 'jetpack' ),
2961                'type'              => 'array',
2962                'items'             => array(
2963                    'type' => 'string',
2964                ),
2965                'default'           => array( 'administrator' ),
2966                'validate_callback' => __CLASS__ . '::validate_stats_roles',
2967                'sanitize_callback' => __CLASS__ . '::sanitize_stats_allowed_roles',
2968                'jp_group'          => 'stats',
2969            ),
2970            'count_roles'                               => array(
2971                'description'       => esc_html__( 'Count the page views of registered users who are logged in.', 'jetpack' ),
2972                'type'              => 'array',
2973                'items'             => array(
2974                    'type' => 'string',
2975                ),
2976                'default'           => array( 'administrator' ),
2977                'validate_callback' => __CLASS__ . '::validate_stats_roles',
2978                'jp_group'          => 'stats',
2979            ),
2980            'blog_id'                                   => array(
2981                'description'       => esc_html__( 'Blog ID.', 'jetpack' ),
2982                'type'              => 'boolean',
2983                'default'           => 0,
2984                'validate_callback' => __CLASS__ . '::validate_boolean',
2985                'jp_group'          => 'stats',
2986            ),
2987            'do_not_track'                              => array(
2988                'description'       => esc_html__( 'Do not track.', 'jetpack' ),
2989                'type'              => 'boolean',
2990                'default'           => 1,
2991                'validate_callback' => __CLASS__ . '::validate_boolean',
2992                'jp_group'          => 'stats',
2993            ),
2994            'version'                                   => array(
2995                'description'       => esc_html__( 'Version.', 'jetpack' ),
2996                'type'              => 'integer',
2997                'default'           => 9,
2998                'validate_callback' => __CLASS__ . '::validate_posint',
2999                'jp_group'          => 'stats',
3000            ),
3001            'collapse_nudges'                           => array(
3002                'description'       => esc_html__( 'Collapse upgrade nudges', 'jetpack' ),
3003                'type'              => 'boolean',
3004                'default'           => 0,
3005                'validate_callback' => __CLASS__ . '::validate_boolean',
3006                'jp_group'          => 'stats',
3007            ),
3008
3009            // Whether to share stats views with WordPress.com Reader.
3010            'wpcom_reader_views_enabled'                => array(
3011                'description'       => esc_html__( 'Show post views in the WordPress.com Reader.', 'jetpack' ),
3012                'type'              => 'boolean',
3013                'default'           => 1,
3014                'validate_callback' => __CLASS__ . '::validate_boolean',
3015                'jp_group'          => 'settings',
3016            ),
3017
3018            // Akismet - Not a module, but a plugin. The options can be passed and handled differently.
3019            'akismet_show_user_comments_approved'       => array(
3020                'description'       => '',
3021                'type'              => 'boolean',
3022                'default'           => 0,
3023                'validate_callback' => __CLASS__ . '::validate_boolean',
3024                'jp_group'          => 'settings',
3025            ),
3026
3027            'wordpress_api_key'                         => array(
3028                'description'       => '',
3029                'type'              => 'string',
3030                'default'           => '',
3031                'validate_callback' => __CLASS__ . '::validate_alphanum',
3032                'jp_group'          => 'settings',
3033            ),
3034
3035            // Empty stats card dismiss.
3036            'dismiss_empty_stats_card'                  => array(
3037                'description'       => '',
3038                'type'              => 'boolean',
3039                'default'           => 0,
3040                'validate_callback' => __CLASS__ . '::validate_boolean',
3041                'jp_group'          => 'settings',
3042            ),
3043
3044            // Backup Getting Started card on dashboard.
3045            'dismiss_dash_backup_getting_started'       => array(
3046                'description'       => '',
3047                'type'              => 'boolean',
3048                'default'           => 0,
3049                'validate_callback' => __CLASS__ . '::validate_boolean',
3050                'jp_group'          => 'settings',
3051            ),
3052
3053            // Agencies Learn More card on dashboard.
3054            'dismiss_dash_agencies_learn_more'          => array(
3055                'description'       => '',
3056                'type'              => 'boolean',
3057                'default'           => 0,
3058                'validate_callback' => __CLASS__ . '::validate_boolean',
3059                'jp_group'          => 'settings',
3060            ),
3061
3062            'lang_id'                                   => array(
3063                'description' => esc_html__( 'Primary language for the site.', 'jetpack' ),
3064                'type'        => 'string',
3065                'default'     => 'en_US',
3066                'jp_group'    => 'settings',
3067            ),
3068
3069            // SEO Tools.
3070            'advanced_seo_front_page_description'       => array(
3071                'description'       => esc_html__( 'Front page meta description.', 'jetpack' ),
3072                'type'              => 'string',
3073                'default'           => '',
3074                'sanitize_callback' => 'Jetpack_SEO_Utils::sanitize_front_page_meta_description',
3075                'jp_group'          => 'seo-tools',
3076            ),
3077
3078            'advanced_seo_title_formats'                => array(
3079                'description'       => esc_html__( 'SEO page title structures.', 'jetpack' ),
3080                'type'              => 'object',
3081                'default'           => array(
3082                    'archives'   => array(),
3083                    'front_page' => array(),
3084                    'groups'     => array(),
3085                    'pages'      => array(),
3086                    'posts'      => array(),
3087                ),
3088                'jp_group'          => 'seo-tools',
3089                'validate_callback' => 'Jetpack_SEO_Titles::are_valid_title_formats',
3090                'sanitize_callback' => 'Jetpack_SEO_Titles::sanitize_title_formats',
3091            ),
3092
3093            // VideoPress.
3094            'videopress_private_enabled_for_site'       => array(
3095                'description'       => esc_html__( 'Video Privacy: Restrict views to members of this site', 'jetpack' ),
3096                'type'              => 'boolean',
3097                'default'           => 0,
3098                'validate_callback' => __CLASS__ . '::validate_boolean',
3099                'jp_group'          => 'videopress',
3100            ),
3101        );
3102
3103        // SEO Tools - SEO Enhancer.
3104        // TODO: move this to the main options array? The filter was there while developing the feature.
3105        // It might come in handy to hold its availability behind the filter since it still depends on AI to be available.
3106        if ( apply_filters( 'ai_seo_enhancer_enabled', true ) ) {
3107            $options['ai_seo_enhancer_enabled'] = array(
3108                'description'       => esc_html__( 'Automatically generate SEO title, SEO description, and image alt text for new posts.', 'jetpack' ),
3109                'type'              => 'boolean',
3110                'default'           => 0,
3111                'validate_callback' => __CLASS__ . '::validate_boolean',
3112                'jp_group'          => 'seo-tools',
3113            );
3114        }
3115
3116        // Add modules to list so they can be toggled.
3117        $modules = Jetpack::get_available_modules();
3118        if ( is_array( $modules ) && ! empty( $modules ) ) {
3119            $module_args = array(
3120                'description'       => '',
3121                'type'              => 'boolean',
3122                'default'           => 0,
3123                'validate_callback' => __CLASS__ . '::validate_boolean',
3124                'jp_group'          => 'modules',
3125            );
3126            foreach ( $modules as $module ) {
3127                $options[ $module ] = $module_args;
3128            }
3129        }
3130
3131        if ( is_array( $selector ) ) {
3132
3133            // Return only those options whose keys match $selector keys.
3134            return array_intersect_key( $options, $selector );
3135        }
3136
3137        if ( 'any' === $selector ) {
3138
3139            // Toggle module or update any module option or any general setting.
3140            return $options;
3141        }
3142
3143        // We're updating the options for a single module.
3144        if ( empty( $selector ) ) {
3145            $selector = self::get_module_requested();
3146        }
3147        $selected = array();
3148        foreach ( $options as $option => $attributes ) {
3149
3150            // Not adding an isset( $attributes['jp_group'] ) because if it's not set, it must be fixed, otherwise options will fail.
3151            if ( $selector === $attributes['jp_group'] ) {
3152                $selected[ $option ] = $attributes;
3153            }
3154        }
3155        return $selected;
3156    }
3157
3158    /**
3159     * Validates that the parameters are proper values that can be set during Jetpack onboarding.
3160     *
3161     * @since 5.4.0
3162     *
3163     * @deprecated since 13.9
3164     *
3165     * @param array           $onboarding_data Values to check.
3166     * @param WP_REST_Request $request         The request sent to the WP REST API.
3167     * @param string          $param           Name of the parameter passed to endpoint holding $value.
3168     *
3169     * @return bool|WP_Error
3170     */
3171    public static function validate_onboarding( $onboarding_data, $request, $param ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
3172        _deprecated_function( __METHOD__, '13.9' );
3173        return true;
3174    }
3175
3176    /**
3177     * Validates that the parameter is either a pure boolean or a numeric string that can be mapped to a boolean.
3178     *
3179     * @since 4.3.0
3180     *
3181     * @param string|bool     $value Value to check.
3182     * @param WP_REST_Request $request The request sent to the WP REST API.
3183     * @param string          $param Name of the parameter passed to endpoint holding $value.
3184     *
3185     * @return bool|WP_Error
3186     */
3187    public static function validate_boolean( $value, $request, $param ) {
3188        // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- Other code depends on loose comparison here.
3189        if ( ! is_bool( $value ) && ! ( ctype_digit( (string) $value ) && in_array( $value, array( 0, 1 ) ) ) ) {
3190            return new WP_Error(
3191                'invalid_param',
3192                sprintf(
3193                    /* Translators: Placeholder is a parameter name. */
3194                    esc_html__( '%s must be true, false, 0 or 1.', 'jetpack' ),
3195                    $param
3196                )
3197            );
3198        }
3199        return true;
3200    }
3201
3202    /**
3203     * Validates that the parameter is a positive integer.
3204     *
3205     * @since 4.3.0
3206     *
3207     * @param int             $value Value to check.
3208     * @param WP_REST_Request $request The request sent to the WP REST API.
3209     * @param string          $param Name of the parameter passed to endpoint holding $value.
3210     *
3211     * @return bool|WP_Error
3212     */
3213    public static function validate_posint( $value, $request, $param ) {
3214        if ( ! is_numeric( $value ) || $value <= 0 ) {
3215            return new WP_Error(
3216                'invalid_param',
3217                sprintf(
3218                    /* Translators: Placeholder is a parameter name. */
3219                    esc_html__( '%s must be a positive integer.', 'jetpack' ),
3220                    $param
3221                )
3222            );
3223        }
3224        return true;
3225    }
3226
3227    /**
3228     * Validates that the parameter is a non-negative integer (includes 0).
3229     *
3230     * @since 10.4.0
3231     *
3232     * @param int             $value Value to check.
3233     * @param WP_REST_Request $request The request sent to the WP REST API.
3234     * @param string          $param Name of the parameter passed to endpoint holding $value.
3235     *
3236     * @return bool|WP_Error
3237     */
3238    public static function validate_non_neg_int( $value, $request, $param ) {
3239        if ( ! is_numeric( $value ) || $value < 0 ) {
3240            return new WP_Error(
3241                'invalid_param',
3242                /* translators: %s: The literal parameter name. Should not be translated. */
3243                sprintf( esc_html__( '%s must be a non-negative integer.', 'jetpack' ), $param )
3244            );
3245        }
3246        return true;
3247    }
3248
3249    /**
3250     * Validates that the parameter belongs to a list of admitted values.
3251     *
3252     * @since 4.3.0
3253     *
3254     * @param string          $value Value to check.
3255     * @param WP_REST_Request $request The request sent to the WP REST API.
3256     * @param string          $param Name of the parameter passed to endpoint holding $value.
3257     *
3258     * @return bool|WP_Error
3259     */
3260    public static function validate_list_item( $value, $request, $param ) {
3261        $attributes = $request->get_attributes();
3262        if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
3263            return new WP_Error(
3264                'invalid_param',
3265                sprintf(
3266                    /* Translators: Placeholder is a parameter name. */
3267                    esc_html__( '%s not recognized', 'jetpack' ),
3268                    $param
3269                )
3270            );
3271        }
3272        $args = $attributes['args'][ $param ];
3273        if ( ! empty( $args['enum'] ) ) {
3274            // If it's an associative array, use the keys to check that the value is among those admitted.
3275            $enum = ( count( array_filter( array_keys( $args['enum'] ), 'is_string' ) ) > 0 )
3276                ? array_keys( $args['enum'] )
3277                : $args['enum'];
3278            $enum = array_map( 'strval', $enum );
3279            if ( ! in_array( $value, $enum, true ) ) {
3280                return new WP_Error(
3281                    'invalid_param_value',
3282                    sprintf(
3283                    /* Translators: first variable is the parameter passed to endpoint that holds the list item, the second is a list of admitted values. */
3284                        esc_html__( '%1$s must be one of %2$s', 'jetpack' ),
3285                        $param,
3286                        implode( ', ', $enum )
3287                    )
3288                );
3289            }
3290        }
3291        return true;
3292    }
3293
3294    /**
3295     * Validates that the parameter belongs to a list of admitted values.
3296     *
3297     * @since 4.3.0
3298     *
3299     * @param string          $value Value to check.
3300     * @param WP_REST_Request $request The request sent to the WP REST API.
3301     * @param string          $param Name of the parameter passed to endpoint holding $value.
3302     *
3303     * @return bool|WP_Error
3304     */
3305    public static function validate_module_list( $value, $request, $param ) {
3306        if ( ! is_array( $value ) ) {
3307            return new WP_Error(
3308                'invalid_param_value',
3309                sprintf(
3310                    /* Translators: Placeholder is a parameter name. */
3311                    esc_html__( '%s must be an array', 'jetpack' ),
3312                    $param
3313                )
3314            );
3315        }
3316
3317        $modules = Jetpack::get_available_modules();
3318
3319        if ( count( array_intersect( $value, $modules ) ) !== count( $value ) ) {
3320            return new WP_Error(
3321                'invalid_param_value',
3322                sprintf(
3323                    /* Translators: Placeholder is a parameter name. */
3324                    esc_html__( '%s must be a list of valid modules', 'jetpack' ),
3325                    $param
3326                )
3327            );
3328        }
3329
3330        return true;
3331    }
3332
3333    /**
3334     * Validates that the parameter is an alphanumeric or empty string (to be able to clear the field).
3335     *
3336     * @since 4.3.0
3337     *
3338     * @param string          $value Value to check.
3339     * @param WP_REST_Request $request The request sent to the WP REST API.
3340     * @param string          $param Name of the parameter passed to endpoint holding $value.
3341     *
3342     * @return bool|WP_Error
3343     */
3344    public static function validate_alphanum( $value, $request, $param ) {
3345        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^[a-z0-9]+$/i', $value ) ) ) {
3346            return new WP_Error(
3347                'invalid_param',
3348                sprintf(
3349                    /* Translators: Placeholder is a parameter name. */
3350                    esc_html__( '%s must be an alphanumeric string.', 'jetpack' ),
3351                    $param
3352                )
3353            );
3354        }
3355        return true;
3356    }
3357
3358    /**
3359     * Validates that the parameter is a tag or id for a verification service, or an empty string (to be able to clear the field).
3360     *
3361     * @since 4.6.0
3362     *
3363     * @param string          $value   Value to check.
3364     * @param WP_REST_Request $request The request sent to the WP REST API.
3365     * @param string          $param   Name of the parameter passed to endpoint holding $value.
3366     *
3367     * @return bool|WP_Error
3368     */
3369    public static function validate_verification_service( $value, $request, $param ) {
3370        if ( ! empty( $value ) && ! ( is_string( $value ) && ( preg_match( '/^[a-z0-9_-]+$/i', $value ) || jetpack_verification_get_code( $value ) !== false ) ) ) {
3371            return new WP_Error(
3372                'invalid_param',
3373                sprintf(
3374                    /* Translators: Placeholder is a verification string used to verify a service like Google Webmaster Console. */
3375                    esc_html__( '%s must be an alphanumeric string or a verification tag.', 'jetpack' ),
3376                    $param
3377                )
3378            );
3379        }
3380        return true;
3381    }
3382
3383    /**
3384     * Validates that the parameter is among the roles allowed for Stats.
3385     *
3386     * @since 4.3.0
3387     *
3388     * @param string|bool     $value Value to check.
3389     * @param WP_REST_Request $request The request sent to the WP REST API.
3390     * @param string          $param Name of the parameter passed to endpoint holding $value.
3391     *
3392     * @return bool|WP_Error
3393     */
3394    public static function validate_stats_roles( $value, $request, $param ) {
3395        if ( ! function_exists( 'get_editable_roles' ) ) {
3396            require_once ABSPATH . 'wp-admin/includes/user.php';
3397        }
3398        $editable_roles = array_keys( get_editable_roles() );
3399        if ( ! empty( $value ) && ! array_intersect( $editable_roles, $value ) ) {
3400            return new WP_Error(
3401                'invalid_param',
3402                sprintf(
3403                    /* 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. */
3404                    esc_html__( '%1$s must be %2$s.', 'jetpack' ),
3405                    $param,
3406                    implode( ', ', $editable_roles )
3407                )
3408            );
3409        }
3410        return true;
3411    }
3412
3413    /**
3414     * Validates that the parameter is among the views where the Sharing can be displayed.
3415     *
3416     * @since 4.3.0
3417     *
3418     * @param string|bool     $value Value to check.
3419     * @param WP_REST_Request $request The request sent to the WP REST API.
3420     * @param string          $param Name of the parameter passed to endpoint holding $value.
3421     *
3422     * @return bool|WP_Error
3423     */
3424    public static function validate_sharing_show( $value, $request, $param ) {
3425        $views = array( 'index', 'post', 'page', 'attachment', 'jetpack-portfolio' );
3426        if ( ! is_array( $value ) ) {
3427            return new WP_Error(
3428                'invalid_param',
3429                sprintf(
3430                    /* Translators: Placeholder is a parameter name. */
3431                    esc_html__( '%s must be an array of post types.', 'jetpack' ),
3432                    $param
3433                )
3434            );
3435        }
3436        if ( ! array_intersect( $views, $value ) ) {
3437            return new WP_Error(
3438                'invalid_param',
3439                sprintf(
3440                    /* 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 */
3441                    esc_html__( '%1$s must be %2$s.', 'jetpack' ),
3442                    $param,
3443                    implode( ', ', $views )
3444                )
3445            );
3446        }
3447        return true;
3448    }
3449
3450    /**
3451     * Validates that the parameter is among the valid reply-to types for subscriptions.
3452     *
3453     * @since 4.3.0
3454     *
3455     * @param string|bool     $value Value to check.
3456     * @param WP_REST_Request $request The request sent to the WP REST API.
3457     * @param string          $param Name of the parameter passed to endpoint holding $value.
3458     *
3459     * @return bool|WP_Error
3460     */
3461    public static function validate_subscriptions_reply_to( $value, $request, $param ) {
3462        require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
3463        if ( ! empty( $value ) && ! Automattic\Jetpack\Modules\Subscriptions\Settings::is_valid_reply_to( $value ) ) {
3464            return new WP_Error(
3465                'invalid_param',
3466                sprintf(
3467                    /* Translators: Placeholder is a parameter name. */
3468                    esc_html__( '%s must be a valid type.', 'jetpack' ),
3469                    $param
3470                )
3471            );
3472        }
3473        return true;
3474    }
3475
3476    /**
3477     * Validates that the parameter is among the valid reply-to types for subscriptions.
3478     *
3479     * @since 4.3.0
3480     *
3481     * @param string|bool     $value Value to check.
3482     * @param WP_REST_Request $request The request sent to the WP REST API.
3483     * @param string          $param Name of the parameter passed to endpoint holding $value.
3484     *
3485     * @return bool|WP_Error
3486     */
3487    public static function validate_subscriptions_reply_to_name( $value, $request, $param ) {
3488        if ( ! empty( $value ) && ! is_string( $value ) ) {
3489            return new WP_Error(
3490                'invalid_param',
3491                sprintf(
3492                    /* Translators: Placeholder is a parameter name. */
3493                    esc_html__( '%s must be a valid type.', 'jetpack' ),
3494                    $param
3495                )
3496            );
3497        }
3498        return true;
3499    }
3500
3501    /**
3502     * Validates that the parameter is among the views where the Sharing can be displayed.
3503     *
3504     * @since 4.3.0
3505     *
3506     * @param string|bool     $value {
3507     *         Value to check received by request.
3508     *
3509     *     @type array $visible List of slug of services to share to that are displayed directly in the page.
3510     *     @type array $hidden  List of slug of services to share to that are concealed in a folding menu.
3511     * }
3512     * @param WP_REST_Request $request The request sent to the WP REST API.
3513     * @param string          $param Name of the parameter passed to endpoint holding $value.
3514     *
3515     * @return bool|WP_Error
3516     */
3517    public static function validate_services( $value, $request, $param ) {
3518        if ( ! is_array( $value ) || ! isset( $value['visible'] ) || ! isset( $value['hidden'] ) ) {
3519            return new WP_Error(
3520                'invalid_param',
3521                sprintf(
3522                    /* Translators: Placeholder is a parameter name. */
3523                    esc_html__( '%s must be an array with visible and hidden items.', 'jetpack' ),
3524                    $param
3525                )
3526            );
3527        }
3528
3529        // Allow to clear everything.
3530        if ( empty( $value['visible'] ) && empty( $value['hidden'] ) ) {
3531            return true;
3532        }
3533
3534        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3535            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3536        }
3537        $sharer   = new Sharing_Service();
3538        $services = array_keys( $sharer->get_all_services() );
3539
3540        if (
3541            ( ! empty( $value['visible'] ) && ! array_intersect( $value['visible'], $services ) )
3542            ||
3543            ( ! empty( $value['hidden'] ) && ! array_intersect( $value['hidden'], $services ) ) ) {
3544            return new WP_Error(
3545                'invalid_param',
3546                sprintf(
3547                    /* Translators: placeholder 1 is a parameter holding the services passed to endpoint, placeholder 2 is a list of all Jetpack Sharing services */
3548                    esc_html__( '%1$s visible and hidden items must be a list of %2$s.', 'jetpack' ),
3549                    $param,
3550                    implode( ', ', $services )
3551                )
3552            );
3553        }
3554        return true;
3555    }
3556
3557    /**
3558     * Validates that the parameter has enough information to build a custom sharing button.
3559     *
3560     * @since 4.3.0
3561     *
3562     * @param string|bool     $value Value to check.
3563     * @param WP_REST_Request $request The request sent to the WP REST API.
3564     * @param string          $param Name of the parameter passed to endpoint holding $value.
3565     *
3566     * @return bool|WP_Error
3567     */
3568    public static function validate_custom_service( $value, $request, $param ) {
3569        if ( ! is_array( $value ) || ! isset( $value['sharing_name'] ) || ! isset( $value['sharing_url'] ) || ! isset( $value['sharing_icon'] ) ) {
3570            return new WP_Error(
3571                'invalid_param',
3572                sprintf(
3573                    /* Translators: Placeholder is a parameter name. */
3574                    esc_html__( '%s must be an array with sharing name, url and icon.', 'jetpack' ),
3575                    $param
3576                )
3577            );
3578        }
3579
3580        // Allow to clear everything.
3581        if ( empty( $value['sharing_name'] ) && empty( $value['sharing_url'] ) && empty( $value['sharing_icon'] ) ) {
3582            return true;
3583        }
3584
3585        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3586            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3587        }
3588
3589        if ( ( ! empty( $value['sharing_name'] ) && ! is_string( $value['sharing_name'] ) )
3590        || ( ! empty( $value['sharing_url'] ) && ! is_string( $value['sharing_url'] ) )
3591        || ( ! empty( $value['sharing_icon'] ) && ! is_string( $value['sharing_icon'] ) ) ) {
3592            return new WP_Error(
3593                'invalid_param',
3594                sprintf(
3595                    /* Translators: Placeholder is a parameter name. */
3596                    esc_html__( '%s needs sharing name, url and icon.', 'jetpack' ),
3597                    $param
3598                )
3599            );
3600        }
3601        return true;
3602    }
3603
3604    /**
3605     * Validates that the parameter is a custom sharing service ID like 'custom-1461976264'.
3606     *
3607     * @since 4.3.0
3608     *
3609     * @param string          $value Value to check.
3610     * @param WP_REST_Request $request The request sent to the WP REST API.
3611     * @param string          $param Name of the parameter passed to endpoint holding $value.
3612     *
3613     * @return bool|WP_Error
3614     */
3615    public static function validate_custom_service_id( $value, $request, $param ) {
3616        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/custom\-[0-1]+/i', $value ) ) ) {
3617            return new WP_Error(
3618                'invalid_param',
3619                sprintf(
3620                    /* Translators: Placeholder is a parameter name. */
3621                    esc_html__( "%s must be a string prefixed with 'custom-' and followed by a numeric ID.", 'jetpack' ),
3622                    $param
3623                )
3624            );
3625        }
3626
3627        if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3628            return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
3629        }
3630        $sharer   = new Sharing_Service();
3631        $services = $sharer->get_all_services();
3632
3633        if ( ! empty( $value ) && ! isset( $services[ $value ] ) ) {
3634            return new WP_Error(
3635                'invalid_param',
3636                sprintf(
3637                    /* Translators: Placeholder is a parameter name. */
3638                    esc_html__( '%s is not a registered custom sharing service.', 'jetpack' ),
3639                    $param
3640                )
3641            );
3642        }
3643
3644        return true;
3645    }
3646
3647    /**
3648     * Validates that the parameter is a Twitter username or empty string (to be able to clear the field).
3649     *
3650     * @since 4.3.0
3651     *
3652     * @param string          $value   Value to check.
3653     * @param WP_REST_Request $request The request sent to the WP REST API.
3654     * @param string          $param   Name of the parameter passed to endpoint holding $value.
3655     *
3656     * @return bool|WP_Error
3657     */
3658    public static function validate_twitter_username( $value, $request, $param ) {
3659        if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^@?\w{1,15}$/i', $value ) ) ) {
3660            return new WP_Error(
3661                'invalid_param',
3662                sprintf(
3663                    /* Translators: Placeholder is a twitter name. */
3664                    esc_html__( '%s must be a Twitter username.', 'jetpack' ),
3665                    $param
3666                )
3667            );
3668        }
3669        return true;
3670    }
3671
3672    /**
3673     * Validates that the parameter is a string.
3674     *
3675     * @since 4.3.0
3676     *
3677     * @param string          $value Value to check.
3678     * @param WP_REST_Request $request The request sent to the WP REST API.
3679     * @param string          $param Name of the parameter passed to endpoint holding $value.
3680     *
3681     * @return bool|WP_Error
3682     */
3683    public static function validate_string( $value, $request, $param ) {
3684        if ( ! is_string( $value ) ) {
3685            return new WP_Error(
3686                'invalid_param',
3687                sprintf(
3688                    /* Translators: Placeholder is a parameter name. */
3689                    esc_html__( '%s must be a string.', 'jetpack' ),
3690                    $param
3691                )
3692            );
3693        }
3694        return true;
3695    }
3696
3697    /**
3698     * Validates that the parameter is an array of strings.
3699     *
3700     * @param array           $value Value to check.
3701     * @param WP_REST_Request $request The request sent to the WP REST API.
3702     * @param string          $param Name of the parameter passed to the endpoint holding $value.
3703     *
3704     * @return bool|WP_Error
3705     */
3706    public static function validate_array_of_strings( $value, $request, $param ) {
3707        foreach ( $value as $array_item ) {
3708            $validate = self::validate_string( $array_item, $request, $param );
3709            if ( is_wp_error( $validate ) ) {
3710                return $validate;
3711            }
3712        }
3713
3714        return true;
3715    }
3716
3717    /**
3718     * Validates the subscription_options parameter.
3719     *
3720     * @param array $values Value to check.
3721     *
3722     * @return bool|WP_Error
3723     */
3724    public static function validate_subscription_options( $values ) {
3725        if ( is_object( $values ) ) {
3726            return new WP_Error(
3727                'invalid_param',
3728                /* Translators: subscription_options is a variable name, and shouldn't be translated. */
3729                esc_html__( 'subscription_options must be an object.', 'jetpack' )
3730            );
3731        }
3732        foreach ( array_keys( $values ) as $key ) {
3733            if ( ! in_array( $key, array( 'welcome', 'invitation', 'comment_follow' ), true ) ) {
3734                return new WP_Error(
3735                    'invalid_param',
3736                    sprintf(
3737                        /* Translators: Placeholder is the invalid param being sent. */
3738                        esc_html__( '%s is not one of the allowed members of subscription_options.', 'jetpack' ),
3739                        $key
3740                    )
3741                );
3742            }
3743        }
3744        return true;
3745    }
3746
3747    /**
3748     * Validates that the parameter is an array.
3749     *
3750     * @param array           $values Value to check.
3751     * @param WP_REST_Request $request The request sent to the WP REST API.
3752     * @param string          $param Name of the parameter passed to the endpoint holding $value.
3753     *
3754     * @return bool|WP_Error
3755     */
3756    public static function validate_array( $values, $request, $param ) {
3757        if ( ! is_array( $values ) ) {
3758            return new WP_Error(
3759                'invalid_param',
3760                sprintf(
3761                    /* Translators: Placeholder is a parameter name. */
3762                    esc_html__( '%s must be an object.', 'jetpack' ),
3763                    $param
3764                )
3765            );
3766        }
3767        return true;
3768    }
3769
3770    /**
3771     * If for some reason the roles allowed to see Stats are empty (for example, user tampering with checkboxes),
3772     * return an array with only 'administrator' as the allowed role and save it for 'roles' option.
3773     *
3774     * @since 4.3.0
3775     *
3776     * @param string|bool $value Value to check.
3777     *
3778     * @return bool|array
3779     */
3780    public static function sanitize_stats_allowed_roles( $value ) {
3781        if ( empty( $value ) ) {
3782            return array( 'administrator' );
3783        }
3784        return $value;
3785    }
3786
3787    /**
3788     * Get the currently accessed route and return the module slug in it.
3789     *
3790     * @since 4.3.0
3791     *
3792     * @param string $route Regular expression for the endpoint with the module slug to return.
3793     *
3794     * @return array|string
3795     */
3796    public static function get_module_requested( $route = '/module/(?P<slug>[a-z\-]+)' ) {
3797
3798        if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) || ! is_string( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
3799            return '';
3800        }
3801
3802        preg_match( "#$route#", $GLOBALS['wp']->query_vars['rest_route'], $module );
3803
3804        if ( empty( $module['slug'] ) ) {
3805            return '';
3806        }
3807
3808        return $module['slug'];
3809    }
3810
3811    /**
3812     * Adds extra information for modules.
3813     *
3814     * @since 4.3.0
3815     *
3816     * @param string|array $modules Can be a single module or a list of modules.
3817     * @param null|string  $slug    Slug of the module in the first parameter.
3818     *
3819     * @return array|string
3820     */
3821    public static function prepare_modules_for_response( $modules = '', $slug = null ) {
3822        global $wp_rewrite;
3823
3824        /** This filter is documented in modules/sitemaps/sitemaps.php */
3825        $location = apply_filters( 'jetpack_sitemap_location', '' );
3826
3827        if ( $wp_rewrite->using_index_permalinks() ) {
3828            $sitemap_url      = home_url( '/index.php' . $location . '/sitemap.xml' );
3829            $news_sitemap_url = home_url( '/index.php' . $location . '/news-sitemap.xml' );
3830        } elseif ( $wp_rewrite->using_permalinks() ) {
3831            $sitemap_url      = home_url( $location . '/sitemap.xml' );
3832            $news_sitemap_url = home_url( $location . '/news-sitemap.xml' );
3833        } else {
3834            $sitemap_url      = home_url( $location . '/?jetpack-sitemap=sitemap.xml' );
3835            $news_sitemap_url = home_url( $location . '/?jetpack-sitemap=news-sitemap.xml' );
3836        }
3837
3838        if ( $slug === null && isset( $modules['sitemaps'] ) ) {
3839            // Is a list of modules.
3840            $modules['sitemaps']['extra']['sitemap_url']      = $sitemap_url;
3841            $modules['sitemaps']['extra']['news_sitemap_url'] = $news_sitemap_url;
3842        } elseif ( 'sitemaps' === $slug ) {
3843            // It's a single module.
3844            $modules['extra']['sitemap_url']      = $sitemap_url;
3845            $modules['extra']['news_sitemap_url'] = $news_sitemap_url;
3846        }
3847        return $modules;
3848    }
3849
3850    /**
3851     * Remove 'validate_callback' item from options available for module.
3852     * Fetch current option value and add to array of module options.
3853     * Prepare values of module options that need special handling, like those saved in wpcom.
3854     *
3855     * @since 4.3.0
3856     *
3857     * @param string $module Module slug.
3858     * @return array
3859     */
3860    public static function prepare_options_for_response( $module = '' ) {
3861        $options = self::get_updateable_data_list( $module );
3862
3863        if ( ! is_array( $options ) || empty( $options ) ) {
3864            return $options;
3865        }
3866
3867        // Some modules need special treatment.
3868        switch ( $module ) {
3869
3870            case 'monitor':
3871                // Status of user notifications.
3872                $options['monitor_receive_notifications']['current_value'] = self::cast_value( self::get_remote_value( 'monitor', 'monitor_receive_notifications' ), $options['monitor_receive_notifications'] );
3873                break;
3874
3875            case 'post-by-email':
3876                // Email address.
3877                $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'] );
3878                break;
3879
3880            case 'protect':
3881                // Protect.
3882                $options['jetpack_protect_key']['current_value']              = get_site_option( 'jetpack_protect_key', false );
3883                $options['jetpack_protect_global_whitelist']['current_value'] = Brute_Force_Protection_Shared_Functions::format_allow_list();
3884                break;
3885
3886            case 'related-posts':
3887                // It's local, but it must be broken apart since it's saved as an array.
3888                $options = self::split_options( $options, Jetpack_Options::get_option( 'relatedposts' ) );
3889                break;
3890
3891            case 'verification-tools':
3892                // It's local, but it must be broken apart since it's saved as an array.
3893                $options = self::split_options( $options, get_option( 'verification_services_codes' ) );
3894                break;
3895
3896            case 'google-analytics':
3897                $wga  = get_option( 'jetpack_wga' );
3898                $code = '';
3899                if ( is_array( $wga ) && array_key_exists( 'code', $wga ) ) {
3900                    $code = $wga['code'];
3901                }
3902                $options['google_analytics_tracking_id']['current_value'] = $code;
3903                break;
3904
3905            case 'sharedaddy':
3906                // It's local, but it must be broken apart since it's saved as an array.
3907                if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
3908                    break;
3909                }
3910                $sharer                                       = new Sharing_Service();
3911                $options                                      = self::split_options( $options, $sharer->get_global_options() );
3912                $options['sharing_services']['current_value'] = $sharer->get_blog_services();
3913                $other_sharedaddy_options                     = array( 'jetpack-twitter-cards-site-tag', 'sharedaddy_disable_resources', 'sharing_delete_service' );
3914                foreach ( $other_sharedaddy_options as $key ) {
3915                    $default_value                    = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
3916                    $current_value                    = get_option( $key, $default_value );
3917                    $options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
3918                }
3919                break;
3920
3921            case 'stats':
3922                // It's local, but it must be broken apart since it's saved as an array.
3923                $options = self::split_options( $options, Stats_Options::get_options() );
3924                break;
3925            default:
3926                // These option are just stored as plain WordPress options.
3927                foreach ( $options as $key => $value ) {
3928                    $default_value                    = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
3929                    $current_value                    = get_option( $key, $default_value );
3930                    $options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
3931                }
3932        }
3933        // At this point some options have current_value not set because they're options
3934        // that only get written on update, so we set current_value to the default one.
3935        foreach ( $options as $key => $value ) {
3936            // We don't need validate_callback in the response.
3937            if ( isset( $options[ $key ]['validate_callback'] ) ) {
3938                unset( $options[ $key ]['validate_callback'] );
3939            }
3940            $default_value = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
3941            if ( ! array_key_exists( 'current_value', $options[ $key ] ) ) {
3942                $options[ $key ]['current_value'] = self::cast_value( $default_value, $options[ $key ] );
3943            }
3944        }
3945        return $options;
3946    }
3947
3948    /**
3949     * Splits module options saved as arrays like relatedposts or verification_services_codes into separate options to be returned in the response.
3950     *
3951     * @since 4.3.0
3952     *
3953     * @param array  $separate_options Array of options admitted by the module.
3954     * @param array  $grouped_options Option saved as array to be splitted.
3955     * @param string $prefix Optional prefix for the separate option keys.
3956     *
3957     * @return array
3958     */
3959    public static function split_options( $separate_options, $grouped_options, $prefix = '' ) {
3960        if ( is_array( $grouped_options ) ) {
3961            foreach ( $grouped_options as $key => $value ) {
3962                $option_key = $prefix . $key;
3963                if ( isset( $separate_options[ $option_key ] ) ) {
3964                    $separate_options[ $option_key ]['current_value'] = self::cast_value( $grouped_options[ $key ], $separate_options[ $option_key ] );
3965                }
3966            }
3967        }
3968        return $separate_options;
3969    }
3970
3971    /**
3972     * Perform a casting to the value specified in the option definition.
3973     *
3974     * @since 4.3.0
3975     *
3976     * @param mixed $value Value to cast to the proper type.
3977     * @param array $definition Type to cast the value to.
3978     *
3979     * @return bool|float|int|string
3980     */
3981    public static function cast_value( $value, $definition ) {
3982        if ( 'NULL' === $value ) {
3983            return null;
3984        }
3985
3986        if ( isset( $definition['type'] ) ) {
3987            switch ( $definition['type'] ) {
3988                case 'boolean':
3989                    if ( 'true' === $value || 'on' === $value ) {
3990                        return true;
3991                    } elseif ( 'false' === $value || 'off' === $value ) {
3992                        return false;
3993                    }
3994                    $value = (bool) $value;
3995                    break;
3996
3997                case 'integer':
3998                    $value = (int) $value;
3999                    break;
4000
4001                case 'float':
4002                    $value = (float) $value;
4003                    break;
4004
4005                case 'string':
4006                    $value = (string) $value;
4007                    break;
4008            }
4009        }
4010        return $value;
4011    }
4012
4013    /**
4014     * Get a value not saved locally.
4015     *
4016     * @since 4.3.0
4017     *
4018     * @param string $module Module slug.
4019     * @param string $option Option name.
4020     *
4021     * @return bool Whether user is receiving notifications or not.
4022     */
4023    public static function get_remote_value( $module, $option ) {
4024
4025        if ( in_array( $module, array( 'post-by-email' ), true ) ) {
4026            $option .= get_current_user_id();
4027        }
4028
4029        // If option doesn't exist, 'does_not_exist' will be returned.
4030        $value = get_option( $option, 'does_not_exist' );
4031
4032        // If option exists, just return it.
4033        if ( 'does_not_exist' !== $value ) {
4034            return $value;
4035        }
4036
4037        // Only check a remote option if Jetpack is connected.
4038        if ( ! Jetpack::is_connection_ready() ) {
4039            return false;
4040        }
4041
4042        // Do what is necessary for each module.
4043        switch ( $module ) {
4044            case 'monitor':
4045                // Load the class to use the method. If class can't be found, do nothing.
4046                if ( ! class_exists( 'Jetpack_Monitor' ) && ! include_once Jetpack::get_module_path( $module ) ) {
4047                    return false;
4048                }
4049                $value = Jetpack_Monitor::user_receives_notifications( false );
4050                break;
4051
4052            case 'post-by-email':
4053                // Load the class to use the method. If class can't be found, do nothing.
4054                if ( ! class_exists( 'Jetpack_Post_By_Email' ) && ! include_once Jetpack::get_module_path( $module ) ) {
4055                    return false;
4056                }
4057                $value = Jetpack_Post_By_Email::init()->get_post_by_email_address();
4058                if ( null === $value ) {
4059                    $value = 'NULL'; // sentinel value so it actually gets set.
4060                }
4061                break;
4062        }
4063
4064        // Normalize value to boolean.
4065        if ( is_wp_error( $value ) || $value === null ) {
4066            $value = false;
4067        }
4068
4069        // Save option to use it next time.
4070        update_option( $option, $value );
4071
4072        return $value;
4073    }
4074
4075    /**
4076     * Get number of plugin updates available.
4077     *
4078     * @since 4.3.0
4079     *
4080     * @return mixed|WP_Error Number of plugin updates available. Otherwise, a WP_Error instance with the corresponding error.
4081     */
4082    public static function get_plugin_update_count() {
4083        $updates = wp_get_update_data();
4084        if ( isset( $updates['counts'] ) && isset( $updates['counts']['plugins'] ) ) {
4085            $count = $updates['counts']['plugins'];
4086            if ( 0 === $count ) {
4087                $response = array(
4088                    'code'    => 'success',
4089                    'message' => esc_html__( 'All plugins are up-to-date. Keep up the good work!', 'jetpack' ),
4090                    'count'   => 0,
4091                );
4092            } else {
4093                $response = array(
4094                    'code'    => 'updates-available',
4095                    'message' => esc_html(
4096                        sprintf(
4097                            /* Translators: placeholders are numbers. */
4098                            _n( '%s plugin needs updating.', '%s plugins need updating.', $count, 'jetpack' ),
4099                            $count
4100                        )
4101                    ),
4102                    'count'   => $count,
4103                );
4104            }
4105            return rest_ensure_response( $response );
4106        }
4107
4108        return new WP_Error( 'not_found', esc_html__( 'Could not check updates for plugins on this site.', 'jetpack' ), array( 'status' => 404 ) );
4109    }
4110
4111    /**
4112     * Get plugins data in site.
4113     *
4114     * @since 4.2.0
4115     *
4116     * @return WP_REST_Response|WP_Error List of plugins in the site. Otherwise, a WP_Error instance with the corresponding error.
4117     */
4118    public static function get_plugins() {
4119        $plugins = Plugins_Installer::get_plugins();
4120
4121        if ( ! empty( $plugins ) ) {
4122            return rest_ensure_response( $plugins );
4123        }
4124
4125        return new WP_Error( 'not_found', esc_html__( 'Unable to list plugins.', 'jetpack' ), array( 'status' => 404 ) );
4126    }
4127
4128    /**
4129     * Install a specific plugin and optionally activates it.
4130     *
4131     * @since 8.9.0
4132     *
4133     * @param WP_REST_Request $request {
4134     *     Array of parameters received by request.
4135     *
4136     *     @type string $slug   Plugin slug.
4137     *     @type string $status Plugin status.
4138     *     @type string $source Where did the plugin installation request originate.
4139     * }
4140     *
4141     * @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.
4142     */
4143    public static function install_plugin( $request ) {
4144        $plugin = stripslashes( $request['slug'] );
4145
4146        // Let's make sure the plugin isn't already installed.
4147        $plugin_id = Plugins_Installer::get_plugin_id_by_slug( $plugin );
4148
4149        // If not installed, let's install now.
4150        if ( ! $plugin_id ) {
4151            $result = Plugins_Installer::install_plugin( $plugin );
4152
4153            if ( is_wp_error( $result ) ) {
4154                return new WP_Error(
4155                    'install_plugin_failed',
4156                    sprintf(
4157                        /* translators: %1$s: plugin name. -- %2$s: error message. */
4158                        __( 'Unable to install %1$s: %2$s ', 'jetpack' ),
4159                        $plugin,
4160                        $result->get_error_message()
4161                    ),
4162                    array( 'status' => 500 )
4163                );
4164            }
4165        }
4166
4167        /*
4168         * We may want to activate the plugin as well.
4169         * Let's check for the status parameter in the request to find out.
4170         * If none was passed (or something other than active), let's return now.
4171         */
4172        if ( empty( $request['status'] ) || 'active' !== $request['status'] ) {
4173            return rest_ensure_response(
4174                array(
4175                    'code'    => 'success',
4176                    'message' => esc_html(
4177                        sprintf(
4178                            /* translators: placeholder is a plugin name. */
4179                            __( 'Installed %s', 'jetpack' ),
4180                            $plugin
4181                        )
4182                    ),
4183                )
4184            );
4185        }
4186
4187        /*
4188         * Proceed with plugin activation.
4189         * Let's check again for the plugin's ID if we don't already have it.
4190         */
4191        if ( ! $plugin_id ) {
4192            $plugin_id = Plugins_Installer::get_plugin_id_by_slug( $plugin );
4193            if ( ! $plugin_id ) {
4194                return new WP_Error(
4195                    'unable_to_determine_installed_plugin',
4196                    __( 'Unable to determine what plugin was installed.', 'jetpack' ),
4197                    array( 'status' => 500 )
4198                );
4199            }
4200        }
4201
4202        $source      = ! empty( $request['source'] ) ? stripslashes( $request['source'] ) : 'rest_api';
4203        $plugin_args = array(
4204            'plugin' => substr( $plugin_id, 0, - 4 ),
4205            'status' => 'active',
4206            'source' => $source,
4207        );
4208        return self::activate_plugin( $plugin_args );
4209    }
4210
4211    /**
4212     * Activate a specific plugin.
4213     *
4214     * @since 8.9.0
4215     *
4216     * @param WP_REST_Request $request {
4217     *     Array of parameters received by request.
4218     *
4219     *     @type string $plugin Plugin long slug (slug/index-file)
4220     *     @type string $status Plugin status. We only support active in Jetpack.
4221     *     @type string $source Where did the plugin installation request originate.
4222     * }
4223     *
4224     * @return WP_REST_Response|WP_Error A response object if the activation was successful, or a WP_Error object if the activation failed.
4225     */
4226    public static function activate_plugin( $request ) {
4227        /*
4228         * We need an "active" status parameter to be passed to the request
4229         * just like the core plugins endpoind we'll eventually switch to.
4230         */
4231        if ( empty( $request['status'] ) || 'active' !== $request['status'] ) {
4232            return new WP_Error(
4233                'missing_status_parameter',
4234                esc_html__( 'Status parameter missing.', 'jetpack' ),
4235                array( 'status' => 403 )
4236            );
4237        }
4238
4239        $plugins = Plugins_Installer::get_plugins();
4240
4241        if ( empty( $plugins ) ) {
4242            return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
4243        }
4244
4245        if ( empty( $request['plugin'] ) ) {
4246            return new WP_Error( 'no_plugin_specified', esc_html__( 'You did not specify a plugin.', 'jetpack' ), array( 'status' => 404 ) );
4247        }
4248
4249        $plugin = $request['plugin'] . '.php';
4250
4251        // Is the plugin installed?
4252        if ( ! array_key_exists( $plugin, $plugins ) ) {
4253            return new WP_Error(
4254                'plugin_not_found',
4255                esc_html(
4256                    sprintf(
4257                        /* translators: placeholder is a plugin slug. */
4258                        __( 'Plugin %s is not installed.', 'jetpack' ),
4259                        $plugin
4260                    )
4261                ),
4262                array( 'status' => 404 )
4263            );
4264        }
4265
4266        // Is the plugin active already?
4267        $status = Plugins_Installer::get_plugin_status( $plugin );
4268        if ( in_array( $status, array( 'active', 'network-active' ), true ) ) {
4269            return new WP_Error(
4270                'plugin_already_active',
4271                esc_html(
4272                    sprintf(
4273                        /* translators: placeholder is a plugin slug. */
4274                        __( 'Plugin %s is already active.', 'jetpack' ),
4275                        $plugin
4276                    )
4277                ),
4278                array( 'status' => 404 )
4279            );
4280        }
4281
4282        // Now try to activate the plugin.
4283        $activated = activate_plugin( $plugin );
4284
4285        if ( is_wp_error( $activated ) ) {
4286            return $activated;
4287        } else {
4288            $source = ! empty( $request['source'] ) ? stripslashes( $request['source'] ) : 'rest_api';
4289            /**
4290             * Fires when Jetpack installs a plugin for you.
4291             *
4292             * @since 8.9.0
4293             *
4294             * @param string $plugin_file Plugin file.
4295             * @param string $source      Where did the plugin installation originate.
4296             */
4297            do_action( 'jetpack_activated_plugin', $plugin, $source );
4298            return rest_ensure_response(
4299                array(
4300                    'code'    => 'success',
4301                    'message' => sprintf(
4302                        /* translators: placeholder is a plugin name. */
4303                        esc_html__( 'Activated %s', 'jetpack' ),
4304                        $plugin
4305                    ),
4306                )
4307            );
4308        }
4309    }
4310
4311    /**
4312     * Check if a plugin can be activated.
4313     *
4314     * @since 8.9.0
4315     *
4316     * @param string|bool     $value   Value to check.
4317     * @param WP_REST_Request $request The request sent to the WP REST API.
4318     * @param string          $param   Name of the parameter passed to endpoint holding $value.
4319     */
4320    public static function validate_activate_plugin( $value, $request, $param ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
4321        return 'active' === $value;
4322    }
4323
4324    /**
4325     * Get data about the queried plugin. Currently it only returns whether the plugin is active or not.
4326     *
4327     * @since 4.2.0
4328     *
4329     * @param WP_REST_Request $request {
4330     *     Array of parameters received by request.
4331     *
4332     *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
4333     * }
4334     *
4335     * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
4336     */
4337    public static function get_plugin( $request ) {
4338        $plugins = Plugins_Installer::get_plugins();
4339
4340        if ( empty( $plugins ) ) {
4341            return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
4342        }
4343
4344        $plugin = stripslashes( $request['plugin'] );
4345
4346        if ( ! array_key_exists( $plugin, $plugins ) ) {
4347            return new WP_Error(
4348                'plugin_not_found',
4349                esc_html(
4350                    sprintf(
4351                        /* Translators: placeholder is a plugin name. */
4352                        __( 'Plugin %s is not installed.', 'jetpack' ),
4353                        $plugin
4354                    )
4355                ),
4356                array( 'status' => 404 )
4357            );
4358        }
4359
4360        $plugin_data = $plugins[ $plugin ];
4361
4362        $plugin_data['active'] = in_array( Plugins_Installer::get_plugin_status( $plugin ), array( 'active', 'network-active' ), true );
4363
4364        return rest_ensure_response(
4365            array(
4366                'code'    => 'success',
4367                'message' => esc_html__( 'Plugin found.', 'jetpack' ),
4368                'data'    => $plugin_data,
4369            )
4370        );
4371    }
4372
4373    /**
4374     * Returns the Jetpack CRM data.
4375     *
4376     * @return WP_REST_Response A response object containing the Jetpack CRM data.
4377     */
4378    public static function get_jetpack_crm_data() {
4379        $jetpack_crm_data = ( new Jetpack_CRM_Data() )->get_crm_data();
4380        return rest_ensure_response( $jetpack_crm_data );
4381    }
4382
4383    /**
4384     * Activates Jetpack CRM's Jetpack Forms extension.
4385     *
4386     * @param WP_REST_Request $request The request sent to the WP REST API.
4387     * @return WP_REST_Response|WP_Error A response object if the extension activation was successful, or a WP_Error object if it failed.
4388     */
4389    public static function activate_crm_jetpack_forms_extension( $request ) {
4390        if ( ! isset( $request['extension'] ) || 'jetpackforms' !== $request['extension'] ) {
4391            return new WP_Error( 'invalid_param', esc_html__( 'Missing or invalid extension parameter.', 'jetpack' ), array( 'status' => 404 ) );
4392        }
4393
4394        $result = ( new Jetpack_CRM_Data() )->activate_crm_jetpackforms_extension();
4395
4396        if ( is_wp_error( $result ) ) {
4397            return $result;
4398        }
4399
4400        return rest_ensure_response( array( 'code' => 'success' ) );
4401    }
4402
4403    /**
4404     * Verifies that the current user has the required permission for accessing the CRM data.
4405     *
4406     * @return true|WP_Error Returns true if the user has the required capability, else a WP_Error object.
4407     */
4408    public static function jetpack_crm_data_permission_check() {
4409        if ( current_user_can( 'publish_posts' ) ) {
4410            return true;
4411        }
4412
4413        return new WP_Error(
4414            'invalid_user_permission_jetpack_crm_data',
4415            REST_Connector::get_user_permissions_error_msg(),
4416            array( 'status' => rest_authorization_required_code() )
4417        );
4418    }
4419
4420    /**
4421     * Verifies that the current user has the required capability for activating Jetpack CRM extensions.
4422     *
4423     * @return true|WP_Error Returns true if the user has the required capability, else a WP_Error object.
4424     */
4425    public static function activate_crm_extensions_permission_check() {
4426        // phpcs:ignore WordPress.WP.Capabilities.Unknown
4427        if ( current_user_can( 'admin_zerobs_manage_options' ) ) {
4428            return true;
4429        }
4430
4431        return new WP_Error(
4432            'invalid_user_permission_activate_jetpack_crm_ext',
4433            REST_Connector::get_user_permissions_error_msg(),
4434            array( 'status' => rest_authorization_required_code() )
4435        );
4436    }
4437
4438    /**
4439     * Set hasSeenWCConnectionModal to true when the site has displayed it
4440     *
4441     * @since 10.4.0
4442     *
4443     * @return bool
4444     */
4445    public static function set_has_seen_wc_connection_modal() {
4446        $updated_option = Jetpack_Options::update_option( 'has_seen_wc_connection_modal', true );
4447
4448        return rest_ensure_response( array( 'success' => $updated_option ) );
4449    }
4450
4451    /**
4452     * Fetch introdution offers.
4453     *
4454     * @since 10.9
4455     *
4456     * @return array|WP_Error
4457     */
4458    public static function get_intro_offers() {
4459        $site_id = Jetpack_Options::get_option( 'id' );
4460
4461        if ( ! $site_id ) {
4462            return new WP_Error(
4463                'site_id_missing',
4464                esc_html__( 'Site ID is missing.', 'jetpack' ),
4465                array( 'status' => 400 )
4466            );
4467        }
4468
4469        $response = Client::wpcom_json_api_request_as_user(
4470            '/introductory-offers',
4471            '2',
4472            array(
4473                'method'  => 'GET',
4474                'headers' => array(
4475                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
4476                ),
4477            )
4478        );
4479
4480        $response_code = wp_remote_retrieve_response_code( $response );
4481
4482        if ( 200 !== $response_code ) {
4483            return new WP_Error(
4484                'intro_offers_fetch_failed',
4485                esc_html__( 'Could not retrieve intro offers.', 'jetpack' ),
4486                array( 'status' => $response_code )
4487            );
4488        }
4489
4490        $data = json_decode( wp_remote_retrieve_body( $response ) );
4491
4492        if ( ! isset( $data ) ) {
4493            return new WP_Error(
4494                'intro_offers_error',
4495                esc_html__( 'Could not parse intro offers.', 'jetpack' ),
4496                array( 'status' => 204 ) // no content.
4497            );
4498        }
4499
4500        return rest_ensure_response(
4501            array(
4502                'code' => 'success',
4503                'data' => $data,
4504            )
4505        );
4506    }
4507
4508    /**
4509     * Return the list of available features.
4510     *
4511     * @return array
4512     */
4513    public static function get_features_available() {
4514        $raw_modules = Jetpack::get_available_modules();
4515        $modules     = array();
4516        foreach ( $raw_modules as $module ) {
4517            $modules[] = Jetpack::get_module_slug( $module );
4518        }
4519
4520        return $modules;
4521    }
4522
4523    /**
4524     * Returns what features are enabled. Uses the slug of the modules files.
4525     *
4526     * @return array
4527     */
4528    public static function get_features_enabled() {
4529        $raw_modules = Jetpack::get_active_modules();
4530        $modules     = array();
4531        foreach ( $raw_modules as $module ) {
4532            $modules[] = Jetpack::get_module_slug( $module );
4533        }
4534
4535        return $modules;
4536    }
4537
4538    /**
4539     * Verify that the API client is allowed to replace user token.
4540     *
4541     * @since 1.29.0
4542     *
4543     * @return bool|WP_Error
4544     */
4545    public static function get_features_permission_check() {
4546        if ( ! Rest_Authentication::is_signed_with_blog_token() ) {
4547            $message = esc_html__(
4548                'You do not have the correct user permissions to perform this action. Please contact your site admin if you think this is a mistake.',
4549                'jetpack'
4550            );
4551            return new WP_Error( 'invalid_permission_fetch_features', $message, array( 'status' => rest_authorization_required_code() ) );
4552        }
4553
4554        return true;
4555    }
4556} // class end