Completed
Push — add/mailchimp-groups-merge-fie... ( c88508...48f203 )
by
unknown
07:48 queued 01:05
created

_inc/lib/class.core-rest-api-endpoints.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use Automattic\Jetpack\Connection\Client;
4
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
5
use Automattic\Jetpack\JITM;
6
use Automattic\Jetpack\Tracking;
7
use Automattic\Jetpack\Status;
8
9
/**
10
 * Register WP REST API endpoints for Jetpack.
11
 *
12
 * @author Automattic
13
 */
14
15
/**
16
 * Disable direct access.
17
 */
18
if ( ! defined( 'ABSPATH' ) ) {
19
	exit;
20
}
21
22
// Load WP_Error for error messages.
23
require_once ABSPATH . '/wp-includes/class-wp-error.php';
24
25
// Register endpoints when WP REST API is initialized.
26
add_action( 'rest_api_init', array( 'Jetpack_Core_Json_Api_Endpoints', 'register_endpoints' ) );
27
// Load API endpoints that are synced with WP.com
28
// Each of these is a class that will register its own routes on 'rest_api_init'.
29
require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/load-wpcom-endpoints.php';
30
31
/**
32
 * Class Jetpack_Core_Json_Api_Endpoints
33
 *
34
 * @since 4.3.0
35
 */
36
class Jetpack_Core_Json_Api_Endpoints {
37
38
	/**
39
	 * @var string Generic error message when user is not allowed to perform an action.
40
	 */
41
	public static $user_permissions_error_msg;
42
43
	/**
44
	 * @var array Roles that can access Stats once they're granted access.
45
	 */
46
	public static $stats_roles;
47
48
	/**
49
	 * Declare the Jetpack REST API endpoints.
50
	 *
51
	 * @since 4.3.0
52
	 */
53
	public static function register_endpoints() {
54
55
		// Load API endpoint base classes
56
		require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php';
57
58
		// Load API endpoints
59
		require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php';
60
		require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php';
61
		require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php';
62
63
		self::$user_permissions_error_msg = esc_html__(
64
			'You do not have the correct user permissions to perform this action.
65
			Please contact your site admin if you think this is a mistake.',
66
			'jetpack'
67
		);
68
69
		self::$stats_roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
70
71
		$ixr_client = new Jetpack_IXR_Client( array( 'user_id' => get_current_user_id() ) );
72
		$core_api_endpoint = new Jetpack_Core_API_Data( $ixr_client );
73
		$module_list_endpoint = new Jetpack_Core_API_Module_List_Endpoint();
74
		$module_data_endpoint = new Jetpack_Core_API_Module_Data_Endpoint();
75
		$module_toggle_endpoint = new Jetpack_Core_API_Module_Toggle_Endpoint( new Jetpack_IXR_Client() );
76
		$site_endpoint = new Jetpack_Core_API_Site_Endpoint();
77
		$widget_endpoint = new Jetpack_Core_API_Widget_Endpoint();
78
79
		register_rest_route( 'jetpack/v4', 'plans', array(
80
			'methods'             => WP_REST_Server::READABLE,
81
			'callback'            => __CLASS__ . '::get_plans',
82
			'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
83
84
		) );
85
86
		register_rest_route( 'jetpack/v4', 'marketing/survey', array(
87
			'methods'             => WP_REST_Server::CREATABLE,
88
			'callback'            => __CLASS__ . '::submit_survey',
89
			'permission_callback' => __CLASS__ . '::disconnect_site_permission_callback',
90
91
		) );
92
93
		register_rest_route( 'jetpack/v4', '/jitm', array(
94
			'methods'  => WP_REST_Server::READABLE,
95
			'callback' => __CLASS__ . '::get_jitm_message',
96
		) );
97
98
		register_rest_route( 'jetpack/v4', '/jitm', array(
99
			'methods'  => WP_REST_Server::CREATABLE,
100
			'callback' => __CLASS__ . '::delete_jitm_message'
101
		) );
102
103
		// Authorize a remote user
104
		register_rest_route( 'jetpack/v4', '/remote_authorize', array(
105
			'methods' => WP_REST_Server::EDITABLE,
106
			'callback' => __CLASS__ . '::remote_authorize',
107
		) );
108
109
		// Get current connection status of Jetpack
110
		register_rest_route( 'jetpack/v4', '/connection', array(
111
			'methods' => WP_REST_Server::READABLE,
112
			'callback' => __CLASS__ . '::jetpack_connection_status',
113
		) );
114
115
		// Test current connection status of Jetpack
116
		register_rest_route( 'jetpack/v4', '/connection/test', array(
117
			'methods' => WP_REST_Server::READABLE,
118
			'callback' => __CLASS__ . '::jetpack_connection_test',
119
			'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
120
		) );
121
122
		// Endpoint specific for privileged servers to request detailed debug information.
123
		register_rest_route( 'jetpack/v4', '/connection/test-wpcom/', array(
124
			'methods' => WP_REST_Server::READABLE,
125
			'callback' => __CLASS__ . '::jetpack_connection_test_for_external',
126
			'permission_callback' => __CLASS__ . '::view_jetpack_connection_test_check',
127
		) );
128
129
		register_rest_route( 'jetpack/v4', '/rewind', array(
130
			'methods' => WP_REST_Server::READABLE,
131
			'callback' => __CLASS__ . '::get_rewind_data',
132
			'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
133
		) );
134
135
		// Fetches a fresh connect URL
136
		register_rest_route( 'jetpack/v4', '/connection/url', array(
137
			'methods' => WP_REST_Server::READABLE,
138
			'callback' => __CLASS__ . '::build_connect_url',
139
			'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
140
		) );
141
142
		// Get current user connection data
143
		register_rest_route( 'jetpack/v4', '/connection/data', array(
144
			'methods' => WP_REST_Server::READABLE,
145
			'callback' => __CLASS__ . '::get_user_connection_data',
146
			'permission_callback' => __CLASS__ . '::get_user_connection_data_permission_callback',
147
		) );
148
149
		// Start the connection process by registering the site on WordPress.com servers.
150
		register_rest_route( 'jetpack/v4', '/connection/register', array(
151
			'methods'             => WP_REST_Server::EDITABLE,
152
			'callback'            => __CLASS__ . '::register_site',
153
			'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
154
			'args'                => array(
155
				'registration_nonce' => array( 'type' => 'string' ),
156
			),
157
		) );
158
159
		// Set the connection owner
160
		register_rest_route( 'jetpack/v4', '/connection/owner', array(
161
			'methods' => WP_REST_Server::EDITABLE,
162
			'callback' => __CLASS__ . '::set_connection_owner',
163
			'permission_callback' => __CLASS__ . '::set_connection_owner_permission_callback',
164
		) );
165
166
		// Current user: get or set tracking settings.
167
		register_rest_route( 'jetpack/v4', '/tracking/settings', array(
168
			array(
169
				'methods'             => WP_REST_Server::READABLE,
170
				'callback'            => __CLASS__ . '::get_user_tracking_settings',
171
				'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
172
			),
173
			array(
174
				'methods'             => WP_REST_Server::EDITABLE,
175
				'callback'            => __CLASS__ . '::update_user_tracking_settings',
176
				'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
177
				'args'                => array(
178
					'tracks_opt_out' => array( 'type' => 'boolean' ),
179
				),
180
			),
181
		) );
182
183
		// Disconnect site from WordPress.com servers
184
		register_rest_route( 'jetpack/v4', '/connection', array(
185
			'methods' => WP_REST_Server::EDITABLE,
186
			'callback' => __CLASS__ . '::disconnect_site',
187
			'permission_callback' => __CLASS__ . '::disconnect_site_permission_callback',
188
		) );
189
190
		// Disconnect/unlink user from WordPress.com servers
191
		register_rest_route( 'jetpack/v4', '/connection/user', array(
192
			'methods' => WP_REST_Server::EDITABLE,
193
			'callback' => __CLASS__ . '::unlink_user',
194
			'permission_callback' => __CLASS__ . '::unlink_user_permission_callback',
195
		) );
196
197
		// Get current site data
198
		register_rest_route( 'jetpack/v4', '/site', array(
199
			'methods' => WP_REST_Server::READABLE,
200
			'callback' => __CLASS__ . '::get_site_data',
201
			'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
202
		) );
203
204
		// Get current site data
205
		register_rest_route( 'jetpack/v4', '/site/features', array(
206
			'methods' => WP_REST_Server::READABLE,
207
			'callback' => array( $site_endpoint, 'get_features' ),
208
			'permission_callback' => array( $site_endpoint , 'can_request' ),
209
		) );
210
211
		// Get current site benefits
212
		register_rest_route( 'jetpack/v4', '/site/benefits', array(
213
			'methods'             => WP_REST_Server::READABLE,
214
			'callback'            => array( $site_endpoint, 'get_benefits' ),
215
			'permission_callback' => array( $site_endpoint, 'can_request' ),
216
		) );
217
218
		// Get Activity Log data for this site.
219
		register_rest_route( 'jetpack/v4', '/site/activity', array(
220
			'methods' => WP_REST_Server::READABLE,
221
			'callback' => __CLASS__ . '::get_site_activity',
222
			'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
223
		) );
224
225
		// Confirm that a site in identity crisis should be in staging mode
226
		register_rest_route( 'jetpack/v4', '/identity-crisis/confirm-safe-mode', array(
227
			'methods' => WP_REST_Server::EDITABLE,
228
			'callback' => __CLASS__ . '::confirm_safe_mode',
229
			'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
230
		) );
231
232
		// IDC resolve: create an entirely new shadow site for this URL.
233
		register_rest_route( 'jetpack/v4', '/identity-crisis/start-fresh', array(
234
			'methods' => WP_REST_Server::EDITABLE,
235
			'callback' => __CLASS__ . '::start_fresh_connection',
236
			'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
237
		) );
238
239
		// Handles the request to migrate stats and subscribers during an identity crisis.
240
		register_rest_route( 'jetpack/v4', 'identity-crisis/migrate', array(
241
			'methods' => WP_REST_Server::EDITABLE,
242
			'callback' => __CLASS__ . '::migrate_stats_and_subscribers',
243
			'permissison_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
244
		) );
245
246
		// Return all modules
247
		register_rest_route( 'jetpack/v4', '/module/all', array(
248
			'methods' => WP_REST_Server::READABLE,
249
			'callback' => array( $module_list_endpoint, 'process' ),
250
			'permission_callback' => array( $module_list_endpoint, 'can_request' ),
251
		) );
252
253
		// Activate many modules
254
		register_rest_route( 'jetpack/v4', '/module/all/active', array(
255
			'methods' => WP_REST_Server::EDITABLE,
256
			'callback' => array( $module_list_endpoint, 'process' ),
257
			'permission_callback' => array( $module_list_endpoint, 'can_request' ),
258
			'args' => array(
259
				'modules' => array(
260
					'default'           => '',
261
					'type'              => 'array',
262
					'items'             => array(
263
						'type'          => 'string',
264
					),
265
					'required'          => true,
266
					'validate_callback' => __CLASS__ . '::validate_module_list',
267
				),
268
				'active' => array(
269
					'default'           => true,
270
					'type'              => 'boolean',
271
					'required'          => false,
272
					'validate_callback' => __CLASS__ . '::validate_boolean',
273
				),
274
			)
275
		) );
276
277
		// Return a single module and update it when needed
278
		register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)', array(
279
			'methods' => WP_REST_Server::READABLE,
280
			'callback' => array( $core_api_endpoint, 'process' ),
281
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
282
		) );
283
284
		// Activate and deactivate a module
285
		register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)/active', array(
286
			'methods' => WP_REST_Server::EDITABLE,
287
			'callback' => array( $module_toggle_endpoint, 'process' ),
288
			'permission_callback' => array( $module_toggle_endpoint, 'can_request' ),
289
			'args' => array(
290
				'active' => array(
291
					'default'           => true,
292
					'type'              => 'boolean',
293
					'required'          => true,
294
					'validate_callback' => __CLASS__ . '::validate_boolean',
295
				),
296
			)
297
		) );
298
299
		// Update a module
300
		register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)', array(
301
			'methods' => WP_REST_Server::EDITABLE,
302
			'callback' => array( $core_api_endpoint, 'process' ),
303
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
304
			'args' => self::get_updateable_parameters( 'any' )
305
		) );
306
307
		// Get data for a specific module, i.e. Protect block count, WPCOM stats,
308
		// Akismet spam count, etc.
309
		register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)/data', array(
310
			'methods' => WP_REST_Server::READABLE,
311
			'callback' => array( $module_data_endpoint, 'process' ),
312
			'permission_callback' => array( $module_data_endpoint, 'can_request' ),
313
			'args' => array(
314
				'range' => array(
315
					'default'           => 'day',
316
					'type'              => 'string',
317
					'required'          => false,
318
					'validate_callback' => __CLASS__ . '::validate_string',
319
				),
320
			)
321
		) );
322
323
		// Check if the API key for a specific service is valid or not
324
		register_rest_route( 'jetpack/v4', '/module/(?P<service>[a-z\-]+)/key/check', 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
		register_rest_route( 'jetpack/v4', '/module/(?P<service>[a-z\-]+)/key/check', array(
332
			'methods' => WP_REST_Server::EDITABLE,
333
			'callback' => array( $module_data_endpoint, 'key_check' ),
334
			'permission_callback' => __CLASS__ . '::update_settings_permission_check',
335
			'sanitize_callback' => 'sanitize_text_field',
336
			'args' => array(
337
				'api_key' => array(
338
					'default'           => '',
339
					'type'              => 'string',
340
					'validate_callback' => __CLASS__ . '::validate_alphanum',
341
				),
342
			)
343
		) );
344
345
		// Update any Jetpack module option or setting
346
		register_rest_route( 'jetpack/v4', '/settings', array(
347
			'methods' => WP_REST_Server::EDITABLE,
348
			'callback' => array( $core_api_endpoint, 'process' ),
349
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
350
			'args' => self::get_updateable_parameters( 'any' )
351
		) );
352
353
		// Update a module
354
		register_rest_route( 'jetpack/v4', '/settings/(?P<slug>[a-z\-]+)', 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()
359
		) );
360
361
		// Return all module settings
362
		register_rest_route( 'jetpack/v4', '/settings/', array(
363
			'methods' => WP_REST_Server::READABLE,
364
			'callback' => array( $core_api_endpoint, 'process' ),
365
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
366
		) );
367
368
		// Reset all Jetpack options
369
		register_rest_route( 'jetpack/v4', '/options/(?P<options>[a-z\-]+)', array(
370
			'methods' => WP_REST_Server::EDITABLE,
371
			'callback' => __CLASS__ . '::reset_jetpack_options',
372
			'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
373
		) );
374
375
		// Updates: get number of plugin updates available
376
		register_rest_route( 'jetpack/v4', '/updates/plugins', array(
377
			'methods' => WP_REST_Server::READABLE,
378
			'callback' => __CLASS__ . '::get_plugin_update_count',
379
			'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
380
		) );
381
382
		// Dismiss Jetpack Notices
383
		register_rest_route( 'jetpack/v4', '/notice/(?P<notice>[a-z\-_]+)', array(
384
			'methods' => WP_REST_Server::EDITABLE,
385
			'callback' => __CLASS__ . '::dismiss_notice',
386
			'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
387
		) );
388
389
		// Plugins: get list of all plugins.
390
		register_rest_route( 'jetpack/v4', '/plugins', array(
391
			'methods' => WP_REST_Server::READABLE,
392
			'callback' => __CLASS__ . '::get_plugins',
393
			'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
394
		) );
