Completed
Push — try/sync-package ( 228b13 )
by Marin
07:37
created

Module_Users::sanitize_user_and_expand()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Automattic\Jetpack\Sync;
4
5
class Module_Users extends Module {
6
	const MAX_INITIAL_SYNC_USERS = 100;
7
8
	protected $flags = array();
9
10
	function name() {
11
		return 'users';
12
	}
13
14
	// this is here to support the backfill API
15
	public function get_object_by_id( $object_type, $id ) {
16
		if ( $object_type === 'user' && $user = get_user_by( 'id', intval( $id ) ) ) {
17
			return $this->sanitize_user_and_expand( $user );
18
		}
19
20
		return false;
21
	}
22
23
	public function init_listeners( $callable ) {
24
25
		// users
26
		add_action( 'user_register', array( $this, 'user_register_handler' ) );
27
		add_action( 'profile_update', array( $this, 'save_user_handler' ), 10, 2 );
28
29
		add_action( 'add_user_to_blog', array( $this, 'add_user_to_blog_handler' ) );
30
		add_action( 'jetpack_sync_add_user', $callable, 10, 2 );
31
32
		add_action( 'jetpack_sync_register_user', $callable, 10, 2 );
33
		add_action( 'jetpack_sync_save_user', $callable, 10, 2 );
34
35
		add_action( 'jetpack_sync_user_locale', $callable, 10, 2 );
36
		add_action( 'jetpack_sync_user_locale_delete', $callable, 10, 1 );
37
38
		add_action( 'deleted_user', array( $this, 'deleted_user_handler' ), 10, 2 );
39
		add_action( 'jetpack_deleted_user', $callable, 10, 3 );
40
		add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 2 );
41
		add_action( 'jetpack_removed_user_from_blog', $callable, 10, 2 );
42
43
		// user roles
44
		add_action( 'add_user_role', array( $this, 'save_user_role_handler' ), 10, 2 );
45
		add_action( 'set_user_role', array( $this, 'save_user_role_handler' ), 10, 3 );
46
		add_action( 'remove_user_role', array( $this, 'save_user_role_handler' ), 10, 2 );
47
48
		// user capabilities
49
		add_action( 'added_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
50
		add_action( 'updated_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
51
		add_action( 'deleted_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
52
53
		// user authentication
54
		add_filter( 'authenticate', array( $this, 'authenticate_handler' ), 1000, 3 );
55
		add_action( 'wp_login', array( $this, 'wp_login_handler' ), 10, 2 );
56
57
		add_action( 'jetpack_wp_login', $callable, 10, 3 );
58
59
		add_action( 'wp_logout', $callable, 10, 0 );
60
		add_action( 'wp_masterbar_logout', $callable, 10, 0 );
61
62
		// Add on init
63
		add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_user', array( $this, 'expand_action' ) );
64
		add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_register_user', array( $this, 'expand_action' ) );
65
		add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_user', array( $this, 'expand_action' ) );
66
	}
67
68
	public function init_full_sync_listeners( $callable ) {
69
		add_action( 'jetpack_full_sync_users', $callable );
70
	}
71
72
	public function init_before_send() {
73
74
		add_filter( 'jetpack_sync_before_send_jetpack_wp_login', array( $this, 'expand_login_username' ), 10, 1 );
75
		add_filter( 'jetpack_sync_before_send_wp_logout', array( $this, 'expand_logout_username' ), 10, 2 );
76
77
		// full sync
78
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_users', array( $this, 'expand_users' ) );
79
	}
80
81
	private function get_user( $user ) {
82
		if ( is_numeric( $user ) ) {
83
			$user = get_user_by( 'id', $user );
84
		}
85
		if ( $user instanceof WP_User ) {
0 ignored issues
show
Bug introduced by
The class Automattic\Jetpack\Sync\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...
86
			return $user;
87
		}
88
		return null;
89
	}
90
91
	public function sanitize_user( $user ) {
92
		$user = $this->get_user( $user );
93
		// this create a new user object and stops the passing of the object by reference.
94
		$user = unserialize( serialize( $user ) );
95
96
		if ( is_object( $user ) && is_object( $user->data ) ) {
97
			unset( $user->data->user_pass );
98
		}
99
		return $user;
100
	}
101
102
	public function expand_user( $user ) {
103
		if ( ! is_object( $user ) ) {
104
			return null;
105
		}
106
		$user->allowed_mime_types = get_allowed_mime_types( $user );
107
		$user->allcaps            = $this->get_real_user_capabilities( $user );
108
109
		// Only set the user locale if it is different from the site local
110
		if ( get_locale() !== get_user_locale( $user->ID ) ) {
111
			$user->locale = get_user_locale( $user->ID );
112
		}
113
114
		return $user;
115
	}
116
117
	public function get_real_user_capabilities( $user ) {
118
		$user_capabilities = array();
119
		if ( is_wp_error( $user ) ) {
120
			return $user_capabilities;
121
		}
122
		foreach ( Defaults::get_capabilities_whitelist() as $capability ) {
123
			if ( $user_has_capabilities = user_can( $user, $capability ) ) {
0 ignored issues
show
Unused Code introduced by
$user_has_capabilities is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
124
				$user_capabilities[ $capability ] = true;
125
			}
126
		}
127
		return $user_capabilities;
128
	}
129
130
	public function sanitize_user_and_expand( $user ) {
131
		$user = $this->get_user( $user );
132
		$user = $this->expand_user( $user );
133
		return $this->sanitize_user( $user );
134
	}
135
136
	public function expand_action( $args ) {
137
		// the first argument is always the user
138
		list( $user ) = $args;
139
		if ( $user ) {
140
			$args[0] = $this->sanitize_user_and_expand( $user );
141
			return $args;
142
		}
143
144
		return false;
145
	}
146
147
	public function expand_login_username( $args ) {
148
		list( $login, $user, $flags ) = $args;
149
		$user                         = $this->sanitize_user( $user );
150
151
		return array( $login, $user, $flags );
152
	}
153
154
	public function expand_logout_username( $args, $user_id ) {
155
		$user = get_userdata( $user_id );
156
		$user = $this->sanitize_user( $user );
157
158
		$login = '';
159
		if ( is_object( $user ) && is_object( $user->data ) ) {
160
			$login = $user->data->user_login;
161
		}
162
		// if we don't have a user here lets not send anything.
163
		if ( empty( $login ) ) {
164
			return false;
165
		}
166
167
		return array( $login, $user );
168
	}
169
170
	/**
171
	 * Additional processing is needed for wp_login so we introduce this wrapper
172
	 * handler.
173
	 *
174
	 * @param String  $user_login the user login.
175
	 * @param WP_User $user       the user object.
176
	 */
177
	 function wp_login_handler( $user_login, $user ) {
178
		/**
179
		 * Fires when a user is logged into a site.
180
		 *
181
		 * @since 7.2.0
182
		 *
183
		 * @param Numeric $user_id The user ID.
184
		 * @param WP_User $user  The User Object  of the user that currently logged in
185
		 * @param Array   $params Any Flags that have been added during login
186
		 */
187
		do_action( 'jetpack_wp_login', $user->ID, $user, $this->get_flags( $user->ID ) );
188
		$this->clear_flags( $user->ID );
189
	}
190
191
	/**
192
	 * A hook for the authenticate event that checks the password strength.
193
	 *
194
	 * @param WP_Error|WP_User $user     the user object, or an error.
195
	 * @param String           $username the username.
196
	 * @param Sting            $password the password used to authenticate.
197
	 * @return WP_Error|WP_User the same object that was passed into the function.
198
	 */
199
	public function authenticate_handler( $user, $username, $password ) {
200
		// In case of cookie authentication we don't do anything here.
201
		if ( empty( $password ) ) {
202
			return $user;
203
		}
204
205
		// We are only interested in successful authentication events.
206
		if ( is_wp_error( $user ) || ! ( $user instanceof WP_User ) ) {
0 ignored issues
show
Bug introduced by
The class Automattic\Jetpack\Sync\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...
207
			return $user;
208
		}
209
210
		jetpack_require_lib( 'class.jetpack-password-checker' );
211
		$password_checker = new Jetpack_Password_Checker( $user->ID );
212
213
		$test_results = $password_checker->test( $password, true );
214
215
		// If the password passes tests, we don't do anything.
216
		if ( empty( $test_results['test_results']['failed'] ) ) {
217
			return $user;
218
		}
219
220
		$this->add_flags(
221
			$user->ID,
222
			array(
223
				'warning'          => 'The password failed at least one strength test.',
224
				'failures'         => $test_results['test_results']['failed'],
225
			)
226
		);
227
228
		return $user;
229
	}
230
231
	public function deleted_user_handler( $deleted_user_id, $reassigned_user_id = '' ) {
232
		$is_multisite = is_multisite();
233
		/**
234
		 * Fires when a user is deleted on a site
235
		 *
236
		 * @since 5.4.0
237
		 *
238
		 * @param int $deleted_user_id - ID of the deleted user
239
		 * @param int $reassigned_user_id - ID of the user the deleted user's posts is reassigned to (if any)
240
		 * @param bool $is_multisite - Whether this site is a multisite installation
241
		 */
242
		do_action( 'jetpack_deleted_user', $deleted_user_id, $reassigned_user_id, $is_multisite );
243
	}
244
245 View Code Duplication
	function user_register_handler( $user_id, $old_user_data = null ) {
246
		// ensure we only sync users who are members of the current blog
247
		if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
248
			return;
249
		}
250
251
		if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
252
			$this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
253
		}
254
		/**
255
		 * Fires when a new user is registered on a site
256
		 *
257
		 * @since 4.9.0
258
		 *
259
		 * @param object The WP_User object
260
		 */
261
		do_action( 'jetpack_sync_register_user', $user_id, $this->get_flags( $user_id ) );
262
		$this->clear_flags( $user_id );
263
264
	}
265
266 View Code Duplication
	function add_user_to_blog_handler( $user_id, $old_user_data = null ) {
267
		// ensure we only sync users who are members of the current blog
268
		if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
269
			return;
270
		}
271
272
		if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
273
			$this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
274
		}
