Completed
Push — gm-17/payment-widget ( cb2702...55e70e )
by
unknown
13:32 queued 03:19
created

_inc/lib/class.core-rest-api-endpoints.php (2 issues)

Labels
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
 * Register WP REST API endpoints for Jetpack.
4
 *
5
 * @author Automattic
6
 */
7
8
/**
9
 * Disable direct access.
10
 */
11
if ( ! defined( 'ABSPATH' ) ) {
12
	exit;
13
}
14
15
// Load WP_Error for error messages.
16
require_once ABSPATH . '/wp-includes/class-wp-error.php';
17
18
// Register endpoints when WP REST API is initialized.
19
add_action( 'rest_api_init', array( 'Jetpack_Core_Json_Api_Endpoints', 'register_endpoints' ) );
20
21
/**
22
 * Class Jetpack_Core_Json_Api_Endpoints
23
 *
24
 * @since 4.3.0
25
 */
26
class Jetpack_Core_Json_Api_Endpoints {
27
28
	/**
29
	 * @var string Generic error message when user is not allowed to perform an action.
30
	 */
31
	public static $user_permissions_error_msg;
32
33
	/**
34
	 * @var array Roles that can access Stats once they're granted access.
35
	 */
36
	public static $stats_roles;
37
38
	/**
39
	 * Declare the Jetpack REST API endpoints.
40
	 *
41
	 * @since 4.3.0
42
	 */
43
	public static function register_endpoints() {
44
45
		// Load API endpoint base classes
46
		require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php';
47
48
		// Load API endpoints
49
		require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php';
50
		require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php';
51
52
		self::$user_permissions_error_msg = esc_html__(
53
			'You do not have the correct user permissions to perform this action.
54
			Please contact your site admin if you think this is a mistake.',
55
			'jetpack'
56
		);
57
58
		self::$stats_roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
59
60
		Jetpack::load_xml_rpc_client();
61
		$ixr_client = new Jetpack_IXR_Client( array( 'user_id' => get_current_user_id() ) );
62
		$core_api_endpoint = new Jetpack_Core_API_Data( $ixr_client );
63
		$module_list_endpoint = new Jetpack_Core_API_Module_List_Endpoint();
64
		$module_data_endpoint = new Jetpack_Core_API_Module_Data_Endpoint();
65
		$module_toggle_endpoint = new Jetpack_Core_API_Module_Toggle_Endpoint( new Jetpack_IXR_Client() );
66
		$site_endpoint = new Jetpack_Core_API_Site_Endpoint();
67
68
		register_rest_route( 'jetpack/v4', '/jitm', array(
69
			'methods'  => WP_REST_Server::READABLE,
70
			'callback' => __CLASS__ . '::get_jitm_message',
71
		) );
72
73
		register_rest_route( 'jetpack/v4', '/jitm', array(
74
			'methods'  => WP_REST_Server::CREATABLE,
75
			'callback' => __CLASS__ . '::delete_jitm_message'
76
		) );
77
78
		// Register a site
79
		register_rest_route( 'jetpack/v4', '/verify_registration', array(
80
			'methods' => WP_REST_Server::EDITABLE,
81
			'callback' => __CLASS__ . '::verify_registration',
82
		) );
83
84
		// Authorize a remote user
85
		register_rest_route( 'jetpack/v4', '/remote_authorize', array(
86
			'methods' => WP_REST_Server::EDITABLE,
87
			'callback' => __CLASS__ . '::remote_authorize',
88
		) );
89
90
		// Get current connection status of Jetpack
91
		register_rest_route( 'jetpack/v4', '/connection', array(
92
			'methods' => WP_REST_Server::READABLE,
93
			'callback' => __CLASS__ . '::jetpack_connection_status',
94
		) );
95
96
		// Fetches a fresh connect URL
97
		register_rest_route( 'jetpack/v4', '/connection/url', array(
98
			'methods' => WP_REST_Server::READABLE,
99
			'callback' => __CLASS__ . '::build_connect_url',
100
			'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
101
		) );
102
103
		// Get current user connection data
104
		register_rest_route( 'jetpack/v4', '/connection/data', array(
105
			'methods' => WP_REST_Server::READABLE,
106
			'callback' => __CLASS__ . '::get_user_connection_data',
107
			'permission_callback' => __CLASS__ . '::get_user_connection_data_permission_callback',
108
		) );
109
110
		// Disconnect site from WordPress.com servers
111
		register_rest_route( 'jetpack/v4', '/connection', array(
112
			'methods' => WP_REST_Server::EDITABLE,
113
			'callback' => __CLASS__ . '::disconnect_site',
114
			'permission_callback' => __CLASS__ . '::disconnect_site_permission_callback',
115
		) );
116
117
		// Disconnect/unlink user from WordPress.com servers
118
		register_rest_route( 'jetpack/v4', '/connection/user', array(
119
			'methods' => WP_REST_Server::EDITABLE,
120
			'callback' => __CLASS__ . '::unlink_user',
121
			'permission_callback' => __CLASS__ . '::unlink_user_permission_callback',
122
		) );
123
124
		// Get current site data
125
		register_rest_route( 'jetpack/v4', '/site', array(
126
			'methods' => WP_REST_Server::READABLE,
127
			'callback' => __CLASS__ . '::get_site_data',
128
			'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
129
		) );
130
131
		// Get current site data
132
		register_rest_route( 'jetpack/v4', '/site/features', array(
133
			'methods' => WP_REST_Server::READABLE,
134
			'callback' => array( $site_endpoint, 'get_features' ),
135
			'permission_callback' => array( $site_endpoint , 'can_request' ),
136
		) );
137
138
		// Confirm that a site in identity crisis should be in staging mode
139
		register_rest_route( 'jetpack/v4', '/identity-crisis/confirm-safe-mode', array(
140
			'methods' => WP_REST_Server::EDITABLE,
141
			'callback' => __CLASS__ . '::confirm_safe_mode',
142
			'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
143
		) );
144
145
		// IDC resolve: create an entirely new shadow site for this URL.
146
		register_rest_route( 'jetpack/v4', '/identity-crisis/start-fresh', array(
147
			'methods' => WP_REST_Server::EDITABLE,
148
			'callback' => __CLASS__ . '::start_fresh_connection',
149
			'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
150
		) );
151
152
		// Handles the request to migrate stats and subscribers during an identity crisis.
153
		register_rest_route( 'jetpack/v4', 'identity-crisis/migrate', array(
154
			'methods' => WP_REST_Server::EDITABLE,
155
			'callback' => __CLASS__ . '::migrate_stats_and_subscribers',
156
			'permissison_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
157
		) );
158
159
		// Return all modules
160
		register_rest_route( 'jetpack/v4', '/module/all', array(
161
			'methods' => WP_REST_Server::READABLE,
162
			'callback' => array( $module_list_endpoint, 'process' ),
163
			'permission_callback' => array( $module_list_endpoint, 'can_request' ),
164
		) );
165
166
		// Activate many modules
167
		register_rest_route( 'jetpack/v4', '/module/all/active', array(
168
			'methods' => WP_REST_Server::EDITABLE,
169
			'callback' => array( $module_list_endpoint, 'process' ),
170
			'permission_callback' => array( $module_list_endpoint, 'can_request' ),
171
			'args' => array(
172
				'modules' => array(
173
					'default'           => '',
174
					'type'              => 'array',
175
					'items'             => array(
176
						'type'          => 'string',
177
					),
178
					'required'          => true,
179
					'validate_callback' => __CLASS__ . '::validate_module_list',
180
				),
181
				'active' => array(
182
					'default'           => true,
183
					'type'              => 'boolean',
184
					'required'          => false,
185
					'validate_callback' => __CLASS__ . '::validate_boolean',
186
				),
187
			)
188
		) );
189
190
		// Return a single module and update it when needed
191
		register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)', array(
192
			'methods' => WP_REST_Server::READABLE,
193
			'callback' => array( $core_api_endpoint, 'process' ),
194
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
195
		) );
196
197
		// Activate and deactivate a module
198
		register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)/active', array(
199
			'methods' => WP_REST_Server::EDITABLE,
200
			'callback' => array( $module_toggle_endpoint, 'process' ),
201
			'permission_callback' => array( $module_toggle_endpoint, 'can_request' ),
202
			'args' => array(
203
				'active' => array(
204
					'default'           => true,
205
					'type'              => 'boolean',
206
					'required'          => true,
207
					'validate_callback' => __CLASS__ . '::validate_boolean',
208
				),
209
			)
210
		) );
211
212
		// Update a module
213
		register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)', array(
214
			'methods' => WP_REST_Server::EDITABLE,
215
			'callback' => array( $core_api_endpoint, 'process' ),
216
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
217
			'args' => self::get_updateable_parameters( 'any' )
218
		) );
219
220
		// Get data for a specific module, i.e. Protect block count, WPCOM stats,
221
		// Akismet spam count, etc.
222
		register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)/data', array(
223
			'methods' => WP_REST_Server::READABLE,
224
			'callback' => array( $module_data_endpoint, 'process' ),
225
			'permission_callback' => array( $module_data_endpoint, 'can_request' ),
226
			'args' => array(
227
				'range' => array(
228
					'default'           => 'day',
229
					'type'              => 'string',
230
					'required'          => false,
231
					'validate_callback' => __CLASS__ . '::validate_string',
232
				),
233
			)
234
		) );
235
236
		// Check if the API key for a specific service is valid or not
237
		register_rest_route( 'jetpack/v4', '/module/(?P<service>[a-z\-]+)/key/check', array(
238
			'methods' => WP_REST_Server::READABLE,
239
			'callback' => array( $module_data_endpoint, 'key_check' ),
240
			'permission_callback' => __CLASS__ . '::update_settings_permission_check',
241
			'sanitize_callback' => 'sanitize_text_field',
242
		) );
243
244
		register_rest_route( 'jetpack/v4', '/module/(?P<service>[a-z\-]+)/key/check', array(
245
			'methods' => WP_REST_Server::EDITABLE,
246
			'callback' => array( $module_data_endpoint, 'key_check' ),
247
			'permission_callback' => __CLASS__ . '::update_settings_permission_check',
248
			'sanitize_callback' => 'sanitize_text_field',
249
			'args' => array(
250
				'api_key' => array(
251
					'default'           => '',
252
					'type'              => 'string',
253
					'validate_callback' => __CLASS__ . '::validate_alphanum',
254
				),
255
			)
256
		) );
257
258
		// Update any Jetpack module option or setting
259
		register_rest_route( 'jetpack/v4', '/settings', array(
260
			'methods' => WP_REST_Server::EDITABLE,
261
			'callback' => array( $core_api_endpoint, 'process' ),
262
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
263
			'args' => self::get_updateable_parameters( 'any' )
264
		) );
265
266
		// Update a module
267
		register_rest_route( 'jetpack/v4', '/settings/(?P<slug>[a-z\-]+)', array(
268
			'methods' => WP_REST_Server::EDITABLE,
269
			'callback' => array( $core_api_endpoint, 'process' ),
270
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
271
			'args' => self::get_updateable_parameters()
272
		) );
273
274
		// Return all module settings
275
		register_rest_route( 'jetpack/v4', '/settings/', array(
276
			'methods' => WP_REST_Server::READABLE,
277
			'callback' => array( $core_api_endpoint, 'process' ),
278
			'permission_callback' => array( $core_api_endpoint, 'can_request' ),
279
		) );
280
281
		// Reset all Jetpack options
282
		register_rest_route( 'jetpack/v4', '/options/(?P<options>[a-z\-]+)', array(
283
			'methods' => WP_REST_Server::EDITABLE,
284
			'callback' => __CLASS__ . '::reset_jetpack_options',
285
			'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
286
		) );
287
288
		// Return current Jumpstart status
289
		register_rest_route( 'jetpack/v4', '/jumpstart', array(
290
			'methods'             => WP_REST_Server::READABLE,
291
			'callback'            => __CLASS__ . '::jumpstart_status',
292
			'permission_callback' => __CLASS__ . '::update_settings_permission_check',
293
		) );
294
295
		// Update Jumpstart
296
		register_rest_route( 'jetpack/v4', '/jumpstart', array(
297
			'methods'             => WP_REST_Server::EDITABLE,
298
			'callback'            => __CLASS__ . '::jumpstart_toggle',
299
			'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
300
			'args'                => array(
301
				'active' => array(
302
					'required'          => true,
303
					'validate_callback' => __CLASS__  . '::validate_boolean',
304
				),
305
			),
306
		) );
307
308
		// Updates: get number of plugin updates available
309
		register_rest_route( 'jetpack/v4', '/updates/plugins', array(
310
			'methods' => WP_REST_Server::READABLE,
311
			'callback' => __CLASS__ . '::get_plugin_update_count',
312
			'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
313
		) );
314
315
		// Dismiss Jetpack Notices
316
		register_rest_route( 'jetpack/v4', '/notice/(?P<notice>[a-z\-_]+)', array(
317
			'methods' => WP_REST_Server::EDITABLE,
318
			'callback' => __CLASS__ . '::dismiss_notice',
319
			'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
320
		) );
321
322
		// Plugins: get list of all plugins.
323
		register_rest_route( 'jetpack/v4', '/plugins', array(
324
			'methods' => WP_REST_Server::READABLE,
325
			'callback' => __CLASS__ . '::get_plugins',
326
			'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
327
		) );
328
329
		// Plugins: check if the plugin is active.
