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