Completed
Push — try/sync-package ( 228b13 )
by Marin
07:37
created

Actions::add_woocommerce_sync_module()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
441
				)
442
			);
443
		}
444
	}
445
446
	/**
447
	 * Get the sync status
448
	 *
449
	 * @param string|null $fields A comma-separated string of the fields to include in the array from the JSON response.
450
	 * @return array
451
	 */
452
	static function get_sync_status( $fields = null ) {
453
		self::initialize_sender();
454
455
		$sync_module     = Modules::get_module( 'full-sync' );
456
		$queue           = self::$sender->get_sync_queue();
457
		$full_queue      = self::$sender->get_full_sync_queue();
458
		$cron_timestamps = array_keys( _get_cron_array() );
459
		$next_cron       = $cron_timestamps[0] - time();
460
461
		$checksums = array();
462
463
		if ( ! empty( $fields ) ) {
464
			$store         = new WP_Replicastore();
465
			$fields_params = array_map( 'trim', explode( ',', $fields ) );
466
467
			if ( in_array( 'posts_checksum', $fields_params, true ) ) {
468
				$checksums['posts_checksum'] = $store->posts_checksum();
469
			}
470
			if ( in_array( 'comments_checksum', $fields_params, true ) ) {
471
				$checksums['comments_checksum'] = $store->comments_checksum();
472
			}
473
			if ( in_array( 'post_meta_checksum', $fields_params, true ) ) {
474
				$checksums['post_meta_checksum'] = $store->post_meta_checksum();
475
			}
476
			if ( in_array( 'comment_meta_checksum', $fields_params, true ) ) {
477
				$checksums['comment_meta_checksum'] = $store->comment_meta_checksum();
478
			}
479
		}
480
481
		$full_sync_status = ( $sync_module ) ? $sync_module->get_status() : array();
482
483
		return array_merge(
484
			$full_sync_status,
485
			$checksums,
486
			array(
487
				'cron_size'            => count( $cron_timestamps ),
488
				'next_cron'            => $next_cron,
489
				'queue_size'           => $queue->size(),
490
				'queue_lag'            => $queue->lag(),
491
				'queue_next_sync'      => ( self::$sender->get_next_sync_time( 'sync' ) - microtime( true ) ),
492
				'full_queue_size'      => $full_queue->size(),
493
				'full_queue_lag'       => $full_queue->lag(),
494
				'full_queue_next_sync' => ( self::$sender->get_next_sync_time( 'full_sync' ) - microtime( true ) ),
495
			)
496
		);
497
	}
498
}
499
500
// Check for WooCommerce support
501
add_action( 'plugins_loaded', array( 'Actions', 'initialize_woocommerce' ), 5 );
502
503
// Check for WP Super Cache
504
add_action( 'plugins_loaded', array( 'Actions', 'initialize_wp_super_cache' ), 5 );
505
506
/*
507
 * Init after plugins loaded and before the `init` action. This helps with issues where plugins init
508
 * with a high priority or sites that use alternate cron.
509
 */
510
add_action( 'plugins_loaded', array( 'Actions', 'init' ), 90 );
511
512
513
514
// We need to define this here so that it's hooked before `updating_jetpack_version` is called
515
add_action( 'updating_jetpack_version', array( 'Actions', 'cleanup_on_upgrade' ), 10, 2 );
516
add_action( 'jetpack_user_authorized', array( 'Actions', 'do_initial_sync' ), 10, 0 );
517