Completed
Push — add/eta-to-sync-status ( d17f42...551096 )
by
unknown
06:17
created

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