330
		register_rest_route( 'jetpack/v4', '/plugin/(?P<plugin>[a-z\/\.\-_]+)', array(
331
			'methods' => WP_REST_Server::READABLE,
332
			'callback' => __CLASS__ . '::get_plugin',
333
			'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
334
		) );
335
	}
336
337
	/**
338
	 * Asks for a jitm, unless they've been disabled, in which case it returns an empty array
339
	 *
340
	 * @param $request WP_REST_Request
341
	 *
342
	 * @return array An array of jitms
343
	 */
344
	public static function get_jitm_message( $request ) {
345
		require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-jitm.php' );
346
347
		$jitm = Jetpack_JITM::init();
348
349
		if ( ! $jitm ) {
350
			return array();
351
		}
352
353
		return $jitm->get_messages( $request['message_path'], urldecode_deep( $request['query'] ) );
354
	}
355
356
	/**
357
	 * Dismisses a jitm
358
	 * @param $request WP_REST_Request The request
359
	 *
360
	 * @return bool Always True
361
	 */
362
	public static function delete_jitm_message( $request ) {
363
		require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-jitm.php' );
364
365
		$jitm = Jetpack_JITM::init();
366
367
		if ( ! $jitm ) {
368
			return true;
369
		}
370
371
		return $jitm->dismiss( $request['id'], $request['feature_class'] );
372
	}
373
374
	/**
375
	 * Handles verification that a site is registered
376
	 *
377
	 * @since 5.4.0
378
	 *
379
	 * @param WP_REST_Request $request The request sent to the WP REST API.
380
	 *
381
	 * @return array|wp-error
382
	 */
383
	public static function verify_registration( $request ) {
384
		require_once JETPACK__PLUGIN_DIR . 'class.jetpack-xmlrpc-server.php';
385
		$xmlrpc_server = new Jetpack_XMLRPC_Server();
386
		$result = $xmlrpc_server->verify_registration( array( $request['secret_1'], $request['state'] ) );
387
388
		if ( is_a( $result, 'IXR_Error' ) ) {
389
			$result = new WP_Error( $result->code, $result->message );
390
		}
391
392
		return $result;
393
	}
394
395
	/**
396
	 * Handles verification that a site is registered
397
	 *
398
	 * @since 5.4.0
399
	 *
400
	 * @param WP_REST_Request $request The request sent to the WP REST API.
401
	 *
402
	 * @return array|wp-error
403
	 */
404
	 public static function remote_authorize( $request ) {
405
		require_once JETPACK__PLUGIN_DIR . 'class.jetpack-xmlrpc-server.php';
406
		$xmlrpc_server = new Jetpack_XMLRPC_Server();
407
		$result = $xmlrpc_server->remote_authorize( $request );
408
409
		if ( is_a( $result, 'IXR_Error' ) ) {
410
			$result = new WP_Error( $result->code, $result->message );
411
		}
412
413
		return $result;
414
	 }
415
416
	/**
417
	 * Handles dismissing of Jetpack Notices
418
	 *
419
	 * @since 4.3.0
420
	 *
421
	 * @param WP_REST_Request $request The request sent to the WP REST API.
422
	 *
423
	 * @return array|wp-error
424
	 */
425
	public static function dismiss_notice( $request ) {
426
		$notice = $request['notice'];
427
428
		if ( ! isset( $request['dismissed'] ) || $request['dismissed'] !== true ) {
429
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid parameter "dismissed".', 'jetpack' ), array( 'status' => 404 ) );
430
		}
431
432
		if ( isset( $notice ) && ! empty( $notice ) ) {
433
			switch( $notice ) {
434
				case 'feedback_dash_request':
435
				case 'welcome':
436
					$notices = get_option( 'jetpack_dismissed_notices', array() );
437
					$notices[ $notice ] = true;
438
					update_option( 'jetpack_dismissed_notices', $notices );
439
					return rest_ensure_response( get_option( 'jetpack_dismissed_notices', array() ) );
440
441
				default:
442
					return new WP_Error( 'invalid_param', esc_html__( 'Invalid parameter "notice".', 'jetpack' ), array( 'status' => 404 ) );
443
			}
444
		}
445
446
		return new WP_Error( 'required_param', esc_html__( 'Missing parameter "notice".', 'jetpack' ), array( 'status' => 404 ) );
447
	}
448
449
	/**
450
	 * Verify that the user can disconnect the site.
451
	 *
452
	 * @since 4.3.0
453
	 *
454
	 * @return bool|WP_Error True if user is able to disconnect the site.
455
	 */
456 View Code Duplication
	public static function disconnect_site_permission_callback() {
457
		if ( current_user_can( 'jetpack_disconnect' ) ) {
458
			return true;
459
		}
460
461
		return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
462
463
	}
464
465
	/**
466
	 * Verify that the user can get a connect/link URL
467
	 *
468
	 * @since 4.3.0
469
	 *
470
	 * @return bool|WP_Error True if user is able to disconnect the site.
471
	 */
472 View Code Duplication
	public static function connect_url_permission_callback() {
473
		if ( current_user_can( 'jetpack_connect_user' ) ) {
474
			return true;
475
		}
476
477
		return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
478
479
	}
480
481
	/**
482
	 * Verify that a user can get the data about the current user.
483
	 * Only those who can connect.
484
	 *
485
	 * @since 4.3.0
486
	 *
487
	 * @uses Jetpack::is_user_connected();
488
	 *
489
	 * @return bool|WP_Error True if user is able to unlink.
490
	 */
