Completed
Push — update/sync-package ( 660c46 )
by
unknown
08:05
created

Sync::init()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 7
nop 0
dl 0
loc 38
rs 9.0008
c 0
b 0
f 0
1
<?php
2
namespace Automattic\Jetpack;
3
4
/**
5
 * The role of this class is to hook the Sync subsystem into WordPress - when to listen for actions,
6
 * when to send, when to perform a full sync, etc.
7
 *
8
 * It also binds the action to send data to WPCOM to Jetpack's XMLRPC client object.
9
 */
10
class Sync {
11
	public $sender                         = null;
12
	private $listener                      = null;
13
	const DEFAULT_SYNC_CRON_INTERVAL_NAME  = 'jetpack_sync_interval';
14
	const DEFAULT_SYNC_CRON_INTERVAL_VALUE = 300; // 5 * MINUTE_IN_SECONDS;
15
16
	function __construct() {
17
		// Check for WooCommerce support.
18
		add_action( 'plugins_loaded', array( $this, 'initialize_woocommerce' ), 5 );
19
20
		// Check for WP Super Cache.
21
		add_action( 'plugins_loaded', array( $this, 'initialize_wp_super_cache' ), 5 );
22
23
24
		// We need to define this here so that it's hooked before `updating_jetpack_version` is called.
25
		add_action( 'updating_jetpack_version', array( $this, 'cleanup_on_upgrade' ), 10, 2 );
26
		add_action( 'jetpack_user_authorized', array( $this, 'do_initial_sync' ), 10, 0 );
27
28
		add_action( 'plugins_loaded', array( $this, 'init' ), 90 );
29
	}
30
31
	function init() {
32
		// everything below this point should only happen if we're a valid sync site
33
		if ( ! $this->sync_allowed() ) {
34
			return;
35
		}
36
37
		if ( $this->sync_via_cron_allowed() ) {
38
			$this->init_sync_cron_jobs();
39
		} elseif ( wp_next_scheduled( 'jetpack_sync_cron' ) ) {
40
			$this->clear_sync_cron_jobs();
41
		}
42
		// When importing via cron, do not sync
43
		add_action( 'wp_cron_importer_hook', array( $this, 'set_is_importing_true' ), 1 );
44
45
		// Sync connected user role changes to .com
46
		\Jetpack_Sync_Users::init();
47
48
		// publicize filter to prevent publicizing blacklisted post types
49
		add_filter( 'publicize_should_publicize_published_post', array( $this, 'prevent_publicize_blacklisted_posts' ), 10, 2 );
50
51
		/**
52
		 * Fires on every request before default loading sync listener code.
53
		 * Return false to not load sync listener code that monitors common
54
		 * WP actions to be serialized.
55
		 *
56
		 * By default this returns true for cron jobs, non-GET-requests, or requests where the
57
		 * user is logged-in.
58
		 *
59
		 * @since 4.2.0
60
		 *
61
		 * @param bool should we load sync listener code for this request
62
		 */
63
		if ( apply_filters( 'jetpack_sync_listener_should_load', true ) ) {
64
			$this->initialize_listener();
65
		}
66
67
		add_action( 'init', array( $this, 'add_sender_shutdown' ), 90 );
68
	}
69
70
	function add_sender_shutdown() {
71
		/**
72
		 * Fires on every request before default loading sync sender code.
73
		 * Return false to not load sync sender code that serializes pending
74
		 * data and sends it to WPCOM for processing.
75
		 *
76
		 * By default this returns true for cron jobs, POST requests, admin requests, or requests
77
		 * by users who can manage_options.
78
		 *
79
		 * @since 4.2.0
80
		 *
81
		 * @param bool should we load sync sender code for this request
82
		 */
83
		if ( apply_filters(
84
			'jetpack_sync_sender_should_load',
85
			$this->should_initialize_sender()
86
		) ) {
87
			$this->initialize_sender();
88
			add_action( 'shutdown', array( $this->sender, 'do_sync' ) );
89
			add_action( 'shutdown', array( $this->sender, 'do_full_sync' ) );
90
		}
91
	}
92
93
	function should_initialize_sender() {
94
		if ( Constants::is_true( 'DOING_CRON' ) ) {
95
			return $this->sync_via_cron_allowed();
96
		}
97
98
		if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
99
			return true;
100
		}
101
102
		if ( current_user_can( 'manage_options' ) ) {
103
			return true;
104
		}
105
106
		if ( is_admin() ) {
107
			return true;
108
		}
109
110
		if ( defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) {
111
			return true;
112
		}
113
114
		return false;
115
	}
