Completed
Push — add/min-max-id-endpoints ( bb0816...ca6a2e )
by
unknown
06:21
created

Users::table_name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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