Completed
Push — try/e2e-fix-failures ( 67a85e...7aa346 )
by Yaroslav
11:36 queued 05:10
created

Users::is_function_in_backtrace()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 8
nop 1
dl 0
loc 23
rs 9.2408
c 0
b 0
f 0
1
<?php
2
/**
3
 * Users sync module.
4
 *
5
 * @package automattic/jetpack-sync
6
 */
7
8
namespace Automattic\Jetpack\Sync\Modules;
9
10
use Automattic\Jetpack\Constants as Jetpack_Constants;
11
use Automattic\Jetpack\Sync\Defaults;
12
13
/**
14
 * Class to handle sync for users.
15
 */
16
class Users extends Module {
17
	/**
18
	 * Maximum number of users to sync initially.
19
	 *
20
	 * @var int
21
	 */
22
	const MAX_INITIAL_SYNC_USERS = 100;
23
24
	/**
25
	 * User flags we care about.
26
	 *
27
	 * @access protected
28
	 *
29
	 * @var array
30
	 */
31
	protected $flags = array();
32
33
	/**
34
	 * Sync module name.
35
	 *
36
	 * @access public
37
	 *
38
	 * @return string
39
	 */
40
	public function name() {
41
		return 'users';
42
	}
43
44
	/**
45
	 * Retrieve a user by its ID.
46
	 * This is here to support the backfill API.
47
	 *
48
	 * @access public
49
	 *
50
	 * @param string $object_type Type of the sync object.
51
	 * @param int    $id          ID of the sync object.
52
	 * @return \WP_User|bool Filtered \WP_User object, or false if the object is not a user.
53
	 */
54
	public function get_object_by_id( $object_type, $id ) {
55
		if ( 'user' === $object_type ) {
56
			$user = get_user_by( 'id', intval( $id ) );
57
			if ( $user ) {
58
				return $this->sanitize_user_and_expand( $user );
59
			}
60
		}
61
62
		return false;
63
	}
64
65
	/**
66
	 * Initialize users action listeners.
67
	 *
68
	 * @access public
69
	 *
70
	 * @param callable $callable Action handler callable.
71
	 */
72
	public function init_listeners( $callable ) {
73
		// Users.
74
		add_action( 'user_register', array( $this, 'user_register_handler' ) );
75
		add_action( 'profile_update', array( $this, 'save_user_handler' ), 10, 2 );
76
77
		add_action( 'add_user_to_blog', array( $this, 'add_user_to_blog_handler' ) );
78
		add_action( 'jetpack_sync_add_user', $callable, 10, 2 );
79
80
		add_action( 'jetpack_sync_register_user', $callable, 10, 2 );
81
		add_action( 'jetpack_sync_save_user', $callable, 10, 2 );
82
83
		add_action( 'jetpack_sync_user_locale', $callable, 10, 2 );
84
		add_action( 'jetpack_sync_user_locale_delete', $callable, 10, 1 );
85
86
		add_action( 'deleted_user', array( $this, 'deleted_user_handler' ), 10, 2 );
87
		add_action( 'jetpack_deleted_user', $callable, 10, 3 );
88
		add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 2 );
89
		add_action( 'jetpack_removed_user_from_blog', $callable, 10, 2 );
90
91
		// User roles.
92
		add_action( 'add_user_role', array( $this, 'save_user_role_handler' ), 10, 2 );
93
		add_action( 'set_user_role', array( $this, 'save_user_role_handler' ), 10, 3 );
94
		add_action( 'remove_user_role', array( $this, 'save_user_role_handler' ), 10, 2 );
95
96
		// User capabilities.
97
		add_action( 'added_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
98
		add_action( 'updated_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
99
		add_action( 'deleted_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
100
101
		// User authentication.
102
		add_filter( 'authenticate', array( $this, 'authenticate_handler' ), 1000, 3 );
103
		add_action( 'wp_login', array( $this, 'wp_login_handler' ), 10, 2 );
104
105
		add_action( 'jetpack_wp_login', $callable, 10, 3 );
106
107
		add_action( 'wp_logout', $callable, 10, 0 );
108
		add_action( 'wp_masterbar_logout', $callable, 10, 0 );
109
110
		// Add on init.
111
		add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_user', array( $this, 'expand_action' ) );
112
		add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_register_user', array( $this, 'expand_action' ) );
113
		add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_user', array( $this, 'expand_action' ) );
114
	}
115
116
	/**
117
	 * Initialize users action listeners for full sync.
118
	 *
119
	 * @access public
120
	 *
121
	 * @param callable $callable Action handler callable.
122
	 */
123
	public function init_full_sync_listeners( $callable ) {
124
		add_action( 'jetpack_full_sync_users', $callable );
125
	}
126
127
	/**
128
	 * Initialize the module in the sender.
129
	 *
130
	 * @access public
131
	 */
132
	public function init_before_send() {
133
		add_filter( 'jetpack_sync_before_send_jetpack_wp_login', array( $this, 'expand_login_username' ), 10, 1 );
134
		add_filter( 'jetpack_sync_before_send_wp_logout', array( $this, 'expand_logout_username' ), 10, 2 );
135
136
		// Full sync.
137
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_users', array( $this, 'expand_users' ) );
138
	}
139
140
	/**
141
	 * Retrieve a user by a user ID or object.
142
	 *
143
	 * @access private
144
	 *
145
	 * @param mixed $user User object or ID.
146
	 * @return \WP_User User object, or `null` if user invalid/not found.
147
	 */
148
	private function get_user( $user ) {
149
		if ( is_numeric( $user ) ) {
150
			$user = get_user_by( 'id', $user );
151
		}
152
		if ( $user instanceof \WP_User ) {
0 ignored issues
show
Bug introduced by
The class WP_User does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
153
			return $user;
154
		}
155
		return null;
156
	}
157
158
	/**
159
	 * Sanitize a user object.
160
	 * Removes the password from the user object because we don't want to sync it.
161
	 *
162
	 * @access public
163
	 *
164
	 * @todo Refactor `serialize`/`unserialize` to `wp_json_encode`/`wp_json_decode`.
165
	 *
166
	 * @param \WP_User $user User object.
167
	 * @return \WP_User Sanitized user object.
168
	 */
169
	public function sanitize_user( $user ) {
170
		$user = $this->get_user( $user );
171
		// This creates a new user object and stops the passing of the object by reference.
172
		// // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize, WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
173
		$user = unserialize( serialize( $user ) );
174
175
		if ( is_object( $user ) && is_object( $user->data ) ) {
176
			unset( $user->data->user_pass );
177
		}
178
		return $user;
179
	}
180
181
	/**
182
	 * Expand a particular user.
183
	 *
184
	 * @access public
185
	 *
186
	 * @param \WP_User $user User object.
187
	 * @return \WP_User Expanded user object.
188
	 */
189
	public function expand_user( $user ) {
190
		if ( ! is_object( $user ) ) {
191
			return null;
192
		}
193
		$user->allowed_mime_types = get_allowed_mime_types( $user );
194
		$user->allcaps            = $this->get_real_user_capabilities( $user );
195
196
		// Only set the user locale if it is different from the site locale.
197
		if ( get_locale() !== get_user_locale( $user->ID ) ) {
198
			$user->locale = get_user_locale( $user->ID );
199
		}
200
201
		return $user;
202
	}
203
204
	/**
205
	 * Retrieve capabilities we care about for a particular user.
206
	 *
207
	 * @access public
208
	 *
209
	 * @param \WP_User $user User object.
210
	 * @return array User capabilities.
211
	 */
212
	public function get_real_user_capabilities( $user ) {
213
		$user_capabilities = array();
214
		if ( is_wp_error( $user ) ) {
215
			return $user_capabilities;
216
		}
217
		foreach ( Defaults::get_capabilities_whitelist() as $capability ) {
218
			if ( user_can( $user, $capability ) ) {
219
				$user_capabilities[ $capability ] = true;
220
			}
221
		}
222
		return $user_capabilities;
223
	}
224
225
	/**
226
	 * Retrieve, expand and sanitize a user.
227
	 * Can be directly used in the sync user action handlers.
228
	 *
229
	 * @access public
230
	 *
231
	 * @param mixed $user User ID or user object.
232
	 * @return \WP_User Expanded and sanitized user object.
233
	 */
234
	public function sanitize_user_and_expand( $user ) {
235
		$user = $this->get_user( $user );
236
		$user = $this->expand_user( $user );
0 ignored issues
show
Bug introduced by
It seems like $user defined by $this->expand_user($user) on line 236 can be null; however, Automattic\Jetpack\Sync\...es\Users::expand_user() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
237
		return $this->sanitize_user( $user );
0 ignored issues
show
Bug introduced by
It seems like $user defined by $this->expand_user($user) on line 236 can be null; however, Automattic\Jetpack\Sync\...\Users::sanitize_user() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
238
	}
239
240
	/**
241
	 * Expand the user within a hook before it is serialized and sent to the server.
242
	 *
243
	 * @access public
244
	 *
245
	 * @param array $args The hook arguments.
246
	 * @return array $args The hook arguments.
247
	 */
248
	public function expand_action( $args ) {
249
		// The first argument is always the user.
250
		list( $user ) = $args;
251
		if ( $user ) {
252
			$args[0] = $this->sanitize_user_and_expand( $user );
253
			return $args;
254
		}
255
256
		return false;
257
	}
258
259
	/**
260
	 * Expand the user username at login before being sent to the server.
261
	 *
262
	 * @access public
263
	 *
264
	 * @param array $args The hook arguments.
265
	 * @return array $args Expanded hook arguments.
266
	 */
267
	public function expand_login_username( $args ) {
268
		list( $login, $user, $flags ) = $args;
269
		$user                         = $this->sanitize_user( $user );
270
271
		return array( $login, $user, $flags );
272
	}
273
274
	/**
275
	 * Expand the user username at logout before being sent to the server.
276
	 *
277
	 * @access public
278
	 *
279
	 * @param  array $args The hook arguments.
280
	 * @param  int   $user_id ID of the user.
281
	 * @return array $args Expanded hook arguments.
282
	 */
283
	public function expand_logout_username( $args, $user_id ) {
284
		$user = get_userdata( $user_id );
285
		$user = $this->sanitize_user( $user );
286
287
		$login = '';
288
		if ( is_object( $user ) && is_object( $user->data ) ) {
289
			$login = $user->data->user_login;
290
		}
291
292
		// If we don't have a user here lets not send anything.
293
		if ( empty( $login ) ) {
294
			return false;
295
		}
296
297
		return array( $login, $user );
298
	}
299
300
	/**
301
	 * Additional processing is needed for wp_login so we introduce this wrapper handler.
302
	 *
303
	 * @access public
304
	 *
305
	 * @param string   $user_login The user login.
306
	 * @param \WP_User $user       The user object.
307
	 */
308
	public function wp_login_handler( $user_login, $user ) {
309
		/**
310
		 * Fires when a user is logged into a site.
311
		 *
312
		 * @since 7.2.0
313
		 *
314
		 * @param int      $user_id The user ID.
315
		 * @param \WP_User $user    The User Object  of the user that currently logged in.
316
		 * @param array    $params  Any Flags that have been added during login.
317
		 */
318
		do_action( 'jetpack_wp_login', $user->ID, $user, $this->get_flags( $user->ID ) );
319
		$this->clear_flags( $user->ID );
320
	}
321
322
	/**
323
	 * A hook for the authenticate event that checks the password strength.
324
	 *
325
	 * @access public
326
	 *
327
	 * @param \WP_Error|\WP_User $user     The user object, or an error.
328
	 * @param string             $username The username.
329
	 * @param string             $password The password used to authenticate.
330
	 * @return \WP_Error|\WP_User the same object that was passed into the function.
331
	 */
332
	public function authenticate_handler( $user, $username, $password ) {
333
		// In case of cookie authentication we don't do anything here.
334
		if ( empty( $password ) ) {
335
			return $user;
336
		}
337
338
		// We are only interested in successful authentication events.
339
		if ( is_wp_error( $user ) || ! ( $user instanceof \WP_User ) ) {
0 ignored issues
show
Bug introduced by
The class WP_User does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
340
			return $user;
341
		}
342
343
		jetpack_require_lib( 'class.jetpack-password-checker' );
344
		$password_checker = new \Jetpack_Password_Checker( $user->ID );
345
346
		$test_results = $password_checker->test( $password, true );
347
348
		// If the password passes tests, we don't do anything.
349
		if ( empty( $test_results['test_results']['failed'] ) ) {
350
			return $user;
351
		}
352
353
		$this->add_flags(
354
			$user->ID,
355
			array(
356
				'warning'  => 'The password failed at least one strength test.',
357
				'failures' => $test_results['test_results']['failed'],
358
			)
359
		);
360
361
		return $user;
362
	}
363
364
	/**
365
	 * Handler for after the user is deleted.
366
	 *
367
	 * @access public
368
	 *
369
	 * @param int $deleted_user_id    ID of the deleted user.
370
	 * @param int $reassigned_user_id ID of the user the deleted user's posts are reassigned to (if any).
0 ignored issues
show
Documentation introduced by
Should the type for parameter $reassigned_user_id not be string|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
371
	 */
372
	public function deleted_user_handler( $deleted_user_id, $reassigned_user_id = '' ) {
373
		$is_multisite = is_multisite();
374
		/**
375
		 * Fires when a user is deleted on a site
376
		 *
377
		 * @since 5.4.0
378
		 *
379
		 * @param int $deleted_user_id - ID of the deleted user.
380
		 * @param int $reassigned_user_id - ID of the user the deleted user's posts are reassigned to (if any).
381
		 * @param bool $is_multisite - Whether this site is a multisite installation.
382
		 */
383
		do_action( 'jetpack_deleted_user', $deleted_user_id, $reassigned_user_id, $is_multisite );
384
	}
385
386
	/**
387
	 * Handler for user registration.
388
	 *
389
	 * @access public
390
	 *
391
	 * @param int $user_id ID of the deleted user.
392
	 */
393 View Code Duplication
	public function user_register_handler( $user_id ) {
394
		// Ensure we only sync users who are members of the current blog.
395
		if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
396
			return;
397
		}
398
399
		if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
400
			$this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
401
		}
402
		/**
403
		 * Fires when a new user is registered on a site
404
		 *
405
		 * @since 4.9.0
406
		 *
407
		 * @param object The WP_User object
408
		 */
409
		do_action( 'jetpack_sync_register_user', $user_id, $this->get_flags( $user_id ) );
410
		$this->clear_flags( $user_id );
411
412
	}
413
414
	/**
415
	 * Handler for user addition to the current blog.
416
	 *
417
	 * @access public
418
	 *
419
	 * @param int $user_id ID of the user.
420
	 */
421 View Code Duplication
	public function add_user_to_blog_handler( $user_id ) {
422
		// Ensure we only sync users who are members of the current blog.
423
		if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
424
			return;
425
		}
426
427
		if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
428
			$this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
429
		}