116
117
	function sync_allowed() {
118
		if ( defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) {
119
			return true;
120
		}
121
		if ( ! \Jetpack_Sync_Settings::is_sync_enabled() ) {
122
			return false;
123
		}
124
		if ( \Jetpack::is_development_mode() ) {
125
			return false;
126
		}
127
		if ( \Jetpack::is_staging_site() ) {
128
			return false;
129
		}
130
		if ( ! \Jetpack::is_active() ) {
131
			if ( ! doing_action( 'jetpack_user_authorized' ) ) {
132
				return false;
133
			}
134
		}
135
136
		return true;
137
	}
138
139
	function sync_via_cron_allowed() {
140
		return ( \Jetpack_Sync_Settings::get_setting( 'sync_via_cron' ) );
141
	}
142
143
	function prevent_publicize_blacklisted_posts( $should_publicize, $post ) {
144
		if ( in_array( $post->post_type, \Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) ) ) {
145
			return false;
146
		}
147
148
		return $should_publicize;
149
	}
150
151
	function set_is_importing_true() {
152
		\Jetpack_Sync_Settings::set_importing( true );
153
	}
154
155
	function send_data( $data, $codec_name, $sent_timestamp, $queue_id, $checkout_duration, $preprocess_duration ) {
156
		\Jetpack::load_xml_rpc_client();
157
158
		$query_args = array(
159
			'sync'      => '1',             // add an extra parameter to the URL so we can tell it's a sync action
160
			'codec'     => $codec_name,     // send the name of the codec used to encode the data
161
			'timestamp' => $sent_timestamp, // send current server time so we can compensate for clock differences
162
			'queue'     => $queue_id,       // sync or full_sync
163
			'home'      => \Jetpack_Sync_Functions::home_url(),  // Send home url option to check for Identity Crisis server-side
164
			'siteurl'   => \Jetpack_Sync_Functions::site_url(),  // Send siteurl option to check for Identity Crisis server-side
165
			'cd'        => sprintf( '%.4f', $checkout_duration ),   // Time spent retrieving queue items from the DB
166
			'pd'        => sprintf( '%.4f', $preprocess_duration ), // Time spent converting queue items into data to send
167
		);
168
169
		// Has the site opted in to IDC mitigation?
170
		if ( \Jetpack::sync_idc_optin() ) {
171
			$query_args['idc'] = true;
172
		}
173
174
		if ( \Jetpack_Options::get_option( 'migrate_for_idc', false ) ) {
175
			$query_args['migrate_for_idc'] = true;
176
		}
177
178
		$query_args['timeout'] = \Jetpack_Sync_Settings::is_doing_cron() ? 30 : 15;
179
180
		/**
181
		 * Filters query parameters appended to the Sync request URL sent to WordPress.com.
182
		 *
183
		 * @since 4.7.0
184
		 *
185
		 * @param array $query_args associative array of query parameters.
186
		 */
187
		$query_args = apply_filters( 'jetpack_sync_send_data_query_args', $query_args );
188
189
		$url = add_query_arg( $query_args, \Jetpack::xmlrpc_api_url() );
190
191
		$rpc = new \Jetpack_IXR_Client(
192
			array(
193
				'url'     => $url,
194
				'user_id' => JETPACK_MASTER_USER,
195
				'timeout' => $query_args['timeout'],
196
			)
197
		);
198
199
		$result = $rpc->query( 'jetpack.syncActions', $data );
200
201
		if ( ! $result ) {
202
			return $rpc->get_jetpack_error();
203
		}
204
205
		$response = $rpc->getResponse();
206
207
		// Check if WordPress.com IDC mitigation blocked the sync request
208
		if ( is_array( $response ) && isset( $response['error_code'] ) ) {
209
			$error_code              = $response['error_code'];
210
			$allowed_idc_error_codes = array(
211
				'jetpack_url_mismatch',
212
				'jetpack_home_url_mismatch',
213
				'jetpack_site_url_mismatch',
214
			);
215
216
			if ( in_array( $error_code, $allowed_idc_error_codes ) ) {
217
				\Jetpack_Options::update_option(
218
					'sync_error_idc',
219
					\Jetpack::get_sync_error_idc_option( $response )
220
				);
221
			}
222
223
			return new WP_Error(
224
				'sync_error_idc',
225
				esc_html__( 'Sync has been blocked from WordPress.com because it would cause an identity crisis', 'jetpack' )
226
			);
227
		}
228
229
		return $response;
230
	}
