Completed
Push — update/sync-posts-less-events ( df315a )
by
unknown
09:51
created

is_valid_editpost_action()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 8
nc 4
nop 1
dl 0
loc 12
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
4
require_once dirname( __FILE__ ) . '/class.jetpack-sync-item.php';
5
6
class Jetpack_Sync_Module_Posts extends Jetpack_Sync_Module {
7
8
	private $action_handler;
9
10
	private $sync_items = array(); // TODO: add to parent class
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
11
12
	const DEFAULT_PREVIOUS_STATE = 'new';
13
14
	public function name() {
15
		return 'posts';
16
	}
17
18
	public function get_object_by_id( $object_type, $id ) {
19
		if ( $object_type === 'post' && $post = get_post( intval( $id ) ) ) {
20
			return $this->filter_post_content_and_add_links( $post );
21
		}
22
23
		return false;
24
	}
25
26
	public function init_listeners( $callable ) {
27
		$this->action_handler = $callable;
28
29
		// Core < 4.7 doesn't deal with nested wp_insert_post calls very well
30
		global $wp_version;
31
		$priority = version_compare( $wp_version, '4.7-alpha', '<' ) ? 0 : 11;
32
		// Our aim is to initialize `sync_items` as early as possible, so that other areas of the code base can know
33
		// that we are within a post-saving operation. `wp_insert_post_parent` happens early within the action stack.
34
		// And we can catch editpost actions early by hooking to `check_admin_referrer`.
35
		add_filter( 'wp_insert_post_parent', array( $this, 'maybe_initialize_post_sync_item' ), 10, 3 );
36
		add_action( 'check_admin_referer', array( $this, 'maybe_initialize_post_sync_item_referer' ), 10, 2 );
37
38
		add_action( 'deleted_post', $callable );
39
40
		add_action( 'wp_insert_post', array( $this, 'maybe_sync_save_post' ), $priority, 3 );
41
		add_action( 'jetpack_post_saved', $callable, 10, 1 );
42
		add_action( 'jetpack_post_published', $callable, 10, 1 );
43
44
		add_action( 'transition_post_status', array( $this, 'save_published' ), 10, 3 );
45
		add_filter( 'jetpack_sync_before_enqueue_jetpack_post_saved', array( $this, 'filter_blacklisted_post_types' ) );
46
		add_filter( 'jetpack_sync_before_enqueue_jetpack_post_published', array( $this, 'filter_blacklisted_post_types' ) );
47
48
49
		add_action( 'added_post_meta', array( $this, 'sync_post_meta' ), 10, 4 );
50
		add_action( 'updated_post_meta', array( $this, 'sync_post_meta' ), 10, 4 );
51
		add_action( 'deleted_post_meta', array( $this, 'sync_post_meta' ), 10, 4 );
52
53
		add_action( 'set_object_terms', array( $this, 'sync_object_terms' ), 10, 6 );
54
55
		// Batch Akismet meta cleanup
56
		add_action( 'jetpack_daily_akismet_meta_cleanup_before', array( $this, 'daily_akismet_meta_cleanup_before' ) );
57
		add_action( 'jetpack_daily_akismet_meta_cleanup_after', array( $this, 'daily_akismet_meta_cleanup_after' ) );
58
		add_action( 'jetpack_post_meta_batch_delete', $callable, 10, 2 );
59
	}
60
61
	public function init_full_sync_listeners( $callable ) {
62
		add_action( 'jetpack_full_sync_posts', $callable ); // also sends post meta
63
	}
64
65
	function get_full_sync_actions() {
66
		return array( 'jetpack_full_sync_posts' );
67
	}
68
69
	public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
70
		global $wpdb;
71
		return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_posts', $wpdb->posts, 'ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
72
	}
73
74 View Code Duplication
	public function estimate_full_sync_actions( $config ) {
75
		global $wpdb;
76
		$query = "SELECT count(*) FROM $wpdb->posts WHERE " . $this->get_where_sql( $config );
77
		$count = $wpdb->get_var( $query );
78
		return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
79
	}