430
431
		/**
432
		 * Fires when a user is added on a site
433
		 *
434
		 * @since 4.9.0
435
		 *
436
		 * @param object The WP_User object
437
		 */
438
		do_action( 'jetpack_sync_add_user', $user_id, $this->get_flags( $user_id ) );
439
		$this->clear_flags( $user_id );
440
	}
441
442
	/**
443
	 * Handler for user save.
444
	 *
445
	 * @access public
446
	 *
447
	 * @param int      $user_id ID of the user.
448
	 * @param \WP_User $old_user_data User object before the changes.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $old_user_data not be \WP_User|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
449
	 */
450
	public function save_user_handler( $user_id, $old_user_data = null ) {
451
		// Ensure we only sync users who are members of the current blog.
452
		if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
453
			return;
454
		}
455
456
		$user = get_user_by( 'id', $user_id );
457
458
		// Older versions of WP don't pass the old_user_data in ->data.
459
		if ( isset( $old_user_data->data ) ) {
460
			$old_user = $old_user_data->data;
461
		} else {
462
			$old_user = $old_user_data;
463
		}
464
465
		if ( null !== $old_user && $user->user_pass !== $old_user->user_pass ) {
466
			$this->flags[ $user_id ]['password_changed'] = true;
467
		}
468
		if ( null !== $old_user && $user->data->user_email !== $old_user->user_email ) {
469
			/**
470
			 * The '_new_email' user meta is deleted right after the call to wp_update_user
471
			 * that got us to this point so if it's still set then this was a user confirming
472
			 * their new email address.
473
			 */
474
			if ( 1 === intval( get_user_meta( $user->ID, '_new_email', true ) ) ) {
475
				$this->flags[ $user_id ]['email_changed'] = true;
476
			}
477
		}