491 View Code Duplication
	public static function get_user_connection_data_permission_callback() {
492
		if ( current_user_can( 'jetpack_connect_user' ) ) {
493
			return true;
494
		}
495
496
		return new WP_Error( 'invalid_user_permission_user_connection_data', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
497
	}
498
499
	/**
500
	 * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
501
	 *
502
	 * @since 4.3.0
503
	 *
504
	 * @uses Jetpack::is_user_connected();
505
	 *
506
	 * @return bool|WP_Error True if user is able to unlink.
507
	 */
508
	public static function unlink_user_permission_callback() {
509
		if ( current_user_can( 'jetpack_connect_user' ) && Jetpack::is_user_connected( get_current_user_id() ) ) {
510
			return true;
511
		}
512
513
		return new WP_Error( 'invalid_user_permission_unlink_user', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
514
	}
515
516
	/**
517
	 * Verify that user can manage Jetpack modules.
518
	 *
519
	 * @since 4.3.0
520
	 *
521
	 * @return bool Whether user has the capability 'jetpack_manage_modules'.
522
	 */
523
	public static function manage_modules_permission_check() {
524
		if ( current_user_can( 'jetpack_manage_modules' ) ) {
525
			return true;
526
		}
527
528
		return new WP_Error( 'invalid_user_permission_manage_modules', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
529
	}
530
531
	/**
532
	 * Verify that user can update Jetpack modules.
533
	 *
534
	 * @since 4.3.0
535
	 *
536
	 * @return bool Whether user has the capability 'jetpack_configure_modules'.
537
	 */
538 View Code Duplication
	public static function configure_modules_permission_check() {
539
		if ( current_user_can( 'jetpack_configure_modules' ) ) {
540
			return true;
541
		}
542
543
		return new WP_Error( 'invalid_user_permission_configure_modules', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
544
	}
545
546
	/**
547
	 * Verify that user can view Jetpack admin page.
548
	 *
549
	 * @since 4.3.0
550
	 *
551
	 * @return bool Whether user has the capability 'jetpack_admin_page'.
552
	 */
553 View Code Duplication
	public static function view_admin_page_permission_check() {
554
		if ( current_user_can( 'jetpack_admin_page' ) ) {
555
			return true;
556
		}
557
558
		return new WP_Error( 'invalid_user_permission_view_admin', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
559
	}
560
561
	/**
562
	 * Verify that user can mitigate an identity crisis.
563
	 *
564
	 * @since 4.4.0
565
	 *
566
	 * @return bool Whether user has capability 'jetpack_disconnect'.
567
	 */
568 View Code Duplication
	public static function identity_crisis_mitigation_permission_check() {
569
		if ( current_user_can( 'jetpack_disconnect' ) ) {
570
			return true;
571
		}
572
573
		return new WP_Error( 'invalid_user_permission_identity_crisis', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
574
	}
575
576
	/**
577
	 * Verify that user can update Jetpack general settings.
578
	 *
579
	 * @since 4.3.0
580
	 *
581
	 * @return bool Whether user has the capability 'update_settings_permission_check'.
582
	 */
583 View Code Duplication
	public static function update_settings_permission_check() {
584
		if ( current_user_can( 'jetpack_configure_modules' ) ) {
585
			return true;
586
		}
587
588
		return new WP_Error( 'invalid_user_permission_manage_settings', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
589
	}
590
591
	/**
592
	 * Verify that user can view Jetpack admin page and can activate plugins.
593
	 *
594
	 * @since 4.3.0
595
	 *
596
	 * @return bool Whether user has the capability 'jetpack_admin_page' and 'activate_plugins'.
597
	 */
598 View Code Duplication
	public static function activate_plugins_permission_check() {
599
		if ( current_user_can( 'jetpack_admin_page' ) && current_user_can( 'activate_plugins' ) ) {
600
			return true;
601
		}
602
603
		return new WP_Error( 'invalid_user_permission_activate_plugins', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
604
	}
605
606
	/**
607
	 * Contextual HTTP error code for authorization failure.
608
	 *
609
	 * Taken from rest_authorization_required_code() in WP-API plugin until is added to core.
610
	 * @see https://github.com/WP-API/WP-API/commit/7ba0ae6fe4f605d5ffe4ee85b1cd5f9fb46900a6
611
	 *
612
	 * @since 4.3.0
613
	 *
614
	 * @return int
615
	 */
616
	public static function rest_authorization_required_code() {
617
		return is_user_logged_in() ? 403 : 401;
618
	}
619
620
	/**
621
	 * Get connection status for this Jetpack site.
622
	 *
623
	 * @since 4.3.0
624
	 *
625
	 * @return bool True if site is connected
626
	 */
627
	public static function jetpack_connection_status() {
628
		return rest_ensure_response( array(
629
				'isActive'  => Jetpack::is_active(),
630
				'isStaging' => Jetpack::is_staging_site(),
631
				'devMode'   => array(
632
					'isActive' => Jetpack::is_development_mode(),
633
					'constant' => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
634
					'url'      => site_url() && false === strpos( site_url(), '.' ),
635
					'filter'   => apply_filters( 'jetpack_development_mode', false ),
636
				),
637
			)
638
		);
639
	}
640
641
	/**
642
	 * Disconnects Jetpack from the WordPress.com Servers
643
	 *
644
	 * @uses Jetpack::disconnect();
645
	 * @since 4.3.0
646
	 *
647
	 * @param WP_REST_Request $request The request sent to the WP REST API.
648
	 *
649
	 * @return bool|WP_Error True if Jetpack successfully disconnected.
650
	 */
651 View Code Duplication
	public static function disconnect_site( $request ) {
652
653
		if ( ! isset( $request['isActive'] ) || $request['isActive'] !== false ) {
654
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
655
		}
656
657
		if ( Jetpack::is_active() ) {
658
			Jetpack::disconnect();
659
			return rest_ensure_response( array( 'code' => 'success' ) );
660
		}
661
662
		return new WP_Error( 'disconnect_failed', esc_html__( 'Was not able to disconnect the site.  Please try again.', 'jetpack' ), array( 'status' => 400 ) );
663
	}
664
665
	/**
666
	 * Gets a new connect raw URL with fresh nonce.
667
	 *
668
	 * @uses Jetpack::disconnect();
669
	 * @since 4.3.0
670
	 *
671
	 * @param WP_REST_Request $request The request sent to the WP REST API.
672
	 *
673
	 * @return string|WP_Error A raw URL if the connection URL could be built; error message otherwise.
674
	 */
675
	public static function build_connect_url() {
676
		$url = Jetpack::init()->build_connect_url( true, false, false );
677
		if ( $url ) {
678
			return rest_ensure_response( $url );
679
		}
680
681
		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 ) );
682
	}
683
684
	/**
685
	 * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
686
	 * Information about the master/primary user.
687
	 * Information about the current user.
688
	 *
689
	 * @since 4.3.0
690
	 *
691
	 * @param WP_REST_Request $request The request sent to the WP REST API.
692
	 *
693
	 * @return object
694
	 */
695
	public static function get_user_connection_data() {
696
		require_once( JETPACK__PLUGIN_DIR . '_inc/lib/admin-pages/class.jetpack-react-page.php' );
697
698
		$response = array(
699
//			'othersLinked' => Jetpack::get_other_linked_admins(),
700
			'currentUser'  => jetpack_current_user_data(),
701
		);
702
		return rest_ensure_response( $response );
703
	}
704
705
	/**
706
	 * Returns the proper name for Jetpack Holiday Snow setting.
707
	 * When the REST route starts, the holiday-snow.php file where jetpack_holiday_snow_option_name() function is defined is not loaded,
708
	 * so where using this to replicate it and have the same functionality.
709
	 *
710
	 * @since 4.4.0
711
	 *
712
	 * @return string
713
	 */
714
	public static function holiday_snow_option_name() {
715
		/** This filter is documented in modules/holiday-snow.php */
716
		return apply_filters( 'jetpack_holiday_snow_option_name', 'jetpack_holiday_snow_enabled' );
717
	}
718
719
	/**
720
	 * Update a single miscellaneous setting for this Jetpack installation, like Holiday Snow.
721
	 *
722
	 * @since 4.3.0
723
	 *
724
	 * @param WP_REST_Request $request The request sent to the WP REST API.
725
	 *
726
	 * @return object Jetpack miscellaneous settings.
727
	 */
728
	public static function update_setting( $request ) {
729
		// Get parameters to update the module.
730
		$param = $request->get_params();
731
732
		// Exit if no parameters were passed.
733 View Code Duplication
		if ( ! is_array( $param ) ) {
734
			return new WP_Error( 'missing_setting', esc_html__( 'Missing setting.', 'jetpack' ), array( 'status' => 404 ) );
735
		}
736
737
		// Get option name and value.
738
		$option = key( $param );
739
		$value  = current( $param );
740
741
		// Log success or not
742
		$updated = false;
743
744
		switch ( $option ) {
745
			case self::holiday_snow_option_name():
746
				$updated = update_option( $option, ( true == (bool) $value ) ? 'letitsnow' : '' );
747
				break;
748
		}
749
750
		if ( $updated ) {
751
			return rest_ensure_response( array(
752
				'code' 	  => 'success',
753
				'message' => esc_html__( 'Setting updated.', 'jetpack' ),
754
				'value'   => $value,
755
			) );
756
		}
757
758
		return new WP_Error( 'setting_not_updated', esc_html__( 'The setting was not updated.', 'jetpack' ), array( 'status' => 400 ) );
759
	}
760
761
	/**
762
	 * Unlinks current user from the WordPress.com Servers.
763
	 *
764
	 * @since 4.3.0
765
	 * @uses  Jetpack::unlink_user
766
	 *
767
	 * @param WP_REST_Request $request The request sent to the WP REST API.
768
	 *
769
	 * @return bool|WP_Error True if user successfully unlinked.
770
	 */
771 View Code Duplication
	public static function unlink_user( $request ) {
772
773
		if ( ! isset( $request['linked'] ) || $request['linked'] !== false ) {
774
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
775
		}
776
777
		if ( Jetpack::unlink_user() ) {
778
			return rest_ensure_response(
779
				array(
780
					'code' => 'success'
781
				)
782
			);
783
		}
784
785
		return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user.  Please try again.', 'jetpack' ), array( 'status' => 400 ) );
786
	}
787
788
	/**
789
	 * Fetch site data from .com including the site's current plan.
790
	 *
791
	 * @since 5.5.0
792
	 *
793
	 * @return array Array of site properties.
794
	 */
795
	public static function site_data() {
796
		$site_id = Jetpack_Options::get_option( 'id' );
797
798
		if ( ! $site_id ) {
799
			 new WP_Error( 'site_id_missing' );
800
		}
801
802
		$response = Jetpack_Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d', $site_id ) .'?force=wpcom', '1.1' );
803
804
		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
805
			return new WP_Error( 'site_data_fetch_failed' );
806
		}
807
808
		// Save plan details in the database for future use without API calls
809
		$results = json_decode( $response['body'], true );
810
811
		if ( is_array( $results ) && isset( $results['plan'] ) ) {
812
813
			// Set flag for newly purchased plan
814
			$current_plan = Jetpack::get_active_plan();
815
			if ( $current_plan['product_slug'] !== $results['plan']['product_slug'] && 'jetpack_free' !== $results['plan']['product_slug'] ) {
816
				update_option( 'show_welcome_for_new_plan', true ) ;
817
			}
818
819
			update_option( 'jetpack_active_plan', $results['plan'] );
820
		}
821
		$body = wp_remote_retrieve_body( $response );
822
823
		return json_decode( $body );
824
	}
825
	/**
826
	 * Get site data, including for example, the site's current plan.
827
	 *
828
	 * @since 4.3.0
829
	 *
830
	 * @return array Array of site properties.
831
	 */
832
	public static function get_site_data() {
833
		$site_data = self::site_data();
834
835
		if ( ! is_wp_error( $site_data ) ) {
836
			return rest_ensure_response( array(
837
					'code' => 'success',
838
					'message' => esc_html__( 'Site data correctly received.', 'jetpack' ),
839
					'data' => json_encode( $site_data ),
840
				)
841
			);
842
		}
843 View Code Duplication
		if ( $site_data->get_error_code() === 'site_data_fetch_failed' ) {
0 ignored issues
show
The method get_error_code cannot be called on $site_data (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
844
			return new WP_Error( 'site_data_fetch_failed', esc_html__( 'Failed fetching site data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
845
		}
846
847 View Code Duplication
		if ( $site_data->get_error_code() === 'site_id_missing' ) {
0 ignored issues
show
The method get_error_code cannot be called on $site_data (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
848
			return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
849
		}
850
	}
851
852
	/**
853
	 * Handles identity crisis mitigation, confirming safe mode for this site.
854
	 *
855
	 * @since 4.4.0
856
	 *
857
	 * @return bool | WP_Error True if option is properly set.
858
	 */
859
	public static function confirm_safe_mode() {
860
		$updated = Jetpack_Options::update_option( 'safe_mode_confirmed', true );
861
		if ( $updated ) {
862
			return rest_ensure_response(
863
				array(
864
					'code' => 'success'
865
				)
866
			);
867
		}
868
		return new WP_Error(
869
			'error_setting_jetpack_safe_mode',
870
			esc_html__( 'Could not confirm safe mode.', 'jetpack' ),
871
			array( 'status' => 500 )
872
		);
873
	}
874
875
	/**
876
	 * Handles identity crisis mitigation, migrating stats and subscribers from old url to this, new url.
877
	 *
878
	 * @since 4.4.0
879
	 *
880
	 * @return bool | WP_Error True if option is properly set.
881
	 */
882
	public static function migrate_stats_and_subscribers() {
883
		if ( Jetpack_Options::get_option( 'sync_error_idc' ) && ! Jetpack_Options::delete_option( 'sync_error_idc' ) ) {
884
			return new WP_Error(
885
				'error_deleting_sync_error_idc',
886
				esc_html__( 'Could not delete sync error option.', 'jetpack' ),
887
				array( 'status' => 500 )
888
			);
889
		}
890
891
		if ( Jetpack_Options::get_option( 'migrate_for_idc' ) || Jetpack_Options::update_option( 'migrate_for_idc', true ) ) {
892
			return rest_ensure_response(
893
				array(
894
					'code' => 'success'
895
				)
896
			);
897
		}
898
		return new WP_Error(
899
			'error_setting_jetpack_migrate',
900
			esc_html__( 'Could not confirm migration.', 'jetpack' ),
901
			array( 'status' => 500 )
902
		);
903
	}
904
905
	/**
906
	 * This IDC resolution will disconnect the site and re-connect to a completely new
907
	 * and separate shadow site than the original.
908
	 *
909
	 * It will first will disconnect the site without phoning home as to not disturb the production site.
910
	 * It then builds a fresh connection URL and sends it back along with the response.
911
	 *
912
	 * @since 4.4.0
913
	 * @return bool|WP_Error
914
	 */
915
	public static function start_fresh_connection() {
916
		// First clear the options / disconnect.
917
		Jetpack::disconnect();
918
		return self::build_connect_url();
919
	}
920
921
	/**
922
	 * Reset Jetpack options
923
	 *
924
	 * @since 4.3.0
925
	 *
926
	 * @param WP_REST_Request $request {
927
	 *     Array of parameters received by request.
928
	 *
929
	 *     @type string $options Available options to reset are options|modules
930
	 * }
931
	 *
932
	 * @return bool|WP_Error True if options were reset. Otherwise, a WP_Error instance with the corresponding error.
933
	 */
934
	public static function reset_jetpack_options( $request ) {
935
936
		if ( ! isset( $request['reset'] ) || $request['reset'] !== true ) {
937
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
938
		}
939
940
		if ( isset( $request['options'] ) ) {
941
			$data = $request['options'];
942
943
			switch( $data ) {
944
				case ( 'options' ) :
945
					$options_to_reset = Jetpack::get_jetpack_options_for_reset();
946
947
					// Reset the Jetpack options
948
					foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
949
						Jetpack_Options::delete_option( $option_to_reset );
950
					}
951
952
					foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
953
						delete_option( $option_to_reset );
954
					}
955
956
					// Reset to default modules
957
					$default_modules = Jetpack::get_default_modules();
958
					Jetpack::update_active_modules( $default_modules );
959
960
					// Jumpstart option is special
961
					Jetpack_Options::update_option( 'jumpstart', 'new_connection' );
962
					return rest_ensure_response( array(
963
						'code' 	  => 'success',
964
						'message' => esc_html__( 'Jetpack options reset.', 'jetpack' ),
965
					) );
966
					break;
967
968
				case 'modules':
969
					$default_modules = Jetpack::get_default_modules();
970
					Jetpack::update_active_modules( $default_modules );
971
					return rest_ensure_response( array(
972
						'code' 	  => 'success',
973
						'message' => esc_html__( 'Modules reset to default.', 'jetpack' ),
974
					) );
975
					break;
976
977
				default:
978
					return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
979
			}
980
		}
981
982
		return new WP_Error( 'required_param', esc_html__( 'Missing parameter "type".', 'jetpack' ), array( 'status' => 404 ) );
983
	}
984
985
	/**
986
	 * Retrieves the current status of Jumpstart.
987
	 *
988
	 * @since 4.5.0
989
	 *
990
	 * @return bool
991
	 */
992
	public static function jumpstart_status() {
993
		return array(
994
			'status' => Jetpack_Options::get_option( 'jumpstart' )
995
		);
996
	}
997
998
	/**
999
	 * Toggles activation or deactivation of the JumpStart
1000
	 *
1001
	 * @since 4.3.0
1002
	 *
1003
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1004
	 *
1005
	 * @return bool|WP_Error True if toggling Jumpstart succeeded. Otherwise, a WP_Error instance with the corresponding error.
1006
	 */
1007
	public static function jumpstart_toggle( $request ) {
1008
1009
		if ( $request[ 'active' ] ) {
1010
			return self::jumpstart_activate( $request );
1011
		} else {
1012
			return self::jumpstart_deactivate( $request );
1013
		}
1014
	}
1015
1016
	/**
1017
	 * Activates a series of valid Jetpack modules and initializes some options.
1018
	 *
1019
	 * @since 4.3.0
1020
	 *
1021
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1022
	 *
1023
	 * @return bool|WP_Error True if Jumpstart succeeded. Otherwise, a WP_Error instance with the corresponding error.
1024
	 */
1025
	public static function jumpstart_activate( $request ) {
1026
		$modules = Jetpack::get_available_modules();
1027
		$activate_modules = array();
1028
		foreach ( $modules as $module ) {
1029
			$module_info = Jetpack::get_module( $module );
1030
			if ( isset( $module_info['feature'] ) && is_array( $module_info['feature'] ) && in_array( 'Jumpstart', $module_info['feature'] ) ) {
1031
				$activate_modules[] = $module;
1032
			}
1033
		}
1034
1035
		// Collect success/error messages like modules that are properly activated.
1036
		$result = array(
1037
			'activated_modules' => array(),
1038
			'failed_modules'    => array(),
1039
		);
1040
1041
		// Update the jumpstart option
1042
		if ( 'new_connection' === Jetpack_Options::get_option( 'jumpstart' ) ) {
1043
			$result['jumpstart_activated'] = Jetpack_Options::update_option( 'jumpstart', 'jumpstart_activated' );
1044
		}
1045
1046
		// Check for possible conflicting plugins
1047
		$module_slugs_filtered = Jetpack::init()->filter_default_modules( $activate_modules );
1048
1049
		foreach ( $module_slugs_filtered as $module_slug ) {
1050
			Jetpack::log( 'activate', $module_slug );
1051
			if ( Jetpack::activate_module( $module_slug, false, false ) ) {
1052
				$result['activated_modules'][] = $module_slug;
1053
			} else {
1054
				$result['failed_modules'][] = $module_slug;
1055
			}
1056
		}
1057
1058
		// Set the default sharing buttons and set to display on posts if none have been set.
1059
		$sharing_services = get_option( 'sharing-services' );
1060
		$sharing_options  = get_option( 'sharing-options' );
1061
		if ( empty( $sharing_services['visible'] ) ) {
1062
			// Default buttons to set
1063
			$visible = array(
1064
				'twitter',
1065
				'facebook',
1066
				'google-plus-1',
1067
			);
1068
			$hidden = array();
1069
1070
			// Set some sharing settings
1071
			if ( class_exists( 'Sharing_Service' ) ) {
1072
				$sharing = new Sharing_Service();
1073
				$sharing_options['global'] = array(
1074
					'button_style'  => 'icon',
1075
					'sharing_label' => $sharing->default_sharing_label,
1076
					'open_links'    => 'same',
1077
					'show'          => array( 'post' ),
1078
					'custom'        => isset( $sharing_options['global']['custom'] ) ? $sharing_options['global']['custom'] : array()
1079
				);
1080
1081
				$result['sharing_options']  = update_option( 'sharing-options', $sharing_options );
1082
				$result['sharing_services'] = update_option( 'sharing-services', array( 'visible' => $visible, 'hidden' => $hidden ) );
1083
			}
1084
		}
1085
1086
		// If all Jumpstart modules were activated
1087
		if ( empty( $result['failed_modules'] ) ) {
1088
			return rest_ensure_response( array(
1089
				'code' 	  => 'success',
1090
				'message' => esc_html__( 'Jumpstart done.', 'jetpack' ),
1091
				'data'    => $result,
1092
			) );
1093
		}
1094
1095
		return new WP_Error( 'jumpstart_failed', esc_html( sprintf( _n( 'Jumpstart failed activating this module: %s.', 'Jumpstart failed activating these modules: %s.', count( $result['failed_modules'] ), 'jetpack' ), join( ', ', $result['failed_modules'] ) ) ), array( 'status' => 400 ) );
1096
	}
1097
1098
	/**
1099
	 * Dismisses Jumpstart so user is not prompted to go through it again.
1100
	 *
1101
	 * @since 4.3.0
1102
	 *
1103
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1104
	 *
1105
	 * @return bool|WP_Error True if Jumpstart was disabled or was nothing to dismiss. Otherwise, a WP_Error instance with a message.
1106
	 */
1107
	public static function jumpstart_deactivate( $request ) {
1108
1109
		// If dismissed, flag the jumpstart option as such.
1110
		if ( 'new_connection' === Jetpack_Options::get_option( 'jumpstart' ) ) {
1111
			if ( Jetpack_Options::update_option( 'jumpstart', 'jumpstart_dismissed' ) ) {
1112
				return rest_ensure_response( array(
1113
					'code' 	  => 'success',
1114
					'message' => esc_html__( 'Jumpstart dismissed.', 'jetpack' ),
1115
				) );
1116
			} else {
1117
				return new WP_Error( 'jumpstart_failed_dismiss', esc_html__( 'Jumpstart could not be dismissed.', 'jetpack' ), array( 'status' => 400 ) );
1118
			}
1119
		}
1120
1121
		// If this was not a new connection and there was nothing to dismiss, don't fail.
1122
		return rest_ensure_response( array(
1123
			'code' 	  => 'success',
1124
			'message' => esc_html__( 'Nothing to dismiss. This was not a new connection.', 'jetpack' ),
1125
		) );
1126
	}
1127
1128
	/**
1129
	 * Get the query parameters to update module options or general settings.
1130
	 *
1131
	 * @since 4.3.0
1132
	 * @since 4.4.0 Accepts a $selector parameter.
1133
	 *
1134
	 * @param string $selector Selects a set of options to update, Can be empty, a module slug or 'any'.
1135
	 *
1136
	 * @return array
1137
	 */
1138
	public static function get_updateable_parameters( $selector = '' ) {
1139
		$parameters = array(
1140
			'context'     => array(
1141
				'default' => 'edit',
1142
			),
1143
		);
1144
1145
		return array_merge( $parameters, self::get_updateable_data_list( $selector ) );
1146
	}
1147
1148
	/**
1149
	 * Returns a list of module options or general settings that can be updated.
1150
	 *
1151
	 * @since 4.3.0
1152
	 * @since 4.4.0 Accepts 'any' as a parameter which will make it return the entire list.
1153
	 *
1154
	 * @param string|array $selector Module slug, 'any', or an array of parameters.
1155
	 *                               If empty, it's assumed we're updating a module and we'll try to get its slug.
1156
	 *                               If 'any' the full list is returned.
1157
	 *                               If it's an array of parameters, includes the elements by matching keys.
1158
	 *
1159
	 * @return array
1160
	 */
1161
	public static function get_updateable_data_list( $selector = '' ) {
1162
1163
		$options = array(
1164
1165
			// Carousel
1166
			'carousel_background_color' => array(
1167
				'description'       => esc_html__( 'Color scheme.', 'jetpack' ),
1168
				'type'              => 'string',
1169
				'default'           => 'black',
1170
				'enum'              => array(
1171
					'black',
1172
					'white',
1173
				),
1174
				'enum_labels' => array(
1175
					'black' => esc_html__( 'Black', 'jetpack' ),
1176
					'white' => esc_html__( 'White', 'jetpack' ),
1177
				),
1178
				'validate_callback' => __CLASS__ . '::validate_list_item',
1179
				'jp_group'          => 'carousel',
1180
			),
1181
			'carousel_display_exif' => array(
1182
				'description'       => wp_kses( sprintf( __( 'Show photo metadata (<a href="http://en.wikipedia.org/wiki/Exchangeable_image_file_format" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) ), array( 'a' => array( 'href' => true, 'target' => true ) ) ),
1183
				'type'              => 'boolean',
1184
				'default'           => 0,
1185
				'validate_callback' => __CLASS__ . '::validate_boolean',
1186
				'jp_group'          => 'carousel',
1187
			),
1188
1189
			// Comments
1190
			'highlander_comment_form_prompt' => array(
1191
				'description'       => esc_html__( 'Greeting Text', 'jetpack' ),
1192
				'type'              => 'string',
1193
				'default'           => esc_html__( 'Leave a Reply', 'jetpack' ),
1194
				'sanitize_callback' => 'sanitize_text_field',
1195
				'jp_group'          => 'comments',
1196
			),
1197
			'jetpack_comment_form_color_scheme' => array(
1198
				'description'       => esc_html__( "Color scheme", 'jetpack' ),
1199
				'type'              => 'string',
1200
				'default'           => 'light',
1201
				'enum'              => array(
1202
					'light',
1203
					'dark',
1204
					'transparent',
1205
				),
1206
				'enum_labels' => array(
1207
					'light'       => esc_html__( 'Light', 'jetpack' ),
1208
					'dark'        => esc_html__( 'Dark', 'jetpack' ),
1209
					'transparent' => esc_html__( 'Transparent', 'jetpack' ),
1210
				),
1211
				'validate_callback' => __CLASS__ . '::validate_list_item',
1212
				'jp_group'          => 'comments',
1213
			),
1214
1215
			// Custom Content Types
1216
			'jetpack_portfolio' => array(
1217
				'description'       => esc_html__( 'Enable or disable Jetpack portfolio post type.', 'jetpack' ),
1218
				'type'              => 'boolean',
1219
				'default'           => 0,
1220
				'validate_callback' => __CLASS__ . '::validate_boolean',
1221
				'jp_group'          => 'custom-content-types',
1222
			),
1223
			'jetpack_portfolio_posts_per_page' => array(
1224
				'description'       => esc_html__( 'Number of entries to show at most in Portfolio pages.', 'jetpack' ),
1225
				'type'              => 'integer',
1226
				'default'           => 10,
1227
				'validate_callback' => __CLASS__ . '::validate_posint',
1228
				'jp_group'          => 'custom-content-types',
1229
			),
1230
			'jetpack_testimonial' => array(
1231
				'description'       => esc_html__( 'Enable or disable Jetpack testimonial post type.', 'jetpack' ),
1232
				'type'              => 'boolean',
1233
				'default'           => 0,
1234
				'validate_callback' => __CLASS__ . '::validate_boolean',
1235
				'jp_group'          => 'custom-content-types',
1236
			),
1237
			'jetpack_testimonial_posts_per_page' => array(
1238
				'description'       => esc_html__( 'Number of entries to show at most in Testimonial pages.', 'jetpack' ),
1239
				'type'              => 'integer',
1240
				'default'           => 10,
1241
				'validate_callback' => __CLASS__ . '::validate_posint',
1242
				'jp_group'          => 'custom-content-types',
1243
			),
1244
1245
			// Galleries
1246
			'tiled_galleries' => array(
1247
				'description'       => esc_html__( 'Display all your gallery pictures in a cool mosaic.', 'jetpack' ),
1248
				'type'              => 'boolean',
1249
				'default'           => 0,
1250
				'validate_callback' => __CLASS__ . '::validate_boolean',
1251
				'jp_group'          => 'tiled-gallery',
1252
			),
1253
1254
			'gravatar_disable_hovercards' => array(
1255
				'description'       => esc_html__( "View people's profiles when you mouse over their Gravatars", 'jetpack' ),
1256
				'type'              => 'string',
1257
				'default'           => 'enabled',
1258
				// Not visible. This is used as the checkbox value.
1259
				'enum'              => array(
1260
					'enabled',
1261
					'disabled',
1262
				),
1263
				'enum_labels' => array(
1264
					'enabled'  => esc_html__( 'Enabled', 'jetpack' ),
1265
					'disabled' => esc_html__( 'Disabled', 'jetpack' ),
1266
				),
1267
				'validate_callback' => __CLASS__ . '::validate_list_item',
1268
				'jp_group'          => 'gravatar-hovercards',
1269
			),
1270
1271
			// Infinite Scroll
1272
			'infinite_scroll' => array(
1273
				'description'       => esc_html__( 'To infinity and beyond', 'jetpack' ),
1274
				'type'              => 'boolean',
1275
				'default'           => 1,
1276
				'validate_callback' => __CLASS__ . '::validate_boolean',
1277
				'jp_group'          => 'infinite-scroll',
1278
			),
1279
			'infinite_scroll_google_analytics' => array(
1280
				'description'       => esc_html__( 'Use Google Analytics with Infinite Scroll', 'jetpack' ),
1281
				'type'              => 'boolean',
1282
				'default'           => 0,
1283
				'validate_callback' => __CLASS__ . '::validate_boolean',
1284
				'jp_group'          => 'infinite-scroll',
1285
			),
1286
1287
			// Likes
1288
			'wpl_default' => array(
1289
				'description'       => esc_html__( 'WordPress.com Likes are', 'jetpack' ),
1290
				'type'              => 'string',
1291
				'default'           => 'on',
1292
				'enum'              => array(
1293
					'on',
1294
					'off',
1295
				),
1296
				'enum_labels' => array(
1297
					'on'  => esc_html__( 'On for all posts', 'jetpack' ),
1298
					'off' => esc_html__( 'Turned on per post', 'jetpack' ),
1299
				),
1300
				'validate_callback' => __CLASS__ . '::validate_list_item',
1301
				'jp_group'          => 'likes',
1302
			),
1303
			'social_notifications_like' => array(
1304
				'description'       => esc_html__( 'Send email notification when someone likes a post', 'jetpack' ),
1305
				'type'              => 'boolean',
1306
				'default'           => 1,
1307
				'validate_callback' => __CLASS__ . '::validate_boolean',
1308
				'jp_group'          => 'likes',
1309
			),
1310
1311
			// Markdown
1312
			'wpcom_publish_comments_with_markdown' => array(
1313
				'description'       => esc_html__( 'Use Markdown for comments.', 'jetpack' ),
1314
				'type'              => 'boolean',
1315
				'default'           => 0,
1316
				'validate_callback' => __CLASS__ . '::validate_boolean',
1317
				'jp_group'          => 'markdown',
1318
			),
1319
			'wpcom_publish_posts_with_markdown' => array(
1320
				'description'       => esc_html__( 'Use Markdown for posts.', 'jetpack' ),
1321
				'type'              => 'boolean',
1322
				'default'           => 0,
1323
				'validate_callback' => __CLASS__ . '::validate_boolean',
1324
				'jp_group'          => 'markdown',
1325
			),
1326
1327
			// Mobile Theme
1328
			'wp_mobile_excerpt' => array(
1329
				'description'       => esc_html__( 'Excerpts', 'jetpack' ),
1330
				'type'              => 'boolean',
1331
				'default'           => 0,
1332
				'validate_callback' => __CLASS__ . '::validate_boolean',
1333
				'jp_group'          => 'minileven',
1334
			),
1335
			'wp_mobile_featured_images' => array(
1336
				'description'       => esc_html__( 'Featured Images', 'jetpack' ),
1337
				'type'              => 'boolean',
1338
				'default'           => 0,
1339
				'validate_callback' => __CLASS__ . '::validate_boolean',
1340
				'jp_group'          => 'minileven',
1341
			),
1342
			'wp_mobile_app_promos' => array(
1343
				'description'       => esc_html__( 'Show a promo for the WordPress mobile apps in the footer of the mobile theme.', 'jetpack' ),
1344
				'type'              => 'boolean',
1345
				'default'           => 0,
1346
				'validate_callback' => __CLASS__ . '::validate_boolean',
1347
				'jp_group'          => 'minileven',
1348
			),
1349
1350
			// Monitor
1351
			'monitor_receive_notifications' => array(
1352
				'description'       => esc_html__( 'Receive Monitor Email Notifications.', 'jetpack' ),
1353
				'type'              => 'boolean',
1354
				'default'           => 0,
1355
				'validate_callback' => __CLASS__ . '::validate_boolean',
1356
				'jp_group'          => 'monitor',
1357
			),
1358
1359
			// Post by Email
1360
			'post_by_email_address' => array(
1361
				'description'       => esc_html__( 'Email Address', 'jetpack' ),
1362
				'type'              => 'string',
1363
				'default'           => 'noop',
1364
				'enum'              => array(
1365
					'noop',
1366
					'create',
1367
					'regenerate',
1368
					'delete',
1369
				),
1370
				'enum_labels' => array(
1371
					'noop'       => '',
1372
					'create'     => esc_html__( 'Create Post by Email address', 'jetpack' ),
1373
					'regenerate' => esc_html__( 'Regenerate Post by Email address', 'jetpack' ),
1374
					'delete'     => esc_html__( 'Delete Post by Email address', 'jetpack' ),
1375
				),
1376
				'validate_callback' => __CLASS__ . '::validate_list_item',
1377
				'jp_group'          => 'post-by-email',
1378
			),
1379
1380
			// Protect
1381
			'jetpack_protect_key' => array(
1382
				'description'       => esc_html__( 'Protect API key', 'jetpack' ),
1383
				'type'              => 'string',
1384
				'default'           => '',
1385
				'validate_callback' => __CLASS__ . '::validate_alphanum',
1386
				'jp_group'          => 'protect',
1387
			),
1388
			'jetpack_protect_global_whitelist' => array(
1389
				'description'       => esc_html__( 'Protect global whitelist', 'jetpack' ),
1390
				'type'              => 'string',
1391
				'default'           => '',
1392
				'validate_callback' => __CLASS__ . '::validate_string',
1393
				'sanitize_callback' => 'esc_textarea',
1394
				'jp_group'          => 'protect',
1395
			),
1396
1397
			// Sharing
1398
			'sharing_services' => array(
1399
				'description'       => esc_html__( 'Enabled Services and those hidden behind a button', 'jetpack' ),
1400
				'type'              => 'object',
1401
				'default'           => array(
1402
					'visible' => array( 'twitter', 'facebook', 'google-plus-1' ),
1403
					'hidden'  => array(),
1404
				),
1405
				'validate_callback' => __CLASS__ . '::validate_services',
1406
				'jp_group'          => 'sharedaddy',
1407
			),
1408
			'button_style' => array(
1409
				'description'       => esc_html__( 'Button Style', 'jetpack' ),
1410
				'type'              => 'string',
1411
				'default'           => 'icon',
1412
				'enum'              => array(
1413
					'icon-text',
1414
					'icon',
1415
					'text',
1416
					'official',
1417
				),
1418
				'enum_labels' => array(
1419
					'icon-text' => esc_html__( 'Icon + text', 'jetpack' ),
1420
					'icon'      => esc_html__( 'Icon only', 'jetpack' ),
1421
					'text'      => esc_html__( 'Text only', 'jetpack' ),
1422
					'official'  => esc_html__( 'Official buttons', 'jetpack' ),
1423
				),
1424
				'validate_callback' => __CLASS__ . '::validate_list_item',
1425
				'jp_group'          => 'sharedaddy',
1426
			),
1427
			'sharing_label' => array(
1428
				'description'       => esc_html__( 'Sharing Label', 'jetpack' ),
1429
				'type'              => 'string',
1430
				'default'           => '',
1431
				'validate_callback' => __CLASS__ . '::validate_string',
1432
				'sanitize_callback' => 'esc_html',
1433
				'jp_group'          => 'sharedaddy',
1434
			),
1435
			'show' => array(
1436
				'description'       => esc_html__( 'Views where buttons are shown', 'jetpack' ),
1437
				'type'              => 'array',
1438
				'items'             => array(
1439
					'type' => 'string'
1440
				),
1441
				'default'           => array( 'post' ),
1442
				'validate_callback' => __CLASS__ . '::validate_sharing_show',
1443
				'jp_group'          => 'sharedaddy',
1444
			),
1445
			'jetpack-twitter-cards-site-tag' => array(
1446
				'description'       => esc_html__( "The Twitter username of the owner of this site's domain.", 'jetpack' ),
1447
				'type'              => 'string',
1448
				'default'           => '',
1449
				'validate_callback' => __CLASS__ . '::validate_twitter_username',
1450
				'sanitize_callback' => 'esc_html',
1451
				'jp_group'          => 'sharedaddy',
1452
			),
1453
			'sharedaddy_disable_resources' => array(
1454
				'description'       => esc_html__( 'Disable CSS and JS', 'jetpack' ),
1455
				'type'              => 'boolean',
1456
				'default'           => 0,
1457
				'validate_callback' => __CLASS__ . '::validate_boolean',
1458
				'jp_group'          => 'sharedaddy',
1459
			),
1460
			'custom' => array(
1461
				'description'       => esc_html__( 'Custom sharing services added by user.', 'jetpack' ),
1462
				'type'              => 'object',
1463
				'default'           => array(
1464
					'sharing_name' => '',
1465
					'sharing_url'  => '',
1466
					'sharing_icon' => '',
1467
				),
1468
				'validate_callback' => __CLASS__ . '::validate_custom_service',
1469
				'jp_group'          => 'sharedaddy',
1470
			),
1471
			// Not an option, but an action that can be perfomed on the list of custom services passing the service ID.
1472
			'sharing_delete_service' => array(
1473
				'description'       => esc_html__( 'Delete custom sharing service.', 'jetpack' ),
1474
				'type'              => 'string',
1475
				'default'           => '',
1476
				'validate_callback' => __CLASS__ . '::validate_custom_service_id',
1477
				'jp_group'          => 'sharedaddy',
1478
			),
1479
1480
			// SSO
1481
			'jetpack_sso_require_two_step' => array(
1482
				'description'       => esc_html__( 'Require Two-Step Authentication', 'jetpack' ),
1483
				'type'              => 'boolean',
1484
				'default'           => 0,
1485
				'validate_callback' => __CLASS__ . '::validate_boolean',
1486
				'jp_group'          => 'sso',
1487
			),
1488
			'jetpack_sso_match_by_email' => array(
1489
				'description'       => esc_html__( 'Match by Email', 'jetpack' ),
1490
				'type'              => 'boolean',
1491
				'default'           => 0,
1492
				'validate_callback' => __CLASS__ . '::validate_boolean',
1493
				'jp_group'          => 'sso',
1494
			),
1495
1496
			// Subscriptions
1497
			'stb_enabled' => array(
1498
				'description'       => esc_html__( "Show a <em>'follow blog'</em> option in the comment form", 'jetpack' ),
1499
				'type'              => 'boolean',
1500
				'default'           => 1,
1501
				'validate_callback' => __CLASS__ . '::validate_boolean',
1502
				'jp_group'          => 'subscriptions',
1503
			),
1504
			'stc_enabled' => array(
1505
				'description'       => esc_html__( "Show a <em>'follow comments'</em> option in the comment form", 'jetpack' ),
1506
				'type'              => 'boolean',
1507
				'default'           => 1,
1508
				'validate_callback' => __CLASS__ . '::validate_boolean',
1509
				'jp_group'          => 'subscriptions',
1510
			),
1511
1512
			// Related Posts
1513
			'show_headline' => array(
1514
				'description'       => esc_html__( 'Show a "Related" header to more clearly separate the related section from posts', 'jetpack' ),
1515
				'type'              => 'boolean',
1516
				'default'           => 1,
1517
				'validate_callback' => __CLASS__ . '::validate_boolean',
1518
				'jp_group'          => 'related-posts',
1519
			),
1520
			'show_thumbnails' => array(
1521
				'description'       => esc_html__( 'Use a large and visually striking layout', 'jetpack' ),
1522
				'type'              => 'boolean',
1523
				'default'           => 0,
1524
				'validate_callback' => __CLASS__ . '::validate_boolean',
1525
				'jp_group'          => 'related-posts',
1526
			),
1527
1528
			// Spelling and Grammar - After the Deadline
1529
			'onpublish' => array(
1530
				'description'       => esc_html__( 'Proofread when a post or page is first published.', 'jetpack' ),
1531
				'type'              => 'boolean',
1532
				'default'           => 0,
1533
				'validate_callback' => __CLASS__ . '::validate_boolean',
1534
				'jp_group'          => 'after-the-deadline',
1535
			),
1536
			'onupdate' => array(
1537
				'description'       => esc_html__( 'Proofread when a post or page is updated.', 'jetpack' ),
1538
				'type'              => 'boolean',
1539
				'default'           => 0,
1540
				'validate_callback' => __CLASS__ . '::validate_boolean',
1541
				'jp_group'          => 'after-the-deadline',
1542
			),
1543
			'Bias Language' => array(
1544
				'description'       => esc_html__( 'Bias Language', 'jetpack' ),
1545
				'type'              => 'boolean',
1546
				'default'           => 0,
1547
				'validate_callback' => __CLASS__ . '::validate_boolean',
1548
				'jp_group'          => 'after-the-deadline',
1549
			),
1550
			'Cliches' => array(
1551
				'description'       => esc_html__( 'Clichés', 'jetpack' ),
1552
				'type'              => 'boolean',
1553
				'default'           => 0,
1554
				'validate_callback' => __CLASS__ . '::validate_boolean',
1555
				'jp_group'          => 'after-the-deadline',
1556
			),
1557
			'Complex Expression' => array(
1558
				'description'       => esc_html__( 'Complex Phrases', 'jetpack' ),
1559
				'type'              => 'boolean',
1560
				'default'           => 0,
1561
				'validate_callback' => __CLASS__ . '::validate_boolean',
1562
				'jp_group'          => 'after-the-deadline',
1563
			),
1564
			'Diacritical Marks' => array(
1565
				'description'       => esc_html__( 'Diacritical Marks', 'jetpack' ),
1566
				'type'              => 'boolean',
1567
				'default'           => 0,
1568
				'validate_callback' => __CLASS__ . '::validate_boolean',
1569
				'jp_group'          => 'after-the-deadline',
1570
			),
1571
			'Double Negative' => array(
1572
				'description'       => esc_html__( 'Double Negatives', 'jetpack' ),
1573
				'type'              => 'boolean',
1574
				'default'           => 0,
1575
				'validate_callback' => __CLASS__ . '::validate_boolean',
1576
				'jp_group'          => 'after-the-deadline',
1577
			),
1578
			'Hidden Verbs' => array(
1579
				'description'       => esc_html__( 'Hidden Verbs', 'jetpack' ),
1580
				'type'              => 'boolean',
1581
				'default'           => 0,
1582
				'validate_callback' => __CLASS__ . '::validate_boolean',
1583
				'jp_group'          => 'after-the-deadline',
1584
			),
1585
			'Jargon Language' => array(
1586
				'description'       => esc_html__( 'Jargon', 'jetpack' ),
1587
				'type'              => 'boolean',
1588
				'default'           => 0,
1589
				'validate_callback' => __CLASS__ . '::validate_boolean',
1590
				'jp_group'          => 'after-the-deadline',
1591
			),
1592
			'Passive voice' => array(
1593
				'description'       => esc_html__( 'Passive Voice', 'jetpack' ),
1594
				'type'              => 'boolean',
1595
				'default'           => 0,
1596
				'validate_callback' => __CLASS__ . '::validate_boolean',
1597
				'jp_group'          => 'after-the-deadline',
1598
			),
1599
			'Phrases to Avoid' => array(
1600
				'description'       => esc_html__( 'Phrases to Avoid', 'jetpack' ),
1601
				'type'              => 'boolean',
1602
				'default'           => 0,
1603
				'validate_callback' => __CLASS__ . '::validate_boolean',
1604
				'jp_group'          => 'after-the-deadline',
1605
			),
1606
			'Redundant Expression' => array(
1607
				'description'       => esc_html__( 'Redundant Phrases', 'jetpack' ),
1608
				'type'              => 'boolean',
1609
				'default'           => 0,
1610
				'validate_callback' => __CLASS__ . '::validate_boolean',
1611
				'jp_group'          => 'after-the-deadline',
1612
			),
1613
			'guess_lang' => array(
1614
				'description'       => esc_html__( 'Use automatically detected language to proofread posts and pages', 'jetpack' ),
1615
				'type'              => 'boolean',
1616
				'default'           => 0,
1617
				'validate_callback' => __CLASS__ . '::validate_boolean',
1618
				'jp_group'          => 'after-the-deadline',
1619
			),
1620
			'ignored_phrases' => array(
1621
				'description'       => esc_html__( 'Add Phrase to be ignored', 'jetpack' ),
1622
				'type'              => 'string',
1623
				'default'           => '',
1624
				'sanitize_callback' => 'esc_html',
1625
				'jp_group'          => 'after-the-deadline',
1626
			),
1627
			'unignore_phrase' => array(
1628
				'description'       => esc_html__( 'Remove Phrase from being ignored', 'jetpack' ),
1629
				'type'              => 'string',
1630
				'default'           => '',
1631
				'sanitize_callback' => 'esc_html',
1632
				'jp_group'          => 'after-the-deadline',
1633
			),
1634
1635
			// Verification Tools
1636
			'google' => array(
1637
				'description'       => esc_html__( 'Google Search Console', 'jetpack' ),
1638
				'type'              => 'string',
1639
				'default'           => '',
1640
				'validate_callback' => __CLASS__ . '::validate_verification_service',
1641
				'jp_group'          => 'verification-tools',
1642
			),
1643
			'bing' => array(
1644
				'description'       => esc_html__( 'Bing Webmaster Center', 'jetpack' ),
1645
				'type'              => 'string',
1646
				'default'           => '',
1647
				'validate_callback' => __CLASS__ . '::validate_verification_service',
1648
				'jp_group'          => 'verification-tools',
1649
			),
1650
			'pinterest' => array(
1651
				'description'       => esc_html__( 'Pinterest Site Verification', 'jetpack' ),
1652
				'type'              => 'string',
1653
				'default'           => '',
1654
				'validate_callback' => __CLASS__ . '::validate_verification_service',
1655
				'jp_group'          => 'verification-tools',
1656
			),
1657
			'yandex' => array(
1658
				'description'       => esc_html__( 'Yandex Site Verification', 'jetpack' ),
1659
				'type'              => 'string',
1660
				'default'           => '',
1661
				'validate_callback' => __CLASS__ . '::validate_verification_service',
1662
				'jp_group'          => 'verification-tools',
1663
			),
1664
			'enable_header_ad' => array(
1665
				'description'        => esc_html__( 'Display an ad unit at the top of each page.', 'jetpack' ),
1666
				'type'               => 'boolean',
1667
				'default'            => 0,
1668
				'validate_callback'  => __CLASS__ . '::validate_boolean',
1669
				'jp_group'           => 'wordads',
1670
			),
1671
			'wordads_approved' => array(
1672
				'description'        => esc_html__( 'Is site approved for WordAds?', 'jetpack' ),
1673
				'type'               => 'boolean',
1674
				'default'            => 0,
1675
				'validate_callback'  => __CLASS__ . '::validate_boolean',
1676
				'jp_group'           => 'wordads',
1677
			),
1678
			'wordads_second_belowpost' => array(
1679
				'description'        => esc_html__( 'Display second ad below post?', 'jetpack' ),
1680
				'type'               => 'boolean',
1681
				'default'            => 1,
1682
				'validate_callback'  => __CLASS__ . '::validate_boolean',
1683
				'jp_group'           => 'wordads',
1684
			),
1685
			'wordads_display_front_page' => array(
1686
				'description'        => esc_html__( 'Display ads on the front page?', 'jetpack' ),
1687
				'type'               => 'boolean',
1688
				'default'            => 1,
1689
				'validate_callback'  => __CLASS__ . '::validate_boolean',
1690
				'jp_group'           => 'wordads',
1691
			),
1692
			'wordads_display_post' => array(
1693
				'description'        => esc_html__( 'Display ads on posts?', 'jetpack' ),
1694
				'type'               => 'boolean',
1695
				'default'            => 1,
1696
				'validate_callback'  => __CLASS__ . '::validate_boolean',
1697
				'jp_group'           => 'wordads',
1698
			),
1699
			'wordads_display_page' => array(
1700
				'description'        => esc_html__( 'Display ads on pages?', 'jetpack' ),
1701
				'type'               => 'boolean',
1702
				'default'            => 1,
1703
				'validate_callback'  => __CLASS__ . '::validate_boolean',
1704
				'jp_group'           => 'wordads',
1705
			),
1706
			'wordads_display_archive' => array(
1707
				'description'        => esc_html__( 'Display ads on archive pages?', 'jetpack' ),
1708
				'type'               => 'boolean',
1709
				'default'            => 1,
1710
				'validate_callback'  => __CLASS__ . '::validate_boolean',
1711
				'jp_group'           => 'wordads',
1712
			),
1713
1714
			// Google Analytics
1715
			'google_analytics_tracking_id' => array(
1716
				'description'        => esc_html__( 'Google Analytics', 'jetpack' ),
1717
				'type'               => 'string',
1718
				'default'            => '',
1719
				'validate_callback'  => __CLASS__ . '::validate_alphanum',
1720
				'jp_group'           => 'google-analytics',
1721
			),
1722
1723
			// Stats
1724
			'admin_bar' => array(
1725
				'description'       => esc_html__( 'Put a chart showing 48 hours of views in the admin bar.', 'jetpack' ),
1726
				'type'              => 'boolean',
1727
				'default'           => 1,
1728
				'validate_callback' => __CLASS__ . '::validate_boolean',
1729
				'jp_group'          => 'stats',
1730
			),
1731
			'roles' => array(
1732
				'description'       => esc_html__( 'Select the roles that will be able to view stats reports.', 'jetpack' ),
1733
				'type'              => 'array',
1734
				'items'             => array(
1735
					'type' => 'string'
1736
				),
1737
				'default'           => array( 'administrator' ),
1738
				'validate_callback' => __CLASS__ . '::validate_stats_roles',
1739
				'sanitize_callback' => __CLASS__ . '::sanitize_stats_allowed_roles',
1740
				'jp_group'          => 'stats',
1741
			),
1742
			'count_roles' => array(
1743
				'description'       => esc_html__( 'Count the page views of registered users who are logged in.', 'jetpack' ),
1744
				'type'              => 'array',
1745
				'items'             => array(
1746
					'type' => 'string'
1747
				),
1748
				'default'           => array( 'administrator' ),
1749
				'validate_callback' => __CLASS__ . '::validate_stats_roles',
1750
				'jp_group'          => 'stats',
1751
			),
1752
			'blog_id' => array(
1753
				'description'       => esc_html__( 'Blog ID.', 'jetpack' ),
1754
				'type'              => 'boolean',
1755
				'default'           => 0,
1756
				'validate_callback' => __CLASS__ . '::validate_boolean',
1757
				'jp_group'          => 'stats',
1758
			),
1759
			'do_not_track' => array(
1760
				'description'       => esc_html__( 'Do not track.', 'jetpack' ),
1761
				'type'              => 'boolean',
1762
				'default'           => 1,
1763
				'validate_callback' => __CLASS__ . '::validate_boolean',
1764
				'jp_group'          => 'stats',
1765
			),
1766
			'hide_smile' => array(
1767
				'description'       => esc_html__( 'Hide the stats smiley face image.', 'jetpack' ),
1768
				'type'              => 'boolean',
1769
				'default'           => 1,
1770
				'validate_callback' => __CLASS__ . '::validate_boolean',
1771
				'jp_group'          => 'stats',
1772
			),
1773
			'version' => array(
1774
				'description'       => esc_html__( 'Version.', 'jetpack' ),
1775
				'type'              => 'integer',
1776
				'default'           => 9,
1777
				'validate_callback' => __CLASS__ . '::validate_posint',
1778
				'jp_group'          => 'stats',
1779
			),
1780
1781
			// Settings - Not a module
1782
			self::holiday_snow_option_name() => array(
1783
				'description'       => '',
1784
				'type'              => 'boolean',
1785
				'default'           => 0,
1786
				'validate_callback' => __CLASS__ . '::validate_boolean',
1787
				'jp_group'          => 'settings',
1788
			),
1789
1790
			// Akismet - Not a module, but a plugin. The options can be passed and handled differently.
1791
			'akismet_show_user_comments_approved' => array(
1792
				'description'       => '',
1793
				'type'              => 'boolean',
1794
				'default'           => 0,
1795
				'validate_callback' => __CLASS__ . '::validate_boolean',
1796
				'jp_group'          => 'settings',
1797
			),
1798
1799
			'wordpress_api_key' => array(
1800
				'description'       => '',
1801
				'type'              => 'string',
1802
				'default'           => '',
1803
				'validate_callback' => __CLASS__ . '::validate_alphanum',
1804
				'jp_group'          => 'settings',
1805
			),
1806
1807
			// Apps card on dashboard
1808
			'dismiss_dash_app_card' => array(
1809
				'description'       => '',
1810
				'type'              => 'boolean',
1811
				'default'           => 0,
1812
				'validate_callback' => __CLASS__ . '::validate_boolean',
1813
				'jp_group'          => 'settings',
1814
			),
1815
1816
			// Empty stats card dismiss
1817
			'dismiss_empty_stats_card' => array(
1818
				'description'       => '',
1819
				'type'              => 'boolean',
1820
				'default'           => 0,
1821
				'validate_callback' => __CLASS__ . '::validate_boolean',
1822
				'jp_group'          => 'settings',
1823
			),
1824
1825
			'onboarding' => array(
1826
				'description'       => '',
1827
				'type'              => 'object',
1828
				'default'           => array(
1829
					'token'            => '',
1830
					'siteTitle'        => '',
1831
					'siteDescription'  => '',
1832
					'genre'            => 'blog',
1833
					'businessPersonal' => 'personal',
1834
					'businessInfo'     => array(
1835
						'businessName'     => '',
1836
						'businessAddress'  => '',
1837
						'businessCity'     => '',
1838
						'businessState'    => '',
1839
						'businessZipCode'  => '',
1840
					),
1841
					'homepageFormat'   => 'news',
1842
					'addContactForm'   => false,
1843
					'end'              => false,
1844
				),
1845
				'validate_callback' => __CLASS__ . '::validate_onboarding',
1846
				'jp_group'          => 'settings',
1847
			),
1848
1849
			// Show welcome for newly purchased plan
1850
			'show_welcome_for_new_plan' => array(
1851
				'description'       => '',
1852
				'type'              => 'boolean',
1853
				'default'           => 0,
1854
				'validate_callback' => __CLASS__ . '::validate_boolean',
1855
				'jp_group'          => 'settings',
1856
			),
1857
1858
		);
1859
1860
		// Add modules to list so they can be toggled
1861
		$modules = Jetpack::get_available_modules();
1862
		if ( is_array( $modules ) && ! empty( $modules ) ) {
1863
			$module_args = array(
1864
				'description'       => '',
1865
				'type'              => 'boolean',
1866
				'default'           => 0,
1867
				'validate_callback' => __CLASS__ . '::validate_boolean',
1868
				'jp_group'          => 'modules',
1869
			);
1870
			foreach( $modules as $module ) {
1871
				$options[ $module ] = $module_args;
1872
			}
1873
		}
1874
1875
		if ( is_array( $selector ) ) {
1876
1877
			// Return only those options whose keys match $selector keys
1878
			return array_intersect_key( $options, $selector );
1879
		}
1880
1881
		if ( 'any' === $selector ) {
1882
1883
			// Toggle module or update any module option or any general setting
1884
			return $options;
1885
		}
1886
1887
		// We're updating the options for a single module.
1888
		if ( empty( $selector ) ) {
1889
			$selector = self::get_module_requested();
1890
		}
1891
		$selected = array();
1892
		foreach ( $options as $option => $attributes ) {
1893
1894
			// Not adding an isset( $attributes['jp_group'] ) because if it's not set, it must be fixed, otherwise options will fail.
1895
			if ( $selector === $attributes['jp_group'] ) {
1896
				$selected[ $option ] = $attributes;
1897
			}
1898
		}
1899
		return $selected;
1900
	}
1901
1902
	/**
1903
	 * Validates that the parameters are proper values that can be set during Jetpack onboarding.
1904
	 *
1905
	 * @since 5.4.0
1906
	 *
1907
	 * @param array           $onboarding_data Values to check.
1908
	 * @param WP_REST_Request $request         The request sent to the WP REST API.
1909
	 * @param string          $param           Name of the parameter passed to endpoint holding $value.
1910
	 *
1911
	 * @return bool|WP_Error
1912
	 */
1913
	public static function validate_onboarding( $onboarding_data, $request, $param ) {
1914
		if ( ! is_array( $onboarding_data ) ) {
1915
			return new WP_Error( 'invalid_param', esc_html__( 'Not valid onboarding data.', 'jetpack' ) );
1916
		}
1917
		foreach ( $onboarding_data as $value ) {
1918
			if ( is_string( $value ) ) {
1919
				$onboarding_choice = self::validate_string( $value, $request, $param );
1920
			} elseif ( is_array( $value ) ) {
1921
				$onboarding_choice = self::validate_onboarding( $value, $request, $param );
1922
			} else {
1923
				$onboarding_choice = self::validate_boolean( $value, $request, $param );
1924
			}
1925
			if ( is_wp_error( $onboarding_choice ) ) {
1926
				return $onboarding_choice;
1927
			}
1928
		}
1929
		return true;
1930
	}
1931
1932
	/**
1933
	 * Validates that the parameter is either a pure boolean or a numeric string that can be mapped to a boolean.
1934
	 *
1935
	 * @since 4.3.0
1936
	 *
1937
	 * @param string|bool $value Value to check.
1938
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1939
	 * @param string $param Name of the parameter passed to endpoint holding $value.
1940
	 *
1941
	 * @return bool|WP_Error
1942
	 */
1943
	public static function validate_boolean( $value, $request, $param ) {
1944
		if ( ! is_bool( $value ) && ! ( ( ctype_digit( $value ) || is_numeric( $value ) ) && in_array( $value, array( 0, 1 ) ) ) ) {
1945
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be true, false, 0 or 1.', 'jetpack' ), $param ) );
1946
		}
1947
		return true;
1948
	}
1949
1950
	/**
1951
	 * Validates that the parameter is a positive integer.
1952
	 *
1953
	 * @since 4.3.0
1954
	 *
1955
	 * @param int $value Value to check.
1956
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1957
	 * @param string $param Name of the parameter passed to endpoint holding $value.
1958
	 *
1959
	 * @return bool|WP_Error
1960
	 */
1961
	public static function validate_posint( $value = 0, $request, $param ) {
1962
		if ( ! is_numeric( $value ) || $value <= 0 ) {
1963
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a positive integer.', 'jetpack' ), $param ) );
1964
		}
1965
		return true;
1966
	}
1967
1968
	/**
1969
	 * Validates that the parameter belongs to a list of admitted values.
1970
	 *
1971
	 * @since 4.3.0
1972
	 *
1973
	 * @param string $value Value to check.
1974
	 * @param WP_REST_Request $request The request sent to the WP REST API.
1975
	 * @param string $param Name of the parameter passed to endpoint holding $value.
1976
	 *
1977
	 * @return bool|WP_Error
1978
	 */
1979
	public static function validate_list_item( $value = '', $request, $param ) {
1980
		$attributes = $request->get_attributes();
1981
		if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
1982
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s not recognized', 'jetpack' ), $param ) );
1983
		}
1984
		$args = $attributes['args'][ $param ];
1985
		if ( ! empty( $args['enum'] ) ) {
1986
1987
			// If it's an associative array, use the keys to check that the value is among those admitted.
1988
			$enum = ( count( array_filter( array_keys( $args['enum'] ), 'is_string' ) ) > 0 ) ? array_keys( $args['enum'] ) : $args['enum'];
1989 View Code Duplication
			if ( ! in_array( $value, $enum ) ) {
1990
				return new WP_Error( 'invalid_param_value', sprintf(
1991
					/* Translators: first variable is the parameter passed to endpoint that holds the list item, the second is a list of admitted values. */
1992
					esc_html__( '%1$s must be one of %2$s', 'jetpack' ), $param, implode( ', ', $enum )
1993
				) );
1994
			}
1995
		}
1996
		return true;
1997
	}
1998
1999
	/**
2000
	 * Validates that the parameter belongs to a list of admitted values.
2001
	 *
2002
	 * @since 4.3.0
2003
	 *
2004
	 * @param string $value Value to check.
2005
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2006
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2007
	 *
2008
	 * @return bool|WP_Error
2009
	 */
2010
	public static function validate_module_list( $value = '', $request, $param ) {
2011
		if ( ! is_array( $value ) ) {
2012
			return new WP_Error( 'invalid_param_value', sprintf( esc_html__( '%s must be an array', 'jetpack' ), $param ) );
2013
		}
2014
2015
		$modules = Jetpack::get_available_modules();
2016
2017
		if ( count( array_intersect( $value, $modules ) ) != count( $value ) ) {
2018
			return new WP_Error( 'invalid_param_value', sprintf( esc_html__( '%s must be a list of valid modules', 'jetpack' ), $param ) );
2019
		}
2020
2021
		return true;
2022
	}
2023
2024
	/**
2025
	 * Validates that the parameter is an alphanumeric or empty string (to be able to clear the field).
2026
	 *
2027
	 * @since 4.3.0
2028
	 *
2029
	 * @param string $value Value to check.
2030
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2031
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2032
	 *
2033
	 * @return bool|WP_Error
2034
	 */
2035
	public static function validate_alphanum( $value = '', $request, $param ) {
2036 View Code Duplication
		if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^[a-z0-9]+$/i', $value ) ) ) {
2037
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string.', 'jetpack' ), $param ) );
2038
		}
2039
		return true;
2040
	}
2041
2042
	/**
2043
	 * Validates that the parameter is a tag or id for a verification service, or an empty string (to be able to clear the field).
2044
	 *
2045
	 * @since 4.6.0
2046
	 *
2047
	 * @param string $value Value to check.
2048
	 * @param WP_REST_Request $request
2049
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2050
	 *
2051
	 * @return bool|WP_Error
2052
	 */
2053
	public static function validate_verification_service( $value = '', $request, $param ) {
2054 View Code Duplication
		if ( ! empty( $value ) && ! ( is_string( $value ) && ( preg_match( '/^[a-z0-9_-]+$/i', $value ) || preg_match( '#^<meta name="([a-z0-9_\-.:]+)?" content="([a-z0-9_-]+)?" />$#i', $value ) ) ) ) {
2055
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string or a verification tag.', 'jetpack' ), $param ) );
2056
		}
2057
		return true;
2058
	}
2059
2060
	/**
2061
	 * Validates that the parameter is among the roles allowed for Stats.
2062
	 *
2063
	 * @since 4.3.0
2064
	 *
2065
	 * @param string|bool $value Value to check.
2066
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2067
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2068
	 *
2069
	 * @return bool|WP_Error
2070
	 */
2071
	public static function validate_stats_roles( $value, $request, $param ) {
2072
		if ( ! empty( $value ) && ! array_intersect( self::$stats_roles, $value ) ) {
2073
			return new WP_Error( 'invalid_param', sprintf(
2074
				/* 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. */
2075
				esc_html__( '%1$s must be %2$s.', 'jetpack' ), $param, join( ', ', self::$stats_roles )
2076
			) );
2077
		}
2078
		return true;
2079
	}
2080
2081
	/**
2082
	 * Validates that the parameter is among the views where the Sharing can be displayed.
2083
	 *
2084
	 * @since 4.3.0
2085
	 *
2086
	 * @param string|bool $value Value to check.
2087
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2088
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2089
	 *
2090
	 * @return bool|WP_Error
2091
	 */
2092
	public static function validate_sharing_show( $value, $request, $param ) {
2093
		$views = array( 'index', 'post', 'page', 'attachment', 'jetpack-portfolio' );
2094 View Code Duplication
		if ( ! is_array( $value ) ) {
2095
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array of post types.', 'jetpack' ), $param ) );
2096
		}
2097 View Code Duplication
		if ( ! array_intersect( $views, $value ) ) {
2098
			return new WP_Error( 'invalid_param', sprintf(
2099
				/* 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 */
2100
				esc_html__( '%1$s must be %2$s.', 'jetpack' ), $param, join( ', ', $views )
2101
			) );
2102
		}
2103
		return true;
2104
	}
2105
2106
	/**
2107
	 * Validates that the parameter is among the views where the Sharing can be displayed.
2108
	 *
2109
	 * @since 4.3.0
2110
	 *
2111
	 * @param string|bool $value {
2112
	 *     Value to check received by request.
2113
	 *
2114
	 *     @type array $visible List of slug of services to share to that are displayed directly in the page.
2115
	 *     @type array $hidden  List of slug of services to share to that are concealed in a folding menu.
2116
	 * }
2117
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2118
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2119
	 *
2120
	 * @return bool|WP_Error
2121
	 */
2122
	public static function validate_services( $value, $request, $param ) {
2123 View Code Duplication
		if ( ! is_array( $value ) || ! isset( $value['visible'] ) || ! isset( $value['hidden'] ) ) {
2124
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array with visible and hidden items.', 'jetpack' ), $param ) );
2125
		}
2126
2127
		// Allow to clear everything.
2128
		if ( empty( $value['visible'] ) && empty( $value['hidden'] ) ) {
2129
			return true;
2130
		}
2131
2132 View Code Duplication
		if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
2133
			return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
2134
		}
2135
		$sharer = new Sharing_Service();
2136
		$services = array_keys( $sharer->get_all_services() );
2137
2138
		if (
2139
			( ! empty( $value['visible'] ) && ! array_intersect( $value['visible'], $services ) )
2140
			||
2141
			( ! empty( $value['hidden'] ) && ! array_intersect( $value['hidden'], $services ) ) )
2142
		{
2143
			return new WP_Error( 'invalid_param', sprintf(
2144
				/* Translators: placeholder 1 is a parameter holding the services passed to endpoint, placeholder 2 is a list of all Jetpack Sharing services */
2145
				esc_html__( '%1$s visible and hidden items must be a list of %2$s.', 'jetpack' ), $param, join( ', ', $services )
2146
			) );
2147
		}
2148
		return true;
2149
	}
2150
2151
	/**
2152
	 * Validates that the parameter has enough information to build a custom sharing button.
2153
	 *
2154
	 * @since 4.3.0
2155
	 *
2156
	 * @param string|bool $value Value to check.
2157
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2158
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2159
	 *
2160
	 * @return bool|WP_Error
2161
	 */
2162
	public static function validate_custom_service( $value, $request, $param ) {
2163 View Code Duplication
		if ( ! is_array( $value ) || ! isset( $value['sharing_name'] ) || ! isset( $value['sharing_url'] ) || ! isset( $value['sharing_icon'] ) ) {
2164
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array with sharing name, url and icon.', 'jetpack' ), $param ) );
2165
		}
2166
2167
		// Allow to clear everything.
2168
		if ( empty( $value['sharing_name'] ) && empty( $value['sharing_url'] ) && empty( $value['sharing_icon'] ) ) {
2169
			return true;
2170
		}
2171
2172 View Code Duplication
		if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
2173
			return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
2174
		}
2175
2176
		if ( ( ! empty( $value['sharing_name'] ) && ! is_string( $value['sharing_name'] ) )
2177
		|| ( ! empty( $value['sharing_url'] ) && ! is_string( $value['sharing_url'] ) )
2178
		|| ( ! empty( $value['sharing_icon'] ) && ! is_string( $value['sharing_icon'] ) ) ) {
2179
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s needs sharing name, url and icon.', 'jetpack' ), $param ) );
2180
		}
2181
		return true;
2182
	}
2183
2184
	/**
2185
	 * Validates that the parameter is a custom sharing service ID like 'custom-1461976264'.
2186
	 *
2187
	 * @since 4.3.0
2188
	 *
2189
	 * @param string $value Value to check.
2190
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2191
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2192
	 *
2193
	 * @return bool|WP_Error
2194
	 */
2195
	public static function validate_custom_service_id( $value = '', $request, $param ) {
2196 View Code Duplication
		if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/custom\-[0-1]+/i', $value ) ) ) {
2197
			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 ) );
2198
		}
2199
2200 View Code Duplication
		if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
2201
			return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
2202
		}
2203
		$sharer = new Sharing_Service();
2204
		$services = array_keys( $sharer->get_all_services() );
2205
2206 View Code Duplication
		if ( ! empty( $value ) && ! in_array( $value, $services ) ) {
2207
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s is not a registered custom sharing service.', 'jetpack' ), $param ) );
2208
		}
2209
2210
		return true;
2211
	}
2212
2213
	/**
2214
	 * Validates that the parameter is a Twitter username or empty string (to be able to clear the field).
2215
	 *
2216
	 * @since 4.3.0
2217
	 *
2218
	 * @param string $value Value to check.
2219
	 * @param WP_REST_Request $request
2220
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2221
	 *
2222
	 * @return bool|WP_Error
2223
	 */
2224
	public static function validate_twitter_username( $value = '', $request, $param ) {
2225 View Code Duplication
		if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^@?\w{1,15}$/i', $value ) ) ) {
2226
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a Twitter username.', 'jetpack' ), $param ) );
2227
		}
2228
		return true;
2229
	}
2230
2231
	/**
2232
	 * Validates that the parameter is a string.
2233
	 *
2234
	 * @since 4.3.0
2235
	 *
2236
	 * @param string $value Value to check.
2237
	 * @param WP_REST_Request $request The request sent to the WP REST API.
2238
	 * @param string $param Name of the parameter passed to endpoint holding $value.
2239
	 *
2240
	 * @return bool|WP_Error
2241
	 */
2242
	public static function validate_string( $value = '', $request, $param ) {
2243
		if ( ! is_string( $value ) ) {
2244
			return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a string.', 'jetpack' ), $param ) );
2245
		}
2246
		return true;
2247
	}
2248
2249
	/**
2250
	 * If for some reason the roles allowed to see Stats are empty (for example, user tampering with checkboxes),
2251
	 * return an array with only 'administrator' as the allowed role and save it for 'roles' option.
2252
	 *
2253
	 * @since 4.3.0
2254
	 *
2255
	 * @param string|bool $value Value to check.
2256
	 *
2257
	 * @return bool|array
2258
	 */
2259
	public static function sanitize_stats_allowed_roles( $value ) {
2260
		if ( empty( $value ) ) {
2261
			return array( 'administrator' );
2262
		}
2263
		return $value;
2264
	}
2265
2266
	/**
2267
	 * Get the currently accessed route and return the module slug in it.
2268
	 *
2269
	 * @since 4.3.0
2270
	 *
2271
	 * @param string $route Regular expression for the endpoint with the module slug to return.
2272
	 *
2273
	 * @return array|string
2274
	 */
2275
	public static function get_module_requested( $route = '/module/(?P<slug>[a-z\-]+)' ) {
2276
2277
		if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
2278
			return '';
2279
		}
2280
2281
		preg_match( "#$route#", $GLOBALS['wp']->query_vars['rest_route'], $module );
2282
2283
		if ( empty( $module['slug'] ) ) {
2284
			return '';
2285
		}
2286
2287
		return $module['slug'];
2288
	}