275
		/**
276
		 * Fires when a user is added on a site
277
		 *
278
		 * @since 4.9.0
279
		 *
280
		 * @param object The WP_User object
281
		 */
282
		do_action( 'jetpack_sync_add_user', $user_id, $this->get_flags( $user_id ) );
283
		$this->clear_flags( $user_id );
284
	}
285
286
	function save_user_handler( $user_id, $old_user_data = null ) {
287
		// ensure we only sync users who are members of the current blog
288
		if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
289
			return;
290
		}
291
292
		$user = get_user_by( 'id', $user_id );
293
294
		// Older versions of WP don't pass the old_user_data in ->data
295
		if ( isset( $old_user_data->data ) ) {
296
			$old_user = $old_user_data->data;
297
		} else {
298
			$old_user = $old_user_data;
299
		}
300
301
		if ( $old_user !== null && $user->user_pass !== $old_user->user_pass ) {
302
			$this->flags[ $user_id ]['password_changed'] = true;
303
		}
304
		if ( $old_user !== null && $user->data->user_email !== $old_user->user_email ) {
305
			// The '_new_email' user meta is deleted right after the call to wp_update_user
306
			// that got us to this point so if it's still set then this was a user confirming
307
			// their new email address
308
			if ( 1 === intval( get_user_meta( $user->ID, '_new_email', true ) ) ) {
309
				$this->flags[ $user_id ]['email_changed'] = true;
310
			}
311
		}