80
81 View Code Duplication
	private function get_where_sql( $config ) {
82
		$where_sql = Jetpack_Sync_Settings::get_blacklisted_post_types_sql();
83
		// config is a list of post IDs to sync
84
		if ( is_array( $config ) ) {
85
			$where_sql .= ' AND ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
86
		}
87
		return $where_sql;
88
	}
89
90
	public function init_before_send() {
91
		add_filter( 'jetpack_sync_before_send_jetpack_post_saved', array( $this, 'expand_jetpack_post_saved' ) );
92
		add_filter( 'jetpack_sync_before_send_jetpack_post_published', array( $this, 'expand_jetpack_post_saved' ) );
93
94
		// full sync
95
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'expand_post_ids' ) );
96
	}
97
98
	public function daily_akismet_meta_cleanup_before( $feedback_ids ) {
99
		remove_action( 'deleted_post_meta', array( $this, 'sync_post_meta' ) );
100
		/**
101
		 * Used for syncing deletion of batch post meta
102
		 *
103
		 * @since 6.1.0
104
		 *
105
		 * @module sync
106
		 *
107
		 * $param array $feedback_ids feedback post IDs
108
		 * $param string $meta_key to be deleted
109
		 */
110
		do_action( 'jetpack_post_meta_batch_delete', $feedback_ids, '_feedback_akismet_values');
111
	}
112
	public function daily_akismet_meta_cleanup_after( $feedback_ids ) {
113
		add_action( 'deleted_post_meta', array( $this, 'sync_post_meta' ), 10, 4 );
114
	}
115
116
	public function maybe_initialize_post_sync_item( $post_parent, $post_ID, $args ) {
117
		if ( $post_ID ) {
118
			$this->set_post_sync_item( $post_ID );
119
		} else if ( ! $this->is_attachment( $args ) ) {
120
			$this->set_post_sync_item( 'new' );
121
		}
122
		return $post_parent;
123
	}
124
125
	public function maybe_initialize_post_sync_item_referer( $action, $result ) {
126
		if ( ! $this->is_valid_editpost_action( $result )  ) {
127
			return;
128
		}
129
		$post_ID = $this->get_post_id_from_post_request();
130
		if ( ! $post_ID ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $post_ID of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
131
			return;
132
		}
133
		$this->set_post_sync_item( $post_ID );
134
	}
135
136
	private function is_attachment( $args ) {
137
		return ( isset( $args['post_type'] ) && 'attachment' === $args['post_type'] );
138
	}
139
140
	private function set_post_sync_item( $post_ID ) {
141
		if ( $this->has_sync_item( $post_ID ) ) {
142
			return;
143
		}
144 View Code Duplication
		if ( $this->has_sync_item( 'new' ) && 'new' !== $post_ID ) {
145
			$this->sync_items[ $post_ID ] = $this->sync_items['new'];
146
			unset( $this->sync_items['new'] );
147
			return;
148
		}
149
		$this->sync_items[ $post_ID ] = new Jetpack_Sync_Item( 'save_post' );
150
	}
151
152
	private function get_post_sync_item( $post_ID ) {
153
		$this->set_post_sync_item( $post_ID );
154
		return $this->sync_items[ $post_ID ];
155
	}
156
157
	private function has_sync_item( $post_id ) {
158
		return isset( $this->sync_items[ $post_id ] );
159
	}
160
161
	public function is_valid_editpost_action( $result ) {
162
		if ( ! $result ) {
163
			return false;
164
		}
165
		if ( 'POST' !== $_SERVER['REQUEST_METHOD'] || ! isset( $_POST['action'] ) || ! isset( $_POST['post_ID'] ) ) {
166
			return false;
167
		}
168
		if ( 'editpost' !== $_POST['action'] ) {
169
			return false;
170
		}
171
		return true;
172
	}
173
174
	private function get_post_id_from_post_request() {
175
		if ( ! isset( $_POST['post_ID' ] )  ) {
176
			return null;
177
		}
178
		return (int) $_POST['post_ID' ];
179
	}