2289
2290
	/**
2291
	 * Adds extra information for modules.
2292
	 *
2293
	 * @since 4.3.0
2294
	 *
2295
	 * @param string|array $modules Can be a single module or a list of modules.
2296
	 * @param null|string  $slug    Slug of the module in the first parameter.
2297
	 *
2298
	 * @return array|string
2299
	 */
2300
	public static function prepare_modules_for_response( $modules = '', $slug = null ) {
2301
		global $wp_rewrite;
2302
2303
		/** This filter is documented in modules/sitemaps/sitemaps.php */
2304
		$location = apply_filters( 'jetpack_sitemap_location', '' );
2305
2306
		if ( $wp_rewrite->using_index_permalinks() ) {
2307
			$sitemap_url = home_url( '/index.php' . $location . '/sitemap.xml' );
2308
			$news_sitemap_url = home_url( '/index.php' . $location . '/news-sitemap.xml' );
2309
		} else if ( $wp_rewrite->using_permalinks() ) {
2310
			$sitemap_url = home_url( $location . '/sitemap.xml' );
2311
			$news_sitemap_url = home_url( $location . '/news-sitemap.xml' );
2312
		} else {
2313
			$sitemap_url = home_url( $location . '/?jetpack-sitemap=sitemap.xml' );
2314
			$news_sitemap_url = home_url( $location . '/?jetpack-sitemap=news-sitemap.xml' );
2315
		}
2316
2317
		if ( is_null( $slug ) && isset( $modules['sitemaps'] ) ) {
2318
			// Is a list of modules
2319
			$modules['sitemaps']['extra']['sitemap_url'] = $sitemap_url;
2320
			$modules['sitemaps']['extra']['news_sitemap_url'] = $news_sitemap_url;
2321
		} elseif ( 'sitemaps' == $slug ) {
2322
			// It's a single module
2323
			$modules['extra']['sitemap_url'] = $sitemap_url;
2324
			$modules['extra']['news_sitemap_url'] = $news_sitemap_url;
2325
		}
2326
		return $modules;
2327
	}
