Completed
Push — update/do-not-require-gzencode ( 1df69c...cb5460 )
by
unknown
29:18 queued 20:14
created

sync/class.jetpack-sync-wp-replicastore.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
require_once dirname( __FILE__ ) . '/interface.jetpack-sync-replicastore.php';
4
require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php';
5
6
/**
7
 * An implementation of iJetpack_Sync_Replicastore which returns data stored in a WordPress.org DB.
8
 * This is useful to compare values in the local WP DB to values in the synced replica store
9
 */
10
class Jetpack_Sync_WP_Replicastore implements iJetpack_Sync_Replicastore {
11
12
13
	public function reset() {
14
		global $wpdb;
15
16
		$wpdb->query( "DELETE FROM $wpdb->posts" );
17
		$wpdb->query( "DELETE FROM $wpdb->comments" );
18
19
		// also need to delete terms from cache
20
		$term_ids = $wpdb->get_col( "SELECT term_id FROM $wpdb->terms" );
21
		foreach ( $term_ids as $term_id ) {
22
			wp_cache_delete( $term_id, 'terms' );
23
		}
24
25
		$wpdb->query( "DELETE FROM $wpdb->terms" );
26
27
		$wpdb->query( "DELETE FROM $wpdb->term_taxonomy" );
28
		$wpdb->query( "DELETE FROM $wpdb->term_relationships" );
29
30
		// callables and constants
31
		$wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'jetpack_%'" );
32
		$wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%'" );
33
	}
34
35
	function full_sync_start( $config ) {
36
		$this->reset();
37
	}
38
39
	function full_sync_end( $checksum ) {
40
		// noop right now
41
	}
42
43 View Code Duplication
	public function post_count( $status = null, $min_id = null, $max_id = null ) {
44
		global $wpdb;
45
46
		$where = '';
47
48
		if ( $status ) {
49
			$where = "post_status = '" . esc_sql( $status ) . "'";
50
		} else {
51
			$where = '1=1';
52
		}
53
54
		if ( null != $min_id ) {
55
			$where .= ' AND ID >= ' . intval( $min_id );
56
		}
57
58
		if ( null != $max_id ) {
59
			$where .= ' AND ID <= ' . intval( $max_id );
60
		}
61
62
		return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE $where" );
63
	}
64
65
	// TODO: actually use max_id/min_id
66
	public function get_posts( $status = null, $min_id = null, $max_id = null ) {
67
		$args = array( 'orderby' => 'ID', 'posts_per_page' => -1 );
68
69
		if ( $status ) {
70
			$args['post_status'] = $status;
71
		} else {
72
			$args['post_status'] = 'any';
73
		}
74
75
		return get_posts( $args );
76
	}
77
78
	public function get_post( $id ) {
79
		return get_post( $id );
80
	}
81
82
	public function upsert_post( $post, $silent = false ) {
83
		global $wpdb;
84
85
		// reject the post if it's not a WP_Post
86
		if ( ! $post instanceof WP_Post ) {
87
			return;
88
		}
89
90
		$post = $post->to_array();
91
92
		// reject posts without an ID
93
		if ( ! isset( $post['ID'] ) ) {
94
			return;
95
		}
96
97
		$now     = current_time( 'mysql' );
98
		$now_gmt = get_gmt_from_date( $now );
99
100
		$defaults = array(
101
			'ID'                    => 0,
102
			'post_author'           => '0',
103
			'post_content'          => '',
104
			'post_content_filtered' => '',
105
			'post_title'            => '',
106
			'post_name'             => '',
107
			'post_excerpt'          => '',
108
			'post_status'           => 'draft',
109
			'post_type'             => 'post',
110
			'comment_status'        => 'closed',
111
			'comment_count'         => '0',
112
			'ping_status'           => '',
113
			'post_password'         => '',
114
			'to_ping'               => '',
115
			'pinged'                => '',
116
			'post_parent'           => 0,
117
			'menu_order'            => 0,
118
			'guid'                  => '',
119
			'post_date'             => $now,
120
			'post_date_gmt'         => $now_gmt,
121
			'post_modified'         => $now,
122
			'post_modified_gmt'     => $now_gmt,
123
		);
124
125
		$post = array_intersect_key( $post, $defaults );
126
127
		$post = sanitize_post( $post, 'db' );
128
129
		unset( $post['filter'] );
130
131
		$exists = $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM $wpdb->posts WHERE ID = %d )", $post['ID'] ) );
132
133
		if ( $exists ) {
134
			$wpdb->update( $wpdb->posts, $post, array( 'ID' => $post['ID'] ) );
135
		} else {
136
			$wpdb->insert( $wpdb->posts, $post );
137
		}
138
139
		clean_post_cache( $post['ID'] );
140
	}