180
181
	public function maybe_sync_save_post( $post_ID, $post = null, $update = null ) {
182
		if ( ! is_numeric( $post_ID ) || is_null( $post ) ) {
183
			return;
184
		}
185
		if ( $this->is_revision( $post ) ) {
186
			$this->process_revision( $post, $post_ID ); // todo: better name
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
187
			return;
188
		}
189
		// workaround for https://github.com/woocommerce/woocommerce/issues/18007
190
		if ( $post && 'shop_order' === $post->post_type ) {
191
			$post = get_post( $post_ID );
192
		}
193
194
		$sync_item = $this->get_post_sync_item( $post_ID );
195
		if ( ! $sync_item->state_isset( 'previous_status' ) ) {
196
			$sync_item->set_state_value( 'previous_status', self::DEFAULT_PREVIOUS_STATE );
197
		}
198
		$sync_item->set_state_value( 'is_auto_save', (bool) Jetpack_Constants::get_constant( 'DOING_AUTOSAVE' ) );
199
		$sync_item->set_state_value( 'update', $update );
200
		$author_user_object = get_user_by( 'id', $post->post_author );
201
		if ( $author_user_object ) {
202
			$sync_item->set_state_value( 'author',  array(
203
				'id'              => $post->post_author,
204
				'wpcom_user_id'   => get_user_meta( $post->post_author, 'wpcom_user_id', true ),
205
				'display_name'    => $author_user_object->display_name,
206
				'email'           => $author_user_object->user_email,
207
				'translated_role' => Jetpack::translate_user_to_role( $author_user_object ),
208
			) );
209
		}
210
		$sync_item->set_object( $post );
211
		if ( $sync_item->is_state_value_true( 'is_just_published' ) ) {
212
			$this->send_published( $sync_item );
213
		} else {
214
			/**
215
			 * Action that gets synced when a post type gets saved
216
			 *
217
			 * @since 6.2.0
218
			 *
219
			 * @param array Sync Item Payload [ 'object' => post object, 'terms' => related terms, 'state' => additional info about the post ]
220
			 */
221
			do_action( 'jetpack_post_saved', $sync_item->get_payload() );
222
		}
223
		unset( $this->sync_items[ $post_ID ] );
224
	}
225
226
	public function send_published( $sync_item ) {
227
		$post = $sync_item->get_object();
228
		// Post revisions cause race conditions where this send_published add the action before the actual post gets synced
229
		if ( wp_is_post_autosave( $post ) || wp_is_post_revision( $post ) ) {
230
			return;
231
		}
232
		/**
233
		 * Filter that is used to add to the post state when a post gets published
234
		 *
235
		 * @since 4.4.0
236
		 *
237
		 * @param mixed array Post state
238
		 * @param mixed $post WP_POST object
239
		 */
240
		$sync_item_state = apply_filters( 'jetpack_published_post_flags', $sync_item->get_state(), $post );
241
		$sync_item->set_state( $sync_item_state );
242
		/**
243
		 * Action that gets synced when a post type gets published.
244
		 *
245
		 * @since 6.2.0
246
		 *
247
		 * @param mixed $sync_item  object
248
		 */
249
		do_action( 'jetpack_post_published', $sync_item->get_payload() );
250
	}
251
252
	public function save_published( $new_status, $old_status, $post ) {
253
		$sync_item = $this->get_post_sync_item( $post->ID );
254
		$is_just_published = 'publish' === $new_status && 'publish' !== $old_status;
255
		$sync_item->set_state_value( 'is_just_published', $is_just_published );
256
		$sync_item->set_state_value( 'previous_status', $old_status );
257
	}
258
259
	private function is_revision( $post ) {
260
		return ( wp_is_post_revision( $post ) && $this->is_saving_post( $post->post_parent ) );
261
	}
262
263
	private function process_revision( $post, $post_ID ) {
264
		$post = (array) $post;
265
		unset( $post['post_content'] );
266
		unset( $post['post_title'] );
267
		unset( $post['post_excerpt'] );
268
		$sync_item = $this->sync_items[ $post['post_parent'] ];
269
		$sync_item->set_state_value( 'revision', $post );
270
		unset( $this->sync_items[ $post_ID ] );
271
	}
272
273
	public function filter_blacklisted_post_types( $args ) {
274
		$post = $args[0]['object'];
275
		if ( in_array( $post->post_type, Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) ) ) {
276
			return false;
277
		}