312
313
		/**
314
		 * Fires when the client needs to sync an updated user
315
		 *
316
		 * @since 4.2.0
317
		 *
318
		 * @param object The WP_User object
319
		 * @param array state - New since 5.8.0
320
		 */
321
		do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
322
		$this->clear_flags( $user_id );
323
	}
324
325
	function save_user_role_handler( $user_id, $role, $old_roles = null ) {
326
		$this->add_flags(
327
			$user_id,
328
			array(
329
				'role_changed'  => true,
330
				'previous_role' => $old_roles,
331
			)
332
		);
333
334
		// The jetpack_sync_register_user payload is identical to jetpack_sync_save_user, don't send both
335
		if ( $this->is_create_user() || $this->is_add_user_to_blog() ) {
336
			return;
337
		}
338
		/**
339
		 * This action is documented already in this file
340
		 */
341
		do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
342
		$this->clear_flags( $user_id );
343
	}
344
345
	function get_flags( $user_id ) {
346
		if ( isset( $this->flags[ $user_id ] ) ) {
347
			return $this->flags[ $user_id ];
348
		}
349
		return array();
350
	}
351
352
	function clear_flags( $user_id ) {
353
		if ( isset( $this->flags[ $user_id ] ) ) {
354
			unset( $this->flags[ $user_id ] );
355
		}
356
	}
357
358
	function add_flags( $user_id, $flags ) {
359
		$this->flags[ $user_id ] = wp_parse_args( $flags, $this->get_flags( $user_id ) );
360
	}