231
232
	function do_initial_sync() {
233
		// Lets not sync if we are not suppose to.
234
		if ( ! $this->sync_allowed() ) {
235
			return false;
236
		}
237
238
		$initial_sync_config = array(
239
			'options'   => true,
240
			'functions' => true,
241
			'constants' => true,
242
			'users'     => array( get_current_user_id() ),
243
		);
244
245
		if ( is_multisite() ) {
246
			$initial_sync_config['network_options'] = true;
247
		}
248
249
		$this->do_full_sync( $initial_sync_config );
250
	}
251
252
	function do_full_sync( $modules = null ) {
253
		if ( ! $this->sync_allowed() ) {
254
			return false;
255
		}
256
257
		$full_sync_module = \Jetpack_Sync_Modules::get_module( 'full-sync' );
258
259
		if ( ! $full_sync_module ) {
260
			return false;
261
		}
262
263
		$this->initialize_listener();
264
265
		$full_sync_module->start( $modules );
266
267
		return true;
268
	}
269
270
	function jetpack_cron_schedule( $schedules ) {
271
		if ( ! isset( $schedules[ $this::DEFAULT_SYNC_CRON_INTERVAL_NAME ] ) ) {
272
			$schedules[ $this::DEFAULT_SYNC_CRON_INTERVAL_NAME ] = array(
273
				'interval' => $this::DEFAULT_SYNC_CRON_INTERVAL_VALUE,
274
				'display'  => sprintf(
275
					esc_html( _n( 'Every minute', 'Every %d minutes', intval( $this::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 ), 'jetpack' ) ),
276
					intval( $this::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 )
277
				),
278
			);
279
		}
280
		return $schedules;
281
	}
282
283
	function do_cron_sync() {
284
		$this->do_cron_sync_by_type( 'sync' );
285
	}
286
287
	function do_cron_full_sync() {
288
		$this->do_cron_sync_by_type( 'full_sync' );
289
	}
290
291
	/**
292
	 * Try to send actions until we run out of things to send,
293
	 * or have to wait more than 15s before sending again,
294
	 * or we hit a lock or some other sending issue
295
	 *
296
	 * @param string $type Sync type. Can be `sync` or `full_sync`.
297
	 */
298
	function do_cron_sync_by_type( $type ) {
299
		if ( ! $this->sync_allowed() || ( 'sync' !== $type && 'full_sync' !== $type ) ) {
300
			return;
301
		}
302
303
		$this->initialize_sender();
304
305
		$time_limit = \Jetpack_Sync_Settings::get_setting( 'cron_sync_time_limit' );
306
		$start_time = time();
307
308
		do {
309
			$next_sync_time = $this->sender->get_next_sync_time( $type );
310
311
			if ( $next_sync_time ) {
312
				$delay = $next_sync_time - time() + 1;
313
				if ( $delay > 15 ) {
314
					break;
315
				} elseif ( $delay > 0 ) {
316
					sleep( $delay );
317
				}
318
			}
319
320
			$result = 'full_sync' === $type ? $this->sender->do_full_sync() : $this->sender->do_sync();
321
		} while ( $result && ! is_wp_error( $result ) && ( $start_time + $time_limit ) > time() );
322
	}
323
324
	function initialize_listener() {
325
		$this->listener = new \Jetpack_Sync_Listener( $this );
326
	}
327
328
	function initialize_sender() {
329
		$this->sender = \Jetpack_Sync_Sender::get_instance();
330
331
		// bind the sending process
332
		add_filter( 'jetpack_sync_send_data', array( $this, 'send_data' ), 10, 6 );
333
	}