478
479
		/**
480
		 * Fires when the client needs to sync an updated user.
481
		 *
482
		 * @since 4.2.0
483
		 *
484
		 * @param \WP_User The WP_User object
485
		 * @param array    State - New since 5.8.0
486
		 */
487
		do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
488
		$this->clear_flags( $user_id );
489
	}
490
491
	/**
492
	 * Handler for user role change.
493
	 *
494
	 * @access public
495
	 *
496
	 * @param int    $user_id   ID of the user.
497
	 * @param string $role      New user role.
498
	 * @param array  $old_roles Previous user roles.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $old_roles not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
499
	 */
500
	public function save_user_role_handler( $user_id, $role, $old_roles = null ) {
501
		$this->add_flags(
502
			$user_id,
503
			array(
504
				'role_changed'  => true,
505
				'previous_role' => $old_roles,
506
			)
507
		);
508
509
		// The jetpack_sync_register_user payload is identical to jetpack_sync_save_user, don't send both.
510
		if ( $this->is_create_user() || $this->is_add_user_to_blog() ) {
511
			return;
512
		}
513
		/**
514
		 * This action is documented already in this file
515
		 */
516
		do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
517
		$this->clear_flags( $user_id );
518
	}
519
520
	/**
521
	 * Retrieve current flags for a particular user.
522
	 *
523
	 * @access public
524
	 *
525
	 * @param int $user_id ID of the user.
526
	 * @return array Current flags of the user.
527
	 */
