Completed
Push — try/full-sync-send-immediately ( bef0ec...a5c314 )
by
unknown
06:19
created

Users::clear_flags()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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