334
335
	function initialize_woocommerce() {
336
		if ( false === class_exists( 'WooCommerce' ) ) {
337
			return;
338
		}
339
		add_filter( 'jetpack_sync_modules', array( $this, 'add_woocommerce_sync_module' ) );
340
	}
341
342
	function add_woocommerce_sync_module( $sync_modules ) {
343
		$sync_modules[] = 'Jetpack_Sync_Module_WooCommerce';
344
		return $sync_modules;
345
	}
346
347
	function initialize_wp_super_cache() {
348
		if ( false === function_exists( 'wp_cache_is_enabled' ) ) {
349
			return;
350
		}
351
		add_filter( 'jetpack_sync_modules', array( $this, 'add_wp_super_cache_sync_module' ) );
352
	}
353
354
	function add_wp_super_cache_sync_module( $sync_modules ) {
355
		$sync_modules[] = 'Jetpack_Sync_Module_WP_Super_Cache';
356
		return $sync_modules;
357
	}
358
359
	function sanitize_filtered_sync_cron_schedule( $schedule ) {
360
		$schedule  = sanitize_key( $schedule );
361
		$schedules = wp_get_schedules();
362
363
		// Make sure that the schedule has actually been registered using the `cron_intervals` filter.
364
		if ( isset( $schedules[ $schedule ] ) ) {
365
			return $schedule;
366
		}
367
368
		return $this->DEFAULT_SYNC_CRON_INTERVAL_NAME;
0 ignored issues
show
Bug introduced by
The property DEFAULT_SYNC_CRON_INTERVAL_NAME does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
369
	}
370
371
	function get_start_time_offset( $schedule = '', $hook = '' ) {
372
		$start_time_offset = is_multisite()
373
			? mt_rand( 0, ( 2 * $this->DEFAULT_SYNC_CRON_INTERVAL_VALUE ) )
0 ignored issues
show
Bug introduced by
The property DEFAULT_SYNC_CRON_INTERVAL_VALUE does not seem to exist. Did you mean DEFAULT_SYNC_CRON_INTERVAL_NAME?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
374
			: 0;
375
376
		/**
377
		 * Allows overriding the offset that the sync cron jobs will first run. This can be useful when scheduling
378
		 * cron jobs across multiple sites in a network.
379
		 *
380
		 * @since 4.5.0
381
		 *
382
		 * @param int    $start_time_offset
383
		 * @param string $hook
384
		 * @param string $schedule
385
		 */
386
		return intval(
387
			apply_filters(
388
				'jetpack_sync_cron_start_time_offset',
389
				$start_time_offset,
390
				$hook,
391
				$schedule
392
			)
393
		);
394
	}
395
396
	function maybe_schedule_sync_cron( $schedule, $hook ) {
397
		if ( ! $hook ) {
398
			return;
399
		}
400
		$schedule = $this->sanitize_filtered_sync_cron_schedule( $schedule );
401
402
		$start_time = time() + $this->get_start_time_offset( $schedule, $hook );
403
		if ( ! wp_next_scheduled( $hook ) ) {
404
			// Schedule a job to send pending queue items once a minute
405
			wp_schedule_event( $start_time, $schedule, $hook );
406
		} elseif ( $schedule != wp_get_schedule( $hook ) ) {
407
			// If the schedule has changed, update the schedule
408
			wp_clear_scheduled_hook( $hook );
409
			wp_schedule_event( $start_time, $schedule, $hook );
410
		}
411
	}
412
413
	function clear_sync_cron_jobs() {
414
		wp_clear_scheduled_hook( 'jetpack_sync_cron' );
415
		wp_clear_scheduled_hook( 'jetpack_sync_full_cron' );
416
	}