141
142
	public function delete_post( $post_id ) {
143
		wp_delete_post( $post_id, true );
144
	}
145
146
	public function posts_checksum( $min_id = null, $max_id = null ) {
147
		global $wpdb;
148
		return $this->table_checksum( $wpdb->posts, Jetpack_Sync_Defaults::$default_post_checksum_columns , 'ID', Jetpack_Sync_Settings::get_blacklisted_post_types_sql(), $min_id, $max_id );
149
	}
150
151
	public function post_meta_checksum( $min_id = null, $max_id = null ) {
152
		global $wpdb;
153
		return $this->table_checksum( $wpdb->postmeta, Jetpack_Sync_Defaults::$default_post_meta_checksum_columns , 'meta_id', Jetpack_Sync_Settings::get_whitelisted_post_meta_sql(), $min_id, $max_id );
154
	}
155
156 View Code Duplication
	public function comment_count( $status = null, $min_id = null, $max_id = null ) {
157
		global $wpdb;
158
159
		$comment_approved = $this->comment_status_to_approval_value( $status );
160
161
		if ( $comment_approved !== false ) {
162
			$where = "comment_approved = '" . esc_sql( $comment_approved ) . "'";
163
		} else {
164
			$where = '1=1';
165
		}
166
167
		if ( $min_id != null ) {
168
			$where .= ' AND comment_ID >= ' . intval( $min_id );
169
		}
170
171
		if ( $max_id != null ) {
172
			$where .= ' AND comment_ID <= ' . intval( $max_id );
173
		}
174
175
		return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE $where" );
176
	}
177
178
	private function comment_status_to_approval_value( $status ) {
179
		switch ( $status ) {
180
			case 'approve':
181
				return '1';
182
			case 'hold':
183
				return '0';
184
			case 'spam':
185
				return 'spam';
186
			case 'trash':
187
				return 'trash';
188
			case 'any':
189
				return false;
190
			case 'all':
191
				return false;
192
			default:
193
				return false;
194
		}
195
	}
196
197
	// TODO: actually use max_id/min_id
198
	public function get_comments( $status = null, $min_id = null, $max_id = null ) {
199
		$args = array( 'orderby' => 'ID', 'status' => 'all' );
200
201
		if ( $status ) {
202
			$args['status'] = $status;
203
		}
204
205
		return get_comments( $args );
206
	}
207
208
	public function get_comment( $id ) {
209
		return WP_Comment::get_instance( $id );
210
	}
