Completed
Push — fix/better-meta-table-checksum... ( 8a57b9 )
by
unknown
07:45
created

Replicastore::get_term()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 3
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Sync replicastore.
4
 *
5
 * @package automattic/jetpack-sync
6
 */
7
8
namespace Automattic\Jetpack\Sync;
9
10
/**
11
 * An implementation of Replicastore Interface which returns data stored in a WordPress.org DB.
12
 * This is useful to compare values in the local WP DB to values in the synced replica store
13
 */
14
class Replicastore implements Replicastore_Interface {
15
	/**
16
	 * Empty and reset the replicastore.
17
	 *
18
	 * @access public
19
	 */
20
	public function reset() {
21
		global $wpdb;
22
23
		$wpdb->query( "DELETE FROM $wpdb->posts" );
24
25
		// Delete comments from cache.
26
		$comment_ids = $wpdb->get_col( "SELECT comment_ID FROM $wpdb->comments" );
27
		if ( ! empty( $comment_ids ) ) {
28
			clean_comment_cache( $comment_ids );
29
		}
30
		$wpdb->query( "DELETE FROM $wpdb->comments" );
31
32
		// Also need to delete terms from cache.
33
		$term_ids = $wpdb->get_col( "SELECT term_id FROM $wpdb->terms" );
34
		foreach ( $term_ids as $term_id ) {
35
			wp_cache_delete( $term_id, 'terms' );
36
		}
37
38
		$wpdb->query( "DELETE FROM $wpdb->terms" );
39
40
		$wpdb->query( "DELETE FROM $wpdb->term_taxonomy" );
41
		$wpdb->query( "DELETE FROM $wpdb->term_relationships" );
42
43
		// Callables and constants.
44
		$wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'jetpack_%'" );
45
		$wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%'" );
46
	}
47
48
	/**
49
	 * Ran when full sync has just started.
50
	 *
51
	 * @access public
52
	 *
53
	 * @param array $config Full sync configuration for this sync module.
54
	 */
55
	public function full_sync_start( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
56
		$this->reset();
57
	}
58
59
	/**
60
	 * Ran when full sync has just finished.
61
	 *
62
	 * @access public
63
	 *
64
	 * @param string $checksum Deprecated since 7.3.0.
65
	 */
66
	public function full_sync_end( $checksum ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
67
		// Noop right now.
68
	}
69
70
	/**
71
	 * Retrieve the number of terms.
72
	 *
73
	 * @access public
74
	 *
75
	 * @return int Number of terms.
76
	 */
77
	public function term_count() {
78
		global $wpdb;
79
		return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->terms" );
80
	}
81
82
	/**
83
	 * Retrieve the number of rows in the `term_taxonomy` table.
84
	 *
85
	 * @access public
86
	 *
87
	 * @return int Number of terms.
88
	 */
89
	public function term_taxonomy_count() {
90
		global $wpdb;
91
		return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->term_taxonomy" );
92
	}
93
94
	/**
95
	 * Retrieve the number of term relationships.
96
	 *
97
	 * @access public
98
	 *
99
	 * @return int Number of rows in the term relationships table.
100
	 */
101
	public function term_relationship_count() {
102
		global $wpdb;
103
		return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->term_relationships" );
104
	}
105
106
	/**
107
	 * Retrieve the number of posts with a particular post status within a certain range.
108
	 *
109
	 * @access public
110
	 *
111
	 * @todo Prepare the SQL query before executing it.
112
	 *
113
	 * @param string $status Post status.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $status not be string|null?

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...
114
	 * @param int    $min_id Minimum post ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
115
	 * @param int    $max_id Maximum post ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
116
	 * @return int Number of posts.
117
	 */
118 View Code Duplication
	public function post_count( $status = null, $min_id = null, $max_id = null ) {
119
		global $wpdb;
120
121
		$where = '';
0 ignored issues
show
Unused Code introduced by
$where is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
122
123
		if ( $status ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $status of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
124
			$where = "post_status = '" . esc_sql( $status ) . "'";
125
		} else {
126
			$where = '1=1';
127
		}
128
129
		if ( ! empty( $min_id ) ) {
130
			$where .= ' AND ID >= ' . intval( $min_id );
131
		}
132
133
		if ( ! empty( $max_id ) ) {
134
			$where .= ' AND ID <= ' . intval( $max_id );
135
		}
136
137
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
138
		return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE $where" );
139
	}
140
141
	/**
142
	 * Retrieve the posts with a particular post status.
143
	 *
144
	 * @access public
145
	 *
146
	 * @todo Implement range and actually use max_id/min_id arguments.
147
	 *
148
	 * @param string $status Post status.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $status not be string|null?

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...
149
	 * @param int    $min_id Minimum post ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
150
	 * @param int    $max_id Maximum post ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
151
	 * @return array Array of posts.
152
	 */
153
	public function get_posts( $status = null, $min_id = null, $max_id = null ) {
154
		$args = array(
155
			'orderby'        => 'ID',
156
			'posts_per_page' => -1,
157
		);
158
159
		if ( $status ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $status of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
160
			$args['post_status'] = $status;
161
		} else {
162
			$args['post_status'] = 'any';
163
		}
164
165
		return get_posts( $args );
166
	}
167
168
	/**
169
	 * Retrieve a post object by the post ID.
170
	 *
171
	 * @access public
172
	 *
173
	 * @param int $id Post ID.
174
	 * @return \WP_Post Post object.
175
	 */
176
	public function get_post( $id ) {
177
		return get_post( $id );
178
	}
179
180
	/**
181
	 * Update or insert a post.
182
	 *
183
	 * @access public
184
	 *
185
	 * @param \WP_Post $post   Post object.
186
	 * @param bool     $silent Whether to perform a silent action. Not used in this implementation.
187
	 */
188
	public function upsert_post( $post, $silent = false ) {
189
		global $wpdb;
190
191
		// Reject the post if it's not a \WP_Post.
192
		if ( ! $post instanceof \WP_Post ) {
0 ignored issues
show
Bug introduced by
The class WP_Post does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
193
			return;
194
		}
195
196
		$post = $post->to_array();
197
198
		// Reject posts without an ID.
199
		if ( ! isset( $post['ID'] ) ) {
200
			return;
201
		}
202
203
		$now     = current_time( 'mysql' );
204
		$now_gmt = get_gmt_from_date( $now );
205
206
		$defaults = array(
207
			'ID'                    => 0,
208
			'post_author'           => '0',
209
			'post_content'          => '',
210
			'post_content_filtered' => '',
211
			'post_title'            => '',
212
			'post_name'             => '',
213
			'post_excerpt'          => '',
214
			'post_status'           => 'draft',
215
			'post_type'             => 'post',
216
			'comment_status'        => 'closed',
217
			'comment_count'         => '0',
218
			'ping_status'           => '',
219
			'post_password'         => '',
220
			'to_ping'               => '',
221
			'pinged'                => '',
222
			'post_parent'           => 0,
223
			'menu_order'            => 0,
224
			'guid'                  => '',
225
			'post_date'             => $now,
226
			'post_date_gmt'         => $now_gmt,
227
			'post_modified'         => $now,
228
			'post_modified_gmt'     => $now_gmt,
229
		);
230
231
		$post = array_intersect_key( $post, $defaults );
232
233
		$post = sanitize_post( $post, 'db' );
234
235
		unset( $post['filter'] );
236
237
		$exists = $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM $wpdb->posts WHERE ID = %d )", $post['ID'] ) );
238
239
		if ( $exists ) {
240
			$wpdb->update( $wpdb->posts, $post, array( 'ID' => $post['ID'] ) );
241
		} else {
242
			$wpdb->insert( $wpdb->posts, $post );
243
		}
244
245
		clean_post_cache( $post['ID'] );
246
	}