417
418
	function init_sync_cron_jobs() {
419
		add_filter( 'cron_schedules', array( $this, 'jetpack_cron_schedule' ) );
420
421
		add_action( 'jetpack_sync_cron', array( $this, 'do_cron_sync' ) );
422
		add_action( 'jetpack_sync_full_cron', array( $this, 'do_cron_full_sync' ) );
423
424
		/**
425
		 * Allows overriding of the default incremental sync cron schedule which defaults to once every 5 minutes.
426
		 *
427
		 * @since 4.3.2
428
		 *
429
		 * @param string $this->DEFAULT_SYNC_CRON_INTERVAL_NAME
430
		 */
431
		$incremental_sync_cron_schedule = apply_filters( 'jetpack_sync_incremental_sync_interval', $this::DEFAULT_SYNC_CRON_INTERVAL_NAME );
432
		$this->maybe_schedule_sync_cron( $incremental_sync_cron_schedule, 'jetpack_sync_cron' );
433
434
		/**
435
		 * Allows overriding of the full sync cron schedule which defaults to once every 5 minutes.
436
		 *
437
		 * @since 4.3.2
438
		 *
439
		 * @param string $this->DEFAULT_SYNC_CRON_INTERVAL_NAME
440
		 */
441
		$full_sync_cron_schedule = apply_filters( 'jetpack_sync_full_sync_interval', $this::DEFAULT_SYNC_CRON_INTERVAL_NAME );
442
		$this->maybe_schedule_sync_cron( $full_sync_cron_schedule, 'jetpack_sync_full_cron' );
443
	}
444
445
	function cleanup_on_upgrade( $new_version = null, $old_version = null ) {
446
		if ( wp_next_scheduled( 'jetpack_sync_send_db_checksum' ) ) {
447
			wp_clear_scheduled_hook( 'jetpack_sync_send_db_checksum' );
448
		}
449
450
		$is_new_sync_upgrade = version_compare( $old_version, '4.2', '>=' );
451
		if ( ! empty( $old_version ) && $is_new_sync_upgrade && version_compare( $old_version, '4.5', '<' ) ) {
452
			$this->clear_sync_cron_jobs();
453
			\Jetpack_Sync_Settings::update_settings(
454
				array(
455
					'render_filtered_content' => Jetpack_Sync_Defaults::$default_render_filtered_content,
456
				)
457
			);
458
		}
459
	}
460
461
	/**
462
	 * Get the sync status
463
	 *
464
	 * @param string|null $fields A comma-separated string of the fields to include in the array from the JSON response.
465
	 * @return array
466
	 */
467
	function get_sync_status( $fields = null ) {
468
		$this->initialize_sender();
469
470
		$sync_module     = \Jetpack_Sync_Modules::get_module( 'full-sync' );
471
		$queue           = $this->sender->get_sync_queue();
472
		$full_queue      = $this->sender->get_full_sync_queue();
473
		$cron_timestamps = array_keys( _get_cron_array() );
474
		$next_cron       = $cron_timestamps[0] - time();
475
476
		$checksums = array();
477
478
		if ( ! empty( $fields ) ) {
479
			$store         = new \Jetpack_Sync_WP_Replicastore();
480
			$fields_params = array_map( 'trim', explode( ',', $fields ) );
481
482
			if ( in_array( 'posts_checksum', $fields_params, true ) ) {
483
				$checksums['posts_checksum'] = $store->posts_checksum();
484
			}
485
			if ( in_array( 'comments_checksum', $fields_params, true ) ) {
486
				$checksums['comments_checksum'] = $store->comments_checksum();
487
			}
488
			if ( in_array( 'post_meta_checksum', $fields_params, true ) ) {
489
				$checksums['post_meta_checksum'] = $store->post_meta_checksum();
490
			}
491
			if ( in_array( 'comment_meta_checksum', $fields_params, true ) ) {
492
				$checksums['comment_meta_checksum'] = $store->comment_meta_checksum();
493
			}
494
		}
495
496
		$full_sync_status = ( $sync_module ) ? $sync_module->get_status() : array();
497
498
		return array_merge(
499
			$full_sync_status,
500
			$checksums,
501
			array(
502
				'cron_size'            => count( $cron_timestamps ),
503
				'next_cron'            => $next_cron,
504
				'queue_size'           => $queue->size(),
505
				'queue_lag'            => $queue->lag(),
506
				'queue_next_sync'      => ( $this->sender->get_next_sync_time( 'sync' ) - microtime( true ) ),
507
				'full_queue_size'      => $full_queue->size(),
508
				'full_queue_lag'       => $full_queue->lag(),
509
				'full_queue_next_sync' => ( $this->sender->get_next_sync_time( 'full_sync' ) - microtime( true ) ),
510
			)
511
		);
512
	}
513
}
514