528
	public function get_flags( $user_id ) {
529
		if ( isset( $this->flags[ $user_id ] ) ) {
530
			return $this->flags[ $user_id ];
531
		}
532
		return array();
533
	}
534
535
	/**
536
	 * Clear the flags of a particular user.
537
	 *
538
	 * @access public
539
	 *
540
	 * @param int $user_id ID of the user.
541
	 */
542
	public function clear_flags( $user_id ) {
543
		if ( isset( $this->flags[ $user_id ] ) ) {
544
			unset( $this->flags[ $user_id ] );
545
		}
546
	}
547
548
	/**
549
	 * Add flags to a particular user.
550
	 *
551
	 * @access public
552
	 *
553
	 * @param int   $user_id ID of the user.
554
	 * @param array $flags   New flags to add for the user.
555
	 */
556
	public function add_flags( $user_id, $flags ) {
557
		$this->flags[ $user_id ] = wp_parse_args( $flags, $this->get_flags( $user_id ) );
558
	}
559
560
	/**
561
	 * Save the user meta, if we're interested in it.
562
	 * Also uses the time to add flags for the user.
563
	 *
564
	 * @access public
565
	 *
566
	 * @param int    $meta_id  ID of the meta object.
567
	 * @param int    $user_id  ID of the user.
568
	 * @param string $meta_key Meta key.
569
	 * @param mixed  $value    Meta value.
570
	 */