211
212
	public function upsert_comment( $comment ) {
213
		global $wpdb, $wp_version;
214
215
		if ( version_compare( $wp_version, '4.4', '<' ) ) {
216
			$comment = (array) $comment;
217
		} else {
218
			// WP 4.4 introduced the WP_Comment Class
219
			$comment = $comment->to_array();
220
		}
221
222
		// filter by fields on comment table
223
		$comment_fields_whitelist = array(
224
			'comment_ID',
225
			'comment_post_ID',
226
			'comment_author',
227
			'comment_author_email',
228
			'comment_author_url',
229
			'comment_author_IP',
230
			'comment_date',
231
			'comment_date_gmt',
232
			'comment_content',
233
			'comment_karma',
234
			'comment_approved',
235
			'comment_agent',
236
			'comment_type',
237
			'comment_parent',
238
			'user_id',
239
		);
240
241
		foreach ( $comment as $key => $value ) {
242
			if ( ! in_array( $key, $comment_fields_whitelist ) ) {
243
				unset( $comment[ $key ] );
244
			}
245
		}
246
247
		$exists = $wpdb->get_var(
248
			$wpdb->prepare(
249
				"SELECT EXISTS( SELECT 1 FROM $wpdb->comments WHERE comment_ID = %d )",
250
				$comment['comment_ID']
251
			)
252
		);
253
254
		if ( $exists ) {
255
			$wpdb->update( $wpdb->comments, $comment, array( 'comment_ID' => $comment['comment_ID'] ) );
256
		} else {
257
			$wpdb->insert( $wpdb->comments, $comment );
258
		}
259
260
		wp_update_comment_count( $comment['comment_post_ID'] );
261
	}
262
263
	public function trash_comment( $comment_id ) {
264
		wp_delete_comment( $comment_id );
265
	}
266
267
	public function delete_comment( $comment_id ) {
268
		wp_delete_comment( $comment_id, true );
269
	}
270
271
	public function spam_comment( $comment_id ) {
272
		wp_spam_comment( $comment_id );
273
	}
274
275
	public function trashed_post_comments( $post_id, $statuses ) {
276
		wp_trash_post_comments( $post_id );
277
	}
278
279
	public function untrashed_post_comments( $post_id ) {
280
		wp_untrash_post_comments( $post_id );
281
	}
282
283
	public function comments_checksum( $min_id = null, $max_id = null ) {
284
		global $wpdb;
285
		return $this->table_checksum( $wpdb->comments, Jetpack_Sync_Defaults::$default_comment_checksum_columns, 'comment_ID', Jetpack_Sync_Settings::get_comments_filter_sql(), $min_id, $max_id );
286
	}
287
288
	public function comment_meta_checksum( $min_id = null, $max_id = null ) {
289
		global $wpdb;
290
		return $this->table_checksum( $wpdb->commentmeta, Jetpack_Sync_Defaults::$default_comment_meta_checksum_columns , 'meta_id', Jetpack_Sync_Settings::get_whitelisted_comment_meta_sql(), $min_id, $max_id );
291
	}
292
293
	public function options_checksum() {
294
		global $wpdb;
295
296
		$options_whitelist = "'" . implode( "', '", Jetpack_Sync_Defaults::$default_options_whitelist ) . "'";
297
		$where_sql = "option_name IN ( $options_whitelist )";
298
299
		return $this->table_checksum( $wpdb->options, Jetpack_Sync_Defaults::$default_option_checksum_columns, null, $where_sql, null, null );
300
	}
301
302
303
	public function update_option( $option, $value ) {
304
		return update_option( $option, $value );
305
	}
306
307
	public function get_option( $option, $default = false ) {
308
		return get_option( $option, $default );
309
	}
310
311
	public function delete_option( $option ) {
312
		return delete_option( $option );
313
	}
314
315
	public function set_theme_support( $theme_support ) {
316
		// noop
317
	}
318
319
	public function current_theme_supports( $feature ) {
320
		return current_theme_supports( $feature );
321
	}
322
323
	public function get_metadata( $type, $object_id, $meta_key = '', $single = false ) {
324
		return get_metadata( $type, $object_id, $meta_key, $single );
325
	}
326
327
	/**
328
	 *
329
	 * Stores remote meta key/values alongside an ID mapping key
330
	 *
331
	 * @param $type
332
	 * @param $object_id
333
	 * @param $meta_key
334
	 * @param $meta_value
335
	 * @param $meta_id
336
	 *
337
	 * @return bool
338
	 */
