Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
13.03% covered (danger)
13.03%
31 / 238
7.50% covered (danger)
7.50%
3 / 40
CRAP
0.00% covered (danger)
0.00%
0 / 1
Users
12.71% covered (danger)
12.71%
30 / 236
7.50% covered (danger)
7.50%
3 / 40
5598.42
0.00% covered (danger)
0.00%
0 / 1
 name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 table_name
n/a
0 / 0
n/a
0 / 0
1
 table
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 id_field
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_object_by_id
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 init_listeners
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
1
 init_full_sync_listeners
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init_before_send
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 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 sanitize_user
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 expand_user
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 get_real_user_capabilities
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 sanitize_user_and_expand
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 expand_action
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 expand_login_username
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 expand_logout_username
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 wp_login_handler
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 authenticate_handler
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 deleted_user_handler
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 user_register_handler
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 add_user_to_blog_handler
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 save_user_handler
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
132
 add_user_role_handler
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 remove_user_role_handler
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 save_user_role_handler
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 get_flags
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 clear_flags
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 add_flags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybe_save_user_meta
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 enqueue_full_sync_actions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 estimate_full_sync_actions
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_where_sql
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 get_full_sync_actions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_initial_sync_user_config
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 expand_users
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 remove_user_from_blog_handler
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 is_add_new_user_to_blog
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_create_user_functions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 get_add_user_to_blog_functions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_delete_user_functions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_function_in_backtrace
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Users sync module.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync\Modules;
9
10use Automattic\Jetpack\Connection\Manager;
11use Automattic\Jetpack\Constants as Jetpack_Constants;
12use Automattic\Jetpack\Password_Checker;
13use Automattic\Jetpack\Sync\Defaults;
14
15if ( ! defined( 'ABSPATH' ) ) {
16    exit( 0 );
17}
18
19/**
20 * Class to handle sync for users.
21 */
22class Users extends Module {
23    /**
24     * Maximum number of users to sync initially.
25     *
26     * @var int
27     */
28    const MAX_INITIAL_SYNC_USERS = 100;
29
30    /**
31     * User flags we care about.
32     *
33     * @access protected
34     *
35     * @var array
36     */
37    protected $flags = array();
38
39    /**
40     * Mapping between user fields to flags.
41     *
42     * @var array
43     */
44    protected $user_fields_to_flags_mapping = array(
45        'user_pass'           => 'password_changed',
46        'user_email'          => 'email_changed',
47        'user_nicename'       => 'nicename_changed',
48        'user_url'            => 'url_changed',
49        'user_registered'     => 'registration_date_changed',
50        'user_activation_key' => 'activation_key_changed',
51        'display_name'        => 'display_name_changed',
52    );
53
54    /**
55     * Sync module name.
56     *
57     * @access public
58     *
59     * @return string
60     */
61    public function name() {
62        return 'users';
63    }
64
65    /**
66     * The table name.
67     *
68     * @access public
69     *
70     * @return string
71     * @deprecated since 3.11.0 Use table() instead.
72     */
73    public function table_name() {
74        _deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\Users->table' );
75        return 'usermeta';
76    }
77
78    /**
79     * The table in the database with the prefix.
80     *
81     * @access public
82     *
83     * @return string|bool
84     */
85    public function table() {
86        global $wpdb;
87        return $wpdb->usermeta;
88    }
89
90    /**
91     * The id field in the database.
92     *
93     * @access public
94     *
95     * @return string
96     */
97    public function id_field() {
98        return 'user_id';
99    }
100
101    /**
102     * Retrieve a user by its ID.
103     * This is here to support the backfill API.
104     *
105     * @access public
106     *
107     * @param string $object_type Type of the sync object.
108     * @param int    $id          ID of the sync object.
109     * @return \WP_User|bool Filtered \WP_User object, or false if the object is not a user.
110     */
111    public function get_object_by_id( $object_type, $id ) {
112        if ( 'user' === $object_type ) {
113            $user = get_user_by( 'id', (int) $id );
114            if ( $user ) {
115                return $this->sanitize_user_and_expand( $user );
116            }
117        }
118
119        return false;
120    }
121
122    /**
123     * Initialize users action listeners.
124     *
125     * @access public
126     *
127     * @param callable $callable Action handler callable.
128     */
129    public function init_listeners( $callable ) {
130        // Users.
131        add_action( 'user_register', array( $this, 'user_register_handler' ) );
132        add_action( 'profile_update', array( $this, 'save_user_handler' ), 10, 2 );
133
134        add_action( 'add_user_to_blog', array( $this, 'add_user_to_blog_handler' ) );
135        add_action( 'jetpack_sync_add_user', $callable, 10, 2 );
136
137        add_action( 'jetpack_sync_register_user', $callable, 10, 2 );
138        add_action( 'jetpack_sync_save_user', $callable, 10, 2 );
139
140        add_action( 'jetpack_sync_user_locale', $callable, 10, 2 );
141        add_action( 'jetpack_sync_user_locale_delete', $callable, 10, 1 );
142
143        add_action( 'deleted_user', array( $this, 'deleted_user_handler' ), 10, 2 );
144        add_action( 'jetpack_deleted_user', $callable, 10, 3 );
145        add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 3 );
146        add_action( 'jetpack_removed_user_from_blog', $callable, 10, 2 );
147
148        // User roles.
149        add_action( 'add_user_role', array( $this, 'add_user_role_handler' ), 10, 2 );
150        add_action( 'set_user_role', array( $this, 'save_user_role_handler' ), 10, 3 );
151        add_action( 'remove_user_role', array( $this, 'remove_user_role_handler' ), 10, 2 );
152
153        // User capabilities.
154        add_action( 'added_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
155        add_action( 'updated_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
156        add_action( 'deleted_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
157
158        // User authentication.
159        add_filter( 'authenticate', array( $this, 'authenticate_handler' ), 1000, 3 );
160        add_action( 'wp_login', array( $this, 'wp_login_handler' ), 10, 2 );
161
162        add_action( 'jetpack_wp_login', $callable, 10, 3 );
163
164        add_action( 'wp_logout', $callable, 10, 1 );
165        add_action( 'wp_masterbar_logout', $callable, 10, 1 );
166
167        // Add on init.
168        add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_user', array( $this, 'expand_action' ) );
169        add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_register_user', array( $this, 'expand_action' ) );
170        add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_user', array( $this, 'expand_action' ) );
171        add_filter( 'jetpack_sync_before_enqueue_jetpack_wp_login', array( $this, 'expand_login_username' ), 10, 1 );
172        add_filter( 'jetpack_sync_before_enqueue_wp_logout', array( $this, 'expand_logout_username' ), 10, 1 );
173    }
174
175    /**
176     * Initialize users action listeners for full sync.
177     *
178     * @access public
179     *
180     * @param callable $callable Action handler callable.
181     */
182    public function init_full_sync_listeners( $callable ) {
183        add_action( 'jetpack_full_sync_users', $callable );
184    }
185
186    /**
187     * Initialize the module in the sender.
188     *
189     * @access public
190     */
191    public function init_before_send() {
192        // Full sync.
193        add_filter( 'jetpack_sync_before_send_jetpack_full_sync_users', array( $this, 'expand_users' ) );
194    }
195
196    /**
197     * Retrieve a user by a user ID or object.
198     *
199     * @access private
200     *
201     * @param mixed $user User object or ID.
202     * @return \WP_User|null User object, or `null` if user invalid/not found.
203     */
204    private function get_user( $user ) {
205        if ( is_numeric( $user ) ) {
206            $user = get_user_by( 'id', $user );
207        }
208        if ( $user instanceof \WP_User ) {
209            return $user;
210        }
211        return null;
212    }
213
214    /**
215     * Sanitize a user object.
216     * Removes the password from the user object because we don't want to sync it.
217     *
218     * @access public
219     *
220     * @todo Refactor `serialize`/`unserialize` to `wp_json_encode`/`wp_json_decode`.
221     *
222     * @param \WP_User $user User object.
223     * @return \WP_User Sanitized user object.
224     */
225    public function sanitize_user( $user ) {
226        $user = $this->get_user( $user );
227        // This creates a new user object and stops the passing of the object by reference.
228        // // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize, WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
229        $user = unserialize( serialize( $user ) );
230
231        if ( is_object( $user ) && is_object( $user->data ) ) {
232            unset( $user->data->user_pass );
233        }
234        return $user;
235    }
236
237    /**
238     * Expand a particular user.
239     *
240     * @access public
241     *
242     * @param \WP_User $user User object.
243     * @return \WP_User Expanded user object.
244     */
245    public function expand_user( $user ) {
246        if ( ! is_object( $user ) ) {
247            return null;
248        }
249        $user->allowed_mime_types = get_allowed_mime_types( $user );
250        $user->allcaps            = $this->get_real_user_capabilities( $user );
251
252        // Only set the user locale if it is different from the site locale.
253        if ( get_locale() !== get_user_locale( $user->ID ) ) {
254            $user->locale = get_user_locale( $user->ID );
255        }
256
257        $user->is_connected = ( new Manager( 'jetpack' ) )->is_user_connected( $user->ID );
258
259        return $user;
260    }
261
262    /**
263     * Retrieve capabilities we care about for a particular user.
264     *
265     * @access public
266     *
267     * @param \WP_User $user User object.
268     * @return array User capabilities.
269     */
270    public function get_real_user_capabilities( $user ) {
271        $user_capabilities = array();
272        if ( is_wp_error( $user ) ) {
273            return $user_capabilities;
274        }
275        foreach ( Defaults::get_capabilities_whitelist() as $capability ) {
276            if ( user_can( $user, $capability ) ) {
277                $user_capabilities[ $capability ] = true;
278            }
279        }
280        return $user_capabilities;
281    }
282
283    /**
284     * Retrieve, expand and sanitize a user.
285     * Can be directly used in the sync user action handlers.
286     *
287     * @access public
288     *
289     * @param mixed $user User ID or user object.
290     * @return \WP_User Expanded and sanitized user object.
291     */
292    public function sanitize_user_and_expand( $user ) {
293        $user = $this->get_user( $user );
294        $user = $this->expand_user( $user );
295        return $this->sanitize_user( $user );
296    }
297
298    /**
299     * Expand the user within a hook before it is serialized and sent to the server.
300     *
301     * @access public
302     *
303     * @param array $args The hook arguments.
304     * @return array $args The hook arguments.
305     */
306    public function expand_action( $args ) {
307        // The first argument is always the user.
308        list( $user ) = $args;
309        if ( $user ) {
310            $args[0] = $this->sanitize_user_and_expand( $user );
311            return $args;
312        }
313
314        return false;
315    }
316
317    /**
318     * Expand the user username at login before enqueuing.
319     *
320     * @access public
321     *
322     * @param array $args The hook arguments.
323     * @return array $args Expanded hook arguments.
324     */
325    public function expand_login_username( $args ) {
326        list( $login, $user, $flags ) = $args;
327        $user                         = $this->sanitize_user( $user );
328
329        return array( $login, $user, $flags );
330    }
331
332    /**
333     * Expand the user username at logout before enqueuing.
334     *
335     * @access public
336     *
337     * @param  array $args The hook arguments.
338     * @return false|array $args Expanded hook arguments or false if we don't have a user.
339     */
340    public function expand_logout_username( $args ) {
341        list( $user_id ) = $args;
342
343        $user = get_userdata( $user_id );
344        $user = $this->sanitize_user( $user );
345
346        $login = '';
347        if ( is_object( $user ) && is_object( $user->data ) ) {
348            $login = $user->data->user_login;
349        }
350
351        // If we don't have a user here lets not enqueue anything.
352        if ( empty( $login ) ) {
353            return false;
354        }
355
356        return array( $login, $user );
357    }
358
359    /**
360     * Additional processing is needed for wp_login so we introduce this wrapper handler.
361     *
362     * @access public
363     *
364     * @param string   $user_login The user login.
365     * @param \WP_User $user       The user object.
366     */
367    public function wp_login_handler( $user_login, $user = null ) {
368        if ( ! $user instanceof \WP_User || empty( $user->ID ) ) {
369            return;
370        }
371
372        /**
373         * Fires when a user is logged into a site.
374         *
375         * @since 1.6.3
376         * @since-jetpack 7.2.0
377         *
378         * @param int      $user_id The user ID.
379         * @param \WP_User $user    The User Object  of the user that currently logged in.
380         * @param array    $params  Any Flags that have been added during login.
381         */
382        do_action( 'jetpack_wp_login', $user->ID, $user, $this->get_flags( $user->ID ) );
383        $this->clear_flags( $user->ID );
384    }
385
386    /**
387     * A hook for the authenticate event that checks the password strength.
388     *
389     * @access public
390     *
391     * @param \WP_Error|\WP_User $user     The user object, or an error.
392     * @param string             $username The username.
393     * @param string             $password The password used to authenticate.
394     * @return \WP_Error|\WP_User the same object that was passed into the function.
395     */
396    public function authenticate_handler( $user, $username, $password ) {
397        // In case of cookie authentication we don't do anything here.
398        if ( empty( $password ) ) {
399            return $user;
400        }
401
402        // We are only interested in successful authentication events.
403        if ( is_wp_error( $user ) || ! ( $user instanceof \WP_User ) ) {
404            return $user;
405        }
406
407        $password_checker = new Password_Checker( $user->ID );
408
409        $test_results = $password_checker->test( $password, true );
410
411        // If the password passes tests, we don't do anything.
412        if ( empty( $test_results['test_results']['failed'] ) ) {
413            return $user;
414        }
415
416        $this->add_flags(
417            $user->ID,
418            array(
419                'warning'  => 'The password failed at least one strength test.',
420                'failures' => $test_results['test_results']['failed'],
421            )
422        );
423
424        return $user;
425    }
426
427    /**
428     * Handler for after the user is deleted.
429     *
430     * @access public
431     *
432     * @param int $deleted_user_id    ID of the deleted user.
433     * @param int $reassigned_user_id ID of the user the deleted user's posts are reassigned to (if any).
434     */
435    public function deleted_user_handler( $deleted_user_id, $reassigned_user_id = '' ) {
436        $is_multisite = is_multisite();
437        /**
438         * Fires when a user is deleted on a site
439         *
440         * @since 1.6.3
441         * @since-jetpack 5.4.0
442         *
443         * @param int $deleted_user_id - ID of the deleted user.
444         * @param int $reassigned_user_id - ID of the user the deleted user's posts are reassigned to (if any).
445         * @param bool $is_multisite - Whether this site is a multisite installation.
446         */
447        do_action( 'jetpack_deleted_user', $deleted_user_id, $reassigned_user_id, $is_multisite );
448    }
449
450    /**
451     * Handler for user registration.
452     *
453     * @access public
454     *
455     * @param int $user_id ID of the deleted user.
456     */
457    public function user_register_handler( $user_id ) {
458        // Ensure we only sync users who are members of the current blog.
459        if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
460            return;
461        }
462
463        if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
464            $this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
465        }
466        /**
467         * Fires when a new user is registered on a site
468         *
469         * @since 1.6.3
470         * @since-jetpack 4.9.0
471         *
472         * @param object The WP_User object
473         */
474        do_action( 'jetpack_sync_register_user', $user_id, $this->get_flags( $user_id ) );
475        $this->clear_flags( $user_id );
476    }
477
478    /**
479     * Handler for user addition to the current blog.
480     *
481     * @access public
482     *
483     * @param int $user_id ID of the user.
484     */
485    public function add_user_to_blog_handler( $user_id ) {
486        // Ensure we only sync users who are members of the current blog.
487        if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
488            return;
489        }
490
491        if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
492            $this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
493        }
494
495        /**
496         * Fires when a user is added on a site
497         *
498         * @since 1.6.3
499         * @since-jetpack 4.9.0
500         *
501         * @param object The WP_User object
502         */
503        do_action( 'jetpack_sync_add_user', $user_id, $this->get_flags( $user_id ) );
504        $this->clear_flags( $user_id );
505    }
506
507    /**
508     * Handler for user save.
509     *
510     * @access public
511     *
512     * @param int      $user_id ID of the user.
513     * @param \WP_User $old_user_data User object before the changes.
514     */
515    public function save_user_handler( $user_id, $old_user_data = null ) {
516        // Ensure we only sync users who are members of the current blog.
517        if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
518            return;
519        }
520
521        $user = get_user_by( 'id', $user_id );
522
523        // Older versions of WP don't pass the old_user_data in ->data.
524        if ( isset( $old_user_data->data ) ) {
525            $old_user = $old_user_data->data;
526        } else {
527            $old_user = $old_user_data;
528        }
529
530        if ( ! is_object( $old_user ) ) {
531            return;
532        }
533
534        $old_user_array = get_object_vars( $old_user );
535
536        foreach ( $old_user_array as $user_field => $field_value ) {
537            if ( false === $user->has_prop( $user_field ) ) {
538                continue;
539            }
540            if ( 'ID' === $user_field ) {
541                continue;
542            }
543            if ( $user->$user_field !== $field_value ) {
544                if ( 'user_email' === $user_field ) {
545                    /**
546                     * The '_new_email' user meta is deleted right after the call to wp_update_user
547                     * that got us to this point so if it's still set then this was a user confirming
548                     * their new email address.
549                     */
550                    if ( 1 === (int) get_user_meta( $user->ID, '_new_email', true ) ) {
551                        $this->flags[ $user_id ]['email_changed'] = true;
552                    }
553                    continue;
554                }
555
556                $flag = $this->user_fields_to_flags_mapping[ $user_field ] ?? 'unknown_field_changed';
557
558                $this->flags[ $user_id ][ $flag ] = true;
559            }
560        }
561
562        if ( isset( $this->flags[ $user_id ] ) ) {
563
564            /**
565             * Fires when the client needs to sync an updated user.
566             *
567             * @since 1.6.3
568             * @since-jetpack 4.2.0
569             *
570             * @param \WP_User The WP_User object
571             * @param array    State - New since 5.8.0
572             */
573            do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
574            $this->clear_flags( $user_id );
575        }
576    }
577
578    /**
579     * Handler for add user role change.
580     *
581     * @access public
582     *
583     * @param int    $user_id   ID of the user.
584     * @param string $role      New user role.
585     */
586    public function add_user_role_handler( $user_id, $role ) {
587        $this->add_flags(
588            $user_id,
589            array(
590                'role_added' => $role,
591            )
592        );
593
594        $this->save_user_role_handler( $user_id, $role );
595    }
596
597    /**
598     * Handler for remove user role change.
599     *
600     * @access public
601     *
602     * @param int    $user_id   ID of the user.
603     * @param string $role      Removed user role.
604     */
605    public function remove_user_role_handler( $user_id, $role ) {
606        $this->add_flags(
607            $user_id,
608            array(
609                'role_removed' => $role,
610            )
611        );
612
613        $this->save_user_role_handler( $user_id, $role );
614    }
615
616    /**
617     * Handler for user role change.
618     *
619     * @access public
620     *
621     * @param int    $user_id   ID of the user.
622     * @param string $role      New user role.
623     * @param array  $old_roles Previous user roles.
624     */
625    public function save_user_role_handler( $user_id, $role, $old_roles = null ) {
626        $this->add_flags(
627            $user_id,
628            array(
629                'role_changed'  => true,
630                'previous_role' => $old_roles,
631            )
632        );
633
634        // The jetpack_sync_register_user payload is identical to jetpack_sync_save_user, don't send both.
635        if ( $this->is_function_in_backtrace(
636            array_merge( $this->get_create_user_functions(), $this->get_add_user_to_blog_functions() )
637        ) ) {
638            return;
639        }
640        /**
641         * This action is documented already in this file
642         */
643        do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
644        $this->clear_flags( $user_id );
645    }
646
647    /**
648     * Retrieve current flags for a particular user.
649     *
650     * @access public
651     *
652     * @param int $user_id ID of the user.
653     * @return array Current flags of the user.
654     */
655    public function get_flags( $user_id ) {
656        if ( isset( $this->flags[ $user_id ] ) ) {
657            return $this->flags[ $user_id ];
658        }
659        return array();
660    }
661
662    /**
663     * Clear the flags of a particular user.
664     *
665     * @access public
666     *
667     * @param int $user_id ID of the user.
668     */
669    public function clear_flags( $user_id ) {
670        if ( isset( $this->flags[ $user_id ] ) ) {
671            unset( $this->flags[ $user_id ] );
672        }
673    }
674
675    /**
676     * Add flags to a particular user.
677     *
678     * @access public
679     *
680     * @param int   $user_id ID of the user.
681     * @param array $flags   New flags to add for the user.
682     */
683    public function add_flags( $user_id, $flags ) {
684        $this->flags[ $user_id ] = wp_parse_args( $flags, $this->get_flags( $user_id ) );
685    }
686
687    /**
688     * Save the user meta, if we're interested in it.
689     * Also uses the time to add flags for the user.
690     *
691     * @access public
692     *
693     * @param int    $meta_id  ID of the meta object.
694     * @param int    $user_id  ID of the user.
695     * @param string $meta_key Meta key.
696     * @param mixed  $value    Meta value.
697     */
698    public function maybe_save_user_meta( $meta_id, $user_id, $meta_key, $value ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
699        if ( 'locale' === $meta_key ) {
700            $this->add_flags( $user_id, array( 'locale_changed' => true ) );
701        }
702
703        $user = get_user_by( 'id', $user_id );
704        if ( isset( $user->cap_key ) && $meta_key === $user->cap_key ) {
705            $this->add_flags( $user_id, array( 'capabilities_changed' => true ) );
706        }
707
708        if ( $this->is_function_in_backtrace(
709            array_merge( $this->get_create_user_functions(), $this->get_add_user_to_blog_functions(), $this->get_delete_user_functions() )
710        ) ) {
711            return;
712        }
713
714        if ( isset( $this->flags[ $user_id ] ) ) {
715            /**
716             * This action is documented already in this file
717             */
718            do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
719        }
720    }
721
722    /**
723     * Enqueue the users actions for full sync.
724     *
725     * @access public
726     *
727     * @param array   $config               Full sync configuration for this sync module.
728     * @param int     $max_items_to_enqueue Maximum number of items to enqueue.
729     * @param boolean $state                True if full sync has finished enqueueing this module, false otherwise.
730     * @return array Number of actions enqueued, and next module state.
731     */
732    public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
733        global $wpdb;
734
735        return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_users', $wpdb->usermeta, 'user_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
736    }
737
738    /**
739     * Retrieve an estimated number of actions that will be enqueued.
740     *
741     * @access public
742     *
743     * @todo Refactor to prepare the SQL query before executing it.
744     *
745     * @param array $config Full sync configuration for this sync module.
746     * @return int Number of items yet to be enqueued.
747     */
748    public function estimate_full_sync_actions( $config ) {
749        global $wpdb;
750
751        $query = "SELECT count(*) FROM $wpdb->usermeta";
752
753        $where_sql = $this->get_where_sql( $config );
754        if ( $where_sql ) {
755            $query .= ' WHERE ' . $where_sql;
756        }
757
758        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
759        $count = (int) $wpdb->get_var( $query );
760
761        return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
762    }
763
764    /**
765     * Retrieve the WHERE SQL clause based on the module config.
766     *
767     * @access public
768     *
769     * @param array $config Full sync configuration for this sync module.
770     * @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
771     */
772    public function get_where_sql( $config ) {
773        global $wpdb;
774
775        $query = "meta_key = '{$wpdb->prefix}user_level' AND meta_value > 0";
776
777        // The $config variable is a list of user IDs to sync.
778        if ( is_array( $config ) && ! empty( $config ) ) {
779            $query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
780        }
781
782        return $query;
783    }
784
785    /**
786     * Retrieve the actions that will be sent for this module during a full sync.
787     *
788     * @access public
789     *
790     * @return array Full sync actions of this module.
791     */
792    public function get_full_sync_actions() {
793        return array( 'jetpack_full_sync_users' );
794    }
795
796    /**
797     * Retrieve initial sync user config.
798     *
799     * @access public
800     *
801     * @todo Refactor the SQL query to call $wpdb->prepare() before execution.
802     *
803     * @return array|boolean IDs of users to initially sync, or false if tbe number of users exceed the maximum.
804     */
805    public function get_initial_sync_user_config() {
806        global $wpdb;
807
808        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
809        $user_ids = $wpdb->get_col( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = '{$wpdb->prefix}user_level' AND meta_value > 0 LIMIT " . ( self::MAX_INITIAL_SYNC_USERS + 1 ) );
810        $user_ids_count = is_countable( $user_ids ) ? count( $user_ids ) : 0;
811
812        if ( $user_ids_count <= self::MAX_INITIAL_SYNC_USERS ) {
813            return $user_ids;
814        } else {
815            return false;
816        }
817    }
818
819    /**
820     * Expand the users within a hook before they are serialized and sent to the server.
821     *
822     * @access public
823     *
824     * @param array $args The hook arguments.
825     * @return array $args The hook arguments.
826     */
827    public function expand_users( $args ) {
828        list( $user_ids, $previous_end ) = $args;
829
830        return array(
831            'users'        => array_map(
832                array( $this, 'sanitize_user_and_expand' ),
833                get_users(
834                    array(
835                        'include' => $user_ids,
836                        'orderby' => 'ID',
837                        'order'   => 'DESC',
838                    )
839                )
840            ),
841            'previous_end' => $previous_end,
842        );
843    }
844
845    /**
846     * Handler for user removal from a particular blog.
847     *
848     * @access public
849     *
850     * @param int $user_id  ID of the user.
851     * @param int $blog_id  ID of the blog.
852     * @param int $reassign ID of the user to whom to reassign posts.
853     */
854    public function remove_user_from_blog_handler( $user_id, $blog_id, $reassign = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
855        // User is removed on add, see https://github.com/WordPress/WordPress/blob/0401cee8b36df3def8e807dd766adc02b359dfaf/wp-includes/ms-functions.php#L2114.
856        if ( $this->is_add_new_user_to_blog() ) {
857            return;
858        }
859
860        $reassigned_user_id = $reassign;
861
862        // Note that we are in the context of the blog the user is removed from, see https://github.com/WordPress/WordPress/blob/473e1ba73bc5c18c72d7f288447503713d518790/wp-includes/ms-functions.php#L233.
863        /**
864         * Fires when a user is removed from a blog on a multisite installation
865         *
866         * @since 1.6.3
867         * @since-jetpack 5.4.0
868         *
869         * @param int $user_id - ID of the removed user
870         * @param int $reassigned_user_id - ID of the user the removed user's posts are reassigned to (if any).
871         */
872        do_action( 'jetpack_removed_user_from_blog', $user_id, $reassigned_user_id );
873    }
874
875    /**
876     * Whether we're adding a new user to a blog in this request.
877     *
878     * @access protected
879     *
880     * @return boolean
881     */
882    protected function is_add_new_user_to_blog() {
883        return $this->is_function_in_backtrace( 'add_new_user_to_blog' );
884    }
885
886    /**
887     * Get the function names that indicate a user is being created.
888     *
889     * @access protected
890     *
891     * @return array
892     */
893    protected function get_create_user_functions() {
894        return array(
895            'add_new_user_to_blog', // Used to suppress jetpack_sync_save_user in save_user_cap_handler when user registered on multi site.
896            'wp_create_user', // Used to suppress jetpack_sync_save_user in save_user_role_handler when user registered on multi site.
897            'wp_insert_user', // Used to suppress jetpack_sync_save_user in save_user_cap_handler and save_user_role_handler when user registered on single site.
898        );
899    }
900
901    /**
902     * Get the function names that indicate a user is being added to a blog.
903     *
904     * @access protected
905     *
906     * @return array
907     */
908    protected function get_add_user_to_blog_functions() {
909        return array( 'add_user_to_blog' );
910    }
911
912    /**
913     * Get the function names that indicate a user is being deleted.
914     *
915     * @access protected
916     *
917     * @return array
918     */
919    protected function get_delete_user_functions() {
920        return array( 'wp_delete_user', 'remove_user_from_blog' );
921    }
922
923    /**
924     * Checks if one or more function names is in debug_backtrace.
925     *
926     * @access protected
927     *
928     * @param array|string $names Mixed string name of function or array of string names of functions.
929     * @return bool
930     */
931    protected function is_function_in_backtrace( $names ) {
932        $backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
933        if ( ! is_array( $names ) ) {
934            $names = array( $names );
935        }
936        $names_as_keys = array_flip( $names );
937
938        $backtrace_functions         = array_column( $backtrace, 'function' );
939        $backtrace_functions_as_keys = array_flip( $backtrace_functions );
940        $intersection                = array_intersect_key( $backtrace_functions_as_keys, $names_as_keys );
941        return ! empty( $intersection );
942    }
943}