247
248
	/**
249
	 * Delete a post by the post ID.
250
	 *
251
	 * @access public
252
	 *
253
	 * @param int $post_id Post ID.
254
	 */
255
	public function delete_post( $post_id ) {
256
		wp_delete_post( $post_id, true );
257
	}
258
259
	/**
260
	 * Retrieve the checksum for posts within a range.
261
	 *
262
	 * @access public
263
	 *
264
	 * @param int $min_id Minimum post ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
265
	 * @param int $max_id Maximum post ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
266
	 * @return int The checksum.
267
	 */
268
	public function posts_checksum( $min_id = null, $max_id = null ) {
269
		global $wpdb;
270
		return $this->table_checksum( $wpdb->posts, Defaults::$default_post_checksum_columns, 'ID', Settings::get_blacklisted_post_types_sql(), $min_id, $max_id );
271
	}
272
273
	/**
274
	 * Retrieve the checksum for post meta within a range.
275
	 *
276
	 * @access public
277
	 *
278
	 * @param int $min_id Minimum post meta ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
279
	 * @param int $max_id Maximum post meta ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
280
	 * @return int The checksum.
281
	 */
282
	public function post_meta_checksum( $min_id = null, $max_id = null ) {
283
		global $wpdb;
284
		return $this->table_checksum( $wpdb->postmeta, Defaults::$default_post_meta_checksum_columns, 'meta_id', Settings::get_whitelisted_post_meta_sql(), $min_id, $max_id );
285
	}
286
287
	/**
288
	 * Retrieve the number of comments with a particular comment status within a certain range.
289
	 *
290
	 * @access public
291
	 *
292
	 * @todo Prepare the SQL query before executing it.
293
	 *
294
	 * @param string $status Comment status.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $status not be string|null?

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...
295
	 * @param int    $min_id Minimum comment ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
296
	 * @param int    $max_id Maximum comment ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
297
	 * @return int Number of comments.
298
	 */
299 View Code Duplication
	public function comment_count( $status = null, $min_id = null, $max_id = null ) {
300
		global $wpdb;
301
302
		$comment_approved = $this->comment_status_to_approval_value( $status );
303
304
		if ( false !== $comment_approved ) {
305
			$where = "comment_approved = '" . esc_sql( $comment_approved ) . "'";
306
		} else {
307
			$where = '1=1';
308
		}
309
310
		if ( ! empty( $min_id ) ) {
311
			$where .= ' AND comment_ID >= ' . intval( $min_id );
312
		}
313
314
		if ( ! empty( $max_id ) ) {
315
			$where .= ' AND comment_ID <= ' . intval( $max_id );
316
		}
317
318
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
319
		return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE $where" );
320
	}
321
322
	/**
323
	 * Translate a comment status to a value of the comment_approved field.
324
	 *
325
	 * @access private
326
	 *
327
	 * @param string $status Comment status.
328
	 * @return string|bool New comment_approved value, false if the status doesn't affect it.
329
	 */
330
	private function comment_status_to_approval_value( $status ) {
331
		switch ( $status ) {
332
			case 'approve':
333
				return '1';
334
			case 'hold':
335
				return '0';
336
			case 'spam':
337
				return 'spam';
338
			case 'trash':
339
				return 'trash';
340
			case 'any':
341
				return false;
342
			case 'all':
343
				return false;
344
			default:
345
				return false;
346
		}
347
	}
348
349
	/**
350
	 * Retrieve the comments with a particular comment status.
351
	 *
352
	 * @access public
353
	 *
354
	 * @todo Implement range and actually use max_id/min_id arguments.
355
	 *
356
	 * @param string $status Comment status.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $status not be string|null?

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...
357
	 * @param int    $min_id Minimum comment ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
358
	 * @param int    $max_id Maximum comment ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
359
	 * @return array Array of comments.
360
	 */
361
	public function get_comments( $status = null, $min_id = null, $max_id = null ) {
362
		$args = array(
363
			'orderby' => 'ID',
364
			'status'  => 'all',
365
		);
366
367
		if ( $status ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $status of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
368
			$args['status'] = $status;
369
		}
370
371
		return get_comments( $args );
372
	}
373
374
	/**
375
	 * Retrieve a comment object by the comment ID.
376
	 *
377
	 * @access public
378
	 *
379
	 * @param int $id Comment ID.
380
	 * @return \WP_Comment Comment object.
381
	 */
382
	public function get_comment( $id ) {
383
		return \WP_Comment::get_instance( $id );
384
	}
385
386
	/**
387
	 * Update or insert a comment.
388
	 *
389
	 * @access public
390
	 *
391
	 * @param \WP_Comment $comment Comment object.
392
	 */
393
	public function upsert_comment( $comment ) {
394
		global $wpdb;
395
396
		$comment = $comment->to_array();
397
398
		// Filter by fields on comment table.
399
		$comment_fields_whitelist = array(
400
			'comment_ID',
401
			'comment_post_ID',
402
			'comment_author',
403
			'comment_author_email',
404
			'comment_author_url',
405
			'comment_author_IP',
406
			'comment_date',
407
			'comment_date_gmt',
408
			'comment_content',
409
			'comment_karma',
410
			'comment_approved',
411
			'comment_agent',
412
			'comment_type',
413
			'comment_parent',
414
			'user_id',
415
		);
416
417
		foreach ( $comment as $key => $value ) {
418
			if ( ! in_array( $key, $comment_fields_whitelist, true ) ) {
419
				unset( $comment[ $key ] );
420
			}
421
		}
422
423
		$exists = $wpdb->get_var(
424
			$wpdb->prepare(
425
				"SELECT EXISTS( SELECT 1 FROM $wpdb->comments WHERE comment_ID = %d )",
426
				$comment['comment_ID']
427
			)
428
		);
429
430
		if ( $exists ) {
431
			$wpdb->update( $wpdb->comments, $comment, array( 'comment_ID' => $comment['comment_ID'] ) );
432
		} else {
433
			$wpdb->insert( $wpdb->comments, $comment );
434
		}
435
		// Remove comment from cache.
436
		clean_comment_cache( $comment['comment_ID'] );
437
438
		wp_update_comment_count( $comment['comment_post_ID'] );
439
	}
