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