2328
2329
	/**
2330
	 * Remove 'validate_callback' item from options available for module.
2331
	 * Fetch current option value and add to array of module options.
2332
	 * Prepare values of module options that need special handling, like those saved in wpcom.
2333
	 *
2334
	 * @since 4.3.0
2335
	 *
2336
	 * @param string $module Module slug.
2337
	 * @return array
2338
	 */
2339
	public static function prepare_options_for_response( $module = '' ) {
2340
		$options = self::get_updateable_data_list( $module );
2341
2342
		if ( ! is_array( $options ) || empty( $options ) ) {
2343
			return $options;
2344
		}
2345
2346
		foreach ( $options as $key => $value ) {
2347
2348
			if ( isset( $options[ $key ]['validate_callback'] ) ) {
2349
				unset( $options[ $key ]['validate_callback'] );
2350
			}
2351
2352
			$default_value = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
2353
2354
			$current_value = get_option( $key, $default_value );
2355
2356
			$options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
2357
		}
2358
2359
		// Some modules need special treatment.
2360
		switch ( $module ) {
2361
2362
			case 'monitor':
2363
				// Status of user notifications
2364
				$options['monitor_receive_notifications']['current_value'] = self::cast_value( self::get_remote_value( 'monitor', 'monitor_receive_notifications' ), $options['monitor_receive_notifications'] );
2365
				break;
2366
2367
			case 'post-by-email':
2368
				// Email address
2369
				$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'] );
2370
				break;
2371
2372
			case 'protect':
2373
				// Protect
2374
				$options['jetpack_protect_key']['current_value'] = get_site_option( 'jetpack_protect_key', false );
2375
				if ( ! function_exists( 'jetpack_protect_format_whitelist' ) ) {
2376
					include_once( JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php' );
2377
				}
2378
				$options['jetpack_protect_global_whitelist']['current_value'] = jetpack_protect_format_whitelist();
2379
				break;
2380
2381
			case 'related-posts':
2382
				// It's local, but it must be broken apart since it's saved as an array.
2383
				$options = self::split_options( $options, Jetpack_Options::get_option( 'relatedposts' ) );
2384
				break;
2385
2386
			case 'verification-tools':
2387
				// It's local, but it must be broken apart since it's saved as an array.
2388
				$options = self::split_options( $options, get_option( 'verification_services_codes' ) );
2389
				break;
2390
2391
			case 'google-analytics':
2392
				$wga = get_option( 'jetpack_wga' );
2393
				$code = '';
2394
				if ( is_array( $wga ) && array_key_exists( 'code', $wga ) ) {
2395
					 $code = $wga[ 'code' ];
2396
				}
2397
				$options[ 'google_analytics_tracking_id' ][ 'current_value' ] = $code;
2398
				break;
2399
2400 View Code Duplication
			case 'sharedaddy':
2401
				// It's local, but it must be broken apart since it's saved as an array.
2402
				if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
2403
					break;
2404
				}
2405
				$sharer = new Sharing_Service();
2406
				$options = self::split_options( $options, $sharer->get_global_options() );
2407
				$options['sharing_services']['current_value'] = $sharer->get_blog_services();
2408
				break;
2409
2410
			case 'after-the-deadline':
2411
				if ( ! function_exists( 'AtD_get_options' ) ) {
2412
					include_once( JETPACK__PLUGIN_DIR . 'modules/after-the-deadline.php' );
2413
				}
2414
				$atd_options = array_merge( AtD_get_options( get_current_user_id(), 'AtD_options' ), AtD_get_options( get_current_user_id(), 'AtD_check_when' ) );
2415
				unset( $atd_options['name'] );
2416
				foreach ( $atd_options as $key => $value ) {
2417
					$options[ $key ]['current_value'] = self::cast_value( $value, $options[ $key ] );
2418
				}
2419
				$atd_options = AtD_get_options( get_current_user_id(), 'AtD_guess_lang' );
2420
				$options['guess_lang']['current_value'] = self::cast_value( isset( $atd_options['true'] ), $options[ 'guess_lang' ] );
2421
				$options['ignored_phrases']['current_value'] = AtD_get_setting( get_current_user_id(), 'AtD_ignored_phrases' );
2422
				unset( $options['unignore_phrase'] );
2423
				break;
2424
2425
			case 'stats':
2426
				// It's local, but it must be broken apart since it's saved as an array.
2427
				if ( ! function_exists( 'stats_get_options' ) ) {
2428
					include_once( JETPACK__PLUGIN_DIR . 'modules/stats.php' );
2429
				}
2430
				$options = self::split_options( $options, stats_get_options() );
2431
				break;
2432
		}
2433
2434
		return $options;
2435
	}