440
441
	/**
442
	 * Trash a comment by the comment ID.
443
	 *
444
	 * @access public
445
	 *
446
	 * @param int $comment_id Comment ID.
447
	 */
448
	public function trash_comment( $comment_id ) {
449
		wp_delete_comment( $comment_id );
450
	}
451
452
	/**
453
	 * Delete a comment by the comment ID.
454
	 *
455
	 * @access public
456
	 *
457
	 * @param int $comment_id Comment ID.
458
	 */
459
	public function delete_comment( $comment_id ) {
460
		wp_delete_comment( $comment_id, true );
461
	}
462
463
	/**
464
	 * Mark a comment by the comment ID as spam.
465
	 *
466
	 * @access public
467
	 *
468
	 * @param int $comment_id Comment ID.
469
	 */
470
	public function spam_comment( $comment_id ) {
471
		wp_spam_comment( $comment_id );
472
	}
473
474
	/**
475
	 * Trash the comments of a post.
476
	 *
477
	 * @access public
478
	 *
479
	 * @param int   $post_id  Post ID.
480
	 * @param array $statuses Post statuses. Not used in this implementation.
481
	 */
482
	public function trashed_post_comments( $post_id, $statuses ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
483
		wp_trash_post_comments( $post_id );
484
	}
485
486
	/**
487
	 * Untrash the comments of a post.
488
	 *
489
	 * @access public
490
	 *
491
	 * @param int $post_id Post ID.
492
	 */
493
	public function untrashed_post_comments( $post_id ) {
494
		wp_untrash_post_comments( $post_id );
495
	}
496
497
	/**
498
	 * Retrieve the checksum for comments within a range.
499
	 *
500
	 * @access public
501
	 *
502
	 * @param int $min_id Minimum comment ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
503
	 * @param int $max_id Maximum comment ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
504
	 * @return int The checksum.
505
	 */
506
	public function comments_checksum( $min_id = null, $max_id = null ) {
507
		global $wpdb;
508
		return $this->table_checksum( $wpdb->comments, Defaults::$default_comment_checksum_columns, 'comment_ID', Settings::get_comments_filter_sql(), $min_id, $max_id );
509
	}
510
511
	/**
512
	 * Retrieve the checksum for comment meta within a range.
513
	 *
514
	 * @access public
515
	 *
516
	 * @param int $min_id Minimum comment meta ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
517
	 * @param int $max_id Maximum comment meta ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
518
	 * @return int The checksum.
519
	 */
520
	public function comment_meta_checksum( $min_id = null, $max_id = null ) {
521
		global $wpdb;
522
		return $this->table_checksum( $wpdb->commentmeta, Defaults::$default_comment_meta_checksum_columns, 'meta_id', Settings::get_whitelisted_comment_meta_sql(), $min_id, $max_id );
523
	}
524
525
	/**
526
	 * Retrieve the checksum for all options.
527
	 *
528
	 * @access public
529
	 *
530
	 * @return int The checksum.
531
	 */
532
	public function options_checksum() {
533
		global $wpdb;
534
		$options_whitelist = "'" . implode( "', '", Defaults::$default_options_whitelist ) . "'";
535
		$where_sql         = "option_name IN ( $options_whitelist )";
536
537
		return $this->table_checksum( $wpdb->options, Defaults::$default_option_checksum_columns, null, $where_sql, null, null );
538
	}
539
540
	/**
541
	 * Update the value of an option.
542
	 *
543
	 * @access public
544
	 *
545
	 * @param string $option Option name.
546
	 * @param mixed  $value  Option value.
547
	 * @return bool False if value was not updated and true if value was updated.
548
	 */
549
	public function update_option( $option, $value ) {
550
		return update_option( $option, $value );
551
	}
552
553
	/**
554
	 * Retrieve an option value based on an option name.
555
	 *
556
	 * @access public
557
	 *
558
	 * @param string $option  Name of option to retrieve.
559
	 * @param mixed  $default Optional. Default value to return if the option does not exist.
560
	 * @return mixed Value set for the option.
561
	 */
562
	public function get_option( $option, $default = false ) {
563
		return get_option( $option, $default );
564
	}
565
566
	/**
567
	 * Remove an option by name.
568
	 *
569
	 * @access public
570
	 *
571
	 * @param string $option Name of option to remove.
572
	 * @return bool True, if option is successfully deleted. False on failure.
573
	 */
574
	public function delete_option( $option ) {
575
		return delete_option( $option );
576
	}
577
578
	/**
579
	 * Change the features that the current theme supports.
580
	 * Intentionally not implemented in this replicastore.
581
	 *
582
	 * @access public
583
	 *
584
	 * @param array $theme_support Features that the theme supports.
585
	 */
586
	public function set_theme_support( $theme_support ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
587
		// Noop.
588
	}
589
590
	/**
591
	 * Whether the current theme supports a certain feature.
592
	 *
593
	 * @access public
594
	 *
595
	 * @param string $feature Name of the feature.
596
	 */
597
	public function current_theme_supports( $feature ) {
598
		return current_theme_supports( $feature );
599
	}
600
601
	/**
602
	 * Retrieve metadata for the specified object.
603
	 *
604
	 * @access public
605
	 *
606
	 * @param string $type       Meta type.
607
	 * @param int    $object_id  ID of the object.
608
	 * @param string $meta_key   Meta key.
609
	 * @param bool   $single     If true, return only the first value of the specified meta_key.
610
	 *
611
	 * @return mixed Single metadata value, or array of values.
612
	 */
613
	public function get_metadata( $type, $object_id, $meta_key = '', $single = false ) {
614
		return get_metadata( $type, $object_id, $meta_key, $single );
615
	}
616
617
	/**
618
	 * Stores remote meta key/values alongside an ID mapping key.
619
	 *
620
	 * @access public
621
	 *
622
	 * @todo Refactor to not use interpolated values when preparing the SQL query.
623
	 *
624
	 * @param string $type       Meta type.
625
	 * @param int    $object_id  ID of the object.
626
	 * @param string $meta_key   Meta key.
627
	 * @param mixed  $meta_value Meta value.
628
	 * @param int    $meta_id    ID of the meta.
629
	 *
630
	 * @return bool False if meta table does not exist, true otherwise.
631
	 */
632
	public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ) {
633
		$table = _get_meta_table( $type );
634
		if ( ! $table ) {
635
			return false;
636
		}
637
638
		global $wpdb;
639
640
		$exists = $wpdb->get_var(
641
			$wpdb->prepare(
642
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
643
				"SELECT EXISTS( SELECT 1 FROM $table WHERE meta_id = %d )",
644
				$meta_id
645
			)
646
		);
647
648
		if ( $exists ) {
649
			$wpdb->update(
650
				$table,
651
				array(
652
					'meta_key'   => $meta_key,
653
					'meta_value' => maybe_serialize( $meta_value ),
654
				),
655
				array( 'meta_id' => $meta_id )
656
			);
657
		} else {
658
			$object_id_field = $type . '_id';
659
			$wpdb->insert(
660
				$table,
661
				array(
662
					'meta_id'        => $meta_id,
663
					$object_id_field => $object_id,
664
					'meta_key'       => $meta_key,
665
					'meta_value'     => maybe_serialize( $meta_value ),
666
				)
667
			);
668
		}
669
670
		wp_cache_delete( $object_id, $type . '_meta' );
671
672
		return true;
673
	}