395
396
		register_rest_route( 'jetpack/v4', '/plugins/akismet/activate', array(
397
			'methods' => WP_REST_Server::EDITABLE,
398
			'callback' => __CLASS__ . '::activate_akismet',
399
			'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
400
		) );
401
402
		// Plugins: check if the plugin is active.
403
		register_rest_route( 'jetpack/v4', '/plugin/(?P<plugin>[a-z\/\.\-_]+)', array(
404
			'methods' => WP_REST_Server::READABLE,
405
			'callback' => __CLASS__ . '::get_plugin',
406
			'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
407
		) );
408
409
		// Widgets: get information about a widget that supports it.
410
		register_rest_route( 'jetpack/v4', '/widgets/(?P<id>[0-9a-z\-_]+)', array(
411
			'methods' => WP_REST_Server::READABLE,
412
			'callback' => array( $widget_endpoint, 'process' ),
413
			'permission_callback' => array( $widget_endpoint, 'can_request' ),
414
		) );
415
416
		// Site Verify: check if the site is verified, and a get verification token if not
417
		register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)', array(
418
			'methods' => WP_REST_Server::READABLE,
419
			'callback' => __CLASS__ . '::is_site_verified_and_token',
420
			'permission_callback' => __CLASS__ . '::update_settings_permission_check',
421
		) );
422
423
		register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)/(?<keyring_id>[0-9]+)', array(
424
			'methods' => WP_REST_Server::READABLE,
425
			'callback' => __CLASS__ . '::is_site_verified_and_token',
426
			'permission_callback' => __CLASS__ . '::update_settings_permission_check',
427
		) );
428
429
		// Site Verify: tell a service to verify the site
430
		register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)', array(
431
			'methods' => WP_REST_Server::EDITABLE,
432
			'callback' => __CLASS__ . '::verify_site',
433
			'permission_callback' => __CLASS__ . '::update_settings_permission_check',
434
			'args' => array(
435
				'keyring_id' => array(
436
					'required'          => true,
437
					'type'              => 'integer',
438
					'validate_callback' => __CLASS__  . '::validate_posint',
439
				),
440
			)
441
		) );
442
443
		// Get and set API keys.
444
		// Note: permission_callback intentionally omitted from the GET method.
445
		// Map block requires open access to API keys on the front end.
446
		register_rest_route(
447
			'jetpack/v4',
448
			'/service-api-keys/(?P<service>[a-z\-_]+)',
449
			array(
450
				array(
451
					'methods'             => WP_REST_Server::READABLE,
452
					'callback'            => __CLASS__ . '::get_service_api_key',
453
				),
454
				array(
455
					'methods'             => WP_REST_Server::EDITABLE,
456
					'callback'            => __CLASS__ . '::update_service_api_key',
457
					'permission_callback' => array( 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys','edit_others_posts_check' ),
458
					'args'                => array(
459
						'service_api_key' => array(
460
							'required' => true,
461
							'type'     => 'text',
462
						),
463
					),
464
				),
465
				array(
466
					'methods'             => WP_REST_Server::DELETABLE,
467
					'callback'            => __CLASS__ . '::delete_service_api_key',
468
					'permission_callback' => array( 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys','edit_others_posts_check' ),
469
				),
470
			)
471
		);
472
473
		register_rest_route(
474
			'jetpack/v4',
475
			'/mobile/send-login-email',
476
			array(
477
				'methods'             => WP_REST_Server::EDITABLE,
478
				'callback'            => __CLASS__ . '::send_mobile_magic_link',
479
				'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
480
			)
481
		);
482
	}
483
484
	public static function get_plans( $request ) {
485
		$request = Client::wpcom_json_api_request_as_user(
486
			'/plans?_locale=' . get_user_locale(),
487
			'2',
488
			array(
489
				'method'  => 'GET',
490
				'headers' => array(
491
					'X-Forwarded-For' => Jetpack::current_user_ip( true ),
492
				),
493
			)
494
		);
495
496
		$body = json_decode( wp_remote_retrieve_body( $request ) );
497
		if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
498
			$data = $body;
499
		} else {
500
			// something went wrong so we'll just return the response without caching
501
			return $body;
502
		}
503
504
		return $data;
505
	}
506
507
	public static function submit_survey( $request ) {
508
509
		$wpcom_request = Client::wpcom_json_api_request_as_user(
510
			'/marketing/survey',
511
			'v2',
512
			array(
513
				'method'  => 'POST',
514
				'headers' => array(
515
					'Content-Type'    => 'application/json',
516
					'X-Forwarded-For' => Jetpack::current_user_ip( true ),
517
				),
518
			),
519
			$request->get_json_params()
520
		);
521
522
		$wpcom_request_body = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
523
		if ( 200 === wp_remote_retrieve_response_code( $wpcom_request ) ) {
524
			$data = $wpcom_request_body;
525
		} else {
526
			// something went wrong so we'll just return the response without caching
527
			return $wpcom_request_body;
528
		}
529
530
		return $data;
531
	}
532
533
	/**
534
	 * Asks for a jitm, unless they've been disabled, in which case it returns an empty array
535
	 *
536
	 * @param $request WP_REST_Request
537
	 *
538
	 * @return array An array of jitms
539
	 */
540
	public static function get_jitm_message( $request ) {
541
		$jitm = new JITM();
542
543
		if ( ! $jitm->register() ) {
544
			return array();
545
		}
546
547
		return $jitm->get_messages( $request['message_path'], urldecode_deep( $request['query'] ) );
548
	}
549
550
	/**
551
	 * Dismisses a jitm
552
	 * @param $request WP_REST_Request The request
553
	 *
554
	 * @return bool Always True
555
	 */
556
	public static function delete_jitm_message( $request ) {
557
		$jitm = new JITM();
558
559
		if ( ! $jitm->register() ) {
560
			return true;
561
		}
562
563
		return $jitm->dismiss( $request['id'], $request['feature_class'] );
564
	}
565
566
	/**
567
	 * Checks if this site has been verified using a service - only 'google' supported at present - and a specfic
568
	 *  keyring to use to get the token if it is not
569
	 *
570
	 * Returns 'verified' = true/false, and a token if 'verified' is false and site is ready for verification
571
	 *
572
	 * @since 6.6.0
573
	 *
574
	 * @param WP_REST_Request $request The request sent to the WP REST API.
575
	 *
576
	 * @return array|wp-error
577
	 */