339
	public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ) {
340
341
		$table = _get_meta_table( $type );
342
		if ( ! $table ) {
343
			return false;
344
		}
345
346
		global $wpdb;
347
348
		$exists = $wpdb->get_var( $wpdb->prepare(
349
			"SELECT EXISTS( SELECT 1 FROM $table WHERE meta_id = %d )",
350
			$meta_id
351
		) );
352
353
		if ( $exists ) {
354
			$wpdb->update( $table, array(
355
				'meta_key'   => $meta_key,
356
				'meta_value' => maybe_serialize( $meta_value ),
357
			), array( 'meta_id' => $meta_id ) );
358
		} else {
359
			$object_id_field = $type . '_id';
360
			$wpdb->insert( $table, array(
361
				'meta_id'        => $meta_id,
362
				$object_id_field => $object_id,
363
				'meta_key'       => $meta_key,
364
				'meta_value'     => maybe_serialize( $meta_value ),
365
			) );
366
		}
367
368
		wp_cache_delete( $object_id, $type . '_meta' );
369
370
		return true;
371
	}
372
373
	public function delete_metadata( $type, $object_id, $meta_ids ) {
374
		global $wpdb;
375
376
		$table = _get_meta_table( $type );
377
		if ( ! $table ) {
378
			return false;
379
		}
380
381
		foreach ( $meta_ids as $meta_id ) {
382
			$wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE meta_id = %d", $meta_id ) );
383
		}
384
385
		// if we don't have an object ID what do we do - invalidate ALL meta?
386
		if ( $object_id ) {
387
			wp_cache_delete( $object_id, $type . '_meta' );
388
		}
389
	}
390
391
	// todo: test this out to make sure it works as expected.
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...
392
	public function delete_batch_metadata( $type, $object_ids, $meta_key ) {
393
		global $wpdb;
394
395
		$table = _get_meta_table( $type );
396
		if ( ! $table ) {
397
			return false;
398
		}
399
		$column = sanitize_key($type . '_id' );
400
		$wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE $column IN (%s) && meta_key = %s", implode( ',', $object_ids ),  $meta_key ) );
401
402
		// if we don't have an object ID what do we do - invalidate ALL meta?
403
		foreach ( $object_ids as $object_id ) {
404
			wp_cache_delete( $object_id, $type . '_meta' );
405
		}
406
	}
407
408
	// constants
409
	public function get_constant( $constant ) {
410
		$value = get_option( 'jetpack_constant_' . $constant );
411
412
		if ( $value ) {
413
			return $value;
414
		}
415
416
		return null;
417
	}
418
419
	public function set_constant( $constant, $value ) {
420
		update_option( 'jetpack_constant_' . $constant, $value );
421
	}
422
423
	public function get_updates( $type ) {
424
		$all_updates = get_option( 'jetpack_updates', array() );
425
426
		if ( isset( $all_updates[ $type ] ) ) {
427
			return $all_updates[ $type ];
428
		} else {
429
			return null;
430
		}
431
	}
432
433
	public function set_updates( $type, $updates ) {
434
		$all_updates          = get_option( 'jetpack_updates', array() );
435
		$all_updates[ $type ] = $updates;
436
		update_option( 'jetpack_updates', $all_updates );
437
	}
438
439
	// functions
440
	public function get_callable( $name ) {
441
		$value = get_option( 'jetpack_' . $name );
442
443
		if ( $value ) {
444
			return $value;
445
		}
446
447
		return null;
448
	}
449
450
	public function set_callable( $name, $value ) {
451
		update_option( 'jetpack_' . $name, $value );
452
	}
453
454
	// network options
455
	public function get_site_option( $option ) {
456
		return get_option( 'jetpack_network_' . $option );
457
	}
458
459
	public function update_site_option( $option, $value ) {
460
		return update_option( 'jetpack_network_' . $option, $value );
461
	}
462
463
	public function delete_site_option( $option ) {
464
		return delete_option( 'jetpack_network_' . $option );
465
	}
466
467
	// terms
468
	// terms
469
	public function get_terms( $taxonomy ) {
470
		return get_terms( $taxonomy );
471
	}