674
675
	/**
676
	 * Delete metadata for the specified object.
677
	 *
678
	 * @access public
679
	 *
680
	 * @todo Refactor to not use interpolated values when preparing the SQL query.
681
	 *
682
	 * @param string $type      Meta type.
683
	 * @param int    $object_id ID of the object.
684
	 * @param array  $meta_ids  IDs of the meta objects to delete.
685
	 */
686
	public function delete_metadata( $type, $object_id, $meta_ids ) {
687
		global $wpdb;
688
689
		$table = _get_meta_table( $type );
690
		if ( ! $table ) {
691
			return false;
692
		}
693
694
		foreach ( $meta_ids as $meta_id ) {
695
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
696
			$wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE meta_id = %d", $meta_id ) );
697
		}
698
699
		// If we don't have an object ID what do we do - invalidate ALL meta?
700
		if ( $object_id ) {
701
			wp_cache_delete( $object_id, $type . '_meta' );
702
		}
703
	}
704
705
	/**
706
	 * Delete metadata with a certain key for the specified objects.
707
	 *
708
	 * @access public
709
	 *
710
	 * @todo Test this out to make sure it works as expected.
711
	 * @todo Refactor to not use interpolated values when preparing the SQL query.
712
	 *
713
	 * @param string $type       Meta type.
714
	 * @param array  $object_ids IDs of the objects.
715
	 * @param string $meta_key   Meta key.
716
	 */
717
	public function delete_batch_metadata( $type, $object_ids, $meta_key ) {
718
		global $wpdb;
719
720
		$table = _get_meta_table( $type );
721
		if ( ! $table ) {
722
			return false;
723
		}
724
		$column = sanitize_key( $type . '_id' );
725
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
726
		$wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE $column IN (%s) && meta_key = %s", implode( ',', $object_ids ), $meta_key ) );
727
728
		// If we don't have an object ID what do we do - invalidate ALL meta?
729
		foreach ( $object_ids as $object_id ) {
730
			wp_cache_delete( $object_id, $type . '_meta' );
731
		}
732
	}
733
734
	/**
735
	 * Retrieve value of a constant based on the constant name.
736
	 *
737
	 * @access public
738
	 *
739
	 * @param string $constant Name of constant to retrieve.
740
	 * @return mixed Value set for the constant.
741
	 */
742
	public function get_constant( $constant ) {
743
		$value = get_option( 'jetpack_constant_' . $constant );
744
745
		if ( $value ) {
746
			return $value;
747
		}
748
749
		return null;
750
	}
751
752
	/**
753
	 * Set the value of a constant.
754
	 *
755
	 * @access public
756
	 *
757
	 * @param string $constant Name of constant to retrieve.
758
	 * @param mixed  $value    Value set for the constant.
759
	 */
760
	public function set_constant( $constant, $value ) {
761
		update_option( 'jetpack_constant_' . $constant, $value );
762
	}
763
764
	/**
765
	 * Retrieve the number of the available updates of a certain type.
766
	 * Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`.
767
	 *
768
	 * @access public
769
	 *
770
	 * @param string $type Type of updates to retrieve.
771
	 * @return int|null Number of updates available, `null` if type is invalid or missing.
772
	 */
773
	public function get_updates( $type ) {
774
		$all_updates = get_option( 'jetpack_updates', array() );
775
776
		if ( isset( $all_updates[ $type ] ) ) {
777
			return $all_updates[ $type ];
778
		} else {
779
			return null;
780
		}
781
	}
782
783
	/**
784
	 * Set the available updates of a certain type.
785
	 * Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`.
786
	 *
787
	 * @access public
788
	 *
789
	 * @param string $type    Type of updates to set.
790
	 * @param int    $updates Total number of updates.
791
	 */
792
	public function set_updates( $type, $updates ) {
793
		$all_updates          = get_option( 'jetpack_updates', array() );
794
		$all_updates[ $type ] = $updates;
795
		update_option( 'jetpack_updates', $all_updates );
796
	}
797
798
	/**
799
	 * Retrieve a callable value based on its name.
800
	 *
801
	 * @access public
802
	 *
803
	 * @param string $name Name of the callable to retrieve.
804
	 * @return mixed Value of the callable.
805
	 */
806
	public function get_callable( $name ) {
807
		$value = get_option( 'jetpack_' . $name );
808
809
		if ( $value ) {
810
			return $value;
811
		}
812
813
		return null;
814
	}
815
816
	/**
817
	 * Update the value of a callable.
818
	 *
819
	 * @access public
820
	 *
821
	 * @param string $name  Callable name.
822
	 * @param mixed  $value Callable value.
823
	 */
824
	public function set_callable( $name, $value ) {
825
		update_option( 'jetpack_' . $name, $value );
826
	}
827
828
	/**
829
	 * Retrieve a network option value based on a network option name.
830
	 *
831
	 * @access public
832
	 *
833
	 * @param string $option Name of network option to retrieve.
834
	 * @return mixed Value set for the network option.
835
	 */
836
	public function get_site_option( $option ) {
837
		return get_option( 'jetpack_network_' . $option );
838
	}
839
840
	/**
841
	 * Update the value of a network option.
842
	 *
843
	 * @access public
844
	 *
845
	 * @param string $option Network option name.
846
	 * @param mixed  $value  Network option value.
847
	 * @return bool False if value was not updated and true if value was updated.
848
	 */
849
	public function update_site_option( $option, $value ) {
850
		return update_option( 'jetpack_network_' . $option, $value );
851
	}
852
853
	/**
854
	 * Remove a network option by name.
855
	 *
856
	 * @access public
857
	 *
858
	 * @param string $option Name of option to remove.
859
	 * @return bool True, if option is successfully deleted. False on failure.
860
	 */
861
	public function delete_site_option( $option ) {
862
		return delete_option( 'jetpack_network_' . $option );
863
	}
864
865
	/**
866
	 * Retrieve the terms from a particular taxonomy.
867
	 *
868
	 * @access public
869
	 *
870
	 * @param string $taxonomy Taxonomy slug.
871
	 * @return array Array of terms.
872
	 */
873
	public function get_terms( $taxonomy ) {
874
		return get_terms( $taxonomy );
875
	}