578
	public static function is_site_verified_and_token( $request ) {
579
		/**
580
		 * Return an error if the site uses a Maintenance / Coming Soon plugin
581
		 * and if the plugin is configured to make the site private.
582
		 *
583
		 * We currently handle the following plugins:
584
		 * - https://github.com/mojoness/mojo-marketplace-wp-plugin (used by bluehost)
585
		 * - https://wordpress.org/plugins/mojo-under-construction
586
		 * - https://wordpress.org/plugins/under-construction-page
587
		 * - https://wordpress.org/plugins/ultimate-under-construction
588
		 * - https://wordpress.org/plugins/coming-soon
589
		 *
590
		 * You can handle this in your own plugin thanks to the `jetpack_is_under_construction_plugin` filter.
591
		 * If the filter returns true, we will consider the site as under construction.
592
		 */
593
		$mm_coming_soon                       = get_option( 'mm_coming_soon', null );
594
		$under_construction_activation_status = get_option( 'underConstructionActivationStatus', null );
595
		$ucp_options                          = get_option( 'ucp_options', array() );
596
		$uuc_settings                         = get_option( 'uuc_settings', array() );
597
		$csp4                                 = get_option( 'seed_csp4_settings_content', array() );
598
		if (
599
			( Jetpack::is_plugin_active( 'mojo-marketplace-wp-plugin/mojo-marketplace.php' ) && 'true' === $mm_coming_soon )
600
			|| Jetpack::is_plugin_active( 'mojo-under-construction/mojo-contruction.php' ) && 1 == $under_construction_activation_status // WPCS: loose comparison ok.
601
			|| ( Jetpack::is_plugin_active( 'under-construction-page/under-construction.php' ) && isset( $ucp_options['status'] ) && 1 == $ucp_options['status'] ) // WPCS: loose comparison ok.
602
			|| ( Jetpack::is_plugin_active( 'ultimate-under-construction/ultimate-under-construction.php' ) && isset( $uuc_settings['enable'] ) && 1 == $uuc_settings['enable'] ) // WPCS: loose comparison ok.
603
			|| ( Jetpack::is_plugin_active( 'coming-soon/coming-soon.php' ) &&  isset( $csp4['status'] ) && ( 1 == $csp4['status'] || 2 == $csp4['status'] ) ) // WPCS: loose comparison ok.
604
			/**
605
			 * Allow plugins to mark a site as "under construction".
606
			 *
607
			 * @since 6.7.0
608
			 *
609
			 * @param false bool Is the site under construction? Default to false.
610
			 */
611
			|| true === apply_filters( 'jetpack_is_under_construction_plugin', false )
612
		) {
613
			return new WP_Error( 'forbidden', __( 'Site is under construction and cannot be verified', 'jetpack' ) );
614
		}
615
616
 		$xml = new Jetpack_IXR_Client( array(
617
 			'user_id' => get_current_user_id(),
618
		) );
619
620
		$args = array(
621
			'user_id' => get_current_user_id(),
622
			'service' => $request[ 'service' ],
623
		);
624
625
		if ( isset( $request[ 'keyring_id' ] ) ) {
626
			$args[ 'keyring_id' ] = $request[ 'keyring_id' ];
627
		}
628
629
		$xml->query( 'jetpack.isSiteVerified', $args );
630
631
		if ( $xml->isError() ) {
632
			return new WP_Error( 'error_checking_if_site_verified_google', sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
633
		} else {
634
			return $xml->getResponse();
635
		}
636
	}
637
638
639
640
	public static function verify_site( $request ) {
641
		$xml = new Jetpack_IXR_Client( array(
642
			'user_id' => get_current_user_id(),
643
		) );
644
645
		$params = $request->get_json_params();
646
647
		$xml->query( 'jetpack.verifySite', array(
648
				'user_id' => get_current_user_id(),
649
				'service' => $request[ 'service' ],
650
				'keyring_id' => $params[ 'keyring_id' ],
651
			)
652
		);
653
654
		if ( $xml->isError() ) {
655
			return new WP_Error( 'error_verifying_site_google', sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
656
		} else {
657
			$response = $xml->getResponse();
658
659
			if ( ! empty( $response['errors'] ) ) {
660
				$error = new WP_Error;
661
				$error->errors = $response['errors'];
662
				return $error;
663
			}
664
665
			return $response;
666
		}
667
	}
668
669
	/**
670
	 * Handles verification that a site is registered
671
	 *
672
	 * @since 5.4.0
673
	 *
674
	 * @param WP_REST_Request $request The request sent to the WP REST API.
675
	 *
676
	 * @return array|wp-error
677
	 */
678
	 public static function remote_authorize( $request ) {
679
		$xmlrpc_server = new Jetpack_XMLRPC_Server();
680
		$result = $xmlrpc_server->remote_authorize( $request );
681
682
		if ( is_a( $result, 'IXR_Error' ) ) {
683
			$result = new WP_Error( $result->code, $result->message );
684
		}
685
686
		return $result;
687
	 }
688
689
	/**
690
	 * Handles dismissing of Jetpack Notices
691
	 *
692
	 * @since 4.3.0
693
	 *
694
	 * @param WP_REST_Request $request The request sent to the WP REST API.
695
	 *
696
	 * @return array|wp-error
697
	 */
698
	public static function dismiss_notice( $request ) {
699
		$notice = $request['notice'];
700
701
		if ( ! isset( $request['dismissed'] ) || $request['dismissed'] !== true ) {
702
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid parameter "dismissed".', 'jetpack' ), array( 'status' => 404 ) );
703
		}
704
705
		if ( isset( $notice ) && ! empty( $notice ) ) {
706
			switch( $notice ) {
707
				case 'feedback_dash_request':
708
				case 'welcome':
709
					$notices = get_option( 'jetpack_dismissed_notices', array() );
710
					$notices[ $notice ] = true;
711
					update_option( 'jetpack_dismissed_notices', $notices );
712
					return rest_ensure_response( get_option( 'jetpack_dismissed_notices', array() ) );
713
714
				default:
715
					return new WP_Error( 'invalid_param', esc_html__( 'Invalid parameter "notice".', 'jetpack' ), array( 'status' => 404 ) );
716
			}
717
		}
718
719
		return new WP_Error( 'required_param', esc_html__( 'Missing parameter "notice".', 'jetpack' ), array( 'status' => 404 ) );
720
	}
721
722
	/**
723
	 * Verify that the user can disconnect the site.
724
	 *
725
	 * @since 4.3.0
726
	 *
727
	 * @return bool|WP_Error True if user is able to disconnect the site.
728
	 */
729 View Code Duplication
	public static function disconnect_site_permission_callback() {
730
		if ( current_user_can( 'jetpack_disconnect' ) ) {
731
			return true;
732
		}
733
734
		return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
735
736
	}
737
738
	/**
739
	 * Verify that the user can get a connect/link URL
740
	 *
741
	 * @since 4.3.0
742
	 *
743
	 * @return bool|WP_Error True if user is able to disconnect the site.
744
	 */
745 View Code Duplication
	public static function connect_url_permission_callback() {
746
		if ( current_user_can( 'jetpack_connect_user' ) ) {
747
			return true;
748
		}
749
750
		return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'invalid_user_permission_jetpack_disconnect'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
751
752
	}
753
754
	/**
755
	 * Verify that a user can get the data about the current user.
756
	 * Only those who can connect.
757
	 *
758
	 * @since 4.3.0
759
	 *
760
	 * @uses Jetpack::is_user_connected();
761
	 *
762
	 * @return bool|WP_Error True if user is able to unlink.
763
	 */
764
	public static function get_user_connection_data_permission_callback() {
765
		if ( current_user_can( 'jetpack_connect_user' ) ) {
766
			return true;
767
		}
768
769
		return new WP_Error( 'invalid_user_permission_user_connection_data', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
770
	}
771
772
	/**
773
	 * Check that user has permission to change the master user.
774
	 *
775
	 * @since 6.2.0
776
	 * @since 7.7.0 Update so that any user with jetpack_disconnect privs can set owner.
777
	 *
778
	 * @return bool|WP_Error True if user is able to change master user.
779
	 */
780 View Code Duplication
	public static function set_connection_owner_permission_callback() {
781
		if ( current_user_can( 'jetpack_disconnect' ) ) {
782
			return true;
783
		}
784
785
		return new WP_Error( 'invalid_user_permission_set_connection_owner', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
786
	}
787
788
	/**
789
	 * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
790
	 *
791
	 * @since 4.3.0
792
	 *
793
	 * @uses Jetpack::is_user_connected();
794
	 *
795
	 * @return bool|WP_Error True if user is able to unlink.
796
	 */
797
	public static function unlink_user_permission_callback() {
798
		if ( current_user_can( 'jetpack_connect_user' ) && Jetpack::is_user_connected( get_current_user_id() ) ) {
799
			return true;
800
		}
801
802
		return new WP_Error( 'invalid_user_permission_unlink_user', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
803
	}
804
805
	/**
806
	 * Verify that user can manage Jetpack modules.
807
	 *
808
	 * @since 4.3.0
809
	 *
810
	 * @return bool Whether user has the capability 'jetpack_manage_modules'.
811
	 */
812
	public static function manage_modules_permission_check() {
813
		if ( current_user_can( 'jetpack_manage_modules' ) ) {
814
			return true;
815
		}
816
817
		return new WP_Error( 'invalid_user_permission_manage_modules', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
818
	}
819
820
	/**
821
	 * Verify that user can update Jetpack modules.
822
	 *
823
	 * @since 4.3.0
824
	 *
825
	 * @return bool Whether user has the capability 'jetpack_configure_modules'.
826
	 */
827 View Code Duplication
	public static function configure_modules_permission_check() {
828
		if ( current_user_can( 'jetpack_configure_modules' ) ) {
829
			return true;
830
		}
831
832
		return new WP_Error( 'invalid_user_permission_configure_modules', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
833
	}
834
835
	/**
836
	 * Verify that user can view Jetpack admin page.
837
	 *
838
	 * @since 4.3.0
839
	 *
840
	 * @return bool Whether user has the capability 'jetpack_admin_page'.
841
	 */
842 View Code Duplication
	public static function view_admin_page_permission_check() {
843
		if ( current_user_can( 'jetpack_admin_page' ) ) {
844
			return true;
845
		}
846
847
		return new WP_Error( 'invalid_user_permission_view_admin', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
848
	}
849
850
	/**
851
	 * Verify that user can mitigate an identity crisis.
852
	 *
853
	 * @since 4.4.0
854
	 *
855
	 * @return bool Whether user has capability 'jetpack_disconnect'.
856
	 */
857 View Code Duplication
	public static function identity_crisis_mitigation_permission_check() {
858
		if ( current_user_can( 'jetpack_disconnect' ) ) {
859
			return true;
860
		}
861
862
		return new WP_Error( 'invalid_user_permission_identity_crisis', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
863
	}
864
865
	/**
866
	 * Verify that user can update Jetpack general settings.
867
	 *
868
	 * @since 4.3.0
869
	 *
870
	 * @return bool Whether user has the capability 'update_settings_permission_check'.
871
	 */
872 View Code Duplication
	public static function update_settings_permission_check() {
873
		if ( current_user_can( 'jetpack_configure_modules' ) ) {
874
			return true;
875
		}
876
877
		return new WP_Error( 'invalid_user_permission_manage_settings', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
878
	}
879
880
	/**
881
	 * Verify that user can view Jetpack admin page and can activate plugins.
882
	 *
883
	 * @since 4.3.0
884
	 *
885
	 * @return bool Whether user has the capability 'jetpack_admin_page' and 'activate_plugins'.
886
	 */
887 View Code Duplication
	public static function activate_plugins_permission_check() {
888
		if ( current_user_can( 'jetpack_admin_page' ) && current_user_can( 'activate_plugins' ) ) {
889
			return true;
890
		}
891
892
		return new WP_Error( 'invalid_user_permission_activate_plugins', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
893
	}
894
895
	/**
896
	 * Verify that user can edit other's posts (Editors and Administrators).
897
	 *
898
	 * @return bool Whether user has the capability 'edit_others_posts'.
899
	 */
900
	public static function edit_others_posts_check() {
901
		if ( current_user_can( 'edit_others_posts' ) ) {
902
			return true;
903
		}
904
905
		return new WP_Error( 'invalid_user_permission_edit_others_posts', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
906
	}
907
908
	/**
909
	 * Contextual HTTP error code for authorization failure.
910
	 *
911
	 * Taken from rest_authorization_required_code() in WP-API plugin until is added to core.
912
	 * @see https://github.com/WP-API/WP-API/commit/7ba0ae6fe4f605d5ffe4ee85b1cd5f9fb46900a6
913
	 *
914
	 * @since 4.3.0
915
	 *
916
	 * @return int
917
	 */
918
	public static function rest_authorization_required_code() {
919
		return is_user_logged_in() ? 403 : 401;
920
	}
921
922
	/**
923
	 * Get connection status for this Jetpack site.
924
	 *
925
	 * @since 4.3.0
926
	 *
927
	 * @return bool True if site is connected
928
	 */
929
	public static function jetpack_connection_status() {
930
		return rest_ensure_response( array(
931
				'isActive'     => Jetpack::is_active(),
932
				'isStaging'    => Jetpack::is_staging_site(),
933
				'isRegistered' => Jetpack::connection()->is_registered(),
934
				'devMode'      => array(
935
					'isActive' => ( new Status() )->is_development_mode(),
936
					'constant' => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
937
					'url'      => site_url() && false === strpos( site_url(), '.' ),
938
					'filter'   => apply_filters( 'jetpack_development_mode', false ),
939
				),
940
			)
941
		);
942
	}
943
944
	/**
945
	 * Test connection status for this Jetpack site.
946
	 *
947
	 * @since 6.8.0
948
	 *
949
	 * @return array|WP_Error WP_Error returned if connection test does not succeed.
950
	 */
951
	public static function jetpack_connection_test() {
952
		jetpack_require_lib( 'debugger' );
953
		$cxntests = new Jetpack_Cxn_Tests();
954
955
		if ( $cxntests->pass() ) {
956
			return rest_ensure_response(
957
				array(
958
					'code'    => 'success',
959
					'message' => __( 'All connection tests passed.', 'jetpack' ),
960
				)
961
			);
962
		} else {
963
			return $cxntests->output_fails_as_wp_error();
964
		}
965
	}
966
967
	/**
968
	 * Test connection permission check method.
969
	 *
970
	 * @since 7.1.0
971
	 *
972
	 * @return bool
973
	 */
974
	public static function view_jetpack_connection_test_check() {
975
		if ( ! isset( $_GET['signature'], $_GET['timestamp'], $_GET['url'] ) ) {
976
			return false;
977
		}
978
		$signature = base64_decode( $_GET['signature'] );
979
980
		$signature_data = wp_json_encode(
981
			array(
982
				'rest_route' => $_GET['rest_route'],
983
				'timestamp' => intval( $_GET['timestamp'] ),
984
				'url' => wp_unslash( $_GET['url'] ),
985
			)
986
		);
987
988
		if (
989
			! function_exists( 'openssl_verify' )
990
			|| ! openssl_verify(
991
				$signature_data,
992
				$signature,
993
				JETPACK__DEBUGGER_PUBLIC_KEY
994
			)
995
		) {
996
			return false;
997
		}
998
999
		// signature timestamp must be within 5min of current time
1000
		if ( abs( time() - intval( $_GET['timestamp'] ) ) > 300 ) {
1001
			return false;
1002
		}
1003
1004
		return true;
1005
	}
1006
1007
	/**
1008
	 * Test connection status for this Jetpack site, encrypt the results for decryption by a third-party.
1009
	 *
1010
	 * @since 7.1.0
1011
	 *
1012
	 * @return array|mixed|object|WP_Error
1013
	 */
1014
	public static function jetpack_connection_test_for_external() {
1015
		// Since we are running this test for inclusion in the WP.com testing suite, let's not try to run them as part of these results.
1016
		add_filter( 'jetpack_debugger_run_self_test', '__return_false' );
1017
		jetpack_require_lib( 'debugger' );
1018
		$cxntests = new Jetpack_Cxn_Tests();
1019
1020
		if ( $cxntests->pass() ) {
1021
			$result = array(
1022
				'code'    => 'success',
1023
				'message' => __( 'All connection tests passed.', 'jetpack' ),
1024
			);
1025
		} else {
1026
			$error  = $cxntests->output_fails_as_wp_error(); // Using this so the output is similar both ways.
1027
			$errors = array();
1028
1029
			// Borrowed from WP_REST_Server::error_to_response().
1030
			foreach ( (array) $error->errors as $code => $messages ) {
1031
				foreach ( (array) $messages as $message ) {
1032
					$errors[] = array(
1033
						'code'    => $code,
1034
						'message' => $message,
1035
						'data'    => $error->get_error_data( $code ),
1036
					);
1037
				}
1038
			}
1039
1040
			$result = $errors[0];
1041
			if ( count( $errors ) > 1 ) {
1042
				// Remove the primary error.
1043
				array_shift( $errors );
1044
				$result['additional_errors'] = $errors;
1045
			}
1046
		}
1047
1048
		$result = wp_json_encode( $result );
1049
1050
		$encrypted = $cxntests->encrypt_string_for_wpcom( $result );
1051
1052
		if ( ! $encrypted || ! is_array( $encrypted ) ) {
1053
			return rest_ensure_response(
1054
				array(
1055
					'code'    => 'action_required',
1056
					'message' => 'Please request results from the in-plugin debugger',
1057
				)
1058
			);
1059
		}
1060
1061
		return rest_ensure_response(
1062
			array(
1063
				'code'  => 'response',
1064
				'debug' => array(
1065
					'data' => $encrypted['data'],
1066
					'key'  => $encrypted['key'],
1067
				),
1068
			)
1069
		);
1070
	}
1071
1072
	public static function rewind_data() {
1073
		$site_id = Jetpack_Options::get_option( 'id' );
1074
1075
		if ( ! $site_id ) {
1076
			return new WP_Error( 'site_id_missing' );
1077
		}
1078
1079
		$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/rewind', $site_id ) .'?force=wpcom', '2', array(), null, 'wpcom' );
1080
1081
		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1082
			return new WP_Error( 'rewind_data_fetch_failed' );
1083
		}
1084
1085
		$body = wp_remote_retrieve_body( $response );
1086
1087
		return json_decode( $body );
1088
	}
1089
1090
	/**
1091
	 * Get rewind data
1092
	 *
1093
	 * @since 5.7.0
1094
	 *
1095
	 * @return array Array of rewind properties.
1096
	 */
1097
	public static function get_rewind_data() {
1098
		$rewind_data = self::rewind_data();
1099
1100 View Code Duplication
		if ( ! is_wp_error( $rewind_data ) ) {
1101
			return rest_ensure_response( array(
1102
					'code' => 'success',
1103
					'message' => esc_html__( 'Backup & Scan data correctly received.', 'jetpack' ),
1104
					'data' => wp_json_encode( $rewind_data ),
1105
				)
1106
			);
1107
		}
1108
1109 View Code Duplication
		if ( $rewind_data->get_error_code() === 'rewind_data_fetch_failed' ) {
1110
			return new WP_Error( 'rewind_data_fetch_failed', esc_html__( 'Failed fetching rewind data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
1111
		}
1112
1113 View Code Duplication
		if ( $rewind_data->get_error_code() === 'site_id_missing' ) {
1114
			return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
1115
		}
1116
1117
		return new WP_Error(
1118
			'error_get_rewind_data',
1119
			esc_html__( 'Could not retrieve Backup & Scan data.', 'jetpack' ),
1120
			array( 'status' => 500 )
1121
		);
1122
	}
1123
1124
	/**
1125
	 * Disconnects Jetpack from the WordPress.com Servers
1126
	 *
1127
	 * @uses Jetpack::disconnect();
1128
	 * @since 4.3.0
1129
	 *
1130
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1131
	 *
1132
	 * @return bool|WP_Error True if Jetpack successfully disconnected.
1133
	 */
1134 View Code Duplication
	public static function disconnect_site( $request ) {
1135
1136
		if ( ! isset( $request['isActive'] ) || $request['isActive'] !== false ) {
1137
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
1138
		}
1139
1140
		if ( Jetpack::is_active() ) {
1141
			Jetpack::disconnect();
1142
			return rest_ensure_response( array( 'code' => 'success' ) );
1143
		}
1144
1145
		return new WP_Error( 'disconnect_failed', esc_html__( 'Was not able to disconnect the site.  Please try again.', 'jetpack' ), array( 'status' => 400 ) );
1146
	}
1147
1148
	/**
1149
	 * Registers the Jetpack site
1150
	 *
1151
	 * @uses Jetpack::try_registration();
1152
	 * @since 7.7.0
1153
	 *
1154
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1155
	 *
1156
	 * @return bool|WP_Error True if Jetpack successfully registered
1157
	 */
1158
	public static function register_site( $request ) {
1159
		if ( ! wp_verify_nonce( $request->get_param( 'registration_nonce' ), 'jetpack-registration-nonce' ) ) {
1160
			return new WP_Error( 'invalid_nonce', __( 'Unable to verify your request.', 'jetpack' ), array( 'status' => 403 ) );
1161
		}
1162
1163
		$response = Jetpack::try_registration();
1164
1165
		if ( is_wp_error( $response ) ) {
1166
			return $response;
1167
		}
1168
1169
		return rest_ensure_response(
1170
			array(
1171
				'authorizeUrl' => Jetpack::build_authorize_url( false, true )
1172
			) );
1173
	}
1174
1175
	/**
1176
	 * Gets a new connect raw URL with fresh nonce.
1177
	 *
1178
	 * @uses Jetpack::disconnect();
1179
	 * @since 4.3.0
1180
	 *
1181
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1182
	 *
1183
	 * @return string|WP_Error A raw URL if the connection URL could be built; error message otherwise.
1184
	 */
1185
	public static function build_connect_url() {
1186
		$url = Jetpack::init()->build_connect_url( true, false, false );
1187
		if ( $url ) {
1188
			return rest_ensure_response( $url );
1189
		}
1190
1191
		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 ) );
1192
	}
1193
1194
	/**
1195
	 * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
1196
	 * Information about the master/primary user.
1197
	 * Information about the current user.
1198
	 *
1199
	 * @since 4.3.0
1200
	 *
1201
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1202
	 *
1203
	 * @return object
1204
	 */
1205
	public static function get_user_connection_data() {
1206
		require_once( JETPACK__PLUGIN_DIR . '_inc/lib/admin-pages/class.jetpack-react-page.php' );
1207
1208
		$response = array(
1209
//			'othersLinked' => Jetpack::get_other_linked_admins(),
1210
			'currentUser'  => jetpack_current_user_data(),
1211
		);
1212
		return rest_ensure_response( $response );
1213
	}
1214
1215
	/**
1216
	 * Change the master user.
1217
	 *
1218
	 * @since 6.2.0
1219
	 *
1220
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1221
	 *
1222
	 * @return bool|WP_Error True if owner successfully changed.
1223
	 */
1224
	public static function set_connection_owner( $request ) {
1225
		if ( ! isset( $request['owner'] ) ) {
1226
			return new WP_Error(
1227
				'invalid_param',
1228
				esc_html__( 'Invalid Parameter', 'jetpack' ),
1229
				array( 'status' => 400 )
1230
			);
1231
		}
1232
1233
		$new_owner_id = $request['owner'];
1234
		if ( ! user_can( $new_owner_id, 'administrator' ) ) {
1235
			return new WP_Error(
1236
				'new_owner_not_admin',
1237
				esc_html__( 'New owner is not admin', 'jetpack' ),
1238
				array( 'status' => 400 )
1239
			);
1240
		}
1241
1242
		if ( $new_owner_id === get_current_user_id() ) {
1243
			return new WP_Error(
1244
				'new_owner_is_current_user',
1245
				esc_html__( 'New owner is same as current user', 'jetpack' ),
1246
				array( 'status' => 400 )
1247
			);
1248
		}
1249
1250
		if ( ! Jetpack::is_user_connected( $new_owner_id ) ) {
1251
			return new WP_Error(
1252
				'new_owner_not_connected',
1253
				esc_html__( 'New owner is not connected', 'jetpack' ),
1254
				array( 'status' => 400 )
1255
			);
1256
		}
1257
1258
		// Update the master user in Jetpack
1259
		$updated = Jetpack_Options::update_option( 'master_user', $new_owner_id );
1260
1261
		// Notify WPCOM about the master user change
1262
		$xml = new Jetpack_IXR_Client( array(
1263
			'user_id' => get_current_user_id(),
1264
		) );
1265
		$xml->query( 'jetpack.switchBlogOwner', array(
1266
			'new_blog_owner' => $new_owner_id,
1267
		) );
1268
1269
		if ( $updated && ! $xml->isError() ) {
1270
1271
			// Track it
1272
			if ( class_exists( 'Automattic\Jetpack\Tracking' ) ) {
1273
				$tracking = new Tracking();
1274
				$tracking->record_user_event( 'set_connection_owner_success' );
1275
			}
1276
1277
			return rest_ensure_response(
1278
				array(
1279
					'code' => 'success',
1280
				)
1281
			);
1282
		}
1283
		return new WP_Error(
1284
			'error_setting_new_owner',
1285
			esc_html__( 'Could not confirm new owner.', 'jetpack' ),
1286
			array( 'status' => 500 )
1287
		);
1288
	}
1289
1290
	/**
1291
	 * Unlinks current user from the WordPress.com Servers.
1292
	 *
1293
	 * @since 4.3.0
1294
	 * @uses  Automattic\Jetpack\Connection\Manager::disconnect_user
1295
	 *
1296
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1297
	 *
1298
	 * @return bool|WP_Error True if user successfully unlinked.
1299
	 */
1300 View Code Duplication
	public static function unlink_user( $request ) {
1301
1302
		if ( ! isset( $request['linked'] ) || $request['linked'] !== false ) {
1303
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
1304
		}
1305
1306
		if ( Connection_Manager::disconnect_user() ) {
1307
			return rest_ensure_response(
1308
				array(
1309
					'code' => 'success'
1310
				)
1311
			);
1312
		}
1313
1314
		return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user.  Please try again.', 'jetpack' ), array( 'status' => 400 ) );
1315
	}
1316
1317
	/**
1318
	 * Gets current user's tracking settings.
1319
	 *
1320
	 * @since 6.0.0
1321
	 *
1322
	 * @param  WP_REST_Request $request The request sent to the WP REST API.
1323
	 *
1324
	 * @return WP_REST_Response|WP_Error Response, else error.
1325
	 */
1326 View Code Duplication
	public static function get_user_tracking_settings( $request ) {
1327
		if ( ! Jetpack::is_user_connected() ) {
1328
			$response = array(
1329
				'tracks_opt_out' => true, // Default to opt-out if not connected to wp.com.
1330
			);
1331
		} else {
1332
			$response = Client::wpcom_json_api_request_as_user(
1333
				'/jetpack-user-tracking',
1334
				'v2',
1335
				array(
1336
					'method'  => 'GET',
1337
					'headers' => array(
1338
						'X-Forwarded-For' => Jetpack::current_user_ip( true ),
1339
					),
1340
				)
1341
			);
1342
			if ( ! is_wp_error( $response ) ) {
1343
				$response = json_decode( wp_remote_retrieve_body( $response ), true );
1344
			}
1345
		}
1346
1347
		return rest_ensure_response( $response );
1348
	}
1349
1350
	/**
1351
	 * Updates current user's tracking settings.
1352
	 *
1353
	 * @since 6.0.0
1354
	 *
1355
	 * @param  WP_REST_Request $request The request sent to the WP REST API.
1356
	 *
1357
	 * @return WP_REST_Response|WP_Error Response, else error.
1358
	 */
1359 View Code Duplication
	public static function update_user_tracking_settings( $request ) {
1360
		if ( ! Jetpack::is_user_connected() ) {
1361
			$response = array(
1362
				'tracks_opt_out' => true, // Default to opt-out if not connected to wp.com.
1363
			);
1364
		} else {
1365
			$response = Client::wpcom_json_api_request_as_user(
1366
				'/jetpack-user-tracking',
1367
				'v2',
1368
				array(
1369
					'method'  => 'PUT',
1370
					'headers' => array(
1371
						'Content-Type'    => 'application/json',
1372
						'X-Forwarded-For' => Jetpack::current_user_ip( true ),
1373
					),
1374
				),
1375
				wp_json_encode( $request->get_params() )
1376
			);
1377
			if ( ! is_wp_error( $response ) ) {
1378
				$response = json_decode( wp_remote_retrieve_body( $response ), true );
1379
			}
1380
		}
1381
1382
		return rest_ensure_response( $response );
1383
	}
1384
1385
	/**
1386
	 * Fetch site data from .com including the site's current plan.
1387
	 *
1388
	 * @since 5.5.0
1389
	 *
1390
	 * @return array Array of site properties.
1391
	 */
1392
	public static function site_data() {
1393
		$site_id = Jetpack_Options::get_option( 'id' );
1394
1395
		if ( ! $site_id ) {
1396
			new WP_Error( 'site_id_missing' );
1397
		}
1398
1399
		$args = array( 'headers' => array() );
1400
1401
		// Allow use a store sandbox. Internal ref: PCYsg-IA-p2.
1402
		if ( isset( $_COOKIE ) && isset( $_COOKIE['store_sandbox'] ) ) {
1403
			$secret                    = $_COOKIE['store_sandbox'];
1404
			$args['headers']['Cookie'] = "store_sandbox=$secret;";
1405
		}
1406
1407
		$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d', $site_id ) .'?force=wpcom', '1.1', $args );
1408
1409
		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1410
			return new WP_Error( 'site_data_fetch_failed' );
1411
		}
1412
1413
		Jetpack_Plan::update_from_sites_response( $response );
1414
1415
		$body = wp_remote_retrieve_body( $response );
1416
1417
		return json_decode( $body );
1418
	}
1419
	/**
1420
	 * Get site data, including for example, the site's current plan.
1421
	 *
1422
	 * @since 4.3.0
1423
	 *
1424
	 * @return array Array of site properties.
1425
	 */
1426
	public static function get_site_data() {
1427
		$site_data = self::site_data();
1428
1429 View Code Duplication
		if ( ! is_wp_error( $site_data ) ) {
1430
			return rest_ensure_response( array(
1431
					'code' => 'success',
1432
					'message' => esc_html__( 'Site data correctly received.', 'jetpack' ),
1433
					'data' => json_encode( $site_data ),
1434
				)
1435
			);
1436
		}
1437 View Code Duplication
		if ( $site_data->get_error_code() === 'site_data_fetch_failed' ) {
1438
			return new WP_Error( 'site_data_fetch_failed', esc_html__( 'Failed fetching site data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
1439
		}
1440
1441 View Code Duplication
		if ( $site_data->get_error_code() === 'site_id_missing' ) {
1442
			return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
1443
		}
1444
	}
1445
1446
	/**
1447
	 * Fetch AL data for this site and return it.
1448
	 *
1449
	 * @since 7.4
1450
	 *
1451
	 * @return array|WP_Error
1452
	 */
1453
	public static function get_site_activity() {
1454
		$site_id = Jetpack_Options::get_option( 'id' );
1455
1456
		if ( ! $site_id ) {
1457
			return new WP_Error(
1458
				'site_id_missing',
1459
				esc_html__( 'Site ID is missing.', 'jetpack' ),
1460
				array( 'status' => 400 )
1461
			);
1462
		}
1463
1464
		$response = Client::wpcom_json_api_request_as_user( "/sites/$site_id/activity", '2', array(
1465
			'method'  => 'GET',
1466
			'headers' => array(
1467
				'X-Forwarded-For' => Jetpack::current_user_ip( true ),
1468
			),
1469
		), null, 'wpcom' );
1470
		$response_code = wp_remote_retrieve_response_code( $response );
1471
1472 View Code Duplication
		if ( 200 !== $response_code ) {
1473
			return new WP_Error(
1474
				'activity_fetch_failed',
1475
				esc_html__( 'Could not retrieve site activity.', 'jetpack' ),
1476
				array( 'status' => $response_code )
1477
			);
1478
		}
1479
1480
		$data = json_decode( wp_remote_retrieve_body( $response ) );
1481
1482
		if ( ! isset( $data->current->orderedItems ) ) {
1483
			return new WP_Error(
1484
				'activity_not_found',
1485
				esc_html__( 'No activity found', 'jetpack' ),
1486
				array( 'status' => 204 ) // no content
1487
			);
1488
		}
1489
1490
		return rest_ensure_response( array(
1491
				'code' => 'success',
1492
				'data' => $data->current->orderedItems,
1493
			)
1494
		);
1495
	}
1496
1497
	/**
1498
	 * Handles identity crisis mitigation, confirming safe mode for this site.
1499
	 *
1500
	 * @since 4.4.0
1501
	 *
1502
	 * @return bool | WP_Error True if option is properly set.
1503
	 */
1504
	public static function confirm_safe_mode() {
1505
		$updated = Jetpack_Options::update_option( 'safe_mode_confirmed', true );
1506
		if ( $updated ) {
1507
			return rest_ensure_response(
1508
				array(
1509
					'code' => 'success'
1510
				)
1511
			);
1512
		}
1513
		return new WP_Error(
1514
			'error_setting_jetpack_safe_mode',
1515
			esc_html__( 'Could not confirm safe mode.', 'jetpack' ),
1516
			array( 'status' => 500 )
1517
		);
1518
	}
1519
1520
	/**
1521
	 * Handles identity crisis mitigation, migrating stats and subscribers from old url to this, new url.
1522
	 *
1523
	 * @since 4.4.0
1524
	 *
1525
	 * @return bool | WP_Error True if option is properly set.
1526
	 */
1527
	public static function migrate_stats_and_subscribers() {
1528
		if ( Jetpack_Options::get_option( 'sync_error_idc' ) && ! Jetpack_Options::delete_option( 'sync_error_idc' ) ) {
1529
			return new WP_Error(
1530
				'error_deleting_sync_error_idc',
1531
				esc_html__( 'Could not delete sync error option.', 'jetpack' ),
1532
				array( 'status' => 500 )
1533
			);
1534
		}
1535
1536
		if ( Jetpack_Options::get_option( 'migrate_for_idc' ) || Jetpack_Options::update_option( 'migrate_for_idc', true ) ) {
1537
			return rest_ensure_response(
1538
				array(
1539
					'code' => 'success'
1540
				)
1541
			);
1542
		}
1543
		return new WP_Error(
1544
			'error_setting_jetpack_migrate',
1545
			esc_html__( 'Could not confirm migration.', 'jetpack' ),
1546
			array( 'status' => 500 )
1547
		);
1548
	}
1549
1550
	/**
1551
	 * This IDC resolution will disconnect the site and re-connect to a completely new
1552
	 * and separate shadow site than the original.
1553
	 *
1554
	 * It will first will disconnect the site without phoning home as to not disturb the production site.
1555
	 * It then builds a fresh connection URL and sends it back along with the response.
1556
	 *
1557
	 * @since 4.4.0
1558
	 * @return bool|WP_Error
1559
	 */
1560
	public static function start_fresh_connection() {
1561
		// First clear the options / disconnect.
1562
		Jetpack::disconnect();
1563
		return self::build_connect_url();
1564
	}
1565
1566
	/**
1567
	 * Reset Jetpack options
1568
	 *
1569
	 * @since 4.3.0
1570
	 *
1571
	 * @param WP_REST_Request $request {
1572
	 *     Array of parameters received by request.
1573
	 *
1574
	 *     @type string $options Available options to reset are options|modules
1575
	 * }
1576
	 *
1577
	 * @return bool|WP_Error True if options were reset. Otherwise, a WP_Error instance with the corresponding error.
1578
	 */
1579
	public static function reset_jetpack_options( $request ) {
1580
1581
		if ( ! isset( $request['reset'] ) || $request['reset'] !== true ) {
1582
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
1583
		}
1584
1585
		if ( isset( $request['options'] ) ) {
1586
			$data = $request['options'];
1587
1588
			switch( $data ) {
1589
				case ( 'options' ) :
1590
					$options_to_reset = Jetpack::get_jetpack_options_for_reset();
1591
1592
					// Reset the Jetpack options
1593
					foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
1594
						Jetpack_Options::delete_option( $option_to_reset );
1595
					}
1596
1597
					foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
1598
						delete_option( $option_to_reset );
1599
					}
1600
1601
					// Reset to default modules
1602
					$default_modules = Jetpack::get_default_modules();
1603
					Jetpack::update_active_modules( $default_modules );
1604
1605
					return rest_ensure_response( array(
1606
						'code' 	  => 'success',
1607
						'message' => esc_html__( 'Jetpack options reset.', 'jetpack' ),
1608
					) );
1609
					break;
1610
1611
				case 'modules':
1612
					$default_modules = Jetpack::get_default_modules();
1613
					Jetpack::update_active_modules( $default_modules );
1614
					return rest_ensure_response( array(
1615
						'code' 	  => 'success',
1616
						'message' => esc_html__( 'Modules reset to default.', 'jetpack' ),
1617
					) );
1618
					break;
1619
1620
				default:
1621
					return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
1622
			}
1623
		}
1624
1625
		return new WP_Error( 'required_param', esc_html__( 'Missing parameter "type".', 'jetpack' ), array( 'status' => 404 ) );
1626
	}
1627
1628
	/**
1629
	 * Get the query parameters to update module options or general settings.
1630
	 *
1631
	 * @since 4.3.0
1632
	 * @since 4.4.0 Accepts a $selector parameter.
1633
	 *
1634
	 * @param string $selector Selects a set of options to update, Can be empty, a module slug or 'any'.
1635
	 *
1636
	 * @return array
1637
	 */
1638
	public static function get_updateable_parameters( $selector = '' ) {
1639
		$parameters = array(
1640
			'context'     => array(
1641
				'default' => 'edit',
1642
			),
1643
		);
1644
1645
		return array_merge( $parameters, self::get_updateable_data_list( $selector ) );
1646
	}
1647
1648
	/**
1649
	 * Returns a list of module options or general settings that can be updated.
1650
	 *
1651
	 * @since 4.3.0
1652
	 * @since 4.4.0 Accepts 'any' as a parameter which will make it return the entire list.
1653
	 *
1654
	 * @param string|array $selector Module slug, 'any', or an array of parameters.
1655
	 *                               If empty, it's assumed we're updating a module and we'll try to get its slug.
1656
	 *                               If 'any' the full list is returned.
1657
	 *                               If it's an array of parameters, includes the elements by matching keys.
1658
	 *
1659
	 * @return array
1660
	 */
1661
	public static function get_updateable_data_list( $selector = '' ) {
1662
1663
		$options = array(
1664
1665
			// Carousel
1666
			'carousel_background_color' => array(
1667
				'description'       => esc_html__( 'Color scheme.', 'jetpack' ),
1668
				'type'              => 'string',
1669
				'default'           => 'black',
1670
				'enum'              => array(
1671
					'black',
1672
					'white',
1673
				),
1674
				'enum_labels' => array(
1675
					'black' => esc_html__( 'Black', 'jetpack' ),
1676
					'white' => esc_html__( 'White', 'jetpack' ),
1677
				),
1678
				'validate_callback' => __CLASS__ . '::validate_list_item',
1679
				'jp_group'          => 'carousel',
1680
			),
1681
			'carousel_display_exif' => array(
1682
				'description'       => wp_kses( sprintf( __( 'Show photo metadata (<a href="https://en.wikipedia.org/wiki/Exchangeable_image_file_format" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) ), array( 'a' => array( 'href' => true, 'target' => true ) ) ),
1683
				'type'              => 'boolean',
1684
				'default'           => 0,
1685
				'validate_callback' => __CLASS__ . '::validate_boolean',
1686
				'jp_group'          => 'carousel',
1687
			),
1688
1689
			// Comments
1690
			'highlander_comment_form_prompt' => array(
1691
				'description'       => esc_html__( 'Greeting Text', 'jetpack' ),
1692
				'type'              => 'string',
1693
				'default'           => esc_html__( 'Leave a Reply', 'jetpack' ),
1694
				'sanitize_callback' => 'sanitize_text_field',
1695
				'jp_group'          => 'comments',
1696
			),
1697
			'jetpack_comment_form_color_scheme' => array(
1698
				'description'       => esc_html__( "Color scheme", 'jetpack' ),
1699
				'type'              => 'string',
1700
				'default'           => 'light',
1701
				'enum'              => array(
1702
					'light',
1703
					'dark',
1704
					'transparent',
1705
				),
1706
				'enum_labels' => array(
1707
					'light'       => esc_html__( 'Light', 'jetpack' ),
1708
					'dark'        => esc_html__( 'Dark', 'jetpack' ),
1709
					'transparent' => esc_html__( 'Transparent', 'jetpack' ),
1710
				),
1711
				'validate_callback' => __CLASS__ . '::validate_list_item',
1712
				'jp_group'          => 'comments',
1713
			),
1714
1715
			// Custom Content Types
1716
			'jetpack_portfolio' => array(
1717
				'description'       => esc_html__( 'Enable or disable Jetpack portfolio post type.', 'jetpack' ),
1718
				'type'              => 'boolean',
1719
				'default'           => 0,
1720
				'validate_callback' => __CLASS__ . '::validate_boolean',
1721
				'jp_group'          => 'custom-content-types',
1722
			),
1723
			'jetpack_portfolio_posts_per_page' => array(
1724
				'description'       => esc_html__( 'Number of entries to show at most in Portfolio pages.', 'jetpack' ),
1725
				'type'              => 'integer',
1726
				'default'           => 10,
1727
				'validate_callback' => __CLASS__ . '::validate_posint',
1728
				'jp_group'          => 'custom-content-types',
1729
			),
1730
			'jetpack_testimonial' => array(
1731
				'description'       => esc_html__( 'Enable or disable Jetpack testimonial post type.', 'jetpack' ),
1732
				'type'              => 'boolean',
1733
				'default'           => 0,
1734
				'validate_callback' => __CLASS__ . '::validate_boolean',
1735
				'jp_group'          => 'custom-content-types',
1736
			),
1737
			'jetpack_testimonial_posts_per_page' => array(
1738
				'description'       => esc_html__( 'Number of entries to show at most in Testimonial pages.', 'jetpack' ),
1739
				'type'              => 'integer',
1740
				'default'           => 10,
1741
				'validate_callback' => __CLASS__ . '::validate_posint',
1742
				'jp_group'          => 'custom-content-types',
1743
			),
1744
1745
			// Galleries
1746
			'tiled_galleries' => array(
1747
				'description'       => esc_html__( 'Display all your gallery pictures in a cool mosaic.', 'jetpack' ),
1748
				'type'              => 'boolean',
1749
				'default'           => 0,
1750
				'validate_callback' => __CLASS__ . '::validate_boolean',
1751
				'jp_group'          => 'tiled-gallery',
1752
			),
1753
1754
			'gravatar_disable_hovercards' => array(
1755
				'description'       => esc_html__( "View people's profiles when you mouse over their Gravatars", 'jetpack' ),
1756
				'type'              => 'string',
1757
				'default'           => 'enabled',
1758
				// Not visible. This is used as the checkbox value.
1759
				'enum'              => array(
1760
					'enabled',
1761
					'disabled',
1762
				),
1763
				'enum_labels' => array(
1764
					'enabled'  => esc_html__( 'Enabled', 'jetpack' ),
1765
					'disabled' => esc_html__( 'Disabled', 'jetpack' ),
1766
				),
1767
				'validate_callback' => __CLASS__ . '::validate_list_item',
1768
				'jp_group'          => 'gravatar-hovercards',
1769
			),
1770
1771
			// Infinite Scroll
1772
			'infinite_scroll' => array(
1773
				'description'       => esc_html__( 'To infinity and beyond', 'jetpack' ),
1774
				'type'              => 'boolean',
1775
				'default'           => 1,
1776
				'validate_callback' => __CLASS__ . '::validate_boolean',
1777
				'jp_group'          => 'infinite-scroll',
1778
			),
1779
			'infinite_scroll_google_analytics' => array(
1780
				'description'       => esc_html__( 'Use Google Analytics with Infinite Scroll', 'jetpack' ),
1781
				'type'              => 'boolean',
1782
				'default'           => 0,
1783
				'validate_callback' => __CLASS__ . '::validate_boolean',
1784
				'jp_group'          => 'infinite-scroll',
1785
			),
1786
1787
			// Likes
1788
			'wpl_default' => array(
1789
				'description'       => esc_html__( 'WordPress.com Likes are', 'jetpack' ),
1790
				'type'              => 'string',
1791
				'default'           => 'on',
1792
				'enum'              => array(
1793
					'on',
1794
					'off',
1795
				),
1796
				'enum_labels' => array(
1797
					'on'  => esc_html__( 'On for all posts', 'jetpack' ),
1798
					'off' => esc_html__( 'Turned on per post', 'jetpack' ),
1799
				),
1800
				'validate_callback' => __CLASS__ . '::validate_list_item',
1801
				'jp_group'          => 'likes',
1802
			),
1803
			'social_notifications_like' => array(
1804
				'description'       => esc_html__( 'Send email notification when someone likes a post', 'jetpack' ),
1805
				'type'              => 'boolean',
1806
				'default'           => 1,
1807
				'validate_callback' => __CLASS__ . '::validate_boolean',
1808
				'jp_group'          => 'likes',
1809
			),
1810
1811
			// Markdown
1812
			'wpcom_publish_comments_with_markdown' => array(
1813
				'description'       => esc_html__( 'Use Markdown for comments.', 'jetpack' ),
1814
				'type'              => 'boolean',
1815
				'default'           => 0,
1816
				'validate_callback' => __CLASS__ . '::validate_boolean',
1817
				'jp_group'          => 'markdown',
1818
			),
1819
			'wpcom_publish_posts_with_markdown' => array(
1820
				'description'       => esc_html__( 'Use Markdown for posts.', 'jetpack' ),
1821
				'type'              => 'boolean',
1822
				'default'           => 0,
1823
				'validate_callback' => __CLASS__ . '::validate_boolean',
1824
				'jp_group'          => 'markdown',
1825
			),
1826
1827
			// Mobile Theme
1828
			'wp_mobile_excerpt' => array(
1829
				'description'       => esc_html__( 'Excerpts', 'jetpack' ),
1830
				'type'              => 'boolean',
1831
				'default'           => 0,
1832
				'validate_callback' => __CLASS__ . '::validate_boolean',
1833
				'jp_group'          => 'minileven',
1834
			),
1835
			'wp_mobile_featured_images' => array(
1836
				'description'       => esc_html__( 'Featured Images', 'jetpack' ),
1837
				'type'              => 'boolean',
1838
				'default'           => 0,
1839
				'validate_callback' => __CLASS__ . '::validate_boolean',
1840
				'jp_group'          => 'minileven',
1841
			),
1842
			'wp_mobile_app_promos' => array(
1843
				'description'       => esc_html__( 'Show a promo for the WordPress mobile apps in the footer of the mobile theme.', 'jetpack' ),
1844
				'type'              => 'boolean',
1845
				'default'           => 0,
1846
				'validate_callback' => __CLASS__ . '::validate_boolean',
1847
				'jp_group'          => 'minileven',
1848
			),
1849
1850
			// Monitor
1851
			'monitor_receive_notifications' => array(
1852
				'description'       => esc_html__( 'Receive Monitor Email Notifications.', 'jetpack' ),
1853
				'type'              => 'boolean',
1854
				'default'           => 0,
1855
				'validate_callback' => __CLASS__ . '::validate_boolean',
1856
				'jp_group'          => 'monitor',
1857
			),
1858
1859
			// Post by Email
1860
			'post_by_email_address' => array(
1861
				'description'       => esc_html__( 'Email Address', 'jetpack' ),
1862
				'type'              => 'string',
1863
				'default'           => 'noop',
1864
				'enum'              => array(
1865
					'noop',
1866
					'create',
1867
					'regenerate',
1868
					'delete',
1869
				),
1870
				'enum_labels' => array(
1871
					'noop'       => '',
1872
					'create'     => esc_html__( 'Create Post by Email address', 'jetpack' ),
1873
					'regenerate' => esc_html__( 'Regenerate Post by Email address', 'jetpack' ),
1874
					'delete'     => esc_html__( 'Delete Post by Email address', 'jetpack' ),
1875
				),
1876
				'validate_callback' => __CLASS__ . '::validate_list_item',
1877
				'jp_group'          => 'post-by-email',
1878
			),
1879
1880
			// Protect
1881
			'jetpack_protect_key' => array(
1882
				'description'       => esc_html__( 'Protect API key', 'jetpack' ),
1883
				'type'              => 'string',
1884
				'default'           => '',
1885
				'validate_callback' => __CLASS__ . '::validate_alphanum',
1886
				'jp_group'          => 'protect',
1887
			),
1888
			'jetpack_protect_global_whitelist' => array(
1889
				'description'       => esc_html__( 'Protect global whitelist', 'jetpack' ),
1890
				'type'              => 'string',
1891
				'default'           => '',
1892
				'validate_callback' => __CLASS__ . '::validate_string',
1893
				'sanitize_callback' => 'esc_textarea',
1894
				'jp_group'          => 'protect',
1895
			),
1896
1897
			// Sharing
1898
			'sharing_services' => array(
1899
				'description'       => esc_html__( 'Enabled Services and those hidden behind a button', 'jetpack' ),
1900
				'type'              => 'object',
1901
				'default'           => array(
1902
					'visible' => array( 'twitter', 'facebook', 'google-plus-1' ),
1903
					'hidden'  => array(),
1904
				),
1905
				'validate_callback' => __CLASS__ . '::validate_services',
1906
				'jp_group'          => 'sharedaddy',
1907
			),
1908
			'button_style' => array(
1909
				'description'       => esc_html__( 'Button Style', 'jetpack' ),
1910
				'type'              => 'string',
1911
				'default'           => 'icon',
1912
				'enum'              => array(
1913
					'icon-text',
1914
					'icon',
1915
					'text',
1916
					'official',
1917
				),
1918
				'enum_labels' => array(
1919
					'icon-text' => esc_html__( 'Icon + text', 'jetpack' ),
1920
					'icon'      => esc_html__( 'Icon only', 'jetpack' ),
1921
					'text'      => esc_html__( 'Text only', 'jetpack' ),
1922
					'official'  => esc_html__( 'Official buttons', 'jetpack' ),
1923
				),
1924
				'validate_callback' => __CLASS__ . '::validate_list_item',
1925
				'jp_group'          => 'sharedaddy',
1926
			),
1927
			'sharing_label' => array(
1928
				'description'       => esc_html__( 'Sharing Label', 'jetpack' ),
1929
				'type'              => 'string',
1930
				'default'           => '',
1931
				'validate_callback' => __CLASS__ . '::validate_string',
1932
				'sanitize_callback' => 'esc_html',
1933
				'jp_group'          => 'sharedaddy',
1934
			),
1935
			'show' => array(
1936
				'description'       => esc_html__( 'Views where buttons are shown', 'jetpack' ),
1937
				'type'              => 'array',
1938
				'items'             => array(
1939
					'type' => 'string'
1940
				),
1941
				'default'           => array( 'post' ),
1942
				'validate_callback' => __CLASS__ . '::validate_sharing_show',
1943
				'jp_group'          => 'sharedaddy',
1944
			),
1945
			'jetpack-twitter-cards-site-tag' => array(
1946
				'description'       => esc_html__( "The Twitter username of the owner of this site's domain.", 'jetpack' ),
1947
				'type'              => 'string',
1948
				'default'           => '',
1949
				'validate_callback' => __CLASS__ . '::validate_twitter_username',
1950
				'sanitize_callback' => 'esc_html',
1951
				'jp_group'          => 'sharedaddy',
1952
			),
1953
			'sharedaddy_disable_resources' => array(
1954
				'description'       => esc_html__( 'Disable CSS and JS', 'jetpack' ),
1955
				'type'              => 'boolean',
1956
				'default'           => 0,
1957
				'validate_callback' => __CLASS__ . '::validate_boolean',
1958
				'jp_group'          => 'sharedaddy',
1959
			),
1960
			'custom' => array(
1961
				'description'       => esc_html__( 'Custom sharing services added by user.', 'jetpack' ),
1962
				'type'              => 'object',
1963
				'default'           => array(
1964
					'sharing_name' => '',
1965
					'sharing_url'  => '',
1966
					'sharing_icon' => '',
1967
				),
1968
				'validate_callback' => __CLASS__ . '::validate_custom_service',
1969
				'jp_group'          => 'sharedaddy',
1970
			),
1971
			// Not an option, but an action that can be perfomed on the list of custom services passing the service ID.
1972
			'sharing_delete_service' => array(
1973
				'description'       => esc_html__( 'Delete custom sharing service.', 'jetpack' ),
1974
				'type'              => 'string',
1975
				'default'           => '',
1976
				'validate_callback' => __CLASS__ . '::validate_custom_service_id',
1977
				'jp_group'          => 'sharedaddy',
1978
			),
1979
1980
			// SSO
1981
			'jetpack_sso_require_two_step' => array(
1982
				'description'       => esc_html__( 'Require Two-Step Authentication', 'jetpack' ),
1983
				'type'              => 'boolean',
1984
				'default'           => 0,
1985
				'validate_callback' => __CLASS__ . '::validate_boolean',
1986
				'jp_group'          => 'sso',
1987
			),
1988
			'jetpack_sso_match_by_email' => array(
1989
				'description'       => esc_html__( 'Match by Email', 'jetpack' ),
1990
				'type'              => 'boolean',
1991
				'default'           => 0,
1992
				'validate_callback' => __CLASS__ . '::validate_boolean',
1993
				'jp_group'          => 'sso',
1994
			),
1995
1996
			// Subscriptions
1997
			'stb_enabled' => array(
1998
				'description'       => esc_html__( "Show a <em>'follow blog'</em> option in the comment form", 'jetpack' ),
1999
				'type'              => 'boolean',
2000
				'default'           => 1,
2001
				'validate_callback' => __CLASS__ . '::validate_boolean',
2002
				'jp_group'          => 'subscriptions',
2003
			),
2004
			'stc_enabled' => array(
2005
				'description'       => esc_html__( "Show a <em>'follow comments'</em> option in the comment form", 'jetpack' ),
2006
				'type'              => 'boolean',
2007
				'default'           => 1,
2008
				'validate_callback' => __CLASS__ . '::validate_boolean',
2009
				'jp_group'          => 'subscriptions',
2010
			),
2011
2012
			// Related Posts
2013
			'show_headline' => array(
2014
				'description'       => esc_html__( 'Highlight related content with a heading', 'jetpack' ),
2015
				'type'              => 'boolean',
2016
				'default'           => 1,
2017
				'validate_callback' => __CLASS__ . '::validate_boolean',
2018
				'jp_group'          => 'related-posts',
2019
			),
2020
			'show_thumbnails' => array(
2021
				'description'       => esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
2022
				'type'              => 'boolean',
2023
				'default'           => 0,
2024
				'validate_callback' => __CLASS__ . '::validate_boolean',
2025
				'jp_group'          => 'related-posts',
2026
			),
2027
2028
			// Verification Tools
2029
			'google' => array(
2030
				'description'       => esc_html__( 'Google Search Console', 'jetpack' ),
2031
				'type'              => 'string',
2032
				'default'           => '',
2033
				'validate_callback' => __CLASS__ . '::validate_verification_service',
2034
				'jp_group'          => 'verification-tools',
2035
			),
2036
			'bing' => array(
2037
				'description'       => esc_html__( 'Bing Webmaster Center', 'jetpack' ),
2038
				'type'              => 'string',
2039
				'default'           => '',
2040
				'validate_callback' => __CLASS__ . '::validate_verification_service',
2041
				'jp_group'          => 'verification-tools',
2042
			),
2043
			'pinterest' => array(
2044
				'description'       => esc_html__( 'Pinterest Site Verification', 'jetpack' ),
2045
				'type'              => 'string',
2046
				'default'           => '',
2047
				'validate_callback' => __CLASS__ . '::validate_verification_service',
2048
				'jp_group'          => 'verification-tools',
2049
			),
2050
			'yandex' => array(
2051
				'description'       => esc_html__( 'Yandex Site Verification', 'jetpack' ),
2052
				'type'              => 'string',
2053
				'default'           => '',
2054
				'validate_callback' => __CLASS__ . '::validate_verification_service',
2055
				'jp_group'          => 'verification-tools',
2056
			),
2057
			'enable_header_ad' => array(
2058
				'description'        => esc_html__( 'Display an ad unit at the top of each page.', 'jetpack' ),
2059
				'type'               => 'boolean',
2060
				'default'            => 1,
2061
				'validate_callback'  => __CLASS__ . '::validate_boolean',
2062
				'jp_group'           => 'wordads',
2063
			),
2064
			'wordads_approved' => array(
2065
				'description'        => esc_html__( 'Is site approved for WordAds?', 'jetpack' ),
2066
				'type'               => 'boolean',
2067
				'default'            => 0,
2068
				'validate_callback'  => __CLASS__ . '::validate_boolean',
2069
				'jp_group'           => 'wordads',
2070
			),
2071
			'wordads_second_belowpost' => array(
2072
				'description'        => esc_html__( 'Display second ad below post?', 'jetpack' ),
2073
				'type'               => 'boolean',
2074
				'default'            => 1,
2075
				'validate_callback'  => __CLASS__ . '::validate_boolean',
2076
				'jp_group'           => 'wordads',
2077
			),
2078
			'wordads_display_front_page' => array(
2079
				'description'        => esc_html__( 'Display ads on the front page?', 'jetpack' ),
2080
				'type'               => 'boolean',
2081
				'default'            => 1,
2082
				'validate_callback'  => __CLASS__ . '::validate_boolean',
2083
				'jp_group'           => 'wordads',
2084
			),
2085
			'wordads_display_post' => array(
2086
				'description'        => esc_html__( 'Display ads on posts?', 'jetpack' ),
2087
				'type'               => 'boolean',
2088
				'default'            => 1,
2089
				'validate_callback'  => __CLASS__ . '::validate_boolean',
2090
				'jp_group'           => 'wordads',
2091
			),
2092
			'wordads_display_page' => array(
2093
				'description'        => esc_html__( 'Display ads on pages?', 'jetpack' ),
2094
				'type'               => 'boolean',
2095
				'default'            => 1,
2096
				'validate_callback'  => __CLASS__ . '::validate_boolean',
2097
				'jp_group'           => 'wordads',
2098
			),
2099
			'wordads_display_archive' => array(
2100
				'description'        => esc_html__( 'Display ads on archive pages?', 'jetpack' ),
2101
				'type'               => 'boolean',
2102
				'default'            => 1,
2103
				'validate_callback'  => __CLASS__ . '::validate_boolean',
2104
				'jp_group'           => 'wordads',
2105
			),
2106
			'wordads_custom_adstxt' => array(
2107
				'description'        => esc_html__( 'Custom ads.txt entries', 'jetpack' ),
2108
				'type'               => 'string',
2109
				'default'            => '',
2110
				'validate_callback'  => __CLASS__ . '::validate_string',
2111
				'sanitize_callback'  => 'sanitize_textarea_field',
2112
				'jp_group'           => 'wordads',
2113
			),
2114
2115
			// Google Analytics
2116
			'google_analytics_tracking_id' => array(
2117
				'description'        => esc_html__( 'Google Analytics', 'jetpack' ),
2118
				'type'               => 'string',
2119
				'default'            => '',
2120
				'validate_callback'  => __CLASS__ . '::validate_alphanum',
2121
				'jp_group'           => 'google-analytics',
2122
			),
2123
2124
			// Stats
2125
			'admin_bar' => array(
2126
				'description'       => esc_html__( 'Include a small chart in your admin bar with a 48-hour traffic snapshot.', 'jetpack' ),
2127
				'type'              => 'boolean',
2128
				'default'           => 1,
2129
				'validate_callback' => __CLASS__ . '::validate_boolean',
2130
				'jp_group'          => 'stats',
2131
			),
2132
			'roles' => array(
2133
				'description'       => esc_html__( 'Select the roles that will be able to view stats reports.', 'jetpack' ),
2134
				'type'              => 'array',
2135
				'items'             => array(
2136
					'type' => 'string'
2137
				),
2138
				'default'           => array( 'administrator' ),
2139
				'validate_callback' => __CLASS__ . '::validate_stats_roles',
2140
				'sanitize_callback' => __CLASS__ . '::sanitize_stats_allowed_roles',
2141
				'jp_group'          => 'stats',
2142
			),
2143
			'count_roles' => array(
2144
				'description'       => esc_html__( 'Count the page views of registered users who are logged in.', 'jetpack' ),
2145
				'type'              => 'array',
2146
				'items'             => array(
2147
					'type' => 'string'
2148
				),
2149
				'default'           => array( 'administrator' ),
2150
				'validate_callback' => __CLASS__ . '::validate_stats_roles',
2151
				'jp_group'          => 'stats',
2152
			),
2153
			'blog_id' => array(
2154
				'description'       => esc_html__( 'Blog ID.', 'jetpack' ),
2155
				'type'              => 'boolean',
2156
				'default'           => 0,
2157
				'validate_callback' => __CLASS__ . '::validate_boolean',
2158
				'jp_group'          => 'stats',
2159
			),
2160
			'do_not_track' => array(
2161
				'description'       => esc_html__( 'Do not track.', 'jetpack' ),
2162
				'type'              => 'boolean',
2163
				'default'           => 1,
2164
				'validate_callback' => __CLASS__ . '::validate_boolean',
2165
				'jp_group'          => 'stats',
2166
			),
2167
			'hide_smile' => array(
2168
				'description'       => esc_html__( 'Hide the stats smiley face image.', 'jetpack' ),
2169
				'type'              => 'boolean',
2170
				'default'           => 1,
2171
				'validate_callback' => __CLASS__ . '::validate_boolean',
2172
				'jp_group'          => 'stats',
2173
			),
2174
			'version' => array(
2175
				'description'       => esc_html__( 'Version.', 'jetpack' ),
2176
				'type'              => 'integer',
2177
				'default'           => 9,
2178
				'validate_callback' => __CLASS__ . '::validate_posint',
2179
				'jp_group'          => 'stats',
2180
			),
2181
2182
			// Akismet - Not a module, but a plugin. The options can be passed and handled differently.
2183
			'akismet_show_user_comments_approved' => array(
2184
				'description'       => '',
2185
				'type'              => 'boolean',
2186
				'default'           => 0,
2187
				'validate_callback' => __CLASS__ . '::validate_boolean',
2188
				'jp_group'          => 'settings',
2189
			),
2190
2191
			'wordpress_api_key' => array(
2192
				'description'       => '',
2193
				'type'              => 'string',
2194
				'default'           => '',
2195
				'validate_callback' => __CLASS__ . '::validate_alphanum',
2196
				'jp_group'          => 'settings',
2197
			),
2198
2199
			// Apps card on dashboard
2200
			'dismiss_dash_app_card' => array(
2201
				'description'       => '',
2202
				'type'              => 'boolean',
2203
				'default'           => 0,
2204
				'validate_callback' => __CLASS__ . '::validate_boolean',
2205
				'jp_group'          => 'settings',
2206
			),
2207
2208
			// Empty stats card dismiss
2209
			'dismiss_empty_stats_card' => array(
2210
				'description'       => '',
2211
				'type'              => 'boolean',
2212
				'default'           => 0,
2213
				'validate_callback' => __CLASS__ . '::validate_boolean',
2214
				'jp_group'          => 'settings',
2215
			),
2216
2217
			'lang_id' => array(
2218
				'description' => esc_html__( 'Primary language for the site.', 'jetpack' ),
2219
				'type' => 'string',
2220
				'default' => 'en_US',
2221
				'jp_group' => 'settings',
2222
			),
2223
2224
			'onboarding' => array(
2225
				'description'       => '',
2226
				'type'              => 'object',
2227
				'default'           => array(
2228
					'siteTitle'          => '',
2229
					'siteDescription'    => '',
2230
					'siteType'           => 'personal',
2231
					'homepageFormat'     => 'posts',
2232
					'addContactForm'     => 0,
2233
					'businessAddress'    => array(
2234
						'name'   => '',
2235
						'street' => '',
2236
						'city'   => '',
2237
						'state'  => '',
2238
						'zip'    => '',
2239
					),
2240
					'installWooCommerce' => false,
2241
				),
2242
				'validate_callback' => __CLASS__ . '::validate_onboarding',
2243
				'jp_group'          => 'settings',
2244
			),
2245
2246
		);
2247
2248
		// Add modules to list so they can be toggled
2249
		$modules = Jetpack::get_available_modules();
2250
		if ( is_array( $modules ) && ! empty( $modules ) ) {
2251
			$module_args = array(
2252
				'description'       => '',
2253
				'type'              => 'boolean',
2254
				'default'           => 0,
2255
				'validate_callback' => __CLASS__ . '::validate_boolean',
2256
				'jp_group'          => 'modules',
2257
			);
2258
			foreach( $modules as $module ) {
2259
				$options[ $module ] = $module_args;
2260
			}
2261
		}
2262
2263
		if ( is_array( $selector ) ) {
2264
2265
			// Return only those options whose keys match $selector keys
2266
			return array_intersect_key( $options, $selector );
2267
		}
2268
2269
		if ( 'any' === $selector ) {
2270
2271
			// Toggle module or update any module option or any general setting
2272
			return $options;
2273
		}
2274
2275
		// We're updating the options for a single module.
2276
		if ( empty( $selector ) ) {
2277
			$selector = self::get_module_requested();
2278
		}
2279
		$selected = array();
2280
		foreach ( $options as $option => $attributes ) {
2281
2282
			// Not adding an isset( $attributes['jp_group'] ) because if it's not set, it must be fixed, otherwise options will fail.
2283
			if ( $selector === $attributes['jp_group'] ) {
2284
				$selected[ $option ] = $attributes;
2285
			}
2286
		}
2287
		return $selected;
2288
	}
2289
2290
	/**
2291
	 * Validates that the parameters are proper values that can be set during Jetpack onboarding.
2292
	 *
2293
	 * @since 5.4.0
2294
	 *
2295
	 * @param array           $onboarding_data Values to check.
2296
	 * @param WP_REST_Request $request         The request sent to the WP REST API.
2297
	 * @param string          $param           Name of the parameter passed to endpoint holding $value.
2298
	 *
2299
	 * @return bool|WP_Error
2300
	 */
2301
	public static function validate_onboarding( $onboarding_data, $request, $param ) {
2302
		if ( ! is_array( $onboarding_data ) ) {
2303
			return new WP_Error( 'invalid_param', esc_html__( 'Not valid onboarding data.', 'jetpack' ) );
2304
		}
2305
		foreach ( $onboarding_data as $value ) {
2306
			if ( is_string( $value ) ) {
2307
				$onboarding_choice = self::validate_string( $value, $request, $param );
2308
			} elseif ( is_array( $value ) ) {
2309
				$onboarding_choice = self::validate_onboarding( $value, $request, $param );
2310
			} else {
2311
				$onboarding_choice = self::validate_boolean( $value, $request, $param );
2312
			}
2313
			if ( is_wp_error( $onboarding_choice ) ) {
2314
				return $onboarding_choice;
2315
			}
2316
		}
2317
		return true;
2318
	}
2319
2320
	/**
2321
	 * Validates that the parameter is either a pure boolean or a numeric string that can be mapped to a boolean.
2322
	 *
2323
	 * @since 4.3.0
2324
	 *
2325
	 * @param string|bool $value Value to check.
2326
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2327
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2328
	 *
2329
	 * @return bool|WP_Error
2330
	 */
2331
	public static function validate_boolean( $value, $request, $param ) {
2332
		if ( ! is_bool( $value ) && ! ( ( ctype_digit( $value ) || is_numeric( $value ) ) && in_array( $value, array( 0, 1 ) ) ) ) {
2333
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be true, false, 0 or 1.', 'jetpack' ), $param ) );
2334
		}
2335
		return true;
2336
	}
2337
2338
	/**
2339
	 * Validates that the parameter is a positive integer.
2340
	 *
2341
	 * @since 4.3.0
2342
	 *
2343
	 * @param int $value Value to check.
2344
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2345
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2346
	 *
2347
	 * @return bool|WP_Error
2348
	 */
2349
	public static function validate_posint( $value = 0, $request, $param ) {
2350
		if ( ! is_numeric( $value ) || $value <= 0 ) {
2351
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a positive integer.', 'jetpack' ), $param ) );
2352
		}
2353
		return true;
2354
	}
2355
2356
	/**
2357
	 * Validates that the parameter belongs to a list of admitted values.
2358
	 *
2359
	 * @since 4.3.0
2360
	 *
2361
	 * @param string $value Value to check.
2362
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2363
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2364
	 *
2365
	 * @return bool|WP_Error
2366
	 */
2367
	public static function validate_list_item( $value = '', $request, $param ) {
2368
		$attributes = $request->get_attributes();
2369
		if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
2370
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s not recognized', 'jetpack' ), $param ) );
2371
		}
2372
		$args = $attributes['args'][ $param ];
2373
		if ( ! empty( $args['enum'] ) ) {
2374
2375
			// If it's an associative array, use the keys to check that the value is among those admitted.
2376
			$enum = ( count( array_filter( array_keys( $args['enum'] ), 'is_string' ) ) > 0 ) ? array_keys( $args['enum'] ) : $args['enum'];
2377 View Code Duplication
			if ( ! in_array( $value, $enum ) ) {
2378
				return new WP_Error( 'invalid_param_value', sprintf(
2379
					/* Translators: first variable is the parameter passed to endpoint that holds the list item, the second is a list of admitted values. */
2380
					esc_html__( '%1$s must be one of %2$s', 'jetpack' ), $param, implode( ', ', $enum )
2381
				) );
2382
			}
2383
		}
2384
		return true;
2385
	}
2386
2387
	/**
2388
	 * Validates that the parameter belongs to a list of admitted values.
2389
	 *
2390
	 * @since 4.3.0
2391
	 *
2392
	 * @param string $value Value to check.
2393
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2394
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2395
	 *
2396
	 * @return bool|WP_Error
2397
	 */
2398
	public static function validate_module_list( $value = '', $request, $param ) {
2399 View Code Duplication
		if ( ! is_array( $value ) ) {
2400
			return new WP_Error( 'invalid_param_value', sprintf( esc_html__( '%s must be an array', 'jetpack' ), $param ) );
2401
		}
2402
2403
		$modules = Jetpack::get_available_modules();
2404
2405 View Code Duplication
		if ( count( array_intersect( $value, $modules ) ) != count( $value ) ) {
2406
			return new WP_Error( 'invalid_param_value', sprintf( esc_html__( '%s must be a list of valid modules', 'jetpack' ), $param ) );
2407
		}
2408
2409
		return true;
2410
	}
2411
2412
	/**
2413
	 * Validates that the parameter is an alphanumeric or empty string (to be able to clear the field).
2414
	 *
2415
	 * @since 4.3.0
2416
	 *
2417
	 * @param string $value Value to check.
2418
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2419
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2420
	 *
2421
	 * @return bool|WP_Error
2422
	 */
2423
	public static function validate_alphanum( $value = '', $request, $param ) {
2424 View Code Duplication
		if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^[a-z0-9]+$/i', $value ) ) ) {
2425
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string.', 'jetpack' ), $param ) );
2426
		}
2427
		return true;
2428
	}
2429
2430
	/**
2431
	 * Validates that the parameter is a tag or id for a verification service, or an empty string (to be able to clear the field).
2432
	 *
2433
	 * @since 4.6.0
2434
	 *
2435
	 * @param string $value Value to check.
2436
	 * @param WP_REST_Request $request
2437
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2438
	 *
2439
	 * @return bool|WP_Error
2440
	 */
2441
	public static function validate_verification_service( $value = '', $request, $param ) {
2442
		if ( ! empty( $value ) && ! ( is_string( $value ) && ( preg_match( '/^[a-z0-9_-]+$/i', $value ) || jetpack_verification_get_code( $value ) !== false ) ) ) {
2443
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string or a verification tag.', 'jetpack' ), $param ) );
2444
		}
2445
		return true;
2446
	}
2447
2448
	/**
2449
	 * Validates that the parameter is among the roles allowed for Stats.
2450
	 *
2451
	 * @since 4.3.0
2452
	 *
2453
	 * @param string|bool $value Value to check.
2454
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2455
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2456
	 *
2457
	 * @return bool|WP_Error
2458
	 */
2459
	public static function validate_stats_roles( $value, $request, $param ) {
2460
		if ( ! empty( $value ) && ! array_intersect( self::$stats_roles, $value ) ) {
2461
			return new WP_Error( 'invalid_param', sprintf(
2462
				/* 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. */
2463
				esc_html__( '%1$s must be %2$s.', 'jetpack' ), $param, join( ', ', self::$stats_roles )
2464
			) );
2465
		}
2466
		return true;
2467
	}
2468
2469
	/**
2470
	 * Validates that the parameter is among the views where the Sharing can be displayed.
2471
	 *
2472
	 * @since 4.3.0
2473
	 *
2474
	 * @param string|bool $value Value to check.
2475
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2476
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2477
	 *
2478
	 * @return bool|WP_Error
2479
	 */
2480
	public static function validate_sharing_show( $value, $request, $param ) {
2481
		$views = array( 'index', 'post', 'page', 'attachment', 'jetpack-portfolio' );
2482 View Code Duplication
		if ( ! is_array( $value ) ) {
2483
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array of post types.', 'jetpack' ), $param ) );
2484
		}
2485
		if ( ! array_intersect( $views, $value ) ) {
2486
			return new WP_Error( 'invalid_param', sprintf(
2487
				/* 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 */
2488
				esc_html__( '%1$s must be %2$s.', 'jetpack' ), $param, join( ', ', $views )
2489
			) );
2490
		}
2491
		return true;
2492
	}
2493
2494
	/**
2495
	 * Validates that the parameter is among the views where the Sharing can be displayed.
2496
	 *
2497
	 * @since 4.3.0
2498
	 *
2499
	 * @param string|bool $value {
2500
	 *     Value to check received by request.
2501
	 *
2502
	 *     @type array $visible List of slug of services to share to that are displayed directly in the page.
2503
	 *     @type array $hidden  List of slug of services to share to that are concealed in a folding menu.
2504
	 * }
2505
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2506
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2507
	 *
2508
	 * @return bool|WP_Error
2509
	 */
2510
	public static function validate_services( $value, $request, $param ) {
2511
		if ( ! is_array( $value ) || ! isset( $value['visible'] ) || ! isset( $value['hidden'] ) ) {
2512
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array with visible and hidden items.', 'jetpack' ), $param ) );
2513
		}
2514
2515
		// Allow to clear everything.
2516
		if ( empty( $value['visible'] ) && empty( $value['hidden'] ) ) {
2517
			return true;
2518
		}
2519
2520 View Code Duplication
		if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
2521
			return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
2522
		}
2523
		$sharer = new Sharing_Service();
2524
		$services = array_keys( $sharer->get_all_services() );
2525
2526
		if (
2527
			( ! empty( $value['visible'] ) && ! array_intersect( $value['visible'], $services ) )
2528
			||
2529
			( ! empty( $value['hidden'] ) && ! array_intersect( $value['hidden'], $services ) ) )
2530
		{
2531
			return new WP_Error( 'invalid_param', sprintf(
2532
				/* Translators: placeholder 1 is a parameter holding the services passed to endpoint, placeholder 2 is a list of all Jetpack Sharing services */
2533
				esc_html__( '%1$s visible and hidden items must be a list of %2$s.', 'jetpack' ), $param, join( ', ', $services )
2534
			) );
2535
		}
2536
		return true;
2537
	}
2538
2539
	/**
2540
	 * Validates that the parameter has enough information to build a custom sharing button.
2541
	 *
2542
	 * @since 4.3.0
2543
	 *
2544
	 * @param string|bool $value Value to check.
2545
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2546
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2547
	 *
2548
	 * @return bool|WP_Error
2549
	 */
2550
	public static function validate_custom_service( $value, $request, $param ) {
2551
		if ( ! is_array( $value ) || ! isset( $value['sharing_name'] ) || ! isset( $value['sharing_url'] ) || ! isset( $value['sharing_icon'] ) ) {
2552
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array with sharing name, url and icon.', 'jetpack' ), $param ) );
2553
		}
2554
2555
		// Allow to clear everything.
2556
		if ( empty( $value['sharing_name'] ) && empty( $value['sharing_url'] ) && empty( $value['sharing_icon'] ) ) {
2557
			return true;
2558
		}
2559
2560 View Code Duplication
		if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
2561
			return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
2562
		}
2563
2564
		if ( ( ! empty( $value['sharing_name'] ) && ! is_string( $value['sharing_name'] ) )
2565
		|| ( ! empty( $value['sharing_url'] ) && ! is_string( $value['sharing_url'] ) )
2566
		|| ( ! empty( $value['sharing_icon'] ) && ! is_string( $value['sharing_icon'] ) ) ) {
2567
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s needs sharing name, url and icon.', 'jetpack' ), $param ) );
2568
		}
2569
		return true;
2570
	}
2571
2572
	/**
2573
	 * Validates that the parameter is a custom sharing service ID like 'custom-1461976264'.
2574
	 *
2575
	 * @since 4.3.0
2576
	 *
2577
	 * @param string $value Value to check.
2578
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2579
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2580
	 *
2581
	 * @return bool|WP_Error
2582
	 */
2583
	public static function validate_custom_service_id( $value = '', $request, $param ) {
2584 View Code Duplication
		if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/custom\-[0-1]+/i', $value ) ) ) {
2585
			return new WP_Error( 'invalid_param', sprintf( esc_html__( "%s must be a string prefixed with 'custom-' and followed by a numeric ID.", 'jetpack' ), $param ) );
2586
		}
2587
2588 View Code Duplication
		if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
2589
			return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
2590
		}
2591
		$sharer = new Sharing_Service();
2592
		$services = array_keys( $sharer->get_all_services() );
2593
2594 View Code Duplication
		if ( ! empty( $value ) && ! in_array( $value, $services ) ) {
2595
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s is not a registered custom sharing service.', 'jetpack' ), $param ) );
2596
		}
2597
2598
		return true;
2599
	}
2600
2601
	/**
2602
	 * Validates that the parameter is a Twitter username or empty string (to be able to clear the field).
2603
	 *
2604
	 * @since 4.3.0
2605
	 *
2606
	 * @param string $value Value to check.
2607
	 * @param WP_REST_Request $request
2608
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2609
	 *
2610
	 * @return bool|WP_Error
2611
	 */
2612
	public static function validate_twitter_username( $value = '', $request, $param ) {
2613 View Code Duplication
		if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^@?\w{1,15}$/i', $value ) ) ) {
2614
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a Twitter username.', 'jetpack' ), $param ) );
2615
		}
2616
		return true;
2617
	}
2618
2619
	/**
2620
	 * Validates that the parameter is a string.
2621
	 *
2622
	 * @since 4.3.0
2623
	 *
2624
	 * @param string $value Value to check.
2625
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2626
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2627
	 *
2628
	 * @return bool|WP_Error
2629
	 */
2630
	public static function validate_string( $value = '', $request, $param ) {
2631
		if ( ! is_string( $value ) ) {
2632
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a string.', 'jetpack' ), $param ) );
2633
		}
2634
		return true;
2635
	}
2636
2637
	/**
2638
	 * If for some reason the roles allowed to see Stats are empty (for example, user tampering with checkboxes),
2639
	 * return an array with only 'administrator' as the allowed role and save it for 'roles' option.
2640
	 *
2641
	 * @since 4.3.0
2642
	 *
2643
	 * @param string|bool $value Value to check.
2644
	 *
2645
	 * @return bool|array
2646
	 */
2647
	public static function sanitize_stats_allowed_roles( $value ) {
2648
		if ( empty( $value ) ) {
2649
			return array( 'administrator' );
2650
		}
2651
		return $value;
2652
	}
2653
2654
	/**
2655
	 * Get the currently accessed route and return the module slug in it.
2656
	 *
2657
	 * @since 4.3.0
2658
	 *
2659
	 * @param string $route Regular expression for the endpoint with the module slug to return.
2660
	 *
2661
	 * @return array|string
2662
	 */
2663
	public static function get_module_requested( $route = '/module/(?P<slug>[a-z\-]+)' ) {
2664
2665
		if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
2666
			return '';
2667
		}
2668
2669
		preg_match( "#$route#", $GLOBALS['wp']->query_vars['rest_route'], $module );
2670
2671
		if ( empty( $module['slug'] ) ) {
2672
			return '';
2673
		}
2674
2675
		return $module['slug'];
2676
	}
2677
2678
	/**
2679
	 * Adds extra information for modules.
2680
	 *
2681
	 * @since 4.3.0
2682
	 *
2683
	 * @param string|array $modules Can be a single module or a list of modules.
2684
	 * @param null|string  $slug    Slug of the module in the first parameter.
2685
	 *
2686
	 * @return array|string
2687
	 */
2688
	public static function prepare_modules_for_response( $modules = '', $slug = null ) {
2689
		global $wp_rewrite;
2690
2691
		/** This filter is documented in modules/sitemaps/sitemaps.php */
2692
		$location = apply_filters( 'jetpack_sitemap_location', '' );
2693
2694
		if ( $wp_rewrite->using_index_permalinks() ) {
2695
			$sitemap_url = home_url( '/index.php' . $location . '/sitemap.xml' );
2696
			$news_sitemap_url = home_url( '/index.php' . $location . '/news-sitemap.xml' );
2697
		} else if ( $wp_rewrite->using_permalinks() ) {
2698
			$sitemap_url = home_url( $location . '/sitemap.xml' );
2699
			$news_sitemap_url = home_url( $location . '/news-sitemap.xml' );
2700
		} else {
2701
			$sitemap_url = home_url( $location . '/?jetpack-sitemap=sitemap.xml' );
2702
			$news_sitemap_url = home_url( $location . '/?jetpack-sitemap=news-sitemap.xml' );
2703
		}
2704
2705
		if ( is_null( $slug ) && isset( $modules['sitemaps'] ) ) {
2706
			// Is a list of modules
2707
			$modules['sitemaps']['extra']['sitemap_url'] = $sitemap_url;
2708
			$modules['sitemaps']['extra']['news_sitemap_url'] = $news_sitemap_url;
2709
		} elseif ( 'sitemaps' == $slug ) {
2710
			// It's a single module
2711
			$modules['extra']['sitemap_url'] = $sitemap_url;
2712
			$modules['extra']['news_sitemap_url'] = $news_sitemap_url;
2713
		}
2714
		return $modules;
2715
	}
2716
2717
	/**
2718
	 * Remove 'validate_callback' item from options available for module.
2719
	 * Fetch current option value and add to array of module options.
2720
	 * Prepare values of module options that need special handling, like those saved in wpcom.
2721
	 *
2722
	 * @since 4.3.0
2723
	 *
2724
	 * @param string $module Module slug.
2725
	 * @return array
2726
	 */
2727
	public static function prepare_options_for_response( $module = '' ) {
2728
		$options = self::get_updateable_data_list( $module );
2729
2730
		if ( ! is_array( $options ) || empty( $options ) ) {
2731
			return $options;
2732
		}
2733
2734
		// Some modules need special treatment.
2735
		switch ( $module ) {
2736
2737
			case 'monitor':
2738
				// Status of user notifications
2739
				$options['monitor_receive_notifications']['current_value'] = self::cast_value( self::get_remote_value( 'monitor', 'monitor_receive_notifications' ), $options['monitor_receive_notifications'] );
2740
				break;
2741
2742
			case 'post-by-email':
2743
				// Email address
2744
				$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'] );
2745
				break;
2746
2747
			case 'protect':
2748
				// Protect
2749
				$options['jetpack_protect_key']['current_value'] = get_site_option( 'jetpack_protect_key', false );
2750
				if ( ! function_exists( 'jetpack_protect_format_whitelist' ) ) {
2751
					include_once( JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php' );
2752
				}
2753
				$options['jetpack_protect_global_whitelist']['current_value'] = jetpack_protect_format_whitelist();
2754
				break;
2755
2756
			case 'related-posts':
2757
				// It's local, but it must be broken apart since it's saved as an array.
2758
				$options = self::split_options( $options, Jetpack_Options::get_option( 'relatedposts' ) );
2759
				break;
2760
2761
			case 'verification-tools':
2762
				// It's local, but it must be broken apart since it's saved as an array.
2763
				$options = self::split_options( $options, get_option( 'verification_services_codes' ) );
2764
				break;
2765
2766
			case 'google-analytics':
2767
				$wga = get_option( 'jetpack_wga' );
2768
				$code = '';
2769
				if ( is_array( $wga ) && array_key_exists( 'code', $wga ) ) {
2770
					 $code = $wga[ 'code' ];
2771
				}
2772
				$options[ 'google_analytics_tracking_id' ][ 'current_value' ] = $code;
2773
				break;
2774
2775
			case 'sharedaddy':
2776
				// It's local, but it must be broken apart since it's saved as an array.
2777
				if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
2778
					break;
2779
				}
2780
				$sharer = new Sharing_Service();
2781
				$options = self::split_options( $options, $sharer->get_global_options() );
2782
				$options['sharing_services']['current_value'] = $sharer->get_blog_services();
2783
				$other_sharedaddy_options = array( 'jetpack-twitter-cards-site-tag', 'sharedaddy_disable_resources', 'sharing_delete_service' );
2784 View Code Duplication
				foreach ( $other_sharedaddy_options as $key ) {
2785
					$default_value = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
2786
					$current_value = get_option( $key, $default_value );
2787
					$options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
2788
				}
2789
				break;
2790
2791
			case 'stats':
2792
				// It's local, but it must be broken apart since it's saved as an array.
2793
				if ( ! function_exists( 'stats_get_options' ) ) {
2794
					include_once( JETPACK__PLUGIN_DIR . 'modules/stats.php' );
2795
				}
2796
				$options = self::split_options( $options, stats_get_options() );
2797
				break;
2798
			default:
2799
				// These option are just stored as plain WordPress options.
2800 View Code Duplication
				foreach ( $options as $key => $value ) {
2801
					$default_value = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
2802
					$current_value = get_option( $key, $default_value );
2803
					$options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
2804
				}
2805
		}
2806
		// At this point some options have current_value not set because they're options
2807
		// that only get written on update, so we set current_value to the default one.
2808
		foreach ( $options as $key => $value ) {
2809
			// We don't need validate_callback in the response
2810
			if ( isset( $options[ $key ]['validate_callback'] ) ) {
2811
				unset( $options[ $key ]['validate_callback'] );
2812
			}
2813
			$default_value = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
2814
			if ( ! array_key_exists( 'current_value', $options[ $key ] ) ) {
2815
				$options[ $key ]['current_value'] = self::cast_value( $default_value, $options[ $key ] );
2816
			}
2817
		}
2818
		return $options;
2819
	}
2820
2821
	/**
2822
	 * Splits module options saved as arrays like relatedposts or verification_services_codes into separate options to be returned in the response.
2823
	 *
2824
	 * @since 4.3.0
2825
	 *
2826
	 * @param array  $separate_options Array of options admitted by the module.
2827
	 * @param array  $grouped_options Option saved as array to be splitted.
2828
	 * @param string $prefix Optional prefix for the separate option keys.
2829
	 *
2830
	 * @return array
2831
	 */
2832
	public static function split_options( $separate_options, $grouped_options, $prefix = '' ) {
2833
		if ( is_array( $grouped_options ) ) {
2834
			foreach ( $grouped_options as $key => $value ) {
2835
				$option_key = $prefix . $key;
2836
				if ( isset( $separate_options[ $option_key ] ) ) {
2837
					$separate_options[ $option_key ]['current_value'] = self::cast_value( $grouped_options[ $key ], $separate_options[ $option_key ] );
2838
				}
2839
			}
2840
		}
2841
		return $separate_options;
2842
	}
2843
2844
	/**
2845
	 * Perform a casting to the value specified in the option definition.
2846
	 *
2847
	 * @since 4.3.0
2848
	 *
2849
	 * @param mixed $value Value to cast to the proper type.
2850
	 * @param array $definition Type to cast the value to.
2851
	 *
2852
	 * @return bool|float|int|string
2853
	 */
2854
	public static function cast_value( $value, $definition ) {
2855
		if ( $value === 'NULL' ) {
2856
			return null;
2857
		}
2858
2859
		if ( isset( $definition['type'] ) ) {
2860
			switch ( $definition['type'] ) {
2861
				case 'boolean':
2862
					if ( 'true' === $value ) {
2863
						return true;
2864
					} elseif ( 'false' === $value ) {
2865
						return false;
2866
					}
2867
					return (bool) $value;
2868
					break;
2869
2870
				case 'integer':
2871
					return (int) $value;
2872
					break;
2873
2874
				case 'float':
2875
					return (float) $value;
2876
					break;
2877
2878
				case 'string':
2879
					return (string) $value;
2880
					break;
2881
			}
2882
		}
2883
		return $value;
2884
	}
2885
2886
	/**
2887
	 * Get a value not saved locally.
2888
	 *
2889
	 * @since 4.3.0
2890
	 *
2891
	 * @param string $module Module slug.
2892
	 * @param string $option Option name.
2893
	 *
2894
	 * @return bool Whether user is receiving notifications or not.
2895
	 */
2896
	public static function get_remote_value( $module, $option ) {
2897
2898
		if ( in_array( $module, array( 'post-by-email' ), true ) ) {
2899
			$option .= get_current_user_id();
2900
		}
2901
2902
		// If option doesn't exist, 'does_not_exist' will be returned.
2903
		$value = get_option( $option, 'does_not_exist' );
2904
2905
		// If option exists, just return it.
2906
		if ( 'does_not_exist' !== $value ) {
2907
			return $value;
2908
		}
2909
2910
		// Only check a remote option if Jetpack is connected.
2911
		if ( ! Jetpack::is_active() ) {
2912
			return false;
2913
		}
2914
2915
		// Do what is necessary for each module.
2916
		switch ( $module ) {
2917
			case 'monitor':
2918
				// Load the class to use the method. If class can't be found, do nothing.
2919
				if ( ! class_exists( 'Jetpack_Monitor' ) && ! include_once( Jetpack::get_module_path( $module ) ) ) {
2920
					return false;
2921
				}
2922
				$value = Jetpack_Monitor::user_receives_notifications( false );
2923
				break;
2924
2925
			case 'post-by-email':
2926
				// Load the class to use the method. If class can't be found, do nothing.
2927
				if ( ! class_exists( 'Jetpack_Post_By_Email' ) && ! include_once( Jetpack::get_module_path( $module ) ) ) {
2928
					return false;
2929
				}
2930
				$post_by_email = new Jetpack_Post_By_Email();
2931
				$value = $post_by_email->get_post_by_email_address();
2932
				if ( $value === null ) {
2933
					$value = 'NULL'; // sentinel value so it actually gets set
2934
				}
2935
				break;
2936
		}
2937
2938
		// Normalize value to boolean.
2939
		if ( is_wp_error( $value ) || is_null( $value ) ) {
2940
			$value = false;
2941
		}
2942
2943
		// Save option to use it next time.
2944
		update_option( $option, $value );
2945
2946
		return $value;
2947
	}
2948
2949
	/**
2950
	 * Get number of plugin updates available.
2951
	 *
2952
	 * @since 4.3.0
2953
	 *
2954
	 * @return mixed|WP_Error Number of plugin updates available. Otherwise, a WP_Error instance with the corresponding error.
2955
	 */
2956
	public static function get_plugin_update_count() {
2957
		$updates = wp_get_update_data();
2958
		if ( isset( $updates['counts'] ) && isset( $updates['counts']['plugins'] ) ) {
2959
			$count = $updates['counts']['plugins'];
2960
			if ( 0 == $count ) {
2961
				$response = array(
2962
					'code'    => 'success',
2963
					'message' => esc_html__( 'All plugins are up-to-date. Keep up the good work!', 'jetpack' ),
2964
					'count'   => 0,
2965
				);
2966
			} else {
2967
				$response = array(
2968
					'code'    => 'updates-available',
2969
					'message' => esc_html( sprintf( _n( '%s plugin need updating.', '%s plugins need updating.', $count, 'jetpack' ), $count ) ),
2970
					'count'   => $count,
2971
				);
2972
			}
2973
			return rest_ensure_response( $response );
2974
		}
2975
2976
		return new WP_Error( 'not_found', esc_html__( 'Could not check updates for plugins on this site.', 'jetpack' ), array( 'status' => 404 ) );
2977
	}
2978
2979
2980
	/**
2981
	 * Returns a list of all plugins in the site.
2982
	 *
2983
	 * @since 4.2.0
2984
	 * @uses get_plugins()
2985
	 *
2986
	 * @return array
2987
	 */
2988
	private static function core_get_plugins() {
2989
		if ( ! function_exists( 'get_plugins' ) ) {
2990
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
2991
		}
2992
		/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
2993
		$plugins = apply_filters( 'all_plugins', get_plugins() );
2994
2995
		if ( is_array( $plugins ) && ! empty( $plugins ) ) {
2996
			foreach ( $plugins as $plugin_slug => $plugin_data ) {
2997
				$plugins[ $plugin_slug ]['active'] = self::core_is_plugin_active( $plugin_slug );
2998
			}
2999
			return $plugins;
3000
		}
3001
3002
		return array();
3003
	}
3004
3005
	/**
3006
	 * Deprecated - Get third party plugin API keys.
3007
	 * @deprecated
3008
	 *
3009
	 * @param WP_REST_Request $request {
3010
	 *     Array of parameters received by request.
3011
	 *
3012
	 *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
3013
	 * }
3014
	 */
3015
	public static function get_service_api_key( $request ) {
3016
		_deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::get_service_api_key' );
3017
		return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::get_service_api_key( $request );
3018
	}
3019
3020
	/**
3021
	 * Deprecated - Update third party plugin API keys.
3022
	 * @deprecated
3023
	 *
3024
	 * @param WP_REST_Request $request {
3025
	 *     Array of parameters received by request.
3026
	 *
3027
	 *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
3028
	 * }
3029
	 */
3030
	public static function update_service_api_key( $request ) {
3031
		_deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::update_service_api_key' );
3032
		return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::update_service_api_key( $request ) ;
3033
	}
3034
3035
	/**
3036
	 * Deprecated - Delete a third party plugin API key.
3037
	 * @deprecated
3038
	 *
3039
	 * @param WP_REST_Request $request {
3040
	 *     Array of parameters received by request.
3041
	 *
3042
	 *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
3043
	 * }
3044
	 */
3045
	public static function delete_service_api_key( $request ) {
3046
		_deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::delete_service_api_key' );
3047
		return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::delete_service_api_key( $request );
3048
	}
3049
3050
	/**
3051
	 * Deprecated - Validate the service provided in /service-api-keys/ endpoints.
3052
	 * To add a service to these endpoints, add the service name to $valid_services
3053
	 * and add '{service name}_api_key' to the non-compact return array in get_option_names(),
3054
	 * in class-jetpack-options.php
3055
	 * @deprecated
3056
	 *
3057
	 * @param string $service The service the API key is for.
3058
	 * @return string Returns the service name if valid, null if invalid.
3059
	 */
3060
	public static function validate_service_api_service( $service = null ) {
3061
		_deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_service' );
3062
		return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_service( $service );
3063
	}
3064
3065
	/**
3066
	 * Error response for invalid service API key requests with an invalid service.
3067
	 */
3068
	public static function service_api_invalid_service_response() {
3069
		_deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::service_api_invalid_service_response' );
3070
		return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::service_api_invalid_service_response();
3071
	}
3072
3073
	/**
3074
	 * Deprecated - Validate API Key
3075
	 * @deprecated
3076
	 *
3077
	 * @param string $key The API key to be validated.
3078
	 * @param string $service The service the API key is for.
3079
	 *
3080
	 */
3081
	public static function validate_service_api_key( $key = null, $service = null ) {
3082
		_deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_key' );
3083
		return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_key( $key , $service  );
3084
	}
3085
3086
	/**
3087
	 * Deprecated - Validate Mapbox API key
3088
	 * Based loosely on https://github.com/mapbox/geocoding-example/blob/master/php/MapboxTest.php
3089
	 * @deprecated
3090
	 *
3091
	 * @param string $key The API key to be validated.
3092
	 */
3093
	public static function validate_service_api_key_mapbox( $key ) {
3094
		_deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_key' );
3095
		return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_key_mapbox( $key );
3096
3097
	}
3098
3099
	/**
3100
	 * Checks if the queried plugin is active.
3101
	 *
3102
	 * @since 4.2.0
3103
	 * @uses is_plugin_active()
3104
	 *
3105
	 * @return bool
3106
	 */
3107
	private static function core_is_plugin_active( $plugin ) {
3108
		if ( ! function_exists( 'is_plugin_active' ) ) {
3109
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
3110
		}
3111
3112
		return is_plugin_active( $plugin );
3113
	}
3114
3115
	/**
3116
	 * Get plugins data in site.
3117
	 *
3118
	 * @since 4.2.0
3119
	 *
3120
	 * @return WP_REST_Response|WP_Error List of plugins in the site. Otherwise, a WP_Error instance with the corresponding error.
3121
	 */
3122
	public static function get_plugins() {
3123
		$plugins = self::core_get_plugins();
3124
3125
		if ( ! empty( $plugins ) ) {
3126
			return rest_ensure_response( $plugins );
3127
		}
3128
3129
		return new WP_Error( 'not_found', esc_html__( 'Unable to list plugins.', 'jetpack' ), array( 'status' => 404 ) );
3130
	}
3131
3132
	/**
3133
	 * Ensures that Akismet is installed and activated.
3134
	 *
3135
	 * @since 7.7
3136
	 *
3137
	 * @return WP_REST_Response A response indicating whether or not the installation was successful.
3138
	 */
3139
	public static function activate_akismet() {
3140
		jetpack_require_lib( 'plugins' );
3141
		$result = Jetpack_Plugins::install_and_activate_plugin('akismet');
3142
3143
		if ( is_wp_error( $result ) ) {
3144
			return rest_ensure_response( array(
3145
				'code'    => 'failure',
3146
				'message' => esc_html__( 'Unable to activate Akismet', 'jetpack' )
3147
			) );
3148
		} else {
3149
			return rest_ensure_response( array(
3150
				'code'    => 'success',
3151
				'message' => esc_html__( 'Activated Akismet', 'jetpack' )
3152
			) );
3153
		}
3154
	}
3155
3156
	/**
3157
	 * Get data about the queried plugin. Currently it only returns whether the plugin is active or not.
3158
	 *
3159
	 * @since 4.2.0
3160
	 *
3161
	 * @param WP_REST_Request $request {
3162
	 *     Array of parameters received by request.
3163
	 *
3164
	 *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
3165
	 * }
3166
	 *
3167
	 * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
3168
	 */
3169
	public static function get_plugin( $request ) {
3170
3171
		$plugins = self::core_get_plugins();
3172
3173
		if ( empty( $plugins ) ) {
3174
			return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
3175
		}
3176
3177
		$plugin = stripslashes( $request['plugin'] );
3178
3179
		if ( ! in_array( $plugin, array_keys( $plugins ) ) ) {
3180
			return new WP_Error( 'plugin_not_found', esc_html( sprintf( __( 'Plugin %s is not installed.', 'jetpack' ), $plugin ) ), array( 'status' => 404 ) );
3181
		}
3182
3183
		$plugin_data = $plugins[ $plugin ];
3184
3185
		$plugin_data['active'] = self::core_is_plugin_active( $plugin );
3186
3187
		return rest_ensure_response( array(
3188
			'code'    => 'success',
3189
			'message' => esc_html__( 'Plugin found.', 'jetpack' ),
3190
			'data'    => $plugin_data
3191
		) );
3192
	}
3193
3194
	/**
3195
	 * Proxies a request to WordPress.com to request that a magic link be sent to the current user
3196
	 * to log this user in to the mobile app via email.
3197
	 *
3198
	 * @param WP_REST_REQUEST $request The request parameters.
3199
	 * @return bool|WP_Error
3200
	 */
3201
	public static function send_mobile_magic_link( $request ) {
3202
		$xml = new Jetpack_IXR_Client(
3203
			array(
3204
				'user_id' => get_current_user_id(),
3205
			)
3206
		);
3207
3208
		$xml->query( 'jetpack.sendMobileMagicLink', array() );
3209
		if ( $xml->isError() ) {
3210
			return new WP_Error(
3211
				'error_sending_mobile_magic_link',
3212
				sprintf(
3213
					'%s: %s',
3214
					$xml->getErrorCode(),
3215
					$xml->getErrorMessage()
3216
				)
3217
			);
3218
		}
3219
3220
		$response = $xml->getResponse();
3221
3222
		return rest_ensure_response(
3223
			array(
3224
				'code' => 'success',
3225
			)
3226
		);
3227
	}
3228
} // class end
3229