472
473
	public function get_term( $taxonomy, $term_id, $is_term_id = true ) {
474
		$t = $this->ensure_taxonomy( $taxonomy );
475
		if ( ! $t || is_wp_error( $t ) ) {
476
			return $t;
477
		}
478
479
		return get_term( $term_id, $taxonomy );
480
	}
481
482
	private function ensure_taxonomy( $taxonomy ) {
483
		if ( ! taxonomy_exists( $taxonomy ) ) {
484
			// try re-registering synced taxonomies
485
			$taxonomies = $this->get_callable( 'taxonomies' );
486
			if ( ! isset( $taxonomies[ $taxonomy ] ) ) {
487
				// doesn't exist, or somehow hasn't been synced
488
				return new WP_Error( 'invalid_taxonomy', "The taxonomy '$taxonomy' doesn't exist" );
489
			}
490
			$t = $taxonomies[ $taxonomy ];
491
492
			return register_taxonomy(
493
				$taxonomy,
494
				$t->object_type,
495
				(array) $t
496
			);
497
		}
498
499
		return true;
500
	}
501
502
	public function get_the_terms( $object_id, $taxonomy ) {
503
		return get_the_terms( $object_id, $taxonomy );
504
	}
505
506
	public function update_term( $term_object ) {
507
		$taxonomy = $term_object->taxonomy;
508
		global $wpdb;
509
		$exists = $wpdb->get_var( $wpdb->prepare(
510
			"SELECT EXISTS( SELECT 1 FROM $wpdb->terms WHERE term_id = %d )",
511
			$term_object->term_id
512
		) );
513
		if ( ! $exists ) {
514
			$term_object   = sanitize_term( clone( $term_object ), $taxonomy, 'db' );
515
			$term          = array(
516
				'term_id'    => $term_object->term_id,
517
				'name'       => $term_object->name,
518
				'slug'       => $term_object->slug,
519
				'term_group' => $term_object->term_group,
520
			);
521
			$term_taxonomy = array(
522
				'term_taxonomy_id' => $term_object->term_taxonomy_id,
523
				'term_id'          => $term_object->term_id,
524
				'taxonomy'         => $term_object->taxonomy,
525
				'description'      => $term_object->description,
526
				'parent'           => (int) $term_object->parent,
527
				'count'            => (int) $term_object->count,
528
			);
529
			$wpdb->insert( $wpdb->terms, $term );
530
			$wpdb->insert( $wpdb->term_taxonomy, $term_taxonomy );
531
532
			return true;
533
		}
534
535
		return wp_update_term( $term_object->term_id, $taxonomy, (array) $term_object );
536
	}
537
538
	public function delete_term( $term_id, $taxonomy ) {
539
		return wp_delete_term( $term_id, $taxonomy );
540
	}
541
542
	public function update_object_terms( $object_id, $taxonomy, $terms, $append ) {
543
		wp_set_object_terms( $object_id, $terms, $taxonomy, $append );
544
	}
545
546
	public function delete_object_terms( $object_id, $tt_ids ) {
547
		global $wpdb;
548
549
		if ( is_array( $tt_ids ) && ! empty( $tt_ids ) ) {
550
			// escape
551
			$tt_ids_sanitized = array_map( 'intval', $tt_ids );
552
553
			$taxonomies = array();
554
			foreach ( $tt_ids_sanitized as $tt_id ) {
555
				$term                            = get_term_by( 'term_taxonomy_id', $tt_id );
556
				$taxonomies[ $term->taxonomy ][] = $tt_id;
557
			}
558
			$in_tt_ids = implode( ", ", $tt_ids_sanitized );
559
560
			/**
561
			 * Fires immediately before an object-term relationship is deleted.
562
			 *
563
			 * @since 2.9.0
564
			 *
565
			 * @param int $object_id Object ID.
566
			 * @param array $tt_ids An array of term taxonomy IDs.
567
			 */
568
			do_action( 'delete_term_relationships', $object_id, $tt_ids_sanitized );
569
			$deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) );
