PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 All 502 releases
jetpack / _inc / lib / class.core-rest-api-endpoints.php

class.core-rest-api-endpoints.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at _inc/lib/class.core-rest-api-endpoints.php

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