Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.25% covered (danger)
0.25%
1 / 396
0.00% covered (danger)
0.00%
0 / 68
CRAP
0.00% covered (danger)
0.00%
0 / 1
Replicastore
0.00% covered (danger)
0.00%
0 / 394
0.00% covered (danger)
0.00%
0 / 68
22052
0.00% covered (danger)
0.00%
0 / 1
 reset
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 full_sync_start
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 full_sync_end
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 term_count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 term_taxonomy_count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 term_relationship_count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 post_count
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 get_posts
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 get_post
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 upsert_post
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
20
 delete_post
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 posts_checksum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 post_meta_checksum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 comment_count
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 comment_status_to_approval_value
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
132
 get_comments
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_comment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 upsert_comment
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 trash_comment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 delete_comment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 spam_comment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 trashed_post_comments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 untrashed_post_comments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 comments_checksum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 comment_meta_checksum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update_option
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_option
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 delete_option
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_theme_info
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 current_theme_supports
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_metadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 upsert_metadata
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 delete_metadata
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 delete_batch_metadata
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 delete_metadata_by_key_value
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 get_constant
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 set_constant
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_updates
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 set_updates
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_callable
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 set_callable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_site_option
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update_site_option
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 delete_site_option
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_terms
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 get_term
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 ensure_taxonomy
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 get_the_terms
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update_term
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
6
 delete_term
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 update_object_terms
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 delete_object_terms
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 user_count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_user
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 upsert_user
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 delete_user
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 upsert_user_locale
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 delete_user_locale
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_user_locale
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_allowed_mime_types
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checksum_all
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 summarize_checksum_histogram
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_min_max_object_id
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 checksum_histogram
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
210
 get_checksum_type
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 invalid_call
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 calculate_buckets
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 get_table_checksum_instance
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Sync replicastore.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync;
9
10use Automattic\Jetpack\Sync\Replicastore\Table_Checksum;
11use Automattic\Jetpack\Sync\Replicastore\Table_Checksum_Usermeta;
12use Automattic\Jetpack\Sync\Replicastore\Table_Checksum_Users;
13use Exception;
14use WP_Error;
15
16if ( ! defined( 'ABSPATH' ) ) {
17    exit( 0 );
18}
19
20/**
21 * An implementation of Replicastore Interface which returns data stored in a WordPress.org DB.
22 * This is useful to compare values in the local WP DB to values in the synced replica store
23 */
24class Replicastore implements Replicastore_Interface {
25    /**
26     * Empty and reset the replicastore.
27     *
28     * @access public
29     */
30    public function reset() {
31        global $wpdb;
32
33        $wpdb->query( "DELETE FROM $wpdb->posts" );
34
35        // Delete comments from cache.
36        $comment_ids = $wpdb->get_col( "SELECT comment_ID FROM $wpdb->comments" );
37        if ( ! empty( $comment_ids ) ) {
38            clean_comment_cache( $comment_ids );
39        }
40        $wpdb->query( "DELETE FROM $wpdb->comments" );
41
42        // Also need to delete terms from cache.
43        $term_ids = $wpdb->get_col( "SELECT term_id FROM $wpdb->terms" );
44        foreach ( $term_ids as $term_id ) {
45            wp_cache_delete( $term_id, 'terms' );
46        }
47
48        $wpdb->query( "DELETE FROM $wpdb->terms" );
49
50        $wpdb->query( "DELETE FROM $wpdb->term_taxonomy" );
51        $wpdb->query( "DELETE FROM $wpdb->term_relationships" );
52
53        // Callables and constants.
54        $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'jetpack_%'" );
55        $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%'" );
56    }
57
58    /**
59     * Ran when full sync has just started.
60     *
61     * @access public
62     *
63     * @param array $config Full sync configuration for this sync module.
64     */
65    public function full_sync_start( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
66        $this->reset();
67    }
68
69    /**
70     * Ran when full sync has just finished.
71     *
72     * @access public
73     *
74     * @param string $checksum Deprecated since 7.3.0.
75     */
76    public function full_sync_end( $checksum ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
77        // Noop right now.
78    }
79
80    /**
81     * Retrieve the number of terms.
82     *
83     * @access public
84     *
85     * @return int Number of terms.
86     */
87    public function term_count() {
88        global $wpdb;
89        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
90        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->terms" );
91    }
92
93    /**
94     * Retrieve the number of rows in the `term_taxonomy` table.
95     *
96     * @access public
97     *
98     * @return int Number of terms.
99     */
100    public function term_taxonomy_count() {
101        global $wpdb;
102        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
103        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->term_taxonomy" );
104    }
105
106    /**
107     * Retrieve the number of term relationships.
108     *
109     * @access public
110     *
111     * @return int Number of rows in the term relationships table.
112     */
113    public function term_relationship_count() {
114        global $wpdb;
115        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
116        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->term_relationships" );
117    }
118
119    /**
120     * Retrieve the number of posts with a particular post status within a certain range.
121     *
122     * @access public
123     *
124     * @todo Prepare the SQL query before executing it.
125     *
126     * @param string $status Post status.
127     * @param int    $min_id Minimum post ID.
128     * @param int    $max_id Maximum post ID.
129     * @return int Number of posts.
130     */
131    public function post_count( $status = null, $min_id = null, $max_id = null ) {
132        global $wpdb;
133
134        $where = '';
135
136        if ( $status ) {
137            $where = "post_status = '" . esc_sql( $status ) . "'";
138        } else {
139            $where = '1=1';
140        }
141
142        if ( ! empty( $min_id ) ) {
143            $where .= ' AND ID >= ' . (int) $min_id;
144        }
145
146        if ( ! empty( $max_id ) ) {
147            $where .= ' AND ID <= ' . (int) $max_id;
148        }
149
150        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
151        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE $where" );
152    }
153
154    /**
155     * Retrieve the posts with a particular post status.
156     *
157     * @access public
158     *
159     * @todo Implement range and actually use max_id/min_id arguments.
160     *
161     * @param string $status Post status.
162     * @param int    $min_id Minimum post ID.
163     * @param int    $max_id Maximum post ID.
164     * @return array Array of posts.
165     */
166    public function get_posts( $status = null, $min_id = null, $max_id = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
167        $args = array(
168            'orderby'        => 'ID',
169            'posts_per_page' => -1,
170        );
171
172        if ( $status ) {
173            $args['post_status'] = $status;
174        } else {
175            $args['post_status'] = 'any';
176        }
177
178        return get_posts( $args );
179    }
180
181    /**
182     * Retrieve a post object by the post ID.
183     *
184     * @access public
185     *
186     * @param int $id Post ID.
187     * @return \WP_Post Post object.
188     */
189    public function get_post( $id ) {
190        return get_post( $id );
191    }
192
193    /**
194     * Update or insert a post.
195     *
196     * @access public
197     *
198     * @param \WP_Post $post   Post object.
199     * @param bool     $silent Whether to perform a silent action. Not used in this implementation.
200     */
201    public function upsert_post( $post, $silent = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
202        global $wpdb;
203
204        // Reject the post if it's not a \WP_Post.
205        if ( ! $post instanceof \WP_Post ) {
206            return;
207        }
208
209        $post = $post->to_array();
210
211        // Reject posts without an ID.
212        if ( ! isset( $post['ID'] ) ) {
213            return;
214        }
215
216        $now     = current_time( 'mysql' );
217        $now_gmt = get_gmt_from_date( $now );
218
219        $defaults = array(
220            'ID'                    => 0,
221            'post_author'           => '0',
222            'post_content'          => '',
223            'post_content_filtered' => '',
224            'post_title'            => '',
225            'post_name'             => '',
226            'post_excerpt'          => '',
227            'post_status'           => 'draft',
228            'post_type'             => 'post',
229            'comment_status'        => 'closed',
230            'comment_count'         => '0',
231            'ping_status'           => '',
232            'post_password'         => '',
233            'to_ping'               => '',
234            'pinged'                => '',
235            'post_parent'           => 0,
236            'menu_order'            => 0,
237            'guid'                  => '',
238            'post_date'             => $now,
239            'post_date_gmt'         => $now_gmt,
240            'post_modified'         => $now,
241            'post_modified_gmt'     => $now_gmt,
242        );
243
244        $post = array_intersect_key( $post, $defaults );
245
246        $post = sanitize_post( $post, 'db' );
247
248        unset( $post['filter'] );
249
250        $exists = $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM $wpdb->posts WHERE ID = %d )", $post['ID'] ) );
251
252        if ( $exists ) {
253            $wpdb->update( $wpdb->posts, $post, array( 'ID' => $post['ID'] ) );
254        } else {
255            $wpdb->insert( $wpdb->posts, $post );
256        }
257
258        clean_post_cache( $post['ID'] );
259    }
260
261    /**
262     * Delete a post by the post ID.
263     *
264     * @access public
265     *
266     * @param int $post_id Post ID.
267     */
268    public function delete_post( $post_id ) {
269        wp_delete_post( $post_id, true );
270    }
271
272    /**
273     * Retrieve the checksum for posts within a range.
274     *
275     * @access public
276     *
277     * @param int $min_id Minimum post ID.
278     * @param int $max_id Maximum post ID.
279     * @return int The checksum.
280     */
281    public function posts_checksum( $min_id = null, $max_id = null ) {
282        return $this->summarize_checksum_histogram( $this->checksum_histogram( 'posts', null, $min_id, $max_id ) );
283    }
284
285    /**
286     * Retrieve the checksum for post meta within a range.
287     *
288     * @access public
289     *
290     * @param int $min_id Minimum post meta ID.
291     * @param int $max_id Maximum post meta ID.
292     * @return int The checksum.
293     */
294    public function post_meta_checksum( $min_id = null, $max_id = null ) {
295        return $this->summarize_checksum_histogram( $this->checksum_histogram( 'postmeta', null, $min_id, $max_id ) );
296    }
297
298    /**
299     * Retrieve the number of comments with a particular comment status within a certain range.
300     *
301     * @access public
302     *
303     * @todo Prepare the SQL query before executing it.
304     *
305     * @param string $status Comment status.
306     * @param int    $min_id Minimum comment ID.
307     * @param int    $max_id Maximum comment ID.
308     * @return int Number of comments.
309     */
310    public function comment_count( $status = null, $min_id = null, $max_id = null ) {
311        global $wpdb;
312
313        $comment_approved = $this->comment_status_to_approval_value( $status );
314
315        if ( false !== $comment_approved ) {
316            $where = "comment_approved = '" . esc_sql( $comment_approved ) . "'";
317        } else {
318            $where = '1=1';
319        }
320
321        if ( ! empty( $min_id ) ) {
322            $where .= ' AND comment_ID >= ' . (int) $min_id;
323        }
324
325        if ( ! empty( $max_id ) ) {
326            $where .= ' AND comment_ID <= ' . (int) $max_id;
327        }
328
329        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
330        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE $where" );
331    }
332
333    /**
334     * Translate a comment status to a value of the comment_approved field.
335     *
336     * @access protected
337     *
338     * @param string $status Comment status.
339     * @return string|bool New comment_approved value, false if the status doesn't affect it.
340     */
341    protected function comment_status_to_approval_value( $status ) {
342        switch ( (string) $status ) {
343            case 'approve':
344            case '1':
345                return '1';
346            case 'hold':
347            case '0':
348                return '0';
349            case 'spam':
350                return 'spam';
351            case 'trash':
352                return 'trash';
353            case 'post-trashed':
354                return 'post-trashed';
355            case 'any':
356            case 'all':
357            default:
358                return false;
359        }
360    }
361
362    /**
363     * Retrieve the comments with a particular comment status.
364     *
365     * @access public
366     *
367     * @todo Implement range and actually use max_id/min_id arguments.
368     *
369     * @param string $status Comment status.
370     * @param int    $min_id Minimum comment ID.
371     * @param int    $max_id Maximum comment ID.
372     * @return array Array of comments.
373     */
374    public function get_comments( $status = null, $min_id = null, $max_id = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
375        $args = array(
376            'orderby' => 'ID',
377            'status'  => 'all',
378        );
379
380        if ( $status ) {
381            $args['status'] = $status;
382        }
383
384        return get_comments( $args );
385    }
386
387    /**
388     * Retrieve a comment object by the comment ID.
389     *
390     * @access public
391     *
392     * @param int $id Comment ID.
393     * @return \WP_Comment Comment object.
394     */
395    public function get_comment( $id ) {
396        return \WP_Comment::get_instance( $id );
397    }
398
399    /**
400     * Update or insert a comment.
401     *
402     * @access public
403     *
404     * @param \WP_Comment $comment Comment object.
405     */
406    public function upsert_comment( $comment ) {
407        global $wpdb;
408
409        $comment = $comment->to_array();
410
411        // Filter by fields on comment table.
412        $comment_fields_whitelist = array(
413            'comment_ID',
414            'comment_post_ID',
415            'comment_author',
416            'comment_author_email',
417            'comment_author_url',
418            'comment_author_IP',
419            'comment_date',
420            'comment_date_gmt',
421            'comment_content',
422            'comment_karma',
423            'comment_approved',
424            'comment_agent',
425            'comment_type',
426            'comment_parent',
427            'user_id',
428        );
429
430        foreach ( $comment as $key => $value ) {
431            if ( ! in_array( $key, $comment_fields_whitelist, true ) ) {
432                unset( $comment[ $key ] );
433            }
434        }
435
436        $exists = $wpdb->get_var(
437            $wpdb->prepare(
438                "SELECT EXISTS( SELECT 1 FROM $wpdb->comments WHERE comment_ID = %d )",
439                $comment['comment_ID']
440            )
441        );
442
443        if ( $exists ) {
444            $wpdb->update( $wpdb->comments, $comment, array( 'comment_ID' => $comment['comment_ID'] ) );
445        } else {
446            $wpdb->insert( $wpdb->comments, $comment );
447        }
448        // Remove comment from cache.
449        clean_comment_cache( $comment['comment_ID'] );
450
451        wp_update_comment_count( $comment['comment_post_ID'] );
452    }
453
454    /**
455     * Trash a comment by the comment ID.
456     *
457     * @access public
458     *
459     * @param int $comment_id Comment ID.
460     */
461    public function trash_comment( $comment_id ) {
462        wp_delete_comment( $comment_id );
463    }
464
465    /**
466     * Delete a comment by the comment ID.
467     *
468     * @access public
469     *
470     * @param int $comment_id Comment ID.
471     */
472    public function delete_comment( $comment_id ) {
473        wp_delete_comment( $comment_id, true );
474    }
475
476    /**
477     * Mark a comment by the comment ID as spam.
478     *
479     * @access public
480     *
481     * @param int $comment_id Comment ID.
482     */
483    public function spam_comment( $comment_id ) {
484        wp_spam_comment( $comment_id );
485    }
486
487    /**
488     * Trash the comments of a post.
489     *
490     * @access public
491     *
492     * @param int   $post_id  Post ID.
493     * @param array $statuses Post statuses. Not used in this implementation.
494     */
495    public function trashed_post_comments( $post_id, $statuses ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
496        wp_trash_post_comments( $post_id );
497    }
498
499    /**
500     * Untrash the comments of a post.
501     *
502     * @access public
503     *
504     * @param int $post_id Post ID.
505     */
506    public function untrashed_post_comments( $post_id ) {
507        wp_untrash_post_comments( $post_id );
508    }
509
510    /**
511     * Retrieve the checksum for comments within a range.
512     *
513     * @access public
514     *
515     * @param int $min_id Minimum comment ID.
516     * @param int $max_id Maximum comment ID.
517     * @return int The checksum.
518     */
519    public function comments_checksum( $min_id = null, $max_id = null ) {
520        return $this->summarize_checksum_histogram( $this->checksum_histogram( 'comments', null, $min_id, $max_id ) );
521    }
522
523    /**
524     * Retrieve the checksum for comment meta within a range.
525     *
526     * @access public
527     *
528     * @param int $min_id Minimum comment meta ID.
529     * @param int $max_id Maximum comment meta ID.
530     * @return int The checksum.
531     */
532    public function comment_meta_checksum( $min_id = null, $max_id = null ) {
533        return $this->summarize_checksum_histogram( $this->checksum_histogram( 'commentmeta', null, $min_id, $max_id ) );
534    }
535
536    /**
537     * Update the value of an option.
538     *
539     * @access public
540     *
541     * @param string $option Option name.
542     * @param mixed  $value  Option value.
543     * @return bool False if value was not updated and true if value was updated.
544     */
545    public function update_option( $option, $value ) {
546        return update_option( $option, $value );
547    }
548
549    /**
550     * Retrieve an option value based on an option name.
551     *
552     * @access public
553     *
554     * @param string $option  Name of option to retrieve.
555     * @param mixed  $default Optional. Default value to return if the option does not exist.
556     * @return mixed Value set for the option.
557     */
558    public function get_option( $option, $default = false ) {
559        return get_option( $option, $default );
560    }
561
562    /**
563     * Remove an option by name.
564     *
565     * @access public
566     *
567     * @param string $option Name of option to remove.
568     * @return bool True, if option is successfully deleted. False on failure.
569     */
570    public function delete_option( $option ) {
571        return delete_option( $option );
572    }
573
574    /**
575     * Change the info of the current theme.
576     *
577     * @access public
578     *
579     * @param array $theme_info Theme info array.
580     */
581    public function set_theme_info( $theme_info ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
582        // Noop.
583    }
584
585    /**
586     * Whether the current theme supports a certain feature.
587     *
588     * @access public
589     *
590     * @param string $feature Name of the feature.
591     */
592    public function current_theme_supports( $feature ) {
593        return current_theme_supports( $feature );
594    }
595
596    /**
597     * Retrieve metadata for the specified object.
598     *
599     * @access public
600     *
601     * @param string $type       Meta type.
602     * @param int    $object_id  ID of the object.
603     * @param string $meta_key   Meta key.
604     * @param bool   $single     If true, return only the first value of the specified meta_key.
605     *
606     * @return mixed Single metadata value, or array of values.
607     */
608    public function get_metadata( $type, $object_id, $meta_key = '', $single = false ) {
609        return get_metadata( $type, $object_id, $meta_key, $single );
610    }
611
612    /**
613     * Stores remote meta key/values alongside an ID mapping key.
614     *
615     * @access public
616     *
617     * @todo Refactor to not use interpolated values when preparing the SQL query.
618     *
619     * @param string $type       Meta type.
620     * @param int    $object_id  ID of the object.
621     * @param string $meta_key   Meta key.
622     * @param mixed  $meta_value Meta value.
623     * @param int    $meta_id    ID of the meta.
624     *
625     * @return bool False if meta table does not exist, true otherwise.
626     */
627    public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ) {
628        $table = _get_meta_table( $type );
629        if ( ! $table ) {
630            return false;
631        }
632
633        global $wpdb;
634
635        $exists = $wpdb->get_var(
636            $wpdb->prepare(
637                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
638                "SELECT EXISTS( SELECT 1 FROM $table WHERE meta_id = %d )",
639                $meta_id
640            )
641        );
642
643        if ( $exists ) {
644            $wpdb->update(
645                $table,
646                array(
647                    'meta_key'   => $meta_key,
648                    'meta_value' => maybe_serialize( $meta_value ),
649                ),
650                array( 'meta_id' => $meta_id )
651            );
652        } else {
653            $object_id_field = $type . '_id';
654            $wpdb->insert(
655                $table,
656                array(
657                    'meta_id'        => $meta_id,
658                    $object_id_field => $object_id,
659                    'meta_key'       => $meta_key,
660                    'meta_value'     => maybe_serialize( $meta_value ),
661                )
662            );
663        }
664
665        wp_cache_delete( $object_id, $type . '_meta' );
666
667        return true;
668    }
669
670    /**
671     * Delete metadata for the specified object.
672     *
673     * @access public
674     *
675     * @todo Refactor to not use interpolated values when preparing the SQL query.
676     *
677     * @param string $type      Meta type.
678     * @param int    $object_id ID of the object.
679     * @param array  $meta_ids  IDs of the meta objects to delete.
680     */
681    public function delete_metadata( $type, $object_id, $meta_ids ) {
682        global $wpdb;
683
684        $table = _get_meta_table( $type );
685        if ( ! $table ) {
686            return false;
687        }
688
689        foreach ( $meta_ids as $meta_id ) {
690            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
691            $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE meta_id = %d", $meta_id ) );
692        }
693
694        // If we don't have an object ID what do we do - invalidate ALL meta?
695        if ( $object_id ) {
696            wp_cache_delete( $object_id, $type . '_meta' );
697        }
698    }
699
700    /**
701     * Delete metadata with a certain key for the specified objects.
702     *
703     * @access public
704     *
705     * @todo Test this out to make sure it works as expected.
706     * @todo Refactor to not use interpolated values when preparing the SQL query.
707     *
708     * @param string $type       Meta type.
709     * @param array  $object_ids IDs of the objects.
710     * @param string $meta_key   Meta key.
711     */
712    public function delete_batch_metadata( $type, $object_ids, $meta_key ) {
713        global $wpdb;
714
715        $table = _get_meta_table( $type );
716        if ( ! $table ) {
717            return false;
718        }
719        $column = sanitize_key( $type . '_id' );
720        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
721        $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE $column IN (%s) && meta_key = %s", implode( ',', $object_ids ), $meta_key ) );
722
723        // If we don't have an object ID what do we do - invalidate ALL meta?
724        foreach ( $object_ids as $object_id ) {
725            wp_cache_delete( $object_id, $type . '_meta' );
726        }
727    }
728
729    /**
730     * Delete metadata with a certain key and value for all objects.
731     *
732     * @access public
733     *
734     * @param string $type       Meta type.
735     * @param string $meta_key   Meta key.
736     * @param mixed  $meta_value Meta value to match.
737     */
738    public function delete_metadata_by_key_value( $type, $meta_key, $meta_value ) {
739        global $wpdb;
740
741        $table = _get_meta_table( $type );
742        if ( ! $table ) {
743            return false;
744        }
745
746        if ( '' === $meta_value || null === $meta_value || false === $meta_value ) {
747            return false;
748        }
749
750        $column     = sanitize_key( $type . '_id' );
751        $meta_value = maybe_serialize( $meta_value );
752
753        $object_ids = $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT %i FROM %i WHERE meta_key = %s AND meta_value = %s', $column, $table, $meta_key, $meta_value ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct replica query is needed to identify caches invalidated below.
754        $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Direct replica mutation.
755            $table,
756            array(
757                'meta_key'   => $meta_key,
758                'meta_value' => $meta_value,
759            ),
760            array( '%s', '%s' )
761        );
762
763        foreach ( $object_ids as $object_id ) {
764            wp_cache_delete( $object_id, $type . '_meta' );
765        }
766    }
767
768    /**
769     * Retrieve value of a constant based on the constant name.
770     *
771     * We explicitly return null instead of false if the constant doesn't exist.
772     *
773     * @access public
774     *
775     * @param string $constant Name of constant to retrieve.
776     * @return mixed Value set for the constant.
777     */
778    public function get_constant( $constant ) {
779        $value = get_option( 'jetpack_constant_' . $constant );
780
781        if ( $value ) {
782            return $value;
783        }
784
785        return null;
786    }
787
788    /**
789     * Set the value of a constant.
790     *
791     * @access public
792     *
793     * @param string $constant Name of constant to retrieve.
794     * @param mixed  $value    Value set for the constant.
795     */
796    public function set_constant( $constant, $value ) {
797        update_option( 'jetpack_constant_' . $constant, $value );
798    }
799
800    /**
801     * Retrieve the number of the available updates of a certain type.
802     * Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`.
803     *
804     * @access public
805     *
806     * @param string $type Type of updates to retrieve.
807     * @return int|null Number of updates available, `null` if type is invalid or missing.
808     */
809    public function get_updates( $type ) {
810        $all_updates = get_option( 'jetpack_updates', array() );
811
812        if ( isset( $all_updates[ $type ] ) ) {
813            return $all_updates[ $type ];
814        } else {
815            return null;
816        }
817    }
818
819    /**
820     * Set the available updates of a certain type.
821     * Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`.
822     *
823     * @access public
824     *
825     * @param string $type    Type of updates to set.
826     * @param int    $updates Total number of updates.
827     */
828    public function set_updates( $type, $updates ) {
829        $all_updates          = get_option( 'jetpack_updates', array() );
830        $all_updates[ $type ] = $updates;
831        update_option( 'jetpack_updates', $all_updates );
832    }
833
834    /**
835     * Retrieve a callable value based on its name.
836     *
837     * @access public
838     *
839     * @param string $name Name of the callable to retrieve.
840     * @return mixed Value of the callable.
841     */
842    public function get_callable( $name ) {
843        $value = get_option( 'jetpack_' . $name );
844
845        if ( $value ) {
846            return $value;
847        }
848
849        return null;
850    }
851
852    /**
853     * Update the value of a callable.
854     *
855     * @access public
856     *
857     * @param string $name  Callable name.
858     * @param mixed  $value Callable value.
859     */
860    public function set_callable( $name, $value ) {
861        update_option( 'jetpack_' . $name, $value );
862    }
863
864    /**
865     * Retrieve a network option value based on a network option name.
866     *
867     * @access public
868     *
869     * @param string $option Name of network option to retrieve.
870     * @return mixed Value set for the network option.
871     */
872    public function get_site_option( $option ) {
873        return get_option( 'jetpack_network_' . $option );
874    }
875
876    /**
877     * Update the value of a network option.
878     *
879     * @access public
880     *
881     * @param string $option Network option name.
882     * @param mixed  $value  Network option value.
883     * @return bool False if value was not updated and true if value was updated.
884     */
885    public function update_site_option( $option, $value ) {
886        return update_option( 'jetpack_network_' . $option, $value );
887    }
888
889    /**
890     * Remove a network option by name.
891     *
892     * @access public
893     *
894     * @param string $option Name of option to remove.
895     * @return bool True, if option is successfully deleted. False on failure.
896     */
897    public function delete_site_option( $option ) {
898        return delete_option( 'jetpack_network_' . $option );
899    }
900
901    /**
902     * Retrieve the terms from a particular taxonomy.
903     *
904     * @access public
905     *
906     * @param string $taxonomy Taxonomy slug.
907     *
908     * @return array|WP_Error Array of terms or WP_Error object on failure.
909     */
910    public function get_terms( $taxonomy ) {
911        $t = $this->ensure_taxonomy( $taxonomy );
912        if ( ! $t || is_wp_error( $t ) ) {
913            return $t;
914        }
915        return get_terms( $taxonomy );
916    }
917
918    /**
919     * Retrieve a particular term.
920     *
921     * @access public
922     *
923     * @param string|false $taxonomy   Taxonomy slug.
924     * @param int          $term_id    ID of the term.
925     * @param string       $term_key   ID Field `term_id` or `term_taxonomy_id`.
926     *
927     * @return \WP_Term|WP_Error Term object on success, \WP_Error object on failure.
928     */
929    public function get_term( $taxonomy, $term_id, $term_key = 'term_id' ) {
930
931        // Full Sync will pass false for the $taxonomy so a check for term_taxonomy_id is needed before ensure_taxonomy.
932        if ( 'term_taxonomy_id' === $term_key ) {
933            return get_term_by( 'term_taxonomy_id', $term_id );
934        }
935
936        $t = $this->ensure_taxonomy( $taxonomy );
937        if ( ! $t || is_wp_error( $t ) ) {
938            return $t;
939        }
940
941        return get_term( $term_id, $taxonomy );
942    }
943
944    /**
945     * Verify a taxonomy is legitimate and register it if necessary.
946     *
947     * @access private
948     *
949     * @param string $taxonomy Taxonomy slug.
950     *
951     * @return bool|void|WP_Error True if already exists; void if it was registered; \WP_Error on error.
952     */
953    private function ensure_taxonomy( $taxonomy ) {
954        if ( ! taxonomy_exists( $taxonomy ) ) {
955            // Try re-registering synced taxonomies.
956            $taxonomies = $this->get_callable( 'taxonomies' );
957            if ( ! isset( $taxonomies[ $taxonomy ] ) ) {
958                // Doesn't exist, or somehow hasn't been synced.
959                return new WP_Error( 'invalid_taxonomy', "The taxonomy '$taxonomy' doesn't exist" );
960            }
961            $t = $taxonomies[ $taxonomy ];
962
963            return register_taxonomy(
964                $taxonomy,
965                $t->object_type,
966                (array) $t
967            );
968        }
969
970        return true;
971    }
972
973    /**
974     * Retrieve all terms from a taxonomy that are related to an object with a particular ID.
975     *
976     * @access public
977     *
978     * @param int    $object_id Object ID.
979     * @param string $taxonomy  Taxonomy slug.
980     *
981     * @return array|bool|WP_Error Array of terms on success, `false` if no terms or post doesn't exist, \WP_Error on failure.
982     */
983    public function get_the_terms( $object_id, $taxonomy ) {
984        return get_the_terms( $object_id, $taxonomy );
985    }
986
987    /**
988     * Insert or update a term.
989     *
990     * @access public
991     *
992     * @param \WP_Term $term_object Term object.
993     *
994     * @return array|bool|WP_Error Array of term_id and term_taxonomy_id if updated, true if inserted, \WP_Error on failure.
995     */
996    public function update_term( $term_object ) {
997        $taxonomy = $term_object->taxonomy;
998        global $wpdb;
999        $exists = $wpdb->get_var(
1000            $wpdb->prepare(
1001                "SELECT EXISTS( SELECT 1 FROM $wpdb->terms WHERE term_id = %d )",
1002                $term_object->term_id
1003            )
1004        );
1005        if ( ! $exists ) {
1006            $term_object   = sanitize_term( clone $term_object, $taxonomy, 'db' );
1007            $term          = array(
1008                'term_id'    => $term_object->term_id,
1009                'name'       => $term_object->name,
1010                'slug'       => $term_object->slug,
1011                'term_group' => $term_object->term_group,
1012            );
1013            $term_taxonomy = array(
1014                'term_taxonomy_id' => $term_object->term_taxonomy_id,
1015                'term_id'          => $term_object->term_id,
1016                'taxonomy'         => $term_object->taxonomy,
1017                'description'      => $term_object->description,
1018                'parent'           => (int) $term_object->parent,
1019                'count'            => (int) $term_object->count,
1020            );
1021            $wpdb->insert( $wpdb->terms, $term );
1022            $wpdb->insert( $wpdb->term_taxonomy, $term_taxonomy );
1023
1024            return true;
1025        }
1026
1027        return wp_update_term( $term_object->term_id, $taxonomy, (array) $term_object );
1028    }
1029
1030    /**
1031     * Delete a term by the term ID and its corresponding taxonomy.
1032     *
1033     * @access public
1034     *
1035     * @param int    $term_id  Term ID.
1036     * @param string $taxonomy Taxonomy slug.
1037     *
1038     * @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.
1039     */
1040    public function delete_term( $term_id, $taxonomy ) {
1041        $this->ensure_taxonomy( $taxonomy );
1042        return wp_delete_term( $term_id, $taxonomy );
1043    }
1044
1045    /**
1046     * Add/update terms of a particular taxonomy of an object with the specified ID.
1047     *
1048     * @access public
1049     *
1050     * @param int              $object_id The object to relate to.
1051     * @param string           $taxonomy  The context in which to relate the term to the object.
1052     * @param string|int|array $terms     A single term slug, single term id, or array of either term slugs or ids.
1053     * @param bool             $append    Optional. If false will delete difference of terms. Default false.
1054     */
1055    public function update_object_terms( $object_id, $taxonomy, $terms, $append ) {
1056        $this->ensure_taxonomy( $taxonomy );
1057        wp_set_object_terms( $object_id, $terms, $taxonomy, $append );
1058    }
1059
1060    /**
1061     * Remove certain term relationships from the specified object.
1062     *
1063     * @access public
1064     *
1065     * @todo Refactor to not use interpolated values when preparing the SQL query.
1066     *
1067     * @param int   $object_id ID of the object.
1068     * @param array $tt_ids    Term taxonomy IDs.
1069     * @return bool True on success, false on failure.
1070     */
1071    public function delete_object_terms( $object_id, $tt_ids ) {
1072        global $wpdb;
1073
1074        if ( is_array( $tt_ids ) && ! empty( $tt_ids ) ) {
1075            // Escape.
1076            $tt_ids_sanitized = array_map( 'intval', $tt_ids );
1077
1078            $taxonomies = array();
1079            foreach ( $tt_ids_sanitized as $tt_id ) {
1080                $term                            = get_term_by( 'term_taxonomy_id', $tt_id );
1081                $taxonomies[ $term->taxonomy ][] = $tt_id;
1082            }
1083            $in_tt_ids = implode( ', ', $tt_ids_sanitized );
1084
1085            /**
1086             * Fires immediately before an object-term relationship is deleted.
1087             *
1088             * @since 1.6.3
1089             * @since-jetpack 2.9.0
1090             *
1091             * @param int   $object_id Object ID.
1092             * @param array $tt_ids    An array of term taxonomy IDs.
1093             */
1094            do_action( 'delete_term_relationships', $object_id, $tt_ids_sanitized );
1095            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1096            $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) );
1097            foreach ( $taxonomies as $taxonomy => $taxonomy_tt_ids ) {
1098                $this->ensure_taxonomy( $taxonomy );
1099                wp_cache_delete( $object_id, $taxonomy . '_relationships' );
1100                /**
1101                 * Fires immediately after an object-term relationship is deleted.
1102                 *
1103                 * @since 1.6.3
1104                 * @since-jetpack 2.9.0
1105                 *
1106                 * @param int   $object_id Object ID.
1107                 * @param array $tt_ids    An array of term taxonomy IDs.
1108                 */
1109                do_action( 'deleted_term_relationships', $object_id, $taxonomy_tt_ids );
1110                wp_update_term_count( $taxonomy_tt_ids, $taxonomy );
1111            }
1112
1113            return (bool) $deleted;
1114        }
1115
1116        return false;
1117    }
1118
1119    /**
1120     * Retrieve the number of users.
1121     * Not supported in this replicastore.
1122     *
1123     * @access public
1124     */
1125    public function user_count() {
1126        // Noop.
1127    }
1128
1129    /**
1130     * Retrieve a user object by the user ID.
1131     *
1132     * @access public
1133     *
1134     * @param int $user_id User ID.
1135     * @return \WP_User|null User object, or `null` if user invalid/not found.
1136     */
1137    public function get_user( $user_id ) {
1138        $user = get_user_by( 'id', $user_id );
1139        return $user instanceof \WP_User ? $user : null;
1140    }
1141
1142    /**
1143     * Insert or update a user.
1144     * Not supported in this replicastore.
1145     *
1146     * @access public
1147     * @throws Exception If this method is invoked.
1148     *
1149     * @param \WP_User $user User object.
1150     */
1151    public function upsert_user( $user ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1152        $this->invalid_call();
1153    }
1154
1155    /**
1156     * Delete a user.
1157     * Not supported in this replicastore.
1158     *
1159     * @access public
1160     * @throws Exception If this method is invoked.
1161     *
1162     * @param int $user_id User ID.
1163     */
1164    public function delete_user( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1165        $this->invalid_call();
1166    }
1167
1168    /**
1169     * Update/insert user locale.
1170     * Not supported in this replicastore.
1171     *
1172     * @access public
1173     * @throws Exception If this method is invoked.
1174     *
1175     * @param int    $user_id User ID.
1176     * @param string $local   The user locale.
1177     */
1178    public function upsert_user_locale( $user_id, $local ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1179        $this->invalid_call();
1180    }
1181
1182    /**
1183     * Delete user locale.
1184     * Not supported in this replicastore.
1185     *
1186     * @access public
1187     * @throws Exception If this method is invoked.
1188     *
1189     * @param int $user_id User ID.
1190     */
1191    public function delete_user_locale( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1192        $this->invalid_call();
1193    }
1194
1195    /**
1196     * Retrieve the user locale.
1197     *
1198     * @access public
1199     *
1200     * @param int $user_id User ID.
1201     * @return string The user locale.
1202     */
1203    public function get_user_locale( $user_id ) {
1204        return get_user_locale( $user_id );
1205    }
1206
1207    /**
1208     * Retrieve the allowed mime types for the user.
1209     * Not supported in this replicastore.
1210     *
1211     * @access public
1212     *
1213     * @param int $user_id User ID.
1214     */
1215    public function get_allowed_mime_types( $user_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1216        // Noop.
1217    }
1218
1219    /**
1220     * Retrieve all the checksums we are interested in.
1221     *
1222     * @access public
1223     *
1224     * @param boolean $perform_text_conversion If text fields should be latin1 converted.
1225     *
1226     * @return array Checksums.
1227     */
1228    public function checksum_all( $perform_text_conversion = false ) {
1229        $all_checksum_tables = Table_Checksum::get_allowed_tables();
1230
1231        unset( $all_checksum_tables['users'] ); // Handled separately - TODO.
1232        unset( $all_checksum_tables['usermeta'] ); // Handled separately - TODO.
1233        unset( $all_checksum_tables['termmeta'] ); // Handled separately - TODO.
1234        unset( $all_checksum_tables['links'] ); // Not supported yet.  Consider removing from default config.
1235        unset( $all_checksum_tables['options'] );  // Not supported yet. Consider removing from default config.
1236
1237        $all_checksum_tables = array_unique( array_keys( $all_checksum_tables ) );
1238
1239        $result = array();
1240
1241        foreach ( $all_checksum_tables as $table ) {
1242            $result_key = in_array( $table, array( 'postmeta', 'commentmeta' ), true ) ? str_replace( 'meta', '_meta', $table ) : $table;
1243            try {
1244                $checksum              = $this->checksum_histogram( $table, null, null, null, null, true, '', false, false, $perform_text_conversion );
1245                $result[ $result_key ] = $this->summarize_checksum_histogram( $checksum );
1246            } catch ( Exception $ex ) {
1247                $result[ $result_key ] = null;
1248            }
1249        }
1250
1251        return $result;
1252    }
1253
1254    /**
1255     * Return the summarized checksum from buckets or the WP_Error.
1256     *
1257     * @param array $histogram checksum_histogram result.
1258     *
1259     * @return int|WP_Error checksum or Error.
1260     */
1261    protected function summarize_checksum_histogram( $histogram ) {
1262        if ( is_wp_error( $histogram ) ) {
1263            return $histogram;
1264        } else {
1265            return array_sum( $histogram );
1266        }
1267    }
1268
1269    /**
1270     * Grabs the minimum and maximum object ids for the given parameters.
1271     *
1272     * @access public
1273     *
1274     * @param string $id_field     The id column in the table to query.
1275     * @param string $object_table The table to query.
1276     * @param string $where        A sql where clause without 'WHERE'.
1277     * @param int    $bucket_size  The maximum amount of objects to include in the query.
1278     *                             For `term_relationships` table, the bucket size will refer to the amount
1279     *                             of distinct object ids. This will likely include more database rows than
1280     *                             the bucket size implies.
1281     *
1282     * @return object An object with min_id and max_id properties.
1283     */
1284    public function get_min_max_object_id( $id_field, $object_table, $where, $bucket_size ) {
1285        global $wpdb;
1286
1287        // The term relationship table's unique key is a combination of 2 columns. `DISTINCT` helps us get a more acurate query.
1288        $distinct_sql = ( $wpdb->term_relationships === $object_table ) ? 'DISTINCT' : '';
1289        $where_sql    = $where ? "WHERE $where" : '';
1290
1291        // Since MIN() and MAX() do not work with LIMIT, we'll need to adjust the dataset we query if a limit is present.
1292        // With a limit present, we'll look at a dataset consisting of object_ids that meet the constructs of the $where clause.
1293        // Without a limit, we can use the actual table as a dataset.
1294        $from = $bucket_size ?
1295            "( SELECT $distinct_sql $id_field FROM $object_table $where_sql ORDER BY $id_field ASC LIMIT " . ( (int) $bucket_size ) . ' ) as ids' :
1296            "$object_table $where_sql ORDER BY $id_field ASC";
1297
1298        return $wpdb->get_row(
1299        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1300            "SELECT MIN($id_field) as min, MAX($id_field) as max FROM $from"
1301        );
1302    }
1303
1304    /**
1305     * Retrieve the checksum histogram for a specific object type.
1306     *
1307     * @access public
1308     *
1309     * @param string $table                   Object type.
1310     * @param null   $buckets                 Number of buckets to split the objects to.
1311     * @param null   $start_id                Minimum object ID.
1312     * @param null   $end_id                  Maximum object ID.
1313     * @param null   $columns                 Table columns to calculate the checksum from.
1314     * @param bool   $strip_non_ascii         Whether to strip non-ASCII characters.
1315     * @param string $salt                    Salt, used for $wpdb->prepare()'s args.
1316     * @param bool   $only_range_edges        Only return the range edges and not the actual checksums.
1317     * @param bool   $detailed_drilldown      If the call should return a detailed drilldown for the checksum or only the checksum.
1318     * @param bool   $perform_text_conversion If text fields should be converted to latin1 during the checksum calculation.
1319     *
1320     * @return array|WP_Error The checksum histogram.
1321     */
1322    public function checksum_histogram( $table, $buckets = null, $start_id = null, $end_id = null, $columns = null, $strip_non_ascii = true, $salt = '', $only_range_edges = false, $detailed_drilldown = false, $perform_text_conversion = false ) {
1323        global $wpdb;
1324
1325        $wpdb->queries = array();
1326        try {
1327            $checksum_table = $this->get_table_checksum_instance( $table, $salt, $perform_text_conversion, $columns );
1328        } catch ( Exception $ex ) {
1329            return new WP_Error( 'checksum_disabled', $ex->getMessage() );
1330        }
1331
1332        try {
1333            $range_edges = $checksum_table->get_range_edges( $start_id, $end_id );
1334        } catch ( Exception $ex ) {
1335            return new WP_Error( 'invalid_range_edges', '[' . ( $start_id ?? 'null' ) . '-' . ( $end_id ?? 'null' ) . ']: ' . $ex->getMessage() );
1336        }
1337
1338        if ( $only_range_edges ) {
1339            return $range_edges;
1340        }
1341
1342        $object_count = (int) $range_edges['item_count'];
1343
1344        if ( 0 === $object_count ) {
1345            return array();
1346        }
1347
1348        // Validate / Determine Buckets.
1349        if ( $buckets === null || $buckets < 1 ) {
1350            $buckets = $this->calculate_buckets( $table, $object_count );
1351        }
1352
1353        $bucket_size     = (int) ceil( $object_count / $buckets );
1354        $previous_max_id = max( 0, $range_edges['min_range'] );
1355        $histogram       = array();
1356
1357        do {
1358            try {
1359                $ids_range = $checksum_table->get_range_edges( $previous_max_id, null, $bucket_size );
1360            } catch ( Exception $ex ) {
1361                return new WP_Error( 'invalid_range_edges', '[' . $previous_max_id . '- ]: ' . $ex->getMessage() );
1362            }
1363
1364            if ( empty( $ids_range['min_range'] ) || empty( $ids_range['max_range'] ) ) {
1365                // Nothing to checksum here...
1366                break;
1367            }
1368
1369            // Get the checksum value.
1370            $batch_checksum = $checksum_table->calculate_checksum( $ids_range['min_range'], $ids_range['max_range'], null, $detailed_drilldown );
1371
1372            if ( is_wp_error( $batch_checksum ) ) {
1373                return $batch_checksum;
1374            }
1375
1376            if ( $ids_range['min_range'] === $ids_range['max_range'] ) {
1377                $histogram[ $ids_range['min_range'] ] = $batch_checksum;
1378            } else {
1379                $histogram[ "{$ids_range[ 'min_range' ]}-{$ids_range[ 'max_range' ]}" ] = $batch_checksum;
1380            }
1381            // If ids_range['max_range'] is PHP_INT_MAX, we've reached the end of the table. Edge case causing the loop to never end.
1382            if ( PHP_INT_MAX === (int) $ids_range['max_range'] ) {
1383                break;
1384            }
1385            $previous_max_id = $ids_range['max_range'] + 1;
1386            // If we've reached the max_range lets bail out.
1387            if ( $previous_max_id > $range_edges['max_range'] ) {
1388                break;
1389            }
1390        } while ( true );
1391
1392        return $histogram;
1393    }
1394
1395    /**
1396     * Retrieve the type of the checksum.
1397     *
1398     * @access public
1399     *
1400     * @return string Type of the checksum.
1401     */
1402    public function get_checksum_type() {
1403        return 'sum';
1404    }
1405
1406    /**
1407     * Used in methods that are not implemented and shouldn't be invoked.
1408     *
1409     * @access private
1410     * @return never
1411     * @throws Exception If this method is invoked.
1412     */
1413    private function invalid_call() {
1414        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
1415        $backtrace = debug_backtrace();
1416        $caller    = $backtrace[1]['function'];
1417        throw new Exception( "This function $caller is not supported on the WP Replicastore" );
1418    }
1419
1420    /**
1421     * Determine number of buckets to use in full table checksum.
1422     *
1423     * @param string $table Object Type.
1424     * @param int    $object_count Object count.
1425     * @return int Number of Buckets to use.
1426     */
1427    private function calculate_buckets( $table, $object_count ) {
1428        // Ensure no division by 0.
1429        if ( 0 === (int) $object_count ) {
1430            return 1;
1431        }
1432
1433        // Default Bucket sizes.
1434        $bucket_size = 10000; // Default bucket size is 10,000 items.
1435        switch ( $table ) {
1436            case 'postmeta':
1437            case 'commentmeta':
1438            case 'order_itemmeta':
1439                $bucket_size = 1000; // Meta bucket size is restricted to 1000 items.
1440        }
1441
1442        return (int) ceil( $object_count / $bucket_size );
1443    }
1444
1445    /**
1446     * Return an instance for `Table_Checksum`, depending on the table.
1447     *
1448     * Some tables require custom instances, due to different checksum logic.
1449     *
1450     * @param string $table                   The table that we want to get the instance for.
1451     * @param string $salt                    Salt to be used when generating the checksums.
1452     * @param bool   $perform_text_conversion Should we perform text encoding conversion when calculating the checksum.
1453     * @param array  $additional_columns      Additional columns to add to the checksum calculation.
1454     *
1455     * @return Table_Checksum|Table_Checksum_Usermeta
1456     * @throws Exception Might throw an exception if any of the input parameters were invalid.
1457     */
1458    public function get_table_checksum_instance( $table, $salt = null, $perform_text_conversion = false, $additional_columns = null ) {
1459        if ( 'users' === $table ) {
1460            return new Table_Checksum_Users( $table, $salt, $perform_text_conversion, $additional_columns );
1461        }
1462        if ( 'usermeta' === $table ) {
1463            return new Table_Checksum_Usermeta( $table, $salt, $perform_text_conversion, $additional_columns );
1464        }
1465
1466        return new Table_Checksum( $table, $salt, $perform_text_conversion, $additional_columns );
1467    }
1468}