2436
2437
	/**
2438
	 * Splits module options saved as arrays like relatedposts or verification_services_codes into separate options to be returned in the response.
2439
	 *
2440
	 * @since 4.3.0
2441
	 *
2442
	 * @param array  $separate_options Array of options admitted by the module.
2443
	 * @param array  $grouped_options Option saved as array to be splitted.
2444
	 * @param string $prefix Optional prefix for the separate option keys.
2445
	 *
2446
	 * @return array
2447
	 */
2448
	public static function split_options( $separate_options, $grouped_options, $prefix = '' ) {
2449
		if ( is_array( $grouped_options ) ) {
2450
			foreach ( $grouped_options as $key => $value ) {
2451
				$option_key = $prefix . $key;
2452
				if ( isset( $separate_options[ $option_key ] ) ) {
2453
					$separate_options[ $option_key ]['current_value'] = self::cast_value( $grouped_options[ $key ], $separate_options[ $option_key ] );
2454
				}
2455
			}
2456
		}
2457
		return $separate_options;
2458
	}
2459
2460
	/**
2461
	 * Perform a casting to the value specified in the option definition.
2462
	 *
2463
	 * @since 4.3.0
2464
	 *
2465
	 * @param mixed $value Value to cast to the proper type.
2466
	 * @param array $definition Type to cast the value to.
2467
	 *
2468
	 * @return bool|float|int|string
2469
	 */