571
	public function maybe_save_user_meta( $meta_id, $user_id, $meta_key, $value ) {
572
		if ( 'locale' === $meta_key ) {
573
			$this->add_flags( $user_id, array( 'locale_changed' => true ) );
574
		}
575
576
		$user = get_user_by( 'id', $user_id );
577
		if ( isset( $user->cap_key ) && $meta_key === $user->cap_key ) {
578
			$this->add_flags( $user_id, array( 'capabilities_changed' => true ) );
579
		}
580
581
		if ( $this->is_create_user() || $this->is_add_user_to_blog() || $this->is_delete_user() ) {
582
			return;
583
		}
584
585
		if ( isset( $this->flags[ $user_id ] ) ) {
586
			/**
587
			 * This action is documented already in this file
588
			 */
589
			do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
590
		}
591
	}
592
593
	/**
594
	 * Enqueue the users actions for full sync.
595
	 *
596
	 * @access public
597
	 *
598
	 * @param array   $config               Full sync configuration for this sync module.
599
	 * @param int     $max_items_to_enqueue Maximum number of items to enqueue.
600
	 * @param boolean $state                True if full sync has finished enqueueing this module, false otherwise.
601
	 * @return array Number of actions enqueued, and next module state.
602
	 */
603
	public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
604
		global $wpdb;
