Completed
Push — branch-4.0 ( fc0d6f...bf547c )
by
unknown
08:22
created

Jetpack_Sync::transition_post_status_action()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 25
Code Lines 15

Duplication

Lines 6
Ratio 24 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 7
nop 3
dl 6
loc 25
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Request that a piece of data on this WordPress install be synced back to the
5
 * Jetpack server for remote processing/notifications/etc
6
 */
7
class Jetpack_Sync {
8
	// What modules want to sync what content
9
	public $sync_conditions = array( 'posts' => array(), 'comments' => array() );
10
11
	// We keep track of all the options registered for sync so that we can sync them all if needed
12
	public $sync_options = array();
13
14
	public $sync_constants = array();
15
16
	// Keep trac of status transitions, which we wouldn't always know about on the Jetpack Servers but are important when deciding what to do with the sync.
17
	public $post_transitions = array();
18
	public $comment_transitions = array();
19
20
	// Objects to sync
21
	public $sync = array();
22
23
	function __construct() {
24
		// WP Cron action.  Only used on upgrade
25
		add_action( 'jetpack_sync_all_registered_options', array( $this, 'sync_all_registered_options' ) );
26
		add_action( 'jetpack_heartbeat',  array( $this, 'sync_all_registered_options' ) );
27
28
		// Sync constants on heartbeat and plugin upgrade and connects
29
		add_action( 'init', array( $this, 'register_constants_as_options' ) );
30
		add_action( 'jetpack_sync_all_registered_options', array( $this, 'sync_all_constants' ) );
31
		add_action( 'jetpack_heartbeat',  array( $this, 'sync_all_constants' ) );
32
33
		add_action( 'jetpack_activate_module', array( $this, 'sync_module_constants' ), 10, 1 );
34
	}
35
36
/* Static Methods for Modules */
37
38
	/**
39
	 * @param string $file __FILE__
40
	 * @param array settings:
41
	 * 	post_types => array( post_type slugs   ): The post types to sync.  Default: post, page
42
	 *	post_stati => array( post_status slugs ): The post stati to sync.  Default: publish
43
	 */
44 View Code Duplication
	static function sync_posts( $file, array $settings = null ) {
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $settings is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
45
		if ( is_network_admin() ) return;
46
		$jetpack = Jetpack::init();
47
		$args = func_get_args();
48
		return call_user_func_array( array( $jetpack->sync, 'posts' ), $args );
49
	}
50
51
	/**
52
	 * @param string $file __FILE__
53
	 * @param array settings:
54
	 * 	post_types    => array( post_type slugs      ): The post types to sync.     Default: post, page
55
	 *	post_stati    => array( post_status slugs    ): The post stati to sync.     Default: publish
56
	 *	comment_types => array( comment_type slugs   ): The comment types to sync.  Default: '', comment, trackback, pingback
57
	 * 	comment_stati => array( comment_status slugs ): The comment stati to sync.  Default: approved
58
	 */
59 View Code Duplication
	static function sync_comments( $file, array $settings = null ) {
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $settings is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
60
		if ( is_network_admin() ) return;
61
		$jetpack = Jetpack::init();
62
		$args = func_get_args();
63
		return call_user_func_array( array( $jetpack->sync, 'comments' ), $args );
64
	}
65
66
	/**
67
	 * @param string $file __FILE__
68
	 * @param string $option, Option name to sync
0 ignored issues
show
Documentation introduced by
There is no parameter named $option,. Did you maybe mean $option?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
69
	 * @param string $option ...
70
	 */
71 View Code Duplication
	static function sync_options( $file, $option /*, $option, ... */ ) {
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $option is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
72
		if ( is_network_admin() ) return;
73
		$jetpack = Jetpack::init();
74
		$args = func_get_args();
75
		return call_user_func_array( array( $jetpack->sync, 'options' ), $args );
76
	}
77
	/**
78
	 * @param string $file __FILE__
79
	 * @param string $option, Option name to sync
0 ignored issues
show
Bug introduced by
There is no parameter named $option,. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
80
	 * @param string $option ...
0 ignored issues
show
Bug introduced by
There is no parameter named $option. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
81
	 */
82 View Code Duplication
	static function sync_constant( $file, $constant ) {
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $constant is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
83
		if ( is_network_admin() ) return;
84
		$jetpack = Jetpack::init();
85
		$args = func_get_args();
86
		return call_user_func_array( array( $jetpack->sync, 'constant' ), $args );
87
	}
88
89
/* Internal Methods */
90
91
	/**
92
	 * Create a sync object/request
93
	 *
94
	 * @param string $object Type of object to sync -- [ post | comment | option ]
95
	 * @param int $id Unique identifier
0 ignored issues
show
Documentation introduced by
Should the type for parameter $id not be false|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
96
	 * @param array $settings
0 ignored issues
show
Documentation introduced by
Should the type for parameter $settings not be null|array?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
97
	 */
98
	function register( $object, $id = false, array $settings = null ) {
99
		// Since we've registered something for sync, hook it up to execute on shutdown if we haven't already
100
		if ( !$this->sync ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sync of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
101
			if ( function_exists( 'ignore_user_abort' ) ) {
102
				ignore_user_abort( true );
103
			}
104
			add_action( 'shutdown', array( $this, 'sync' ), 9 ); // Right before async XML-RPC
105
		}
106
107
		$defaults = array(
108
			'on_behalf_of' => array(), // What modules want this data
109
		);
110
		$settings = wp_parse_args( $settings, $defaults );
111
112
		if ( !isset( $this->sync[$object] ) ) {
113
			$this->sync[$object] = array();
114
		}
115
116
		// Store the settings for this object
117
		if (
118
			// First time for this object
119
			!isset( $this->sync[$object][$id] )
120
		) {
121
			// Easy: store the current settings
122
			$this->sync[$object][$id] = $settings;
123
		} else {
124
			// Not as easy:  we have to manually merge the settings from previous runs for this object with the settings for this run
125
126
			$this->sync[$object][$id]['on_behalf_of'] = array_unique( array_merge( $this->sync[$object][$id]['on_behalf_of'], $settings['on_behalf_of'] ) );
127
		}
128
129
		$delete_prefix = 'delete_';
130
		if ( 0 === strpos( $object, $delete_prefix ) ) {
131
			$unset_object = substr( $object, strlen( $delete_prefix ) );
132
		} else {
133
			$unset_object = "{$delete_prefix}{$object}";
134
		}
135
136
		// Ensure post ... delete_post yields a delete operation
137
		// Ensure delete_post ... post yields a sync post operation
138
		// Ensure update_option() ... delete_option() ends up as a delete
139
		// Ensure delete_option() ... update_option() ends up as an update
140
		// Etc.
141
		unset( $this->sync[$unset_object][$id] );
142
143
		return true;
144
	}
145
146
	function get_common_sync_data() {
147
		$available_modules = Jetpack::get_available_modules();
148
		$active_modules = Jetpack::get_active_modules();
149
		$modules = array();
150
		foreach ( $available_modules as $available_module ) {
151
			$modules[$available_module] = in_array( $available_module, $active_modules );
152
		}
153
		$modules['vaultpress'] = class_exists( 'VaultPress' ) || function_exists( 'vaultpress_contact_service' );
154
155
		$sync_data = array(
156
			'modules' => $modules,
157
			'version' => JETPACK__VERSION,
158
			'is_multisite' => is_multisite(),
159
		);
160
161
		return $sync_data;
162
	}
163
164
	/**
165
	 * Set up all the data and queue it for the outgoing XML-RPC request
166
	 */
167
	function sync() {
168
		if ( !$this->sync ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sync of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
169
			return false;
170
		}
171
172
		// Don't sync anything from a staging site.
173
		if ( Jetpack::is_development_mode() || Jetpack::is_staging_site() ) {
174
			return false;
175
		}
176
177
		$sync_data = $this->get_common_sync_data();
178
179
		$wp_importing = defined( 'WP_IMPORTING' ) && WP_IMPORTING;
180
181
		foreach ( $this->sync as $sync_operation_type => $sync_operations ) {
182
			switch ( $sync_operation_type ) {
183 View Code Duplication
			case 'post':
184
				if ( $wp_importing ) {
185
					break;
186
				}
187
188
				$global_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;
189
				$GLOBALS['post'] = null;
190
				foreach ( $sync_operations as $post_id => $settings ) {
191
					$sync_data['post'][$post_id] = $this->get_post( $post_id );
192
					if ( isset( $this->post_transitions[$post_id] ) ) {
193
						$sync_data['post'][$post_id]['transitions'] = $this->post_transitions[$post_id];
194
					} else {
195
						$sync_data['post'][$post_id]['transitions'] = array( false, false );
196
					}
197
					$sync_data['post'][$post_id]['on_behalf_of'] = $settings['on_behalf_of'];
198
				}
199
				$GLOBALS['post'] = $global_post;
200
				unset( $global_post );
201
				break;
202 View Code Duplication
			case 'comment':
203
				if ( $wp_importing ) {
204
					break;
205
				}
206
207
				$global_comment = isset( $GLOBALS['comment'] ) ? $GLOBALS['comment'] : null;
208
				unset( $GLOBALS['comment'] );
209
				foreach ( $sync_operations as $comment_id => $settings ) {
210
					$sync_data['comment'][$comment_id] = $this->get_comment( $comment_id );
211
					if ( isset( $this->comment_transitions[$comment_id] ) ) {
212
						$sync_data['comment'][$comment_id]['transitions'] = $this->comment_transitions[$comment_id];
213
					} else {
214
						$sync_data['comment'][$comment_id]['transitions'] = array( false, false );
215
					}
216
					$sync_data['comment'][$comment_id]['on_behalf_of'] = $settings['on_behalf_of'];
217
				}
218
				$GLOBALS['comment'] = $global_comment;
219
				unset( $global_comment );
220
				break;
221
			case 'option' :
222
				foreach ( $sync_operations as $option => $settings ) {
223
					$sync_data['option'][ $option ] = array( 'value' => get_option( $option ) );
224
				}
225
				break;
226
227
			case 'constant' :
228
				foreach( $sync_operations as $constant => $settings ) {
229
					$sync_data['constant'][ $constant ] = array( 'value' => $this->get_constant( $constant ) );
230
				}
231
				break;
232
233
			case 'delete_post':
234
			case 'delete_comment':
235
				foreach ( $sync_operations as $object_id => $settings ) {
236
					$sync_data[$sync_operation_type][$object_id] = array( 'on_behalf_of' => $settings['on_behalf_of'] );
237
				}
238
				break;
239
			case 'delete_option' :
240
				foreach ( $sync_operations as $object_id => $settings ) {
241
					$sync_data[$sync_operation_type][$object_id] = true;
242
				}
243
				break;
244
			}
245
		}
246
		Jetpack::xmlrpc_async_call( 'jetpack.syncContent', $sync_data );
247
	}
248
249
	/**
250
	 * Format and return content data from a direct xmlrpc request for it.
251
	 *
252
	 * @param array $content_ids: array( 'posts' => array of ids, 'comments' => array of ids, 'options' => array of options )
0 ignored issues
show
Documentation introduced by
There is no parameter named $content_ids:. Did you maybe mean $content_ids?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
253
	 */
254
	function get_content( $content_ids ) {
255
		$sync_data = $this->get_common_sync_data();
256
257
		if ( isset( $content_ids['posts'] ) ) {
258
			foreach ( $content_ids['posts'] as $id ) {
259
				$sync_data['post'][$id] = $this->get_post( $id );
260
			}
261
		}
262
263
		if ( isset( $content_ids['comments'] ) ) {
264
			foreach ( $content_ids['comments'] as $id ) {
265
				$sync_data['comment'][$id] = $this->get_post( $id );
266
			}
267
		}
268
269
		if ( isset( $content_ids['options'] ) ) {
270
			foreach ( $content_ids['options'] as $option ) {
271
				$sync_data['option'][$option] = array( 'value' => get_option( $option ) );
272
			}
273
		}
274
275
		return $sync_data;
276
	}
277
278
	/**
279
	 * Helper method for registering a post for sync
280
	 *
281
	 * @param int $id wp_posts.ID
282
	 * @param array $settings Sync data
0 ignored issues
show
Documentation introduced by
Should the type for parameter $settings not be null|array?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
283
	 */
284
	function register_post( $id, array $settings = null ) {
285
		$id = (int) $id;
286
		if ( !$id ) {
287
			return false;
288
		}
289
290
		$post = get_post( $id );
291
		if ( !$post ) {
292
			return false;
293
		}
294
295
		$settings = wp_parse_args( $settings, array(
296
			'on_behalf_of' => array(),
297
		) );
298
299
		return $this->register( 'post', $id, $settings );
300
	}
301
302
	/**
303
	 * Helper method for registering a comment for sync
304
	 *
305
	 * @param int $id wp_comments.comment_ID
306
	 * @param array $settings Sync data
0 ignored issues
show
Documentation introduced by
Should the type for parameter $settings not be null|array?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
307
	 */
308
	function register_comment( $id, array $settings = null ) {
309
		$id = (int) $id;
310
		if ( !$id ) {
311
			return false;
312
		}
313
314
		$comment = get_comment( $id );
315
		if ( !$comment || empty( $comment->comment_post_ID ) ) {
316
			return false;
317
		}
318
319
		$post = get_post( $comment->comment_post_ID );
320
		if ( !$post ) {
321
			return false;
322
		}
323
324
		$settings = wp_parse_args( $settings, array(
325
			'on_behalf_of' => array(),
326
		) );
327
328
		return $this->register( 'comment', $id, $settings );
329
	}
330
331
/* Posts Sync */
332
333
	function posts( $file, array $settings = null ) {
334
		$module_slug = Jetpack::get_module_slug( $file );
335
336
		$defaults = array(
337
			'post_types' => array( 'post', 'page' ),
338
			'post_stati' => array( 'publish' ),
339
		);
340
341
		$this->sync_conditions['posts'][$module_slug] = wp_parse_args( $settings, $defaults );
342
343
		add_action( 'transition_post_status', array( $this, 'transition_post_status_action' ), 10, 3 );
344
		add_action( 'delete_post', array( $this, 'delete_post_action' ) );
345
	}
346
347
	function delete_post_action( $post_id ) {
348
		$post = get_post( $post_id );
349
		if ( !$post ) {
350
			return $this->register( 'delete_post', (int) $post_id );
351
		}
352
353
		$this->transition_post_status_action( 'delete', $post->post_status, $post );
354
	}
355
356
	function transition_post_status_action( $new_status, $old_status, $post ) {
357
		$sync = $this->get_post_sync_operation( $new_status, $old_status, $post, $this->sync_conditions['posts'] );
358
		if ( !$sync ) {
359
			// No module wants to sync this post
360
			return false;
361
		}
362
363
		// Track post transitions
364 View Code Duplication
		if ( isset( $this->post_transitions[$post->ID] ) ) {
365
			// status changed more than once - keep tha most recent $new_status
366
			$this->post_transitions[$post->ID][0] = $new_status;
367
		} else {
368
			$this->post_transitions[$post->ID] = array( $new_status, $old_status );
369
		}
370
371
		$operation = $sync['operation'];
372
		unset( $sync['operation'] );
373
374
		switch ( $operation ) {
375
		case 'delete' :
376
			return $this->register( 'delete_post', (int) $post->ID, $sync );
377
		case 'submit' :
378
			return $this->register_post( (int) $post->ID, $sync );
379
		}
380
	}
381
382
	function get_post_sync_operation( $new_status, $old_status, $post, $module_conditions ) {
383
		$delete_on_behalf_of = array();
384
		$submit_on_behalf_of = array();
385
		$delete_stati = array( 'delete' );
386
		$cache_cleared = false;
387
388
		foreach ( $module_conditions as $module => $conditions ) {
389
			if ( !in_array( $post->post_type, $conditions['post_types'] ) ) {
390
				continue;
391
			}
392
393
			$deleted_post = in_array( $new_status, $delete_stati );
394
395
			if ( $deleted_post ) {
396
				$delete_on_behalf_of[] = $module;
397
			} else {
398
				if ( ! $cache_cleared ) {
399
					// inefficient to clear cache more than once
400
					clean_post_cache( $post->ID );
401
					$cache_cleared = true;
402
				}
403
				$new_status = get_post_status( $post->ID ); // Inherited status is resolved here
404
			}
405
406
			$old_status_in_stati = in_array( $old_status, $conditions['post_stati'] );
407
			$new_status_in_stati = in_array( $new_status, $conditions['post_stati'] );
408
409
			if ( $old_status_in_stati && !$new_status_in_stati ) {
410
				// Jetpack no longer needs the post
411
				if ( !$deleted_post ) {
412
					$delete_on_behalf_of[] = $module;
413
				} // else, we've already flagged it above
414
				continue;
415
			}
416
417
			if ( !$new_status_in_stati ) {
418
				continue;
419
			}
420
421
			// At this point, we know we want to sync the post, not delete it
422
			$submit_on_behalf_of[] = $module;
423
		}
424
425
		if ( !empty( $submit_on_behalf_of ) ) {
426
			return array( 'operation' => 'submit', 'on_behalf_of' => $submit_on_behalf_of );
427
		}
428
429
		if ( !empty( $delete_on_behalf_of ) ) {
430
			return array( 'operation' => 'delete', 'on_behalf_of' => $delete_on_behalf_of );
431
		}
432
433
		return false;
434
	}
435
436
	/**
437
	 * Get a post and associated data in the standard JP format.
438
	 * Cannot be called statically
439
	 *
440
	 * @param int $id Post ID
441
	 * @return Array containing full post details
442
	 */
443
	function get_post( $id ) {
444
		$post_obj = get_post( $id );
445
		if ( !$post_obj )
446
			return false;
447
448
		if ( is_callable( $post_obj, 'to_array' ) ) {
449
			// WP >= 3.5
450
			$post = $post_obj->to_array();
0 ignored issues
show
Bug introduced by
The method to_array cannot be called on $post_obj (of type callable).

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...
451
		} else {
452
			// WP < 3.5
453
			$post = get_object_vars( $post_obj );
454
		}
455
456
		if ( 0 < strlen( $post['post_password'] ) ) {
457
			$post['post_password'] = 'auto-' . wp_generate_password( 10, false ); // We don't want the real password.  Just pass something random.
458
		}
459
460
		// local optimizations
461
		unset(
462
			$post['filter'],
463
			$post['ancestors'],
464
			$post['post_content_filtered'],
465
			$post['to_ping'],
466
			$post['pinged']
467
		);
468
469
		if ( $this->is_post_public( $post ) ) {
470
			$post['post_is_public'] = Jetpack_Options::get_option( 'public' );
471
		} else {
472
			//obscure content
473
			$post['post_content'] = '';
474
			$post['post_excerpt'] = '';
475
			$post['post_is_public'] = false;
476
		}
477
		$post_type_obj = get_post_type_object( $post['post_type'] );
478
		$post['post_is_excluded_from_search'] = $post_type_obj->exclude_from_search;
479
480
		$post['tax'] = array();
481
		$taxonomies = get_object_taxonomies( $post_obj );
482
		foreach ( $taxonomies as $taxonomy ) {
483
			$terms = get_object_term_cache( $post_obj->ID, $taxonomy );
484
			if ( empty( $terms ) )
485
				$terms = wp_get_object_terms( $post_obj->ID, $taxonomy );
486
			$term_names = array();
487
			foreach ( $terms as $term ) {
488
				$term_names[] = $term->name;
489
			}
490
			$post['tax'][$taxonomy] = $term_names;
491
		}
492
493
		$meta = get_post_meta( $post_obj->ID, false );
494
		$post['meta'] = array();
495
		foreach ( $meta as $key => $value ) {
496
			$post['meta'][$key] = array_map( 'maybe_unserialize', $value );
497
		}
498
499
		$post['extra'] = array(
500
			'author' => get_the_author_meta( 'display_name', $post_obj->post_author ),
501
			'author_email' => get_the_author_meta( 'email', $post_obj->post_author ),
502
			'dont_email_post_to_subs' => get_post_meta( $post_obj->ID, '_jetpack_dont_email_post_to_subs', true ),
503
		);
504
505
		if ( $fid = get_post_thumbnail_id( $id ) ) {
506
			$feature = wp_get_attachment_image_src( $fid, 'large' );
507
			if ( ! empty( $feature[0] ) ) {
508
				$post['extra']['featured_image'] = $feature[0];
509
			}
510
511
			$attachment = get_post( $fid );
512
			if ( ! empty( $attachment ) ) {
513
				$metadata = wp_get_attachment_metadata( $fid );
514
515
				$post['extra']['post_thumbnail'] = array(
516
					'ID'        => (int) $fid,
517
					'URL'       => (string) wp_get_attachment_url( $fid ),
518
					'guid'      => (string) $attachment->guid,
519
					'mime_type' => (string) $attachment->post_mime_type,
520
					'width'     => (int) isset( $metadata['width'] ) ? $metadata['width'] : 0,
521
					'height'    => (int) isset( $metadata['height'] ) ? $metadata['height'] : 0,
522
				);
523
524
				if ( isset( $metadata['duration'] ) ) {
525
					$post['extra']['post_thumbnail'] = (int) $metadata['duration'];
526
				}
527
528
				/**
529
				 * Filters the Post Thumbnail information returned for a specific post.
530
				 *
531
				 * @since 3.3.0
532
				 *
533
				 * @param array $post['extra']['post_thumbnail'] {
534
				 * 	Array of details about the Post Thumbnail.
535
				 *	@param int ID Post Thumbnail ID.
536
				 *	@param string URL Post thumbnail URL.
537
				 *	@param string guid Post thumbnail guid.
538
				 *	@param string mime_type Post thumbnail mime type.
539
				 *	@param int width Post thumbnail width.
540
				 *	@param int height Post thumbnail height.
541
				 * }
542
				 */
543
				$post['extra']['post_thumbnail'] = (object) apply_filters( 'get_attachment', $post['extra']['post_thumbnail'] );
544
			}
545
		}
546
547
		$post['permalink'] = get_permalink( $post_obj->ID );
548
		$post['shortlink'] = wp_get_shortlink( $post_obj->ID );
549
		/**
550
		 * Allow modules to send extra info on the sync post process.
551
		 *
552
		 * @since 2.8.0
553
		 *
554
		 * @param array $args Array of custom data to attach to a post.
555
		 * @param Object $post_obj Object returned by get_post() for a given post ID.
556
		 */
557
		$post['module_custom_data'] = apply_filters( 'jetpack_sync_post_module_custom_data', array(), $post_obj );
558
		return $post;
559
	}
560
561
	/**
562
	 * Decide whether a post/page/attachment is visible to the public.
563
	 *
564
	 * @param array $post
565
	 * @return bool
566
	 */
567
	function is_post_public( $post ) {
568
		if ( !is_array( $post ) ) {
569
			$post = (array) $post;
570
		}
571
572
		if ( 0 < strlen( $post['post_password'] ) )
573
			return false;
574
		if ( ! in_array( $post['post_type'], get_post_types( array( 'public' => true ) ) ) )
575
			return false;
576
		$post_status = get_post_status( $post['ID'] ); // Inherited status is resolved here.
577
		if ( ! in_array( $post_status, get_post_stati( array( 'public' => true ) ) ) )
578
			return false;
579
		return true;
580
	}
581
582
/* Comments Sync */
583
584
	function comments( $file, array $settings = null ) {
585
		$module_slug = Jetpack::get_module_slug( $file );
586
587
		$defaults = array(
588
			'post_types' => array( 'post', 'page' ),                            // For what post types will we sync comments?
589
			'post_stati' => array( 'publish' ),                                 // For what post stati will we sync comments?
590
			'comment_types' => array( '', 'comment', 'trackback', 'pingback' ), // What comment types will we sync?
591
			'comment_stati' => array( 'approved' ),                             // What comment stati will we sync?
592
		);
593
594
		$settings = wp_parse_args( $settings, $defaults );
595
596
		$this->sync_conditions['comments'][$module_slug] = $settings;
597
598
		add_action( 'wp_insert_comment',         array( $this, 'wp_insert_comment_action' ),         10, 2 );
599
		add_action( 'transition_comment_status', array( $this, 'transition_comment_status_action' ), 10, 3 );
600
		add_action( 'edit_comment',              array( $this, 'edit_comment_action' ) );
601
	}
602
603
	/*
604
	 * This is really annoying.  If you edit a comment, but don't change the status, WordPress doesn't fire the transition_comment_status hook.
605
	 * That means we have to catch these comments on the edit_comment hook, but ignore comments on that hook when the transition_comment_status does fire.
606
	 */
607
	function edit_comment_action( $comment_id ) {
608
		$comment = get_comment( $comment_id );
609
		$new_status = $this->translate_comment_status( $comment->comment_approved );
610
		add_action( "comment_{$new_status}_{$comment->comment_type}", array( $this, 'transition_comment_status_for_comments_whose_status_does_not_change' ), 10, 2 );
611
	}
612
613
	function wp_insert_comment_action( $comment_id, $comment ) {
614
		$this->transition_comment_status_action( $comment->comment_approved, 'new', $comment );
615
	}
616
617
	function transition_comment_status_for_comments_whose_status_does_not_change( $comment_id, $comment ) {
618
		if ( isset( $this->comment_transitions[$comment_id] ) ) {
619
			return $this->transition_comment_status_action( $comment->comment_approved, $this->comment_transitions[$comment_id][1], $comment );
620
		}
621
622
		return $this->transition_comment_status_action( $comment->comment_approved, $comment->comment_approved, $comment );
623
	}
624
625
	function translate_comment_status( $status ) {
626
		switch ( (string) $status ) {
627
		case '0' :
628
		case 'hold' :
629
			return 'unapproved';
630
		case '1' :
631
		case 'approve' :
632
			return 'approved';
633
		}
634
635
		return $status;
636
	}
637
638
	function transition_comment_status_action( $new_status, $old_status, $comment ) {
639
		$post = get_post( $comment->comment_post_ID );
640
		if ( !$post ) {
641
			return false;
642
		}
643
644
		foreach ( array( 'new_status', 'old_status' ) as $_status ) {
645
			$$_status = $this->translate_comment_status( $$_status );
646
		}
647
648
		// Track comment transitions
649 View Code Duplication
		if ( isset( $this->comment_transitions[$comment->comment_ID] ) ) {
650
			// status changed more than once - keep tha most recent $new_status
651
			$this->comment_transitions[$comment->comment_ID][0] = $new_status;
652
		} else {
653
			$this->comment_transitions[$comment->comment_ID] = array( $new_status, $old_status );
654
		}
655
656
		$post_sync = $this->get_post_sync_operation( $post->post_status, '_jetpack_test_sync', $post, $this->sync_conditions['comments'] );
657
658
		if ( !$post_sync ) {
659
			// No module wants to sync this comment because its post doesn't match any sync conditions
660
			return false;
661
		}
662
663
		if ( 'delete' == $post_sync['operation'] ) {
664
			// Had we been looking at post sync operations (instead of comment sync operations),
665
			// this comment's post would have been deleted.  Don't sync the comment.
666
			return false;
667
		}
668
669
		$delete_on_behalf_of = array();
670
		$submit_on_behalf_of = array();
671
		$delete_stati = array( 'delete' );
672
673
		foreach ( $this->sync_conditions['comments'] as $module => $conditions ) {
674
			if ( !in_array( $comment->comment_type, $conditions['comment_types'] ) ) {
675
				continue;
676
			}
677
678
			$deleted_comment = in_array( $new_status, $delete_stati );
679
680
			if ( $deleted_comment ) {
681
				$delete_on_behalf_of[] = $module;
682
			}
683
684
			$old_status_in_stati = in_array( $old_status, $conditions['comment_stati'] );
685
			$new_status_in_stati = in_array( $new_status, $conditions['comment_stati'] );
686
687
			if ( $old_status_in_stati && !$new_status_in_stati ) {
688
				// Jetpack no longer needs the comment
689
				if ( !$deleted_comment ) {
690
					$delete_on_behalf_of[] = $module;
691
				} // else, we've already flagged it above
692
				continue;
693
			}
694
695
			if ( !$new_status_in_stati ) {
696
				continue;
697
			}
698
699
			// At this point, we know we want to sync the comment, not delete it
700
			$submit_on_behalf_of[] = $module;
701
		}
702
703
		if ( ! empty( $submit_on_behalf_of ) ) {
704
			$this->register_post( $comment->comment_post_ID, array( 'on_behalf_of' => $submit_on_behalf_of ) );
705
			return $this->register_comment( $comment->comment_ID, array( 'on_behalf_of' => $submit_on_behalf_of ) );
706
		}
707
708
		if ( !empty( $delete_on_behalf_of ) ) {
709
			return $this->register( 'delete_comment', $comment->comment_ID, array( 'on_behalf_of' => $delete_on_behalf_of ) );
710
		}
711
712
		return false;
713
	}
714
715
	/**
716
	 * Get a comment and associated data in the standard JP format.
717
	 * Cannot be called statically
718
	 *
719
	 * @param int $id Comment ID
720
	 * @return Array containing full comment details
721
	 */
722
	function get_comment( $id ) {
723
		$comment_obj = get_comment( $id );
724
		if ( !$comment_obj )
725
			return false;
726
		$comment = get_object_vars( $comment_obj );
727
728
		$meta = get_comment_meta( $id, false );
729
		$comment['meta'] = array();
730
		foreach ( $meta as $key => $value ) {
731
			$comment['meta'][$key] = array_map( 'maybe_unserialize', $value );
732
		}
733
734
		return $comment;
735
	}
736
737
/* Options Sync */
738
739
	/* Ah... so much simpler than Posts and Comments :) */
740
	function options( $file, $option /*, $option, ... */ ) {
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $option is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
741
		$options = func_get_args();
742
		$file = array_shift( $options );
743
744
		$module_slug = Jetpack::get_module_slug( $file );
745
746
		if ( !isset( $this->sync_options[$module_slug] ) ) {
747
			$this->sync_options[$module_slug] = array();
748
		}
749
750
		foreach ( $options as $option ) {
751
			$this->sync_options[$module_slug][] = $option;
752
			add_action( "delete_option_{$option}", array( $this, 'deleted_option_action' ) );
753
			add_action( "update_option_{$option}", array( $this, 'updated_option_action' ) );
754
			add_action( "add_option_{$option}",    array( $this, 'added_option_action'   ) );
755
		}
756
757
		$this->sync_options[$module_slug] = array_unique( $this->sync_options[$module_slug] );
758
	}
759
760
	function deleted_option_action( $option ) {
761
		$this->register( 'delete_option', $option );
762
	}
763
764
	function updated_option_action() {
765
		// The value of $option isn't passed to the filter
766
		// Calculate it
767
		$option = current_filter();
768
		$prefix = 'update_option_';
769
		if ( 0 !== strpos( $option, $prefix ) ) {
770
			return;
771
		}
772
		$option = substr( $option, strlen( $prefix ) );
773
774
		$this->added_option_action( $option );
775
	}
776
777
	function added_option_action( $option ) {
778
		$this->register( 'option', $option );
779
	}
780
781
	function sync_all_module_options( $module_slug ) {
782
		if ( empty( $this->sync_options[$module_slug] ) ) {
783
			return;
784
		}
785
786
		foreach ( $this->sync_options[$module_slug] as $option ) {
787
			$this->added_option_action( $option );
788
		}
789
	}
790
791
	function sync_all_registered_options() {
792
		if ( 'jetpack_sync_all_registered_options' == current_filter() ) {
793
			add_action( 'shutdown', array( $this, 'register_all_options' ), 8 );
794
		} else {
795
			wp_schedule_single_event( time(), 'jetpack_sync_all_registered_options', array( $this->sync_options ) );
796
		}
797
	}
798
799
	/**
800
	 * All the options that are defined in modules as well as class.jetpack.php will get synced.
801
	 * Registers all options to be synced.
802
	 */
803
	function register_all_options() {
804
		$all_registered_options = array_unique( call_user_func_array( 'array_merge', $this->sync_options ) );
805
		foreach ( $all_registered_options as $option ) {
806
			$this->added_option_action( $option );
807
		}
808
	}
809
810
/* Constants Sync */
811
812
	function get_all_constants() {
813
		return array(
814
			'EMPTY_TRASH_DAYS',
815
			'WP_POST_REVISIONS',
816
			'AUTOMATIC_UPDATER_DISABLED',
817
			'ABSPATH',
818
			'WP_CONTENT_DIR',
819
			'FS_METHOD',
820
			'DISALLOW_FILE_EDIT',
821
			'DISALLOW_FILE_MODS',
822
			'WP_AUTO_UPDATE_CORE',
823
			'WP_HTTP_BLOCK_EXTERNAL',
824
			'WP_ACCESSIBLE_HOSTS',
825
			);
826
	}
827
	/**
828
	 * This lets us get the constant value like get_option( 'jetpack_constant_CONSTANT' );
829
	 * Not the best way to get the constant value but necessery in some cases like in the API.
830
	 */
831
	function register_constants_as_options() {
832
		foreach( $this->get_all_constants() as $constant ) {
833
			add_filter( 'pre_option_jetpack_constant_'. $constant, array( $this, 'get_default_constant' ) );
834
		}
835
	}
836
837
	function sync_all_constants() {
838
		// add the constant to sync.
839
		foreach( $this->get_all_constants() as $constant ) {
840
			$this->register_constant( $constant );
841
		}
842
		add_action( 'shutdown', array( $this, 'register_all_module_constants' ), 8 );
843
	}
844
845
	/**
846
	 * Returns default values of Constants
847
	 */
848
	function default_constant( $constant ) {
849
		switch( $constant ) {
850
			case 'WP_AUTO_UPDATE_CORE':
851
				return 'minor';
852
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
853
854
			default:
855
				return null;
856
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
857
		}
858
	}
859
860
	function register_all_module_constants() {
861
		// also add the contstants from each module to be synced.
862
		foreach( $this->sync_constants as $module ) {
863
			foreach( $module as $constant ) {
864
				$this->register_constant( $constant );
865
			}
866
		}
867
	}
868
869
	/**
870
	 * Sync constants required by the module that was just activated.
871
 	 * If you add Jetpack_Sync::sync_constant( __FILE__, 'HELLO_WORLD' );
872
	 * to the module it will start syncing the constant after the constant has been updated.
873
	 *
874
	 * This function gets called on module activation.
875
	 */
876
	function sync_module_constants( $module ) {
877
878
		if ( isset( $this->sync_constants[ $module ] ) && is_array( $this->sync_constants[ $module ] ) ) {
879
			// also add the contstants from each module to be synced.
880
			foreach( $this->sync_constants[ $module ] as $constant ) {
881
				$this->register_constant(  $constant );
882
			}
883
		}
884
	}
885
886
	public function reindex_needed() {
887
		return ( $this->_get_post_count_local() != $this->_get_post_count_cloud() );
888
	}
889
890
	public function reindex_trigger() {
891
		$response = array( 'status' => 'ERROR' );
892
893
		// Force a privacy check
894
		Jetpack::check_privacy( JETPACK__PLUGIN_FILE );
895
896
		Jetpack::load_xml_rpc_client();
897
		$client = new Jetpack_IXR_Client( array(
898
			'user_id' => JETPACK_MASTER_USER,
899
		) );
900
901
		$client->query( 'jetpack.reindexTrigger' );
902
903
		if ( !$client->isError() ) {
904
			$response = $client->getResponse();
905
			Jetpack_Options::update_option( 'sync_bulk_reindexing', true );
906
		}
907
908
		return $response;
909
	}
910
911
	public function reindex_status() {
912
		$response = array( 'status' => 'ERROR' );
913
914
		// Assume reindexing is done if it was not triggered in the first place
915
		if ( false === Jetpack_Options::get_option( 'sync_bulk_reindexing' ) ) {
916
			return array( 'status' => 'DONE' );
917
		}
918
919
		Jetpack::load_xml_rpc_client();
920
		$client = new Jetpack_IXR_Client( array(
921
			'user_id' => JETPACK_MASTER_USER,
922
		) );
923
924
		$client->query( 'jetpack.reindexStatus' );
925
926
		if ( !$client->isError() ) {
927
			$response = $client->getResponse();
928
			if ( 'DONE' == $response['status'] ) {
929
				Jetpack_Options::delete_option( 'sync_bulk_reindexing' );
930
			}
931
		}
932
933
		return $response;
934
	}
935
936
	public function reindex_ui() {
937
		$strings = json_encode( array(
938
			'WAITING' => array(
939
				'action' => __( 'Refresh Status', 'jetpack' ),
940
				'status' => __( 'Indexing request queued and waiting&hellip;', 'jetpack' ),
941
			),
942
			'INDEXING' => array(
943
				'action' => __( 'Refresh Status', 'jetpack' ),
944
				'status' => __( 'Indexing posts', 'jetpack' ),
945
			),
946
			'DONE' => array(
947
				'action' => __( 'Reindex Posts', 'jetpack' ),
948
				'status' => __( 'Posts indexed.', 'jetpack' ),
949
			),
950
			'ERROR' => array(
951
				'action' => __( 'Refresh Status', 'jetpack' ),
952
				'status' => __( 'Status unknown.', 'jetpack' ),
953
			),
954
			'ERROR:LARGE' => array(
955
				'action' => __( 'Refresh Status', 'jetpack' ),
956
				'status' => __( 'This site is too large, please contact Jetpack support to sync.', 'jetpack' ),
957
			),
958
		) );
959
960
		wp_enqueue_script(
961
			'jetpack_sync_reindex_control',
962
			plugins_url( '_inc/jquery.jetpack-sync.js', JETPACK__PLUGIN_FILE ),
963
			array( 'jquery' ),
964
			JETPACK__VERSION
965
		);
966
967
		$template = <<<EOT
968
			<p class="jetpack_sync_reindex_control" id="jetpack_sync_reindex_control" data-strings="%s">
969
				<input type="submit" class="jetpack_sync_reindex_control_action button" value="%s" disabled />
970
				<span class="jetpack_sync_reindex_control_status">&hellip;</span>
971
			</p>
972
EOT;
973
974
		return sprintf(
975
			$template,
976
			esc_attr( $strings ),
977
			esc_attr__( 'Refresh Status', 'jetpack' )
978
		);
979
	}
980
981
	private function _get_post_count_local() {
982
		global $wpdb;
983
		return (int) $wpdb->get_var(
984
			"SELECT count(*)
985
				FROM {$wpdb->posts}
986
				WHERE post_status = 'publish' AND post_password = ''"
987
		);
988
	}
989
990
	private function _get_post_count_cloud() {
991
		$blog_id = Jetpack::init()->get_option( 'id' );
992
993
		$body = array(
994
			'size' => 1,
995
		);
996
997
		$response = wp_remote_post(
998
			"https://public-api.wordpress.com/rest/v1/sites/$blog_id/search",
999
			array(
1000
				'timeout' => 10,
1001
				'user-agent' => 'jetpack_related_posts',
1002
				'sslverify' => true,
1003
				'body' => $body,
1004
			)
1005
		);
1006
1007
		if ( is_wp_error( $response ) ) {
1008
			return 0;
1009
		}
1010
1011
		$results = json_decode( wp_remote_retrieve_body( $response ), true );
1012
1013
		return isset( $results['results'] ) && isset( $results['results']['total'] ) ? (int) $results['results']['total'] : 0;
1014
	}
1015
1016
	/**
1017
	 * Sometimes we need to fake options to be able to sync data with .com
1018
	 * This is a helper function. That will make it easier to do just that.
1019
	 *
1020
	 * It will make sure that the options are synced when do_action( 'jetpack_sync_all_registered_options' );
1021
	 *
1022
	 * Which should happen everytime we update Jetpack to a new version or daily by Jetpack_Heartbeat.
1023
	 *
1024
	 * $callback is a function that is passed into a filter that returns the value of the option.
1025
	 * This value should never be false. Since we want to short circuit the get_option function
1026
	 * to return the value of the our callback.
1027
	 *
1028
	 * You can also trigger an update when a something else changes by calling the
1029
	 * do_action( 'add_option_jetpack_' . $option, 'jetpack_'.$option, $callback_function );
1030
	 * on the action that should that would trigger the update.
1031
	 *
1032
	 *
1033
	 * @param  string $option   Option will always be prefixed with Jetpack and be saved on .com side
1034
	 * @param  string or array  $callback
1035
	 */
1036
	function mock_option( $option , $callback ) {
1037
		add_filter( 'pre_option_jetpack_'. $option, $callback );
1038
		// This shouldn't happen but if it does we return the same as before.
1039
		add_filter( 'option_jetpack_'. $option, $callback );
1040
		// Instead of passing a file we just pass in a string.
1041
		$this->options( 'mock-option' , 'jetpack_' . $option );
1042
1043
	}
1044
	/**
1045
	 * Sometimes you need to sync constants to .com
1046
	 * Using the function will allow you to do just that.
1047
	 *
1048
	 * @param  'string' $constant Constants defined in code.
0 ignored issues
show
Documentation introduced by
The doc-type 'string' could not be parsed: Unknown type name "'string'" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1049
	 *
1050
	 */
1051
	function register_constant( $constant ) {
1052
		$this->register( 'constant', $constant );
1053
	}
1054
1055
	function get_default_constant() {
1056
		$filter = current_filter();
1057
		// We don't know what the constant is so we get it from the current filter.
1058
		if ( 'pre_option_jetpack_constant_' === substr( $filter, 0, 28 ) ) {
1059
			$constant = substr( $filter, 28 );
1060
			if ( defined( $constant ) ) {
1061
				// If constant is set to false we will not shortcut the get_option function and will return the default value.
1062
				// Hance we set it to null. Which in most cases would produce the same result.
1063
				return false === constant( $constant ) ? null : constant( $constant );
1064
			}
1065
			return $this->default_constant( $constant );
1066
		}
1067
	}
1068
	/**
1069
	 * Simular to $this->options() function.
1070
	 * Add the constant to be synced to .com when we activate the module.
1071
	 * As well as on heartbeat and plugin upgrade and connection to .com.
1072
	 *
1073
	 * @param string $file
1074
	 * @param string $constant
1075
	 */
1076
	function constant( $file, $constant ) {
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $constant is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1077
		$constants = func_get_args();
1078
		$file = array_shift( $constants );
1079
1080
		$module_slug = Jetpack::get_module_slug( $file );
1081
1082
		if ( ! isset( $this->sync_constants[ $module_slug ] ) ) {
1083
			$this->sync_constants[ $module_slug ] = array();
1084
		}
1085
1086
		foreach ( $constants as $constant ) {
1087
			$this->sync_constants[ $module_slug ][] = $constant;
1088
		}
1089
	}
1090
1091
	/**
1092
	 * Helper function to return the constants value.
1093
	 *
1094
	 * @param  string $constant
1095
	 * @return value of the constant or null if the constant is set to false or doesn't exits.
1096
	 */
1097
	static function get_constant( $constant ) {
1098
		if ( defined( $constant ) ) {
1099
			return constant( $constant );
1100
		}
1101
1102
		return null;
1103
	}
1104
}
1105