876
877
	/**
878
	 * Retrieve a particular term.
879
	 *
880
	 * @access public
881
	 *
882
	 * @param string $taxonomy   Taxonomy slug.
883
	 * @param int    $term_id    ID of the term.
884
	 * @param bool   $is_term_id Whether this is a `term_id` or a `term_taxonomy_id`.
885
	 * @return \WP_Term|\WP_Error Term object on success, \WP_Error object on failure.
886
	 */
887
	public function get_term( $taxonomy, $term_id, $is_term_id = true ) {
888
		$t = $this->ensure_taxonomy( $taxonomy );
889
		if ( ! $t || is_wp_error( $t ) ) {
890
			return $t;
891
		}
892
893
		return get_term( $term_id, $taxonomy );
894
	}
895
896
	/**
897
	 * Verify a taxonomy is legitimate and register it if necessary.
898
	 *
899
	 * @access private
900
	 *
901
	 * @param string $taxonomy Taxonomy slug.
902
	 * @return bool|void|\WP_Error True if already exists; void if it was registered; \WP_Error on error.
903
	 */
904
	private function ensure_taxonomy( $taxonomy ) {
905
		if ( ! taxonomy_exists( $taxonomy ) ) {
906
			// Try re-registering synced taxonomies.
907
			$taxonomies = $this->get_callable( 'taxonomies' );
908
			if ( ! isset( $taxonomies[ $taxonomy ] ) ) {
909
				// Doesn't exist, or somehow hasn't been synced.
910
				return new \WP_Error( 'invalid_taxonomy', "The taxonomy '$taxonomy' doesn't exist" );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_taxonomy'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
911
			}
912
			$t = $taxonomies[ $taxonomy ];
913
914
			return register_taxonomy(
915
				$taxonomy,
916
				$t->object_type,
917
				(array) $t
918
			);
919
		}
920
921
		return true;
922
	}
923
924
	/**
925
	 * Retrieve all terms from a taxonomy that are related to an object with a particular ID.
926
	 *
927
	 * @access public
928
	 *
929
	 * @param int    $object_id Object ID.
930
	 * @param string $taxonomy  Taxonomy slug.
931
	 * @return array|bool|\WP_Error Array of terms on success, `false` if no terms or post doesn't exist, \WP_Error on failure.
932
	 */
933
	public function get_the_terms( $object_id, $taxonomy ) {
934
		return get_the_terms( $object_id, $taxonomy );
935
	}
936
937
	/**
938
	 * Insert or update a term.
939
	 *
940
	 * @access public
941
	 *
942
	 * @param \WP_Term $term_object Term object.
943
	 * @return array|bool|\WP_Error Array of term_id and term_taxonomy_id if updated, true if inserted, \WP_Error on failure.
944
	 */
945
	public function update_term( $term_object ) {
946
		$taxonomy = $term_object->taxonomy;
947
		global $wpdb;
948
		$exists = $wpdb->get_var(
949
			$wpdb->prepare(
950
				"SELECT EXISTS( SELECT 1 FROM $wpdb->terms WHERE term_id = %d )",
951
				$term_object->term_id
952
			)
953
		);
954
		if ( ! $exists ) {
955
			$term_object   = sanitize_term( clone( $term_object ), $taxonomy, 'db' );
956
			$term          = array(
957
				'term_id'    => $term_object->term_id,
958
				'name'       => $term_object->name,
959
				'slug'       => $term_object->slug,
960
				'term_group' => $term_object->term_group,
961
			);
962
			$term_taxonomy = array(
963
				'term_taxonomy_id' => $term_object->term_taxonomy_id,
964
				'term_id'          => $term_object->term_id,
965
				'taxonomy'         => $term_object->taxonomy,
966
				'description'      => $term_object->description,
967
				'parent'           => (int) $term_object->parent,
968
				'count'            => (int) $term_object->count,
969
			);
970
			$wpdb->insert( $wpdb->terms, $term );
971
			$wpdb->insert( $wpdb->term_taxonomy, $term_taxonomy );
972
973
			return true;
974
		}
975
976
		return wp_update_term( $term_object->term_id, $taxonomy, (array) $term_object );
977
	}
978
979
	/**
980
	 * Delete a term by the term ID and its corresponding taxonomy.
981
	 *
982
	 * @access public
983
	 *
984
	 * @param int    $term_id  Term ID.
985
	 * @param string $taxonomy Taxonomy slug.
986
	 * @return bool|int|\WP_Error True on success, false if term doesn't exist. Zero if trying with default category. \WP_Error on invalid taxonomy.
987
	 */
988
	public function delete_term( $term_id, $taxonomy ) {
989
		return wp_delete_term( $term_id, $taxonomy );
990
	}
991
992
	/**
993
	 * Add/update terms of a particular taxonomy of an object with the specified ID.
994
	 *
995
	 * @access public
996
	 *
997
	 * @param int              $object_id The object to relate to.
998
	 * @param string           $taxonomy  The context in which to relate the term to the object.
999
	 * @param string|int|array $terms     A single term slug, single term id, or array of either term slugs or ids.
1000
	 * @param bool             $append    Optional. If false will delete difference of terms. Default false.
1001
	 */
1002
	public function update_object_terms( $object_id, $taxonomy, $terms, $append ) {
1003
		wp_set_object_terms( $object_id, $terms, $taxonomy, $append );
1004
	}
1005
1006
	/**
1007
	 * Remove certain term relationships from the specified object.
1008
	 *
1009
	 * @access public
1010
	 *
1011
	 * @todo Refactor to not use interpolated values when preparing the SQL query.
1012
	 *
1013
	 * @param int   $object_id ID of the object.
1014
	 * @param array $tt_ids    Term taxonomy IDs.
1015
	 * @return bool True on success, false on failure.
1016
	 */
1017
	public function delete_object_terms( $object_id, $tt_ids ) {
1018
		global $wpdb;
1019
1020
		if ( is_array( $tt_ids ) && ! empty( $tt_ids ) ) {
1021
			// Escape.
1022
			$tt_ids_sanitized = array_map( 'intval', $tt_ids );
1023
1024
			$taxonomies = array();
1025
			foreach ( $tt_ids_sanitized as $tt_id ) {
1026
				$term                            = get_term_by( 'term_taxonomy_id', $tt_id );
1027
				$taxonomies[ $term->taxonomy ][] = $tt_id;
1028
			}
1029
			$in_tt_ids = implode( ', ', $tt_ids_sanitized );
1030
1031
			/**
1032
			 * Fires immediately before an object-term relationship is deleted.
1033
			 *
1034
			 * @since 2.9.0
1035
			 *
1036
			 * @param int   $object_id Object ID.
1037
			 * @param array $tt_ids    An array of term taxonomy IDs.
1038
			 */
1039
			do_action( 'delete_term_relationships', $object_id, $tt_ids_sanitized );
1040
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1041
			$deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) );