278
		return $args;
279
	}
280
281
	public function sync_post_meta( $meta_id, $post_id, $meta_key, $_meta_value ) {
282
		if ( ! $this->is_post_type_allowed( $post_id ) || ! $this->is_whitelisted_post_meta( $meta_key ) ) {
283
			return;
284
		}
285
286
		if ( ! $this->is_saving_post( $post_id ) ) {
287
			call_user_func_array( $this->action_handler, func_get_args() );
288
			return;
289
		}
290
		// current_action() is 'added_post_meta', 'updated_post_meta' or 'deleted_post_meta'
291
		$sync_item = new Jetpack_Sync_Item( current_action(),
292
			array( $meta_id, $post_id, $meta_key, $_meta_value )
293
		);
294
		$this->add_sync_item( $post_id, $sync_item );
295
	}
296
297
	public function is_saving_post( $post_ID ) {
298
		return $this->has_sync_item( $post_ID ) || $this->has_sync_item( 'new' );
299
	}
300
301
	private function is_post_type_allowed( $post_id ) {
302
		$post = get_post( intval( $post_id ) );
303
		if ( $post->post_type ) {
304
			return ! in_array( $post->post_type, Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) );
305
		}
306
		return false;
307
	}
308
309
	private function is_whitelisted_post_meta( $meta_key ) {
310
		// _wpas_skip_ is used by publicize
311
		return in_array( $meta_key, Jetpack_Sync_Settings::get_setting( 'post_meta_whitelist' ) ) || wp_startswith( $meta_key, '_wpas_skip_' );
312
	}
313
314
	public function sync_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
315
		if ( ! $this->is_saving_post( $post_id ) ) {
316
			return;
317
		}
318
		$sync_item = new Jetpack_Sync_Item( 'set_object_terms',
319
			array( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids )
320
		);
321
		$this->add_sync_item( $post_id, $sync_item );
322
	}
323
324
	public function add_sync_item( $post_id, $sync_item ) {
325 View Code Duplication
		if ( $this->has_sync_item( 'new' ) && ! $this->has_sync_item( $post_id ) ) {
326
			$this->sync_items[ $post_id ] = $this->sync_items['new'];
327
			unset( $this->sync_items['new'] );
328
		}
329
		$this->sync_items[ $post_id ]->add_sync_item( $sync_item );
330
	}
331
332
	/**
333
	 * Process content before send
334
	 *
335
	 * @param array $args sync_post_saved arguments
336
	 *
337
	 * @return array
338
	 */
339
	public function expand_jetpack_post_saved( $args ) {
340
		$args[0]['object'] = $this->filter_post_content_and_add_links( $args[0]['object'] );
341
		return $args;
342
	}
343
344
	// Expands wp_insert_post to include filtered content