605
606
		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 );
607
	}
608
609
	/**
610
	 * Retrieve an estimated number of actions that will be enqueued.
611
	 *
612
	 * @access public
613
	 *
614
	 * @todo Refactor to prepare the SQL query before executing it.
615
	 *
616
	 * @param array $config Full sync configuration for this sync module.
617
	 * @return array Number of items yet to be enqueued.
618
	 */
619 View Code Duplication
	public function estimate_full_sync_actions( $config ) {
620
		global $wpdb;
621
622
		$query = "SELECT count(*) FROM $wpdb->usermeta";
623
624
		$where_sql = $this->get_where_sql( $config );
625
		if ( $where_sql ) {
626
			$query .= ' WHERE ' . $where_sql;
627
		}
628
629
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
630
		$count = $wpdb->get_var( $query );
631
632
		return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
633
	}
634
635
	/**
636
	 * Retrieve the WHERE SQL clause based on the module config.
637
	 *
638
	 * @access private
639
	 *
640
	 * @param array $config Full sync configuration for this sync module.
641
	 * @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
642
	 */
643 View Code Duplication
	private function get_where_sql( $config ) {
644
		global $wpdb;
645
646
		$query = "meta_key = '{$wpdb->prefix}capabilities'";
647
648
		// The $config variable is a list of user IDs to sync.
649
		if ( is_array( $config ) ) {
650
			$query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
651
		}
652
653
		return $query;
654
	}
655
656
	/**
657
	 * Retrieve the actions that will be sent for this module during a full sync.
658
	 *
659
	 * @access public
660
	 *
661
	 * @return array Full sync actions of this module.
662
	 */
663
	public function get_full_sync_actions() {
664
		return array( 'jetpack_full_sync_users' );
665
	}
666
667
	/**
668
	 * Retrieve initial sync user config.
669
	 *
670
	 * @access public
671
	 *
672
	 * @todo Refactor the SQL query to call $wpdb->prepare() before execution.
673
	 *
674
	 * @return array|boolean IDs of users to initially sync, or false if tbe number of users exceed the maximum.
675
	 */
676
	public function get_initial_sync_user_config() {
677
		global $wpdb;
678
679
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
680
		$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 ) );
681
682
		if ( count( $user_ids ) <= self::MAX_INITIAL_SYNC_USERS ) {
683
			return $user_ids;
684
		} else {
685
			return false;
686
		}
687
	}
688
689
	/**
690
	 * Expand the users within a hook before they are serialized and sent to the server.
691
	 *
692
	 * @access public
693
	 *
694
	 * @param array $args The hook arguments.
695
	 * @return array $args The hook arguments.
696
	 */