2470
	public static function cast_value( $value, $definition ) {
2471
		if ( $value === 'NULL' ) {
2472
			return null;
2473
		}
2474
2475
		if ( isset( $definition['type'] ) ) {
2476
			switch ( $definition['type'] ) {
2477
				case 'boolean':
2478
					if ( 'true' === $value ) {
2479
						return true;
2480
					} elseif ( 'false' === $value ) {
2481
						return false;
2482
					}
2483
					return (bool) $value;
2484
					break;
2485
2486
				case 'integer':
2487
					return (int) $value;
2488
					break;
2489
2490
				case 'float':
2491
					return (float) $value;
2492
					break;
2493
2494
				case 'string':
2495
					return (string) $value;
2496
					break;
2497
			}
2498
		}
2499
		return $value;
2500
	}
2501
2502
	/**
2503
	 * Get a value not saved locally.
2504
	 *
2505
	 * @since 4.3.0
2506
	 *
2507
	 * @param string $module Module slug.
2508
	 * @param string $option Option name.
2509
	 *
2510
	 * @return bool Whether user is receiving notifications or not.
2511
	 */
2512
	public static function get_remote_value( $module, $option ) {
2513
2514
		if ( in_array( $module, array( 'post-by-email' ), true ) ) {
2515
			$option .= get_current_user_id();
2516
		}
2517
2518
		// If option doesn't exist, 'does_not_exist' will be returned.
2519
		$value = get_option( $option, 'does_not_exist' );
2520
2521
		// If option exists, just return it.
2522
		if ( 'does_not_exist' !== $value ) {
2523
			return $value;
2524
		}
2525
2526
		// Only check a remote option if Jetpack is connected.
2527
		if ( ! Jetpack::is_active() ) {
2528
			return false;
2529
		}
2530
2531
		// Do what is necessary for each module.
2532
		switch ( $module ) {
2533
			case 'monitor':
2534
				// Load the class to use the method. If class can't be found, do nothing.
2535
				if ( ! class_exists( 'Jetpack_Monitor' ) && ! include_once( Jetpack::get_module_path( $module ) ) ) {
2536
					return false;
2537
				}
2538
				$value = Jetpack_Monitor::user_receives_notifications( false );
2539
				break;
2540
2541
			case 'post-by-email':
2542
				// Load the class to use the method. If class can't be found, do nothing.
2543
				if ( ! class_exists( 'Jetpack_Post_By_Email' ) && ! include_once( Jetpack::get_module_path( $module ) ) ) {
2544
					return false;
2545
				}
2546
				$post_by_email = new Jetpack_Post_By_Email();
2547
				$value = $post_by_email->get_post_by_email_address();
2548
				if ( $value === null ) {
2549
					$value = 'NULL'; // sentinel value so it actually gets set
2550
				}
2551
				break;
2552
		}
2553
2554
		// Normalize value to boolean.
2555
		if ( is_wp_error( $value ) || is_null( $value ) ) {
2556
			$value = false;
2557
		}
2558
2559
		// Save option to use it next time.
2560
		update_option( $option, $value );
2561
2562
		return $value;
2563
	}