345
	public function filter_post_content_and_add_links( $post_object ) {
346
		global $post;
347
		$post = $post_object;
348
		// return non existant post
349
		$post_type = get_post_type_object( $post->post_type );
350 View Code Duplication
		if ( empty( $post_type ) || ! is_object( $post_type ) ) {
351
			$non_existant_post                    = new stdClass();
352
			$non_existant_post->ID                = $post->ID;
353
			$non_existant_post->post_modified     = $post->post_modified;
354
			$non_existant_post->post_modified_gmt = $post->post_modified_gmt;
355
			$non_existant_post->post_status       = 'jetpack_sync_non_registered_post_type';
356
			return $non_existant_post;
357
		}
358
		/**
359
		 * Filters whether to prevent sending post data to .com
360
		 *
361
		 * Passing true to the filter will prevent the post data from being sent
362
		 * to the WordPress.com.
363
		 * Instead we pass data that will still enable us to do a checksum against the
364
		 * Jetpacks data but will prevent us from displaying the data on in the API as well as
365
		 * other services.
366
		 * @since 4.2.0
367
		 *
368
		 * @param boolean false prevent post data from being synced to WordPress.com
369
		 * @param mixed $post WP_POST object
370
		 */
371 View Code Duplication
		if ( apply_filters( 'jetpack_sync_prevent_sending_post_data', false, $post ) ) {
372
			// We only send the bare necessary object to be able to create a checksum.
373
			$blocked_post                    = new stdClass();
374
			$blocked_post->ID                = $post->ID;
375
			$blocked_post->post_modified     = $post->post_modified;
376
			$blocked_post->post_modified_gmt = $post->post_modified_gmt;
377
			$blocked_post->post_status       = 'jetpack_sync_blocked';
378
			return $blocked_post;
379
		}
380
		// lets not do oembed just yet.
381
		$this->remove_embed();
382
		if ( 0 < strlen( $post->post_password ) ) {
383
			$post->post_password = 'auto-' . wp_generate_password( 10, false );
384
		}
385
386
		/** This filter is already documented in core. wp-includes/post-template.php */
387
		if ( Jetpack_Sync_Settings::get_setting( 'render_filtered_content' ) && $post_type->public ) {
388
			global $shortcode_tags;
389
			/**
390
			 * Filter prevents some shortcodes from expanding.
391
			 *
392
			 * Since we can can expand some type of shortcode better on the .com side and make the
393
			 * expansion more relevant to contexts. For example [galleries] and subscription emails
394
			 *
395
			 * @since 4.5.0
396
			 *
397
			 * @param array - of shortcode tags to remove.
398
			 */
399
			$shortcodes_to_remove        = apply_filters( 'jetpack_sync_do_not_expand_shortcodes', array(
400
				'gallery',
401
				'slideshow'
402
			) );
403
			$removed_shortcode_callbacks = array();
404
			foreach ( $shortcodes_to_remove as $shortcode ) {
405
				if ( isset ( $shortcode_tags[ $shortcode ] ) ) {
406
					$removed_shortcode_callbacks[ $shortcode ] = $shortcode_tags[ $shortcode ];
407
				}
408
			}
409
			array_map( 'remove_shortcode', array_keys( $removed_shortcode_callbacks ) );
410
			$post->post_content_filtered = apply_filters( 'the_content', $post->post_content );
411
			$post->post_excerpt_filtered = apply_filters( 'the_excerpt', $post->post_excerpt );
412
			foreach ( $removed_shortcode_callbacks as $shortcode => $callback ) {
413
				add_shortcode( $shortcode, $callback );
414
			}
415
		}
416
417
		$this->add_embed();
418
		if ( has_post_thumbnail( $post->ID ) ) {
419
			$image_attributes = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' );
420
			if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) {
421
				$post->featured_image = $image_attributes[0];
422
			}
423
		}
424
		$post->permalink = get_permalink( $post->ID );
425
		$post->shortlink = wp_get_shortlink( $post->ID );
426
		if ( function_exists( 'amp_get_permalink' ) ) {
427
			$post->amp_permalink = amp_get_permalink( $post->ID );
428
		}
429
		return $post;
430
	}
431
432 View Code Duplication
	private function remove_embed() {
433
		global $wp_embed;
434
		remove_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 );
435
		// remove the embed shortcode since we would do the part later.
436
		remove_shortcode( 'embed' );
437
		// Attempts to embed all URLs in a post
438
		remove_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 );
439
440
	}
441
442 View Code Duplication
	private function add_embed() {
443
		global $wp_embed;
444
		add_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 );
445
		// Shortcode placeholder for strip_shortcodes()
446
		add_shortcode( 'embed', '__return_false' );
447
		// Attempts to embed all URLs in a post
448
		add_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 );
449
	}
450
451
	public function expand_post_ids( $args ) {
452
		$post_ids = $args[0];
453
		$posts = array_filter( array_map( array( 'WP_Post', 'get_instance' ), $post_ids ) );
454
		$posts = array_map( array( $this, 'filter_post_content_and_add_links' ), $posts );
455
		$posts = array_values( $posts ); // reindex in case posts were deleted
456
		return array(
457
			$posts,
458
			$this->get_metadata( $post_ids, 'post', Jetpack_Sync_Settings::get_setting( 'post_meta_whitelist' ) ),
459
			$this->get_term_relationships( $post_ids ),
460
		);
461
	}
462
}
463