570
			foreach ( $taxonomies as $taxonomy => $taxonomy_tt_ids ) {
571
				$this->ensure_taxonomy( $taxonomy );
572
				wp_cache_delete( $object_id, $taxonomy . '_relationships' );
573
				/**
574
				 * Fires immediately after an object-term relationship is deleted.
575
				 *
576
				 * @since 2.9.0
577
				 *
578
				 * @param int $object_id Object ID.
579
				 * @param array $tt_ids An array of term taxonomy IDs.
580
				 */
581
				do_action( 'deleted_term_relationships', $object_id, $taxonomy_tt_ids );
582
				wp_update_term_count( $taxonomy_tt_ids, $taxonomy );
583
			}
584
585
			return (bool) $deleted;
586
		}
587
588
		return false;
589
	}
590
591
	// users
592
	public function user_count() {
593
594
	}
595
596
	public function get_user( $user_id ) {
597
		return WP_User::get_instance( $user_id );
598
	}
599
600
	public function upsert_user( $user ) {
601
		$this->invalid_call();
602
	}
603
604
	public function delete_user( $user_id ) {
605
		$this->invalid_call();
606
	}
607
608
	public function upsert_user_locale( $user_id, $local ) {
609
		$this->invalid_call();
610
	}
611
612
	public function delete_user_locale( $user_id ) {
613
		$this->invalid_call();
614
	}
615
616
	public function get_user_locale( $user_id ) {
617
		return jetpack_get_user_locale( $user_id );
618
	}
619
620
	public function get_allowed_mime_types( $user_id ) {
621
622
	}
623
624
	public function checksum_all() {
625
		$post_meta_checksum = $this->checksum_histogram( 'post_meta', 1 );
626
		$comment_meta_checksum = $this->checksum_histogram( 'comment_meta', 1 );
627
628
		return array(
629
			'posts'    => $this->posts_checksum(),
630
			'comments' => $this->comments_checksum(),
631
			'post_meta'=> reset( $post_meta_checksum ),
632
			'comment_meta'=> reset( $comment_meta_checksum ),
633
		);
634
	}
635
636
	function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null, $columns = null, $strip_non_ascii = true ) {
637
		global $wpdb;
638
639
		$wpdb->queries = array();
640
641
		switch( $object_type ) {
642
			case "posts":
643
				$object_count = $this->post_count( null, $start_id, $end_id );
644
				$object_table = $wpdb->posts;
645
				$id_field     = 'ID';
646
				$where_sql    = Jetpack_Sync_Settings::get_blacklisted_post_types_sql();
647
				if ( empty( $columns ) ) {
648
					$columns  = Jetpack_Sync_Defaults::$default_post_checksum_columns;
649
				}
650
				break;
651 View Code Duplication
			case "post_meta":
652
				$object_table = $wpdb->postmeta;
653
				$where_sql    = Jetpack_Sync_Settings::get_whitelisted_post_meta_sql();
654
				$object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id );
655
				$id_field     = 'meta_id';
656
657
				if ( empty( $columns ) ) {
658
					$columns  = Jetpack_Sync_Defaults::$default_post_meta_checksum_columns;
659
				}
660
				break;
661
			case "comments":
662
				$object_count = $this->comment_count( null, $start_id, $end_id );
663
				$object_table = $wpdb->comments;
664
				$id_field     = 'comment_ID';
665
				$where_sql    = Jetpack_Sync_Settings::get_comments_filter_sql();
666
				if ( empty( $columns ) ) {
667
					$columns  = Jetpack_Sync_Defaults::$default_comment_checksum_columns;
668
				}
669
				break;
670 View Code Duplication
			case "comment_meta":
671
				$object_table = $wpdb->commentmeta;
672
				$where_sql    = Jetpack_Sync_Settings::get_whitelisted_comment_meta_sql();
673
				$object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id );
674
				$id_field     = 'meta_id';
675
				if ( empty( $columns ) ) {
676
					$columns  = Jetpack_Sync_Defaults::$default_post_meta_checksum_columns;
677
				}
678
				break;
679
			default:
680
				return false;
681
		}