2564
2565
	/**
2566
	 * Get number of plugin updates available.
2567
	 *
2568
	 * @since 4.3.0
2569
	 *
2570
	 * @return mixed|WP_Error Number of plugin updates available. Otherwise, a WP_Error instance with the corresponding error.
2571
	 */
2572
	public static function get_plugin_update_count() {
2573
		$updates = wp_get_update_data();
2574
		if ( isset( $updates['counts'] ) && isset( $updates['counts']['plugins'] ) ) {
2575
			$count = $updates['counts']['plugins'];
2576
			if ( 0 == $count ) {
2577
				$response = array(
2578
					'code'    => 'success',
2579
					'message' => esc_html__( 'All plugins are up-to-date. Keep up the good work!', 'jetpack' ),
2580
					'count'   => 0,
2581
				);
2582
			} else {
2583
				$response = array(
2584
					'code'    => 'updates-available',
2585
					'message' => esc_html( sprintf( _n( '%s plugin need updating.', '%s plugins need updating.', $count, 'jetpack' ), $count ) ),
2586
					'count'   => $count,
2587
				);
2588
			}
2589
			return rest_ensure_response( $response );
2590
		}
2591
2592
		return new WP_Error( 'not_found', esc_html__( 'Could not check updates for plugins on this site.', 'jetpack' ), array( 'status' => 404 ) );
2593
	}
2594
2595
2596
	/**
2597
	 * Returns a list of all plugins in the site.
2598
	 *
2599
	 * @since 4.2.0
2600
	 * @uses get_plugins()
2601
	 *
2602
	 * @return array
2603
	 */
2604
	private static function core_get_plugins() {
2605
		if ( ! function_exists( 'get_plugins' ) ) {
2606
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
2607
		}
2608
		/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
2609
		$plugins = apply_filters( 'all_plugins', get_plugins() );
2610
2611
		if ( is_array( $plugins ) && ! empty( $plugins ) ) {
2612
			foreach ( $plugins as $plugin_slug => $plugin_data ) {
2613
				$plugins[ $plugin_slug ]['active'] = self::core_is_plugin_active( $plugin_slug );
2614
			}
2615
			return $plugins;
2616
		}
2617
2618
		return array();
2619
	}
2620
2621
	/**
2622
	 * Checks if the queried plugin is active.
2623
	 *
2624
	 * @since 4.2.0
2625
	 * @uses is_plugin_active()
2626
	 *
2627
	 * @return bool
2628
	 */
2629
	private static function core_is_plugin_active( $plugin ) {
2630
		if ( ! function_exists( 'is_plugin_active' ) ) {
2631
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
2632
		}
2633
2634
		return is_plugin_active( $plugin );
2635
	}
2636
2637
	/**
2638
	 * Get plugins data in site.
2639
	 *
2640
	 * @since 4.2.0
2641
	 *
2642
	 * @return WP_REST_Response|WP_Error List of plugins in the site. Otherwise, a WP_Error instance with the corresponding error.
2643
	 */
2644
	public static function get_plugins() {
2645
		$plugins = self::core_get_plugins();
2646
2647
		if ( ! empty( $plugins ) ) {
2648
			return rest_ensure_response( $plugins );
2649
		}
2650
2651
		return new WP_Error( 'not_found', esc_html__( 'Unable to list plugins.', 'jetpack' ), array( 'status' => 404 ) );
2652
	}
2653
2654
	/**
2655
	 * Get data about the queried plugin. Currently it only returns whether the plugin is active or not.
2656
	 *
2657
	 * @since 4.2.0
2658
	 *
2659
	 * @param WP_REST_Request $request {
2660
	 *     Array of parameters received by request.
2661
	 *
2662
	 *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
2663
	 * }
2664
	 *
2665
	 * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
2666
	 */
2667
	public static function get_plugin( $request ) {
2668
2669
		$plugins = self::core_get_plugins();
2670
2671
		if ( empty( $plugins ) ) {
2672
			return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
2673
		}
2674
2675
		$plugin = stripslashes( $request['plugin'] );
2676
2677
		if ( ! in_array( $plugin, array_keys( $plugins ) ) ) {
2678
			return new WP_Error( 'plugin_not_found', esc_html( sprintf( __( 'Plugin %s is not installed.', 'jetpack' ), $plugin ) ), array( 'status' => 404 ) );
2679
		}
2680
2681
		$plugin_data = $plugins[ $plugin ];
2682
2683
		$plugin_data['active'] = self::core_is_plugin_active( $plugin );
2684
2685
		return rest_ensure_response( array(
2686
			'code'    => 'success',
2687
			'message' => esc_html__( 'Plugin found.', 'jetpack' ),
2688
			'data'    => $plugin_data
2689
		) );
2690
	}
2691
2692
} // class end
2693