1042
			foreach ( $taxonomies as $taxonomy => $taxonomy_tt_ids ) {
1043
				$this->ensure_taxonomy( $taxonomy );
1044
				wp_cache_delete( $object_id, $taxonomy . '_relationships' );
1045
				/**
1046
				 * Fires immediately after an object-term relationship is deleted.
1047
				 *
1048
				 * @since 2.9.0
1049
				 *
1050
				 * @param int   $object_id Object ID.
1051
				 * @param array $tt_ids    An array of term taxonomy IDs.
1052
				 */
1053
				do_action( 'deleted_term_relationships', $object_id, $taxonomy_tt_ids );
1054
				wp_update_term_count( $taxonomy_tt_ids, $taxonomy );
1055
			}
1056
1057
			return (bool) $deleted;
1058
		}
1059
1060
		return false;
1061
	}
1062
1063
	/**
1064
	 * Retrieve the number of users.
1065
	 * Not supported in this replicastore.
1066
	 *
1067
	 * @access public
1068
	 */
1069
	public function user_count() {
1070
		// Noop.
1071
	}
1072
1073
	/**
1074
	 * Retrieve a user object by the user ID.
1075
	 *
1076
	 * @access public
1077
	 *
1078
	 * @param int $user_id User ID.
1079
	 * @return \WP_User User object.
1080
	 */
1081
	public function get_user( $user_id ) {
1082
		return \WP_User::get_instance( $user_id );
1083
	}
1084
1085
	/**
1086
	 * Insert or update a user.
1087
	 * Not supported in this replicastore.
1088
	 *
1089
	 * @access public
1090
	 * @throws \Exception If this method is invoked.
1091
	 *
1092
	 * @param \WP_User $user User object.
1093
	 */
1094
	public function upsert_user( $user ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1095
		$this->invalid_call();
1096
	}
1097
1098
	/**
1099
	 * Delete a user.
1100
	 * Not supported in this replicastore.
1101
	 *
1102
	 * @access public
1103
	 * @throws \Exception If this method is invoked.
1104
	 *
1105
	 * @param int $user_id User ID.
1106
	 */
1107
	public function delete_user( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1108
		$this->invalid_call();
1109
	}
1110
1111
	/**
1112
	 * Update/insert user locale.
1113
	 * Not supported in this replicastore.
1114
	 *
1115
	 * @access public
1116
	 * @throws \Exception If this method is invoked.
1117
	 *
1118
	 * @param int    $user_id User ID.
1119
	 * @param string $local   The user locale.
1120
	 */
1121
	public function upsert_user_locale( $user_id, $local ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1122
		$this->invalid_call();
1123
	}
1124
1125
	/**
1126
	 * Delete user locale.
1127
	 * Not supported in this replicastore.
1128
	 *
1129
	 * @access public
1130
	 * @throws \Exception If this method is invoked.
1131
	 *
1132
	 * @param int $user_id User ID.
1133
	 */
1134
	public function delete_user_locale( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1135
		$this->invalid_call();
1136
	}
1137
1138
	/**
1139
	 * Retrieve the user locale.
1140
	 *
1141
	 * @access public
1142
	 *
1143
	 * @param int $user_id User ID.
1144
	 * @return string The user locale.
1145
	 */
1146
	public function get_user_locale( $user_id ) {
1147
		return get_user_locale( $user_id );
1148
	}
1149
1150
	/**
1151
	 * Retrieve the allowed mime types for the user.
1152
	 * Not supported in this replicastore.
1153
	 *
1154
	 * @access public
1155
	 *
1156
	 * @param int $user_id User ID.
1157
	 */
1158
	public function get_allowed_mime_types( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1159
		// Noop.
1160
	}
1161
1162
	/**
1163
	 * Retrieve all the checksums we are interested in.
1164
	 * Currently that is posts, comments, post meta and comment meta.
1165
	 *
1166
	 * @access public
1167
	 *
1168
	 * @return array Checksums.
1169
	 */
1170
	public function checksum_all() {
1171
		$post_meta_checksum    = $this->checksum_histogram( 'post_meta', 1 );
1172
		$comment_meta_checksum = $this->checksum_histogram( 'comment_meta', 1 );
1173
1174
		return array(
1175
			'posts'        => $this->posts_checksum(),
1176
			'comments'     => $this->comments_checksum(),
1177
			'post_meta'    => reset( $post_meta_checksum ),
1178
			'comment_meta' => reset( $comment_meta_checksum ),
1179
		);
1180
	}
1181
1182
	/**
1183
	 * Retrieve the columns that are needed to calculate a checksum for an object type.
1184
	 *
1185
	 * @access public
1186
	 *
1187
	 * @todo Refactor to not use interpolated values and prepare the SQL query.
1188
	 *
1189
	 * @param string $object_type Object type.
1190
	 * @return array|bool Columns, or false if invalid object type is specified.
1191
	 */
1192
	public function get_checksum_columns_for_object_type( $object_type ) {
1193
		switch ( $object_type ) {
1194
			case 'posts':
1195
				return Defaults::$default_post_checksum_columns;
1196
			case 'post_meta':
1197
				return Defaults::$default_post_meta_checksum_columns;
1198
			case 'comments':
1199
				return Defaults::$default_comment_checksum_columns;
1200
			case 'comment_meta':
1201
				return Defaults::$default_post_meta_checksum_columns;
1202
			case 'terms':
1203
				return Defaults::$default_term_checksum_columns;
1204
			case 'term_taxonomy':
1205
				return Defaults::$default_term_taxonomy_checksum_columns;
1206
			case 'term_relationships':
1207
				return Defaults::$default_term_relationships_checksum_columns;
1208
			default:
1209
				return false;
1210
		}
1211
	}
1212
1213
	/**
1214
	 * Grabs the minimum and maximum object ids for the given parameters.
1215
	 *
1216
	 * @access public
1217
	 *
1218
	 * @param string $id_field     The id column in the table to query.
1219
	 * @param string $object_table The table to query.
1220
	 * @param string $where        A sql where clause without 'WHERE'.
1221
	 * @param int    $bucket_size  The maximum amount of objects to include in the query.
1222
	 *                             For `term_relationships` table, the bucket size will refer to the amount
1223
	 *                             of distinct object ids. This will likely include more database rows than
1224
	 *                             the bucket size implies.
1225
	 *
1226
	 * @return object An object with min_id and max_id properties.
1227
	 */
1228
	public function get_min_max_object_id( $id_field, $object_table, $where, $bucket_size ) {
1229
		global $wpdb;
1230
1231
		// The term relationship table's unique key is a combination of 2 columns. `DISTINCT` helps us get a more acurate query.
1232
		$distinct_sql = ( $wpdb->term_relationships === $object_table || in_array( $object_table, $this->get_meta_tables() ) ) ? 'DISTINCT' : '';
1233
		$where_sql    = $where ? "WHERE $where" : '';
1234
1235
		// Since MIN() and MAX() do not work with LIMIT, we'll need to adjust the dataset we query if a limit is present.
1236
		// With a limit present, we'll look at a dataset consisting of object_ids that meet the constructs of the $where clause.
1237
		// Without a limit, we can use the actual table as a dataset.
1238
		$from = $bucket_size ?
1239
			"( SELECT $distinct_sql $id_field FROM $object_table $where_sql ORDER BY $id_field ASC LIMIT $bucket_size ) as ids" :
1240
			"$object_table $where_sql ORDER BY $id_field ASC";
1241
1242
		error_log( "SELECT MIN($id_field) as min, MAX($id_field) as max FROM $from" );
1243
1244
		return $wpdb->get_row(
1245
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1246
			"SELECT MIN($id_field) as min, MAX($id_field) as max FROM $from"
1247
		);
1248
	}
