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