Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Table_Checksum_Usermeta
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 3
380
0.00% covered (danger)
0.00%
0 / 1
 calculate_checksum
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 1
182
 expand_and_sanitize_user_meta
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 get_user_objects_by_ids
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Table Checksums Class.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync\Replicastore;
9
10use Automattic\Jetpack\Connection\Manager;
11use Automattic\Jetpack\Sync;
12use Automattic\Jetpack\Sync\Modules;
13use WP_Error;
14use WP_User_Query;
15
16if ( ! defined( 'ABSPATH' ) ) {
17    exit( 0 );
18}
19
20/**
21 * Class to handle Table Checksums for the User Meta table.
22 */
23class Table_Checksum_Usermeta extends Table_Checksum_Users {
24    /**
25     * Calculate the checksum based on provided range and filters.
26     *
27     * @param int|null   $range_from          The start of the range.
28     * @param int|null   $range_to            The end of the range.
29     * @param array|null $filter_values       Additional filter values. Not used at the moment.
30     * @param bool       $granular_result     If the returned result should be granular or only the checksum.
31     * @param bool       $simple_return_value If we want to use a simple return value for non-granular results (return only the checksum, without wrappers).
32     *
33     * @return array|mixed|object|WP_Error|null
34     */
35    public function calculate_checksum( $range_from = null, $range_to = null, $filter_values = null, $granular_result = false, $simple_return_value = true ) {
36
37        if ( ! Sync\Settings::is_checksum_enabled() ) {
38            return new WP_Error( 'checksum_disabled', 'Checksums are currently disabled.' );
39        }
40
41        /**
42         * First we need to fetch the user IDs for the users that we want to include in the range.
43         *
44         * To keep things a bit simple and avoid filtering issues, let's reuse the `build_filter_statement` that already
45         * exists. Unfortunately we don't
46         */
47        global $wpdb;
48
49        // This call depends on the `range_field` pointing to the `ID` field of the `users` table. Currently, "ID".
50        $range_filter_statement = $this->build_filter_statement( $range_from, $range_to );
51
52        $query = "
53            SELECT
54                DISTINCT {$this->table}.{$this->range_field}
55            FROM
56                {$this->table}
57            JOIN {$wpdb->usermeta} as um_table ON um_table.user_id = {$this->table}.ID
58            WHERE
59                {$range_filter_statement}
60                AND um_table.meta_key = '{$wpdb->prefix}user_level'
61                  AND um_table.meta_value > 0
62        ";
63
64        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
65        $user_ids = $wpdb->get_col( $query );
66
67        // Chunk the array down to make sure we don't overload the database with queries that are too large.
68        $chunked_user_ids = array_chunk( $user_ids, 500 );
69
70        $checksum_entries = array();
71
72        foreach ( $chunked_user_ids as $user_ids_chunk ) {
73            $user_objects = $this->get_user_objects_by_ids( $user_ids_chunk );
74
75            foreach ( $user_objects as $user_object ) {
76                // expand and sanitize desired meta based on WP.com logic.
77                $user_object = $this->expand_and_sanitize_user_meta( $user_object );
78
79                // Generate checksum entry based on the serialized value if not empty.
80                $checksum_entry = 0;
81                if ( ! empty( $user_object->roles ) ) {
82                    $checksum_entry = crc32( implode( '#', array( $this->salt, 'roles', maybe_serialize( $user_object->roles ) ) ) );
83                }
84
85                // Meta only persisted if user is connected to WP.com.
86                if ( ( new Manager( 'jetpack' ) )->is_user_connected( $user_object->ID ) ) {
87                    if ( ! empty( $user_object->allcaps ) ) {
88                        $checksum_entry += crc32(
89                            implode(
90                                '#',
91                                array(
92                                    $this->salt,
93                                    'capabilities',
94                                    maybe_serialize( $user_object->allcaps ),
95                                )
96                            )
97                        );
98                    }
99                    // Explicitly check that locale is not same as site locale.
100                    if ( ! empty( $user_object->locale ) && get_locale() !== $user_object->locale ) {
101                        $checksum_entry += crc32(
102                            implode(
103                                '#',
104                                array(
105                                    $this->salt,
106                                    'locale',
107                                    maybe_serialize( $user_object->locale ),
108                                )
109                            )
110                        );
111                    }
112                    if ( ! empty( $user_object->allowed_mime_types ) ) {
113                        $checksum_entry += crc32(
114                            implode(
115                                '#',
116                                array(
117                                    $this->salt,
118                                    'allowed_mime_types',
119                                    maybe_serialize( $user_object->allowed_mime_types ),
120                                )
121                            )
122                        );
123                    }
124                }
125
126                $checksum_entries[ $user_object->ID ] = '' . $checksum_entry;
127            }
128        }
129
130        // Non-granular results need only to sum the different entries.
131        if ( ! $granular_result ) {
132            $checksum_sum = 0;
133            foreach ( $checksum_entries as $entry ) {
134                $checksum_sum += intval( $entry );
135            }
136
137            if ( $simple_return_value ) {
138                return '' . $checksum_sum;
139            }
140
141            return array(
142                'range'    => $range_from . '-' . $range_to,
143                'checksum' => '' . $checksum_sum,
144            );
145
146        }
147
148        // Granular results.
149        $response = $checksum_entries;
150
151        // Sort the return value for easier comparisons and code flows further down the line.
152        ksort( $response );
153
154        return $response;
155    }
156
157    /**
158     * Expand the User Object with additional meta santized by WP.com logic.
159     *
160     * @param mixed $user_object User Object from WP_User_Query.
161     *
162     * @return mixed $user_object expanded User Object.
163     */
164    protected function expand_and_sanitize_user_meta( $user_object ) {
165        $user_module = Modules::get_module( 'users' );
166        '@phan-var \Automattic\Jetpack\Sync\Modules\Users $user_module';
167        // Expand User Objects based on Sync logic.
168        $user_object = $user_module->expand_user( $user_object );
169
170        // Sanitize location.
171        if ( ! empty( $user_object->locale ) ) {
172            $user_object->locale = wp_strip_all_tags( $user_object->locale, true );
173        }
174
175        // Sanitize allcaps.
176        if ( ! empty( $user_object->allcaps ) ) {
177            $user_object->allcaps = array_map(
178                function ( $cap ) {
179                    return (bool) $cap;
180                },
181                $user_object->allcaps
182            );
183        }
184
185        // Sanitize allowed_mime_types.
186        $allowed_mime_types = $user_object->allowed_mime_types;
187        foreach ( $allowed_mime_types as $allowed_mime_type_short => $allowed_mime_type_long ) {
188            $allowed_mime_type_short                        = wp_strip_all_tags( (string) $allowed_mime_type_short, true );
189            $allowed_mime_type_long                         = wp_strip_all_tags( (string) $allowed_mime_type_long, true );
190            $allowed_mime_types[ $allowed_mime_type_short ] = $allowed_mime_type_long;
191        }
192        $user_object->allowed_mime_types = $allowed_mime_types;
193
194        // Sanitize roles.
195        if ( is_array( $user_object->roles ) ) {
196            $user_object->roles = array_map( 'sanitize_text_field', $user_object->roles );
197        }
198        return $user_object;
199    }
200
201    /**
202     * Gets a list of `WP_User` objects by their IDs
203     *
204     * @param array $ids List of IDs to fetch.
205     *
206     * @return array
207     */
208    protected function get_user_objects_by_ids( $ids ) {
209        $user_query = new WP_User_Query( array( 'include' => $ids ) );
210
211        return $user_query->get_results();
212    }
213}