1249
1250
	/**
1251
	 * Retrieve the checksum histogram for a specific object type.
1252
	 *
1253
	 * @access public
1254
	 *
1255
	 * @todo Refactor to not use interpolated values and properly prepare the SQL query.
1256
	 *
1257
	 * @param string $object_type     Object type.
1258
	 * @param int    $buckets         Number of buckets to split the objects to.
1259
	 * @param int    $start_id        Minimum object ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $start_id not be integer|null?

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...
1260
	 * @param int    $end_id          Maximum object ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $end_id not be integer|null?

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...
1261
	 * @param array  $columns         Table columns to calculate the checksum from.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $columns not be array|null?

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...
1262
	 * @param bool   $strip_non_ascii Whether to strip non-ASCII characters.
1263
	 * @param string $salt            Salt, used for $wpdb->prepare()'s args.
1264
	 * @return array The checksum histogram.
1265
	 */
1266
	public function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null, $columns = null, $strip_non_ascii = true, $salt = '' ) {
1267
		global $wpdb;
1268
1269
		$wpdb->queries = array();
1270
1271
		if ( empty( $columns ) ) {
1272
			$columns = $this->get_checksum_columns_for_object_type( $object_type );
1273
		}
1274
1275
		switch ( $object_type ) {
1276
			case 'posts':
1277
				$object_count = $this->post_count( null, $start_id, $end_id );
1278
				$object_table = $wpdb->posts;
1279
				$id_field     = 'ID';
1280
				$where_sql    = Settings::get_blacklisted_post_types_sql();
1281
				break;
1282
			case 'post_meta':
1283
				$object_table = $wpdb->postmeta;
1284
				$where_sql    = Settings::get_whitelisted_post_meta_sql();
1285
				$object_count = $this->meta_count( $object_table, 'post_id', $where_sql, $start_id, $end_id );
1286
				$id_field     = 'post_id';
1287
				break;
1288
			case 'comments':
1289
				$object_count = $this->comment_count( null, $start_id, $end_id );
1290
				$object_table = $wpdb->comments;
1291
				$id_field     = 'comment_ID';
1292
				$where_sql    = Settings::get_comments_filter_sql();
1293
				break;
1294
			case 'comment_meta':
1295
				$object_table = $wpdb->commentmeta;
1296
				$where_sql    = Settings::get_whitelisted_comment_meta_sql();
1297
				$object_count = $this->meta_count( $object_table, 'comment_id', $where_sql, $start_id, $end_id );
1298
				$id_field     = 'meta_id';
1299
				break;
1300
			case 'terms':
1301
				$object_table = $wpdb->terms;
1302
				$object_count = $this->term_count();
1303
				$id_field     = 'term_id';
1304
				$where_sql    = '1=1';
1305
				break;
1306
			case 'term_taxonomy':
1307
				$object_table = $wpdb->term_taxonomy;
1308
				$object_count = $this->term_taxonomy_count();
1309
				$id_field     = 'term_taxonomy_id';
1310
				$where_sql    = '1=1';
1311
				break;
1312
			case 'term_relationships':
1313
				$object_table = $wpdb->term_relationships;
1314
				$object_count = $this->term_relationship_count();
1315
				$id_field     = 'object_id';
1316
				$where_sql    = '1=1';
1317
				break;
1318
			default:
1319
				return false;
1320
		}
1321
1322
		$bucket_size     = intval( ceil( $object_count / $buckets ) );
1323
		$previous_max_id = 0;
1324
		$histogram       = array();
1325
1326
		// This is used for the min / max query, while $where_sql is used for the checksum query.
1327
		$where = $where_sql;
1328
1329
		if ( $start_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $start_id of type integer|null is loosely compared to true; 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...
1330
			$where .= " AND $id_field >= " . intval( $start_id );
1331
		}
1332
1333
		if ( $end_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $end_id of type integer|null is loosely compared to true; 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...
1334
			$where .= " AND $id_field <= " . intval( $end_id );
1335
		}
1336
1337
		do {
1338
			$result = $this->get_min_max_object_id(
1339
				$id_field,
1340
				$object_table,
1341
				$where . " AND $id_field > $previous_max_id",
1342
				$bucket_size
1343
			);
1344
1345
			if ( null === $result->min || null === $result->max ) {
1346
				// Nothing to checksum here...
1347
				break;
1348
			}
1349
1350
			// Get the checksum value.
1351
			$value = $this->table_checksum( $object_table, $columns, $id_field, $where_sql, $result->min, $result->max, $strip_non_ascii, $salt );
0 ignored issues
show
Security Bug introduced by
It seems like $columns can also be of type false; however, Automattic\Jetpack\Sync\...store::table_checksum() does only seem to accept array, did you maybe forget to handle an error condition?
Loading history...
1352
1353
			if ( is_wp_error( $value ) ) {
1354
				return $value;
1355
			}
1356
1357
			if ( null === $result->min || null === $result->max ) {
1358
				break;
1359
			} elseif ( $result->min === $result->max ) {
1360
				$histogram[ $result->min ] = $value;
1361
			} else {
1362
				$histogram[ "{$result->min}-{$result->max}" ] = $value;
1363
			}
1364
1365
			$previous_max_id = $result->max;
1366
		} while ( true );
1367
1368
		return $histogram;
1369
	}
1370
1371
	/**
1372
	 * Retrieve the checksum for a specific database table.
1373
	 *
1374
	 * @access private
1375
	 *
1376
	 * @todo Refactor to properly prepare the SQL query.
1377
	 *
1378
	 * @param string $table           Table name.
1379
	 * @param array  $columns         Table columns to calculate the checksum from.
1380
	 * @param int    $id_column       Name of the unique ID column.
1381
	 * @param string $where_sql       Additional WHERE clause SQL.
1382
	 * @param int    $min_id          Minimum object ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $min_id not be integer|null?

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...
1383
	 * @param int    $max_id          Maximum object ID.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $max_id not be integer|null?

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...
1384
	 * @param bool   $strip_non_ascii Whether to strip non-ASCII characters.
1385
	 * @param string $salt            Salt, used for $wpdb->prepare()'s args.
1386
	 * @return int|\WP_Error The table histogram, or \WP_Error on failure.
1387
	 */