697
	public function expand_users( $args ) {
698
		list( $user_ids, $previous_end ) = $args;
699
700
		return array(
701
			'users'        => array_map(
702
				array( $this, 'sanitize_user_and_expand' ),
703
				get_users(
704
					array(
705
						'include' => $user_ids,
706
						'orderby' => 'ID',
707
						'order'   => 'DESC',
708
					)
709
				)
710
			),
711
			'previous_end' => $previous_end,
712
		);
713
	}
714
715
	/**
716
	 * Handler for user removal from a particular blog.
717
	 *
718
	 * @access public
719
	 *
720
	 * @param int $user_id ID of the user.
721
	 * @param int $blog_id ID of the blog.
722
	 */
723
	public function remove_user_from_blog_handler( $user_id, $blog_id ) {
724
		// User is removed on add, see https://github.com/WordPress/WordPress/blob/0401cee8b36df3def8e807dd766adc02b359dfaf/wp-includes/ms-functions.php#L2114.
725
		if ( $this->is_add_new_user_to_blog() ) {
726
			return;
727
		}
728
729
		$reassigned_user_id = $this->get_reassigned_network_user_id();
730
731
		// 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.
732
		/**
733
		 * Fires when a user is removed from a blog on a multisite installation
734
		 *
735
		 * @since 5.4.0
736
		 *
737
		 * @param int $user_id - ID of the removed user
738
		 * @param int $reassigned_user_id - ID of the user the removed user's posts are reassigned to (if any).
739
		 */
740
		do_action( 'jetpack_removed_user_from_blog', $user_id, $reassigned_user_id );
741
	}
742
743
	/**
744
	 * Whether we're adding a new user to a blog in this request.
745
	 *
746
	 * @access protected
747
	 *
748
	 * @return boolean
749
	 */
750
	protected function is_add_new_user_to_blog() {
751
		return \Jetpack::is_function_in_backtrace( 'add_new_user_to_blog' );
752
	}
753
754
	/**
755
	 * Whether we're adding an existing user to a blog in this request.
756
	 *
757
	 * @access protected
758
	 *
759
	 * @return boolean
760
	 */
761
	protected function is_add_user_to_blog() {
762
		return \Jetpack::is_function_in_backtrace( 'add_user_to_blog' );
763
	}
764
765
	/**
766
	 * Whether we're removing a user from a blog in this request.
767
	 *
768
	 * @access protected
769
	 *
770
	 * @return boolean
771
	 */
772
	protected function is_delete_user() {
773
		return \Jetpack::is_function_in_backtrace( array( 'wp_delete_user', 'remove_user_from_blog' ) );
774
	}
775
776
	/**
777
	 * Whether we're creating a user or adding a new user to a blog.
778
	 *
779
	 * @access protected
780
	 *
781
	 * @return boolean
782
	 */
783
	protected function is_create_user() {
784
		$functions = array(
785
			'add_new_user_to_blog', // Used to suppress jetpack_sync_save_user in save_user_cap_handler when user registered on multi site.
786
			'wp_create_user', // Used to suppress jetpack_sync_save_user in save_user_role_handler when user registered on multi site.
787
			'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.
788
		);
789
790
		return \Jetpack::is_function_in_backtrace( $functions );
791
	}
792
793
	/**
794
	 * Retrieve the ID of the user the removed user's posts are reassigned to (if any).
795
	 *
796
	 * @return int ID of the user that got reassigned as the author of the posts.
797
	 */
798
	protected function get_reassigned_network_user_id() {
799
		$backtrace = debug_backtrace( false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
800
		foreach ( $backtrace as $call ) {
801
			if (
802
				'remove_user_from_blog' === $call['function'] &&
803
				3 === count( $call['args'] )
804
			) {
805
				return $call['args'][2];
806
			}
807
		}
808
809
		return false;
810
	}
811
}
812