361
362
	function maybe_save_user_meta( $meta_id, $user_id, $meta_key, $value ) {
363
		if ( $meta_key === 'locale' ) {
364
			$this->add_flags( $user_id, array( 'locale_changed' => true ) );
365
		}
366
367
		$user = get_user_by( 'id', $user_id );
368
		if ( isset( $user->cap_key ) && $meta_key === $user->cap_key ) {
369
			$this->add_flags( $user_id, array( 'capabilities_changed' => true ) );
370
		}
371
372
		if ( $this->is_create_user() || $this->is_add_user_to_blog() || $this->is_delete_user() ) {
373
			return;
374
		}
375
376
		if ( isset( $this->flags[ $user_id ] ) ) {
377
			/**
378
			 * This action is documented already in this file
379
			 */
380
			do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
381
		}
382
	}
383
384
	public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
385
		global $wpdb;
386
387
		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 );
388
	}
389
390 View Code Duplication
	public function estimate_full_sync_actions( $config ) {
391
		global $wpdb;
392
393
		$query = "SELECT count(*) FROM $wpdb->usermeta";
394
395
		if ( $where_sql = $this->get_where_sql( $config ) ) {
396
			$query .= ' WHERE ' . $where_sql;
397
		}
398
399
		$count = $wpdb->get_var( $query );
400
401
		return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
402
	}
403
404 View Code Duplication
	private function get_where_sql( $config ) {
405
		global $wpdb;
406
407
		$query = "meta_key = '{$wpdb->prefix}capabilities'";
408
409
		// config is a list of user IDs to sync
410
		if ( is_array( $config ) ) {
411
			$query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
412
		}
413
414
		return $query;
415
	}
416
417
	function get_full_sync_actions() {
418
		return array( 'jetpack_full_sync_users' );
419
	}
420
421
	function get_initial_sync_user_config() {
422
		global $wpdb;
423
424
		$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 ) );
425
426
		if ( count( $user_ids ) <= self::MAX_INITIAL_SYNC_USERS ) {
427
			return $user_ids;
428
		} else {
429
			return false;
430
		}
431
	}
432
433
	public function expand_users( $args ) {
434
		list( $user_ids, $previous_end ) = $args;
435
436
		return array(
437
			'users' => array_map( array( $this, 'sanitize_user_and_expand' ), get_users(
438
				array(
439
					'include' => $user_ids,
440
					'orderby' => 'ID',
441
					'order' => 'DESC'
442
				)
443
			) ),
444
			'previous_end' => $previous_end
445
		);
446
	}
447
448
	public function remove_user_from_blog_handler( $user_id, $blog_id ) {
449
		// User is removed on add, see https://github.com/WordPress/WordPress/blob/0401cee8b36df3def8e807dd766adc02b359dfaf/wp-includes/ms-functions.php#L2114
450
		if ( $this->is_add_new_user_to_blog() ) {
451
			return;
452
		}
453
454
		$reassigned_user_id = $this->get_reassigned_network_user_id();
455
456
		// 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
457
		/**
458
		 * Fires when a user is removed from a blog on a multisite installation
459
		 *
460
		 * @since 5.4.0
461
		 *
462
		 * @param int $user_id - ID of the removed user
463
		 * @param int $reassigned_user_id - ID of the user the removed user's posts is reassigned to (if any)
464
		 */
465
		do_action( 'jetpack_removed_user_from_blog', $user_id, $reassigned_user_id );
466
	}
467
468
	protected function is_add_new_user_to_blog() {
469
		return Jetpack::is_function_in_backtrace( 'add_new_user_to_blog' );
470
	}
471
472
	protected function is_add_user_to_blog() {
473
		return Jetpack::is_function_in_backtrace( 'add_user_to_blog' );
474
	}
475
476
	protected function is_delete_user() {
477
		return Jetpack::is_function_in_backtrace( array( 'wp_delete_user', 'remove_user_from_blog' ) );
478
	}
479
480
	protected function is_create_user() {
481
		$functions = array(
482
			'add_new_user_to_blog', // Used to suppress jetpack_sync_save_user in save_user_cap_handler when user registered on multi site
483
			'wp_create_user', // Used to suppress jetpack_sync_save_user in save_user_role_handler when user registered on multi site
484
			'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
485
		);
486
487
		return Jetpack::is_function_in_backtrace( $functions );
488
	}
489
490
	protected function get_reassigned_network_user_id() {
491
		$backtrace = debug_backtrace( false ); // phpcs:ignore PHPCompatibility.PHP.NewFunctionParameters.debug_backtrace_optionsFound
492
		foreach ( $backtrace as $call ) {
493
			if (
494
				'remove_user_from_blog' === $call['function'] &&
495
				3 === count( $call['args'] )
496
			) {
497
				return $call['args'][2];
498
			}
499
		}
500
501
		return false;
502
	}
503
}
504