PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 3.6.4
Jetpack – WP Security, Backup, Speed, & Growth v3.6.4
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / class.jetpack-sync.php

class.jetpack-sync.php in Jetpack – WP Security, Backup, Speed, & Growth 3.6.4, at class.jetpack-sync.php

1,035 lines 31.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 var $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 var $sync_options = array();
13
14 var $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 var $post_transitions = array();
18 var $comment_transitions = array();
19
20 // Objects to sync
21 var $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( 'jetpack_sync_all_registered_options', array( $this, 'sync_all_constants' ) );
30 add_action( 'jetpack_heartbeat', array( $this, 'sync_all_constants' ) );
31
32 add_action( 'jetpack_activate_module', array( $this, 'sync_module_constants' ), 10, 1 );
33 }
34
35 /* Static Methods for Modules */
36
37 /**
38 * @param string $file __FILE__
39 * @param array settings:
40 * post_types => array( post_type slugs ): The post types to sync. Default: post, page
41 * post_stati => array( post_status slugs ): The post stati to sync. Default: publish
42 */
43 static function sync_posts( $file, array $settings = null ) {
44 if ( is_network_admin() ) return;
45 $jetpack = Jetpack::init();
46 $args = func_get_args();
47 return call_user_func_array( array( $jetpack->sync, 'posts' ), $args );
48 }
49
50 /**
51 * @param string $file __FILE__
52 * @param array settings:
53 * post_types => array( post_type slugs ): The post types to sync. Default: post, page
54 * post_stati => array( post_status slugs ): The post stati to sync. Default: publish
55 * comment_types => array( comment_type slugs ): The comment types to sync. Default: '', comment, trackback, pingback
56 * comment_stati => array( comment_status slugs ): The comment stati to sync. Default: approved
57 */
58 static function sync_comments( $file, array $settings = null ) {
59 if ( is_network_admin() ) return;
60 $jetpack = Jetpack::init();
61 $args = func_get_args();
62 return call_user_func_array( array( $jetpack->sync, 'comments' ), $args );
63 }
64
65 /**
66 * @param string $file __FILE__
67 * @param string $option, Option name to sync
68 * @param string $option ...
69 */
70 static function sync_options( $file, $option /*, $option, ... */ ) {
71 if ( is_network_admin() ) return;
72 $jetpack = Jetpack::init();
73 $args = func_get_args();
74 return call_user_func_array( array( $jetpack->sync, 'options' ), $args );
75 }
76 /**
77 * @param string $file __FILE__
78 * @param string $option, Option name to sync
79 * @param string $option ...
80 */
81 static function sync_constant( $file, $constant ) {
82 if ( is_network_admin() ) return;
83 $jetpack = Jetpack::init();
84 $args = func_get_args();
85 return call_user_func_array( array( $jetpack->sync, 'constant' ), $args );
86 }
87
88 /* Internal Methods */
89
90 /**
91 * Create a sync object/request
92 *
93 * @param string $object Type of object to sync -- [ post | comment | option ]
94 * @param int $id Unique identifier
95 * @param array $settings
96 */
97 function register( $object, $id = false, array $settings = null ) {
98 // Since we've registered something for sync, hook it up to execute on shutdown if we haven't already
99 if ( !$this->sync ) {
100 if ( function_exists( 'ignore_user_abort' ) ) {
101 ignore_user_abort( true );
102 }
103 add_action( 'shutdown', array( $this, 'sync' ), 9 ); // Right before async XML-RPC
104 }
105
106 $defaults = array(
107 'on_behalf_of' => array(), // What modules want this data
108 );
109 $settings = wp_parse_args( $settings, $defaults );
110
111 if ( !isset( $this->sync[$object] ) ) {
112 $this->sync[$object] = array();
113 }
114
115 // Store the settings for this object
116 if (
117 // First time for this object
118 !isset( $this->sync[$object][$id] )
119 ) {
120 // Easy: store the current settings
121 $this->sync[$object][$id] = $settings;
122 } else {
123 // Not as easy: we have to manually merge the settings from previous runs for this object with the settings for this run
124
125 $this->sync[$object][$id]['on_behalf_of'] = array_unique( array_merge( $this->sync[$object][$id]['on_behalf_of'], $settings['on_behalf_of'] ) );
126 }
127
128 $delete_prefix = 'delete_';
129 if ( 0 === strpos( $object, $delete_prefix ) ) {
130 $unset_object = substr( $object, strlen( $delete_prefix ) );
131 } else {
132 $unset_object = "{$delete_prefix}{$object}";
133 }
134
135 // Ensure post ... delete_post yields a delete operation
136 // Ensure delete_post ... post yields a sync post operation
137 // Ensure update_option() ... delete_option() ends up as a delete
138 // Ensure delete_option() ... update_option() ends up as an update
139 // Etc.
140 unset( $this->sync[$unset_object][$id] );
141
142 return true;
143 }
144
145 function get_common_sync_data() {
146 $available_modules = Jetpack::get_available_modules();
147 $active_modules = Jetpack::get_active_modules();
148 $modules = array();
149 foreach ( $available_modules as $available_module ) {
150 $modules[$available_module] = in_array( $available_module, $active_modules );
151 }
152 $modules['vaultpress'] = class_exists( 'VaultPress' ) || function_exists( 'vaultpress_contact_service' );
153
154 $sync_data = array(
155 'modules' => $modules,
156 'version' => JETPACK__VERSION,
157 'is_multisite' => is_multisite(),
158 );
159
160 return $sync_data;
161 }
162
163 /**
164 * Set up all the data and queue it for the outgoing XML-RPC request
165 */
166 function sync() {
167 if ( !$this->sync ) {
168 return false;
169 }
170
171 // Don't sync anything while in development mode
172 if ( Jetpack::is_development_mode() ) {
173 return false;
174 }
175
176 $sync_data = $this->get_common_sync_data();
177
178 $wp_importing = defined( 'WP_IMPORTING' ) && WP_IMPORTING;
179
180 foreach ( $this->sync as $sync_operation_type => $sync_operations ) {
181 switch ( $sync_operation_type ) {
182 case 'post':
183 if ( $wp_importing ) {
184 break;
185 }
186
187 $global_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;
188 $GLOBALS['post'] = null;
189 foreach ( $sync_operations as $post_id => $settings ) {
190 $sync_data['post'][$post_id] = $this->get_post( $post_id );
191 if ( isset( $this->post_transitions[$post_id] ) ) {
192 $sync_data['post'][$post_id]['transitions'] = $this->post_transitions[$post_id];
193 } else {
194 $sync_data['post'][$post_id]['transitions'] = array( false, false );
195 }
196 $sync_data['post'][$post_id]['on_behalf_of'] = $settings['on_behalf_of'];
197 }
198 $GLOBALS['post'] = $global_post;
199 unset( $global_post );
200 break;
201 case 'comment':
202 if ( $wp_importing ) {
203 break;
204 }
205
206 $global_comment = isset( $GLOBALS['comment'] ) ? $GLOBALS['comment'] : null;
207 unset( $GLOBALS['comment'] );
208 foreach ( $sync_operations as $comment_id => $settings ) {
209 $sync_data['comment'][$comment_id] = $this->get_comment( $comment_id );
210 if ( isset( $this->comment_transitions[$comment_id] ) ) {
211 $sync_data['comment'][$comment_id]['transitions'] = $this->comment_transitions[$comment_id];
212 } else {
213 $sync_data['comment'][$comment_id]['transitions'] = array( false, false );
214 }
215 $sync_data['comment'][$comment_id]['on_behalf_of'] = $settings['on_behalf_of'];
216 }
217 $GLOBALS['comment'] = $global_comment;
218 unset( $global_comment );
219 break;
220 case 'option' :
221 foreach ( $sync_operations as $option => $settings ) {
222 $sync_data['option'][ $option ] = array( 'value' => get_option( $option ) );
223 }
224 break;
225
226 case 'constant' :
227 foreach( $sync_operations as $constant => $settings ) {
228 $sync_data['constant'][ $constant ] = array( 'value' => $this->get_constant( $constant ) );
229 }
230 break;
231
232 case 'delete_post':
233 case 'delete_comment':
234 foreach ( $sync_operations as $object_id => $settings ) {
235 $sync_data[$sync_operation_type][$object_id] = array( 'on_behalf_of' => $settings['on_behalf_of'] );
236 }
237 break;
238 case 'delete_option' :
239 foreach ( $sync_operations as $object_id => $settings ) {
240 $sync_data[$sync_operation_type][$object_id] = true;
241 }
242 break;
243 }
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 )
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
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
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 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
387 foreach ( $module_conditions as $module => $conditions ) {
388 if ( !in_array( $post->post_type, $conditions['post_types'] ) ) {
389 continue;
390 }
391
392 $deleted_post = in_array( $new_status, $delete_stati );
393
394 if ( $deleted_post ) {
395 $delete_on_behalf_of[] = $module;
396 } else {
397 clean_post_cache( $post->ID );
398 $new_status = get_post_status( $post->ID ); // Inherited status is resolved here
399 }
400
401 $old_status_in_stati = in_array( $old_status, $conditions['post_stati'] );
402 $new_status_in_stati = in_array( $new_status, $conditions['post_stati'] );
403
404 if ( $old_status_in_stati && !$new_status_in_stati ) {
405 // Jetpack no longer needs the post
406 if ( !$deleted_post ) {
407 $delete_on_behalf_of[] = $module;
408 } // else, we've already flagged it above
409 continue;
410 }
411
412 if ( !$new_status_in_stati ) {
413 continue;
414 }
415
416 // At this point, we know we want to sync the post, not delete it
417 $submit_on_behalf_of[] = $module;
418 }
419
420 if ( !empty( $submit_on_behalf_of ) ) {
421 return array( 'operation' => 'submit', 'on_behalf_of' => $submit_on_behalf_of );
422 }
423
424 if ( !empty( $delete_on_behalf_of ) ) {
425 return array( 'operation' => 'delete', 'on_behalf_of' => $delete_on_behalf_of );
426 }
427
428 return false;
429 }
430
431 /**
432 * Get a post and associated data in the standard JP format.
433 * Cannot be called statically
434 *
435 * @param int $id Post ID
436 * @return Array containing full post details
437 */
438 function get_post( $id ) {
439 $post_obj = get_post( $id );
440 if ( !$post_obj )
441 return false;
442
443 if ( is_callable( $post_obj, 'to_array' ) ) {
444 // WP >= 3.5
445 $post = $post_obj->to_array();
446 } else {
447 // WP < 3.5
448 $post = get_object_vars( $post_obj );
449 }
450
451 if ( 0 < strlen( $post['post_password'] ) ) {
452 $post['post_password'] = 'auto-' . wp_generate_password( 10, false ); // We don't want the real password. Just pass something random.
453 }
454
455 // local optimizations
456 unset(
457 $post['filter'],
458 $post['ancestors'],
459 $post['post_content_filtered'],
460 $post['to_ping'],
461 $post['pinged']
462 );
463
464 if ( $this->is_post_public( $post ) ) {
465 $post['post_is_public'] = Jetpack_Options::get_option( 'public' );
466 } else {
467 //obscure content
468 $post['post_content'] = '';
469 $post['post_excerpt'] = '';
470 $post['post_is_public'] = false;
471 }
472 $post_type_obj = get_post_type_object( $post['post_type'] );
473 $post['post_is_excluded_from_search'] = $post_type_obj->exclude_from_search;
474
475 $post['tax'] = array();
476 $taxonomies = get_object_taxonomies( $post_obj );
477 foreach ( $taxonomies as $taxonomy ) {
478 $terms = get_object_term_cache( $post_obj->ID, $taxonomy );
479 if ( empty( $terms ) )
480 $terms = wp_get_object_terms( $post_obj->ID, $taxonomy );
481 $term_names = array();
482 foreach ( $terms as $term ) {
483 $term_names[] = $term->name;
484 }
485 $post['tax'][$taxonomy] = $term_names;
486 }
487
488 $meta = get_post_meta( $post_obj->ID, false );
489 $post['meta'] = array();
490 foreach ( $meta as $key => $value ) {
491 $post['meta'][$key] = array_map( 'maybe_unserialize', $value );
492 }
493
494 $post['extra'] = array(
495 'author' => get_the_author_meta( 'display_name', $post_obj->post_author ),
496 'author_email' => get_the_author_meta( 'email', $post_obj->post_author ),
497 );
498
499 if ( $fid = get_post_thumbnail_id( $id ) ) {
500 $feature = wp_get_attachment_image_src( $fid, 'large' );
501 if ( ! empty( $feature[0] ) ) {
502 $post['extra']['featured_image'] = $feature[0];
503 }
504
505 $attachment = get_post( $fid );
506 if ( ! empty( $attachment ) ) {
507 $metadata = wp_get_attachment_metadata( $fid );
508
509 $post['extra']['post_thumbnail'] = array(
510 'ID' => (int) $fid,
511 'URL' => (string) wp_get_attachment_url( $fid ),
512 'guid' => (string) $attachment->guid,
513 'mime_type' => (string) $attachment->post_mime_type,
514 'width' => (int) isset( $metadata['width'] ) ? $metadata['width'] : 0,
515 'height' => (int) isset( $metadata['height'] ) ? $metadata['height'] : 0,
516 );
517
518 if ( isset( $metadata['duration'] ) ) {
519 $post['extra']['post_thumbnail'] = (int) $metadata['duration'];
520 }
521
522 $post['extra']['post_thumbnail'] = (object) apply_filters( 'get_attachment', $post['extra']['post_thumbnail'] );
523 }
524 }
525
526 $post['permalink'] = get_permalink( $post_obj->ID );
527 $post['shortlink'] = wp_get_shortlink( $post_obj->ID );
528 $post['module_custom_data'] = apply_filters( 'jetpack_sync_post_module_custom_data', array(), $post_obj );
529 return $post;
530 }
531
532 /**
533 * Decide whether a post/page/attachment is visible to the public.
534 *
535 * @param array $post
536 * @return bool
537 */
538 function is_post_public( $post ) {
539 if ( !is_array( $post ) ) {
540 $post = (array) $post;
541 }
542
543 if ( 0 < strlen( $post['post_password'] ) )
544 return false;
545 if ( ! in_array( $post['post_type'], get_post_types( array( 'public' => true ) ) ) )
546 return false;
547 $post_status = get_post_status( $post['ID'] ); // Inherited status is resolved here.
548 if ( ! in_array( $post_status, get_post_stati( array( 'public' => true ) ) ) )
549 return false;
550 return true;
551 }
552
553 /* Comments Sync */
554
555 function comments( $file, array $settings = null ) {
556 $module_slug = Jetpack::get_module_slug( $file );
557
558 $defaults = array(
559 'post_types' => array( 'post', 'page' ), // For what post types will we sync comments?
560 'post_stati' => array( 'publish' ), // For what post stati will we sync comments?
561 'comment_types' => array( '', 'comment', 'trackback', 'pingback' ), // What comment types will we sync?
562 'comment_stati' => array( 'approved' ), // What comment stati will we sync?
563 );
564
565 $settings = wp_parse_args( $settings, $defaults );
566
567 $this->sync_conditions['comments'][$module_slug] = $settings;
568
569 add_action( 'wp_insert_comment', array( $this, 'wp_insert_comment_action' ), 10, 2 );
570 add_action( 'transition_comment_status', array( $this, 'transition_comment_status_action' ), 10, 3 );
571 add_action( 'edit_comment', array( $this, 'edit_comment_action' ) );
572 }
573
574 /*
575 * This is really annoying. If you edit a comment, but don't change the status, WordPress doesn't fire the transition_comment_status hook.
576 * 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.
577 */
578 function edit_comment_action( $comment_id ) {
579 $comment = get_comment( $comment_id );
580 $new_status = $this->translate_comment_status( $comment->comment_approved );
581 add_action( "comment_{$new_status}_{$comment->comment_type}", array( $this, 'transition_comment_status_for_comments_whose_status_does_not_change' ), 10, 2 );
582 }
583
584 function wp_insert_comment_action( $comment_id, $comment ) {
585 $this->transition_comment_status_action( $comment->comment_approved, 'new', $comment );
586 }
587
588 function transition_comment_status_for_comments_whose_status_does_not_change( $comment_id, $comment ) {
589 if ( isset( $this->comment_transitions[$comment_id] ) ) {
590 return $this->transition_comment_status_action( $comment->comment_approved, $this->comment_transitions[$comment_id][1], $comment );
591 }
592
593 return $this->transition_comment_status_action( $comment->comment_approved, $comment->comment_approved, $comment );
594 }
595
596 function translate_comment_status( $status ) {
597 switch ( (string) $status ) {
598 case '0' :
599 case 'hold' :
600 return 'unapproved';
601 case '1' :
602 case 'approve' :
603 return 'approved';
604 }
605
606 return $status;
607 }
608
609 function transition_comment_status_action( $new_status, $old_status, $comment ) {
610 $post = get_post( $comment->comment_post_ID );
611 if ( !$post ) {
612 return false;
613 }
614
615 foreach ( array( 'new_status', 'old_status' ) as $_status ) {
616 $$_status = $this->translate_comment_status( $$_status );
617 }
618
619 // Track comment transitions
620 if ( isset( $this->comment_transitions[$comment->comment_ID] ) ) {
621 // status changed more than once - keep tha most recent $new_status
622 $this->comment_transitions[$comment->comment_ID][0] = $new_status;
623 } else {
624 $this->comment_transitions[$comment->comment_ID] = array( $new_status, $old_status );
625 }
626
627 $post_sync = $this->get_post_sync_operation( $post->post_status, '_jetpack_test_sync', $post, $this->sync_conditions['comments'] );
628
629 if ( !$post_sync ) {
630 // No module wants to sync this comment because its post doesn't match any sync conditions
631 return false;
632 }
633
634 if ( 'delete' == $post_sync['operation'] ) {
635 // Had we been looking at post sync operations (instead of comment sync operations),
636 // this comment's post would have been deleted. Don't sync the comment.
637 return false;
638 }
639
640 $delete_on_behalf_of = array();
641 $submit_on_behalf_of = array();
642 $delete_stati = array( 'delete' );
643
644 foreach ( $this->sync_conditions['comments'] as $module => $conditions ) {
645 if ( !in_array( $comment->comment_type, $conditions['comment_types'] ) ) {
646 continue;
647 }
648
649 $deleted_comment = in_array( $new_status, $delete_stati );
650
651 if ( $deleted_comment ) {
652 $delete_on_behalf_of[] = $module;
653 }
654
655 $old_status_in_stati = in_array( $old_status, $conditions['comment_stati'] );
656 $new_status_in_stati = in_array( $new_status, $conditions['comment_stati'] );
657
658 if ( $old_status_in_stati && !$new_status_in_stati ) {
659 // Jetpack no longer needs the comment
660 if ( !$deleted_comment ) {
661 $delete_on_behalf_of[] = $module;
662 } // else, we've already flagged it above
663 continue;
664 }
665
666 if ( !$new_status_in_stati ) {
667 continue;
668 }
669
670 // At this point, we know we want to sync the comment, not delete it
671 $submit_on_behalf_of[] = $module;
672 }
673
674 if ( ! empty( $submit_on_behalf_of ) ) {
675 $this->register_post( $comment->comment_post_ID, array( 'on_behalf_of' => $submit_on_behalf_of ) );
676 return $this->register_comment( $comment->comment_ID, array( 'on_behalf_of' => $submit_on_behalf_of ) );
677 }
678
679 if ( !empty( $delete_on_behalf_of ) ) {
680 return $this->register( 'delete_comment', $comment->comment_ID, array( 'on_behalf_of' => $delete_on_behalf_of ) );
681 }
682
683 return false;
684 }
685
686 /**
687 * Get a comment and associated data in the standard JP format.
688 * Cannot be called statically
689 *
690 * @param int $id Comment ID
691 * @return Array containing full comment details
692 */
693 function get_comment( $id ) {
694 $comment_obj = get_comment( $id );
695 if ( !$comment_obj )
696 return false;
697 $comment = get_object_vars( $comment_obj );
698
699 $meta = get_comment_meta( $id, false );
700 $comment['meta'] = array();
701 foreach ( $meta as $key => $value ) {
702 $comment['meta'][$key] = array_map( 'maybe_unserialize', $value );
703 }
704
705 return $comment;
706 }
707
708 /* Options Sync */
709
710 /* Ah... so much simpler than Posts and Comments :) */
711 function options( $file, $option /*, $option, ... */ ) {
712 $options = func_get_args();
713 $file = array_shift( $options );
714
715 $module_slug = Jetpack::get_module_slug( $file );
716
717 if ( !isset( $this->sync_options[$module_slug] ) ) {
718 $this->sync_options[$module_slug] = array();
719 }
720
721 foreach ( $options as $option ) {
722 $this->sync_options[$module_slug][] = $option;
723 add_action( "delete_option_{$option}", array( $this, 'deleted_option_action' ) );
724 add_action( "update_option_{$option}", array( $this, 'updated_option_action' ) );
725 add_action( "add_option_{$option}", array( $this, 'added_option_action' ) );
726 }
727
728 $this->sync_options[$module_slug] = array_unique( $this->sync_options[$module_slug] );
729 }
730
731 function deleted_option_action( $option ) {
732 $this->register( 'delete_option', $option );
733 }
734
735 function updated_option_action( $old_value ) {
736 // The value of $option isn't passed to the filter
737 // Calculate it
738 $option = current_filter();
739 $prefix = 'update_option_';
740 if ( 0 !== strpos( $option, $prefix ) ) {
741 return;
742 }
743 $option = substr( $option, strlen( $prefix ) );
744
745 $this->added_option_action( $option );
746 }
747
748 function added_option_action( $option ) {
749 $this->register( 'option', $option );
750 }
751
752 function sync_all_module_options( $module_slug ) {
753 if ( empty( $this->sync_options[$module_slug] ) ) {
754 return;
755 }
756
757 foreach ( $this->sync_options[$module_slug] as $option ) {
758 $this->added_option_action( $option );
759 }
760 }
761
762 function sync_all_registered_options( $options = array() ) {
763 if ( 'jetpack_sync_all_registered_options' == current_filter() ) {
764 add_action( 'shutdown', array( $this, 'register_all_options' ), 8 );
765 } else {
766 wp_schedule_single_event( time(), 'jetpack_sync_all_registered_options', array( $this->sync_options ) );
767 }
768 }
769
770 /**
771 * All the options that are defined in modules as well as class.jetpack.php will get synced.
772 * Registers all options to be synced.
773 */
774 function register_all_options() {
775 $all_registered_options = array_unique( call_user_func_array( 'array_merge', $this->sync_options ) );
776 foreach ( $all_registered_options as $option ) {
777 $this->added_option_action( $option );
778 }
779 }
780
781 /* Constants Sync */
782
783 function sync_all_constants() {
784 // list of contants to sync needed by Jetpack
785 $constants = array(
786 'EMPTY_TRASH_DAYS',
787 'WP_POST_REVISIONS',
788 'UPDATER_DISABLED',
789 'AUTOMATIC_UPDATER_DISABLED',
790 'ABSPATH',
791 'WP_CONTENT_DIR'
792 );
793
794 // add the constant to sync.
795 foreach( $constants as $contant ) {
796 $this->register_constant( $contant );
797 }
798
799 add_action( 'shutdown', array( $this, 'register_all_module_constants' ), 8 );
800
801 }
802
803 function register_all_module_constants() {
804 // also add the contstants from each module to be synced.
805 foreach( $this->sync_constants as $module ) {
806 foreach( $module as $constant ) {
807 $this->register_constant( $constant );
808 }
809 }
810 }
811
812 /**
813 * Sync constants required by the module that was just activated.
814 * If you add Jetpack_Sync::sync_constant( __FILE__, 'HELLO_WORLD' );
815 * to the module it will start syncing the constant after the constant has been updated.
816 *
817 * This function gets called on module activation.
818 */
819 function sync_module_constants( $module ) {
820
821 if ( isset( $this->sync_constants[ $module ] ) && is_array( $this->sync_constants[ $module ] ) ) {
822 // also add the contstants from each module to be synced.
823 foreach( $this->sync_constants[ $module ] as $constant ) {
824 $this->register_constant( $constant );
825 }
826 }
827 }
828
829 public function reindex_needed() {
830 return ( $this->_get_post_count_local() != $this->_get_post_count_cloud() );
831 }
832
833 public function reindex_trigger() {
834 $response = array( 'status' => 'ERROR' );
835
836 // Force a privacy check
837 Jetpack::check_privacy( JETPACK__PLUGIN_FILE );
838
839 Jetpack::load_xml_rpc_client();
840 $client = new Jetpack_IXR_Client( array(
841 'user_id' => JETPACK_MASTER_USER,
842 ) );
843
844 $client->query( 'jetpack.reindexTrigger' );
845
846 if ( !$client->isError() ) {
847 $response = $client->getResponse();
848 Jetpack_Options::update_option( 'sync_bulk_reindexing', true );
849 }
850
851 return $response;
852 }
853
854 public function reindex_status() {
855 $response = array( 'status' => 'ERROR' );
856
857 // Assume reindexing is done if it was not triggered in the first place
858 if ( false === Jetpack_Options::get_option( 'sync_bulk_reindexing' ) ) {
859 return array( 'status' => 'DONE' );
860 }
861
862 Jetpack::load_xml_rpc_client();
863 $client = new Jetpack_IXR_Client( array(
864 'user_id' => JETPACK_MASTER_USER,
865 ) );
866
867 $client->query( 'jetpack.reindexStatus' );
868
869 if ( !$client->isError() ) {
870 $response = $client->getResponse();
871 if ( 'DONE' == $response['status'] ) {
872 Jetpack_Options::delete_option( 'sync_bulk_reindexing' );
873 }
874 }
875
876 return $response;
877 }
878
879 public function reindex_ui() {
880 $strings = json_encode( array(
881 'WAITING' => array(
882 'action' => __( 'Refresh Status', 'jetpack' ),
883 'status' => __( 'Indexing request queued and waiting&hellip;', 'jetpack' ),
884 ),
885 'INDEXING' => array(
886 'action' => __( 'Refresh Status', 'jetpack' ),
887 'status' => __( 'Indexing posts', 'jetpack' ),
888 ),
889 'DONE' => array(
890 'action' => __( 'Reindex Posts', 'jetpack' ),
891 'status' => __( 'Posts indexed.', 'jetpack' ),
892 ),
893 'ERROR' => array(
894 'action' => __( 'Refresh Status', 'jetpack' ),
895 'status' => __( 'Status unknown.', 'jetpack' ),
896 ),
897 'ERROR:LARGE' => array(
898 'action' => __( 'Refresh Status', 'jetpack' ),
899 'status' => __( 'This site is too large, please contact Jetpack support to sync.', 'jetpack' ),
900 ),
901 ) );
902
903 wp_enqueue_script(
904 'jetpack_sync_reindex_control',
905 plugins_url( '_inc/jquery.jetpack-sync.js', JETPACK__PLUGIN_FILE ),
906 array( 'jquery' ),
907 JETPACK__VERSION
908 );
909
910 $template = <<<EOT
911 <p class="jetpack_sync_reindex_control" id="jetpack_sync_reindex_control" data-strings="%s">
912 <input type="submit" class="jetpack_sync_reindex_control_action button" value="%s" disabled />
913 <span class="jetpack_sync_reindex_control_status">&hellip;</span>
914 </p>
915 EOT;
916
917 return sprintf(
918 $template,
919 esc_attr( $strings ),
920 esc_attr__( 'Refresh Status', 'jetpack' )
921 );
922 }
923
924 private function _get_post_count_local() {
925 global $wpdb;
926 return (int) $wpdb->get_var(
927 "SELECT count(*)
928 FROM {$wpdb->posts}
929 WHERE post_status = 'publish' AND post_password = ''"
930 );
931 }
932
933 private function _get_post_count_cloud() {
934 $blog_id = Jetpack::init()->get_option( 'id' );
935
936 $body = array(
937 'size' => 1,
938 );
939
940 $response = wp_remote_post(
941 "https://public-api.wordpress.com/rest/v1/sites/$blog_id/search",
942 array(
943 'timeout' => 10,
944 'user-agent' => 'jetpack_related_posts',
945 'sslverify' => true,
946 'body' => $body,
947 )
948 );
949
950 if ( is_wp_error( $response ) ) {
951 return 0;
952 }
953
954 $results = json_decode( wp_remote_retrieve_body( $response ), true );
955
956 return (int) $results['results']['total'];
957 }
958
959 /**
960 * Sometimes we need to fake options to be able to sync data with .com
961 * This is a helper function. That will make it easier to do just that.
962 *
963 * It will make sure that the options are synced when do_action( 'jetpack_sync_all_registered_options' );
964 *
965 * Which should happen everytime we update Jetpack to a new version or daily by Jetpack_Heartbeat.
966 *
967 * $callback is a function that is passed into a filter that returns the value of the option.
968 * This value should never be false. Since we want to short circuit the get_option function
969 * to return the value of the our callback.
970 *
971 * You can also trigger an update when a something else changes by calling the
972 * do_action( 'add_option_jetpack_' . $option, 'jetpack_'.$option, $callback_function );
973 * on the action that should that would trigger the update.
974 *
975 *
976 * @param string $option Option will always be prefixed with Jetpack and be saved on .com side
977 * @param string or array $callback
978 */
979 function mock_option( $option , $callback ) {
980
981 add_filter( 'pre_option_jetpack_'. $option, $callback );
982 // This shouldn't happen but if it does we return the same as before.
983 add_filter( 'option_jetpack_'. $option, $callback );
984 // Instead of passing a file we just pass in a string.
985 $this->options( 'mock-option' , 'jetpack_' . $option );
986
987 }
988 /**
989 * Sometimes you need to sync constants to .com
990 * Using the function will allow you to do just that.
991 *
992 * @param 'string' $constant Constants defined in code.
993 *
994 */
995 function register_constant( $constant ) {
996 $this->register( 'constant', $constant );
997 }
998 /**
999 * Simular to $this->options() function.
1000 * Add the constant to be synced to .com when we activate the module.
1001 * As well as on heartbeat and plugin upgrade and connection to .com.
1002 *
1003 * @param string $file
1004 * @param string $constant
1005 */
1006 function constant( $file, $constant ) {
1007 $constants = func_get_args();
1008 $file = array_shift( $constants );
1009
1010 $module_slug = Jetpack::get_module_slug( $file );
1011
1012 if ( ! isset( $this->sync_constants[ $module_slug ] ) ) {
1013 $this->sync_constants[ $module_slug ] = array();
1014 }
1015
1016 foreach ( $constants as $constant ) {
1017 $this->sync_constants[ $module_slug ][] = $constant;
1018 }
1019 }
1020
1021 /**
1022 * Helper function to return the constants value.
1023 *
1024 * @param string $constant
1025 * @return value of the constant or null if the constant is set to false or doesn't exits.
1026 */
1027 static function get_constant( $constant ) {
1028 if ( defined( $constant ) ) {
1029 return constant( $constant );
1030 }
1031
1032 return null;
1033 }
1034 }
1035