1388
	private function table_checksum( $table, $columns, $id_column, $where_sql = '1=1', $min_id = null, $max_id = null, $strip_non_ascii = true, $salt = '' ) {
1389
		global $wpdb;
1390
1391
		// Sanitize to just valid MySQL column names.
1392
		$sanitized_columns = preg_grep( '/^[0-9,a-z,A-Z$_]+$/i', $columns );
1393
1394
		if ( $strip_non_ascii ) {
1395
			$sanitized_columns = array_map( array( $this, 'strip_non_ascii_sql' ), $sanitized_columns );
1396
		}
1397
1398
		$columns_sql = implode( ',', $sanitized_columns );
1399
1400
		$limits_sql = '';
1401
1402
		if ( null !== $min_id && null !== $max_id ) {
1403
			if ( $min_id === $max_id ) {
1404
				$min_id     = intval( $min_id );
1405
				$where_sql .= " AND $id_column = $min_id LIMIT 1";
1406
			} else {
1407
				$min_id      = intval( $min_id );
1408
				$max_id      = intval( $max_id );
1409
				$size        = $max_id - $min_id;
1410
				$where_sql  .= " AND $id_column >= $min_id AND $id_column <= $max_id";
1411
				$limits_sql .= " LIMIT $size";
1412
			}
1413
		} else {
1414
			if ( null !== $min_id ) {
1415
				$min_id     = intval( $min_id );
1416
				$where_sql .= " AND $id_column >= $min_id";
1417
			}
1418
1419
			if ( null !== $max_id ) {
1420
				$max_id     = intval( $max_id );
1421
				$where_sql .= " AND $id_column <= $max_id";
1422
			}
1423
		}
1424
1425
		if ( in_array( $table, $this->get_meta_tables() ) ) {
1426
			/**
1427
			 * TODO:
1428
			 *  We need a way to specify what the meta columns to order by are.
1429
			 *  Or we can rely on the right one being always first.
1430
			 *  In WordPress the meta tables always have the same structure - `meta_key`, `meta_value` and
1431
			 *  a link to the primary object/table they relate to. We can hardcode `meta_key` here, but
1432
			 *  it won't be very flexible if we want to do this for custom tables or offer it to other plugins
1433
			 *  in the future.
1434
			 */
1435
			/**
1436
			 * Ensure that the results are always ordered the same when calculating the checksum.
1437
			 *
1438
			 * To do this, the concatenated values are ordered by `meta_key` before calculating their checksum.
1439
			 */
1440
			$sort_column = $sanitized_columns[0];
1441
1442
			$query = "
1443
				SELECT
1444
				       CAST( SUM(meta_checksum) AS UNSIGNED INT)
1445
				FROM
1446
				     (
1447
						SELECT
1448
						    {$id_column},
1449
						    CRC32( GROUP_CONCAT(
1450
						        CONCAT_WS( '#', '%s',  {$columns_sql} )
1451
						        ORDER BY {$sort_column} ASC
1452
						        SEPARATOR ';'
1453
						    ) ) AS meta_checksum
1454
						FROM
1455
						    {$table}
1456
						WHERE
1457
							{$where_sql}
1458
						GROUP BY {$id_column}
1459
						{$limits_sql}
1460
					) as meta_checksums
1461
			";
1462
		} else {
1463
			$query = "
1464
				SELECT
1465
					CAST( SUM( CRC32( CONCAT_WS( '#', '%s', {$columns_sql} ) ) ) AS UNSIGNED INT )
1466
				FROM
1467
					$table
1468
				WHERE
1469
					$where_sql
1470
				{$limits_sql}
1471
			";
1472
		}
1473
1474
		$query  = $wpdb->prepare( $query, $salt );
1475
		$result = $wpdb->get_var( $query );
1476
1477
		if ( $wpdb->last_error ) {
1478
			return new \WP_Error( 'database_error', $wpdb->last_error );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'database_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1479
		}
1480
1481
		return $result;
1482
	}
1483
1484
	/**
1485
	 * Retrieve the type of the checksum.
1486
	 *
1487
	 * @access public
1488
	 *
1489
	 * @return string Type of the checksum.
1490
	 */
1491
	public function get_checksum_type() {
1492
		return 'sum';
1493
	}
1494
1495
	/**
1496
	 * Gets a list of the site's meta tables.
1497
	 *
1498
	 * Used to check if a table that we work on is a meta table.
1499
	 *
1500
	 * @return array List of meta tables for the site.
1501
	 */
1502
	public function get_meta_tables() {
1503
		global $wpdb;
1504
		return array( $wpdb->postmeta, $wpdb->commentmeta, $wpdb->termmeta, $wpdb->usermeta );
1505
	}
1506
1507
	/**
1508
	 * Count the meta values in a table, within a specified range.
1509
	 *
1510
	 * @access private
1511
	 *
1512
	 * @param string $table                   Table name.
1513
	 * @param string $parent_object_id_column The meta table parent object ID column
1514
	 * @param string $where_sql               Additional WHERE SQL.
1515
	 * @param int    $min_id                  Minimum meta ID.
1516
	 * @param int    $max_id                  Maximum meta ID.
1517
	 *
1518
	 * @return int Number of meta values.
1519
	 */
1520
	private function meta_count( $table, $parent_object_id_column, $where_sql, $min_id, $max_id ) {
1521
		global $wpdb;
1522
1523
		$replacements = array();
1524
1525
		if ( ! empty( $min_id ) ) {
1526
			$where_sql     .= " AND $parent_object_id_column >= %d ";
1527
			$replacements[] = intval( $min_id );
1528
		}
1529
1530
		if ( ! empty( $max_id ) ) {
1531
			$where_sql     .= " AND $parent_object_id_column <= %d ";
1532
			$replacements[] = intval( $max_id );
1533
		}
1534
1535
		$query_string = "
1536
			SELECT
1537
				COUNT(DISTINCT $parent_object_id_column)
1538
			FROM
1539
				$table
1540
			WHERE
1541
				$where_sql
1542
		";
1543
1544
		$query = $wpdb->prepare( $query_string, $replacements );
1545
1546
		return $wpdb->get_var( $query );
1547
	}
1548
1549
	/**
1550
	 * Wraps a column name in SQL which strips non-ASCII chars.
1551
	 * This helps normalize data to avoid checksum differences caused by
1552
	 * badly encoded data in the DB.
1553
	 *
1554
	 * @param string $column_name Name of the column.
1555
	 * @return string Column name, without the non-ASCII chars.
1556
	 */
1557
	public function strip_non_ascii_sql( $column_name ) {
1558
		return "REPLACE( CONVERT( $column_name USING ascii ), '?', '' )";
1559
	}
1560
1561
	/**
1562
	 * Used in methods that are not implemented and shouldn't be invoked.
1563
	 *
1564
	 * @access private
1565
	 * @throws \Exception If this method is invoked.
1566
	 */
1567
	private function invalid_call() {
1568
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
1569
		$backtrace = debug_backtrace();
1570
		$caller    = $backtrace[1]['function'];
1571
		throw new \Exception( "This function $caller is not supported on the WP Replicastore" );
1572
	}
1573
}
1574