682
683
		$bucket_size  = intval( ceil( $object_count / $buckets ) );
684
		$previous_max_id = 0;
685
		$histogram    = array();
686
687
		$where = '1=1';
688
689
		if ( $start_id ) {
690
			$where .= " AND $id_field >= " . intval( $start_id );
691
		}
692
693
		if ( $end_id ) {
694
			$where .= " AND $id_field <= " . intval( $end_id );
695
		}
696
697
		do {
698
			list( $first_id, $last_id ) = $wpdb->get_row(
699
				"SELECT MIN($id_field) as min_id, MAX($id_field) as max_id FROM ( SELECT $id_field FROM $object_table WHERE $where AND $id_field > $previous_max_id ORDER BY $id_field ASC LIMIT $bucket_size ) as ids",
700
				ARRAY_N
701
			);
702
703
			// get the checksum value
704
			$value = $this->table_checksum( $object_table, $columns, $id_field, $where_sql, $first_id, $last_id, $strip_non_ascii );
705
706
			if ( is_wp_error( $value ) ) {
707
				return $value;
708
			}
709
710
			if ( $first_id === null || $last_id === null ) {
711
				break;
712
			} elseif ( $first_id === $last_id ) {
713
				$histogram[ $first_id ] = $value;
714
			} else {
715
				$histogram[ "{$first_id}-{$last_id}" ] = $value;
716
			}
717
718
			$previous_max_id = $last_id;
719
		} while ( true );
720
721
		return $histogram;
722
	}
723
724
	private function table_checksum( $table, $columns, $id_column, $where_sql = '1=1', $min_id = null, $max_id = null, $strip_non_ascii = true ) {
725
		global $wpdb;
726
727
		// sanitize to just valid MySQL column names
728
		$sanitized_columns = preg_grep ( '/^[0-9,a-z,A-Z$_]+$/i', $columns );
729
730
		if ( $strip_non_ascii ) {
731
			$columns_sql = implode( ',', array_map( array( $this, 'strip_non_ascii_sql' ), $sanitized_columns ) );
732
		} else {
733
			$columns_sql = implode( ',', $sanitized_columns );
734
		}
735
736
		if ( $min_id !== null ) {
737
			$min_id = intval( $min_id );
738
			$where_sql .= " AND $id_column >= $min_id";
739
		}
740
741
		if ( $max_id !== null ) {
742
			$max_id = intval( $max_id );
743
			$where_sql .= " AND $id_column <= $max_id";
744
		}
745
746
		$query = <<<ENDSQL
747
			SELECT CONV(BIT_XOR(CRC32(CONCAT({$columns_sql}))), 10, 16)
748
				FROM $table
749
				WHERE $where_sql
750
ENDSQL;
751
		$result = $wpdb->get_var( $query );
752
753
		if ( $wpdb->last_error ) {
754
			return new WP_Error( 'database_error', $wpdb->last_error );
755
		}
756
757
		return $result;
758
759
	}
760
761
	private function meta_count( $table, $where_sql, $min_id, $max_id ) {
762
		global $wpdb;
763
764
		if ( $min_id != null ) {
765
			$where_sql .= ' AND meta_id >= ' . intval( $min_id );
766
		}
767
768
		if ( $max_id != null ) {
769
			$where_sql .= ' AND meta_id <= ' . intval( $max_id );
770
		}
771
772
		return $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE $where_sql" );
773
	}
774
775
	/**
776
	 * Wraps a column name in SQL which strips non-ASCII chars.
777
	 * This helps normalize data to avoid checksum differences caused by
778
	 * badly encoded data in the DB
779
	 */
780
	function strip_non_ascii_sql( $column_name ) {
781
		return "REPLACE( CONVERT( $column_name USING ascii ), '?', '' )";
782
	}
783
784
	private function invalid_call() {
785
		$backtrace = debug_backtrace();
786
		$caller    = $backtrace[1]['function'];
787
		throw new Exception( "This function $caller is not supported on the WP Replicastore" );
788
	}
789
}
790