WP_REST_Users_Controller   D
last analyzed

Complexity

Total Complexity 133

Size/Duplication

Total Lines 934
Duplicated Lines 8.03 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 75
loc 934
rs 4
c 0
b 0
f 0
wmc 133
lcom 1
cbo 1

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A register_routes() 0 56 1
F get_items() 18 93 10
C get_item_permissions_check() 3 22 9
A get_item() 0 13 3
A get_current_item() 0 14 2
A create_item_permissions_check() 0 8 2
C create_item() 0 67 15
A update_item_permissions_check() 0 14 4
A delete_item_permissions_check() 0 10 2
D delete_item() 5 44 9
B get_items_permissions_check() 0 16 7
F prepare_item_for_response() 0 88 18
A prepare_links() 0 12 1
F prepare_item_for_database() 21 56 23
C check_role_update() 3 30 8
B get_item_schema() 22 147 3
A get_collection_params() 0 59 1
C update_item() 3 55 14

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WP_REST_Users_Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WP_REST_Users_Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Access users
5
 */
6
class WP_REST_Users_Controller extends WP_REST_Controller {
7
8
	public function __construct() {
9
		$this->namespace = 'wp/v2';
10
		$this->rest_base = 'users';
11
	}
12
13
	/**
14
	 * Register the routes for the objects of the controller.
15
	 */
16
	public function register_routes() {
17
18
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
19
			array(
20
				'methods'         => WP_REST_Server::READABLE,
21
				'callback'        => array( $this, 'get_items' ),
22
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
23
				'args'            => $this->get_collection_params(),
24
			),
25
			array(
26
				'methods'         => WP_REST_Server::CREATABLE,
27
				'callback'        => array( $this, 'create_item' ),
28
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
29
				'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
30
			),
31
			'schema' => array( $this, 'get_public_item_schema' ),
32
		) );
33
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
34
			array(
35
				'methods'         => WP_REST_Server::READABLE,
36
				'callback'        => array( $this, 'get_item' ),
37
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
38
				'args'            => array(
39
					'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
40
				),
41
			),
42
			array(
43
				'methods'         => WP_REST_Server::EDITABLE,
44
				'callback'        => array( $this, 'update_item' ),
45
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
46
				'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
47
			),
48
			array(
49
				'methods' => WP_REST_Server::DELETABLE,
50
				'callback' => array( $this, 'delete_item' ),
51
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
52
				'args' => array(
53
					'force'    => array(
54
						'default'     => false,
55
						'description' => __( 'Required to be true, as resource does not support trashing.' ),
56
					),
57
					'reassign' => array(),
58
				),
59
			),
60
			'schema' => array( $this, 'get_public_item_schema' ),
61
		) );
62
63
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/me', array(
64
			'methods'         => WP_REST_Server::READABLE,
65
			'callback'        => array( $this, 'get_current_item' ),
66
			'args'            => array(
67
				'context'          => array(),
68
			),
69
			'schema' => array( $this, 'get_public_item_schema' ),
70
		));
71
	}
72
73
	/**
74
	 * Permissions check for getting all users.
75
	 *
76
	 * @param WP_REST_Request $request Full details about the request.
77
	 * @return WP_Error|boolean
78
	 */
79
	public function get_items_permissions_check( $request ) {
80
		// Check if roles is specified in GET request and if user can list users.
81
		if ( ! empty( $request['roles'] ) && ! current_user_can( 'list_users' ) ) {
82
			return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot filter by role.' ), array( 'status' => rest_authorization_required_code() ) );
83
		}
84
85
		if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
86
			return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
87
		}
88
89
		if ( in_array( $request['orderby'], array( 'email', 'registered_date' ), true ) && ! current_user_can( 'list_users' ) ) {
90
			return new WP_Error( 'rest_forbidden_orderby', __( 'Sorry, you cannot order by this parameter.' ), array( 'status' => rest_authorization_required_code() ) );
91
		}
92
93
		return true;
94
	}
95
96
	/**
97
	 * Get all users
98
	 *
99
	 * @param WP_REST_Request $request Full details about the request.
100
	 * @return WP_Error|WP_REST_Response
101
	 */
102
	public function get_items( $request ) {
103
104
		$prepared_args = array();
105
		$prepared_args['exclude'] = $request['exclude'];
106
		$prepared_args['include'] = $request['include'];
107
		$prepared_args['order'] = $request['order'];
108
		$prepared_args['number'] = $request['per_page'];
109 View Code Duplication
		if ( ! empty( $request['offset'] ) ) {
110
			$prepared_args['offset'] = $request['offset'];
111
		} else {
112
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
113
		}
114
		$orderby_possibles = array(
115
			'id'              => 'ID',
116
			'include'         => 'include',
117
			'name'            => 'display_name',
118
			'registered_date' => 'registered',
119
			'slug'            => 'user_nicename',
120
			'email'           => 'user_email',
121
			'url'             => 'user_url',
122
		);
123
		$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
124
		$prepared_args['search'] = $request['search'];
125
		$prepared_args['role__in'] = $request['roles'];
126
127
		if ( ! current_user_can( 'list_users' ) ) {
128
			$prepared_args['has_published_posts'] = true;
129
		}
130
131
		if ( ! empty( $prepared_args['search'] ) ) {
132
			$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
133
		}
134
135
		if ( ! empty( $request['slug'] ) ) {
136
			$prepared_args['search'] = $request['slug'];
137
			$prepared_args['search_columns'] = array( 'user_nicename' );
138
		}
139
140
		/**
141
		 * Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
142
		 *
143
		 * @see https://developer.wordpress.org/reference/classes/wp_user_query/
144
		 *
145
		 * @param array           $prepared_args Array of arguments for WP_User_Query.
146
		 * @param WP_REST_Request $request       The current request.
147
		 */
148
		$prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );
149
150
		$query = new WP_User_Query( $prepared_args );
151
152
		$users = array();
153
		foreach ( $query->results as $user ) {
154
			$data = $this->prepare_item_for_response( $user, $request );
155
			$users[] = $this->prepare_response_for_collection( $data );
156
		}
157
158
		$response = rest_ensure_response( $users );
159
160
		// Store pagation values for headers then unset for count query.
161
		$per_page = (int) $prepared_args['number'];
162
		$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
163
164
		$prepared_args['fields'] = 'ID';
165
166
		$total_users = $query->get_total();
167
		if ( $total_users < 1 ) {
168
			// Out-of-bounds, run the query again without LIMIT for total count
169
			unset( $prepared_args['number'] );
170
			unset( $prepared_args['offset'] );
171
			$count_query = new WP_User_Query( $prepared_args );
172
			$total_users = $count_query->get_total();
173
		}
174
		$response->header( 'X-WP-Total', (int) $total_users );
175
		$max_pages = ceil( $total_users / $per_page );
176
		$response->header( 'X-WP-TotalPages', (int) $max_pages );
177
178
		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
179 View Code Duplication
		if ( $page > 1 ) {
180
			$prev_page = $page - 1;
181
			if ( $prev_page > $max_pages ) {
182
				$prev_page = $max_pages;
183
			}
184
			$prev_link = add_query_arg( 'page', $prev_page, $base );
185
			$response->link_header( 'prev', $prev_link );
186
		}
187 View Code Duplication
		if ( $max_pages > $page ) {
188
			$next_page = $page + 1;
189
			$next_link = add_query_arg( 'page', $next_page, $base );
190
			$response->link_header( 'next', $next_link );
191
		}
192
193
		return $response;
194
	}
195
196
	/**
197
	 * Check if a given request has access to read a user
198
	 *
199
	 * @param  WP_REST_Request $request Full details about the request.
200
	 * @return WP_Error|boolean
201
	 */
202
	public function get_item_permissions_check( $request ) {
203
204
		$id = (int) $request['id'];
205
		$user = get_userdata( $id );
206
		$types = get_post_types( array( 'show_in_rest' => true ), 'names' );
207
208
		if ( empty( $id ) || empty( $user->ID ) ) {
209
			return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
210
		}
211
212
		if ( get_current_user_id() === $id ) {
213
			return true;
214
		}
215
216
		if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
217
			return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
218 View Code Duplication
		} else if ( ! count_user_posts( $id, $types ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) {
219
			return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this resource.' ), array( 'status' => rest_authorization_required_code() ) );
220
		}
221
222
		return true;
223
	}
224
225
	/**
226
	 * Get a single user
227
	 *
228
	 * @param WP_REST_Request $request Full details about the request.
229
	 * @return WP_Error|WP_REST_Response
230
	 */
231
	public function get_item( $request ) {
232
		$id = (int) $request['id'];
233
		$user = get_userdata( $id );
234
235
		if ( empty( $id ) || empty( $user->ID ) ) {
236
			return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
237
		}
238
239
		$user = $this->prepare_item_for_response( $user, $request );
240
		$response = rest_ensure_response( $user );
241
242
		return $response;
243
	}
244
245
	/**
246
	 * Get the current user
247
	 *
248
	 * @param WP_REST_Request $request Full details about the request.
249
	 * @return WP_Error|WP_REST_Response
250
	 */
251
	public function get_current_item( $request ) {
252
		$current_user_id = get_current_user_id();
253
		if ( empty( $current_user_id ) ) {
254
			return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) );
255
		}
256
257
		$user = wp_get_current_user();
258
		$response = $this->prepare_item_for_response( $user, $request );
259
		$response = rest_ensure_response( $response );
260
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $current_user_id ) ) );
261
		$response->set_status( 302 );
262
263
		return $response;
264
	}
265
266
	/**
267
	 * Check if a given request has access create users
268
	 *
269
	 * @param  WP_REST_Request $request Full details about the request.
270
	 * @return boolean
271
	 */
272
	public function create_item_permissions_check( $request ) {
273
274
		if ( ! current_user_can( 'create_users' ) ) {
275
			return new WP_Error( 'rest_cannot_create_user', __( 'Sorry, you are not allowed to create resource.' ), array( 'status' => rest_authorization_required_code() ) );
276
		}
277
278
		return true;
279
	}
280
281
	/**
282
	 * Create a single user
283
	 *
284
	 * @param WP_REST_Request $request Full details about the request.
285
	 * @return WP_Error|WP_REST_Response
286
	 */
287
	public function create_item( $request ) {
288
		if ( ! empty( $request['id'] ) ) {
289
			return new WP_Error( 'rest_user_exists', __( 'Cannot create existing resource.' ), array( 'status' => 400 ) );
290
		}
291
292
		$schema = $this->get_item_schema();
293
294
		if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
295
			$check_permission = $this->check_role_update( $request['id'], $request['roles'] );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->check_role_update...'], $request['roles']); of type WP_Error|boolean adds the type boolean to the return on line 297 which is incompatible with the return type documented by WP_REST_Users_Controller::create_item of type WP_Error|WP_REST_Response.
Loading history...
296
			if ( is_wp_error( $check_permission ) ) {
297
				return $check_permission;
298
			}
299
		}
300
301
		$user = $this->prepare_item_for_database( $request );
302
303
		if ( is_multisite() ) {
304
			$ret = wpmu_validate_user_signup( $user->user_login, $user->user_email );
305
			if ( is_wp_error( $ret['errors'] ) && ! empty( $ret['errors']->errors ) ) {
306
				return $ret['errors'];
307
			}
308
		}
309
310
		if ( is_multisite() ) {
311
			$user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email );
312
			if ( ! $user_id ) {
313
				return new WP_Error( 'rest_user_create', __( 'Error creating new resource.' ), array( 'status' => 500 ) );
314
			}
315
			$user->ID = $user_id;
316
			$user_id = wp_update_user( $user );
317
			if ( is_wp_error( $user_id ) ) {
318
				return $user_id;
319
			}
320
		} else {
321
			$user_id = wp_insert_user( $user );
322
			if ( is_wp_error( $user_id ) ) {
323
				return $user_id;
324
			}
325
		}
326
327
		$user = get_user_by( 'id', $user_id );
328
		if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
329
			array_map( array( $user, 'add_role' ), $request['roles'] );
330
		}
331
332
		$fields_update = $this->update_additional_fields_for_object( $user, $request );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->update_additional...bject($user, $request); of type boolean|WP_Error adds the type boolean to the return on line 334 which is incompatible with the return type documented by WP_REST_Users_Controller::create_item of type WP_Error|WP_REST_Response.
Loading history...
333
		if ( is_wp_error( $fields_update ) ) {
334
			return $fields_update;
335
		}
336
337
		/**
338
		 * Fires after a user is created or updated via the REST API.
339
		 *
340
		 * @param WP_User         $user      Data used to create the user.
341
		 * @param WP_REST_Request $request   Request object.
342
		 * @param boolean         $creating  True when creating user, false when updating user.
343
		 */
344
		do_action( 'rest_insert_user', $user, $request, true );
345
346
		$request->set_param( 'context', 'edit' );
347
		$response = $this->prepare_item_for_response( $user, $request );
348
		$response = rest_ensure_response( $response );
349
		$response->set_status( 201 );
350
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user_id ) ) );
351
352
		return $response;
353
	}
354
355
	/**
356
	 * Check if a given request has access update a user
357
	 *
358
	 * @param  WP_REST_Request $request Full details about the request.
359
	 * @return boolean
360
	 */
361
	public function update_item_permissions_check( $request ) {
362
363
		$id = (int) $request['id'];
364
365
		if ( ! current_user_can( 'edit_user', $id ) ) {
366
			return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.' ), array( 'status' => rest_authorization_required_code() ) );
367
		}
368
369
		if ( ! empty( $request['roles'] ) && ! current_user_can( 'edit_users' ) ) {
370
			return new WP_Error( 'rest_cannot_edit_roles', __( 'Sorry, you are not allowed to edit roles of this resource.' ), array( 'status' => rest_authorization_required_code() ) );
371
		}
372
373
		return true;
374
	}
375
376
	/**
377
	 * Update a single user
378
	 *
379
	 * @param WP_REST_Request $request Full details about the request.
380
	 * @return WP_Error|WP_REST_Response
381
	 */
382
	public function update_item( $request ) {
383
		$id = (int) $request['id'];
384
385
		$user = get_userdata( $id );
386
		if ( ! $user ) {
387
			return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
388
		}
389
390
		if ( email_exists( $request['email'] ) && $request['email'] !== $user->user_email ) {
391
			return new WP_Error( 'rest_user_invalid_email', __( 'Email address is invalid.' ), array( 'status' => 400 ) );
392
		}
393
394
		if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) {
395
			return new WP_Error( 'rest_user_invalid_argument', __( "Username isn't editable" ), array( 'status' => 400 ) );
396
		}
397
398 View Code Duplication
		if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) {
399
			return new WP_Error( 'rest_user_invalid_slug', __( 'Slug is invalid.' ), array( 'status' => 400 ) );
400
		}
401
402
		if ( ! empty( $request['roles'] ) ) {
403
			$check_permission = $this->check_role_update( $id, $request['roles'] );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->check_role_update($id, $request['roles']); of type WP_Error|boolean adds the type boolean to the return on line 405 which is incompatible with the return type documented by WP_REST_Users_Controller::update_item of type WP_Error|WP_REST_Response.
Loading history...
404
			if ( is_wp_error( $check_permission ) ) {
405
				return $check_permission;
406
			}
407
		}
408
409
		$user = $this->prepare_item_for_database( $request );
410
411
		// Ensure we're operating on the same user we already checked
412
		$user->ID = $id;
413
414
		$user_id = wp_update_user( $user );
415
		if ( is_wp_error( $user_id ) ) {
416
			return $user_id;
417
		}
418
419
		$user = get_user_by( 'id', $id );
420
		if ( ! empty( $request['roles'] ) ) {
421
			array_map( array( $user, 'add_role' ), $request['roles'] );
422
		}
423
424
		$fields_update = $this->update_additional_fields_for_object( $user, $request );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->update_additional...bject($user, $request); of type boolean|WP_Error adds the type boolean to the return on line 426 which is incompatible with the return type documented by WP_REST_Users_Controller::update_item of type WP_Error|WP_REST_Response.
Loading history...
425
		if ( is_wp_error( $fields_update ) ) {
426
			return $fields_update;
427
		}
428
429
		/* This action is documented in lib/endpoints/class-wp-rest-users-controller.php */
430
		do_action( 'rest_insert_user', $user, $request, false );
431
432
		$request->set_param( 'context', 'edit' );
433
		$response = $this->prepare_item_for_response( $user, $request );
434
		$response = rest_ensure_response( $response );
435
		return $response;
436
	}
437
438
	/**
439
	 * Check if a given request has access delete a user
440
	 *
441
	 * @param  WP_REST_Request $request Full details about the request.
442
	 * @return boolean
443
	 */
444
	public function delete_item_permissions_check( $request ) {
445
446
		$id = (int) $request['id'];
447
448
		if ( ! current_user_can( 'delete_user', $id ) ) {
449
			return new WP_Error( 'rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.' ), array( 'status' => rest_authorization_required_code() ) );
450
		}
451
452
		return true;
453
	}
454
455
	/**
456
	 * Delete a single user
457
	 *
458
	 * @param WP_REST_Request $request Full details about the request.
459
	 * @return WP_Error|WP_REST_Response
460
	 */
461
	public function delete_item( $request ) {
462
		$id = (int) $request['id'];
463
		$reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
464
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
465
466
		// We don't support trashing for this type, error out
467
		if ( ! $force ) {
468
			return new WP_Error( 'rest_trash_not_supported', __( 'Users do not support trashing.' ), array( 'status' => 501 ) );
469
		}
470
471
		$user = get_userdata( $id );
472
		if ( ! $user ) {
473
			return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
474
		}
475
476 View Code Duplication
		if ( ! empty( $reassign ) ) {
477
			if ( $reassign === $id || ! get_userdata( $reassign ) ) {
478
				return new WP_Error( 'rest_user_invalid_reassign', __( 'Invalid resource id for reassignment.' ), array( 'status' => 400 ) );
479
			}
480
		}
481
482
		$request->set_param( 'context', 'edit' );
483
		$response = $this->prepare_item_for_response( $user, $request );
484
485
		/** Include admin user functions to get access to wp_delete_user() */
486
		require_once ABSPATH . 'wp-admin/includes/user.php';
487
488
		$result = wp_delete_user( $id, $reassign );
489
490
		if ( ! $result ) {
491
			return new WP_Error( 'rest_cannot_delete', __( 'The resource cannot be deleted.' ), array( 'status' => 500 ) );
492
		}
493
494
		/**
495
		 * Fires after a user is deleted via the REST API.
496
		 *
497
		 * @param WP_User          $user     The user data.
498
		 * @param WP_REST_Response $response The response returned from the API.
499
		 * @param WP_REST_Request  $request  The request sent to the API.
500
		 */
501
		do_action( 'rest_delete_user', $user, $response, $request );
502
503
		return $response;
504
	}
505
506
	/**
507
	 * Prepare a single user output for response
508
	 *
509
	 * @param object $user User object.
510
	 * @param WP_REST_Request $request Request object.
511
	 * @return WP_REST_Response $response Response data.
512
	 */
513
	public function prepare_item_for_response( $user, $request ) {
514
515
		$data = array();
516
		$schema = $this->get_item_schema();
517
		if ( ! empty( $schema['properties']['id'] ) ) {
518
			$data['id'] = $user->ID;
519
		}
520
521
		if ( ! empty( $schema['properties']['username'] ) ) {
522
			$data['username'] = $user->user_login;
523
		}
524
525
		if ( ! empty( $schema['properties']['name'] ) ) {
526
			$data['name'] = $user->display_name;
527
		}
528
529
		if ( ! empty( $schema['properties']['first_name'] ) ) {
530
			$data['first_name'] = $user->first_name;
531
		}
532
533
		if ( ! empty( $schema['properties']['last_name'] ) ) {
534
			$data['last_name'] = $user->last_name;
535
		}
536
537
		if ( ! empty( $schema['properties']['email'] ) ) {
538
			$data['email'] = $user->user_email;
539
		}
540
541
		if ( ! empty( $schema['properties']['url'] ) ) {
542
			$data['url'] = $user->user_url;
543
		}
544
545
		if ( ! empty( $schema['properties']['description'] ) ) {
546
			$data['description'] = $user->description;
547
		}
548
549
		if ( ! empty( $schema['properties']['link'] ) ) {
550
			$data['link'] = get_author_posts_url( $user->ID, $user->user_nicename );
551
		}
552
553
		if ( ! empty( $schema['properties']['nickname'] ) ) {
554
			$data['nickname'] = $user->nickname;
555
		}
556
557
		if ( ! empty( $schema['properties']['slug'] ) ) {
558
			$data['slug'] = $user->user_nicename;
559
		}
560
561
		if ( ! empty( $schema['properties']['roles'] ) ) {
562
			// Defensively call array_values() to ensure an array is returned.
563
			$data['roles'] = array_values( $user->roles );
564
		}
565
566
		if ( ! empty( $schema['properties']['registered_date'] ) ) {
567
			$data['registered_date'] = date( 'c', strtotime( $user->user_registered ) );
568
		}
569
570
		if ( ! empty( $schema['properties']['capabilities'] ) ) {
571
			$data['capabilities'] = (object) $user->allcaps;
572
		}
573
574
		if ( ! empty( $schema['properties']['extra_capabilities'] ) ) {
575
			$data['extra_capabilities'] = (object) $user->caps;
576
		}
577
578
		if ( ! empty( $schema['properties']['avatar_urls'] ) ) {
579
			$data['avatar_urls'] = rest_get_avatar_urls( $user->user_email );
580
		}
581
582
		$context = ! empty( $request['context'] ) ? $request['context'] : 'embed';
583
584
		$data = $this->add_additional_fields_to_object( $data, $request );
585
		$data = $this->filter_response_by_context( $data, $context );
586
587
		// Wrap the data in a response object
588
		$response = rest_ensure_response( $data );
589
590
		$response->add_links( $this->prepare_links( $user ) );
591
592
		/**
593
		 * Filter user data returned from the REST API.
594
		 *
595
		 * @param WP_REST_Response $response  The response object.
596
		 * @param object           $user      User object used to create response.
597
		 * @param WP_REST_Request  $request   Request object.
598
		 */
599
		return apply_filters( 'rest_prepare_user', $response, $user, $request );
600
	}
601
602
	/**
603
	 * Prepare links for the request.
604
	 *
605
	 * @param WP_Post $user User object.
606
	 * @return array Links for the given user.
607
	 */
608
	protected function prepare_links( $user ) {
609
		$links = array(
610
			'self' => array(
611
				'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user->ID ) ),
612
			),
613
			'collection' => array(
614
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
615
			),
616
		);
617
618
		return $links;
619
	}
620
621
	/**
622
	 * Prepare a single user for create or update
623
	 *
624
	 * @param WP_REST_Request $request Request object.
625
	 * @return object $prepared_user User object.
626
	 */
627
	protected function prepare_item_for_database( $request ) {
628
		$prepared_user = new stdClass;
629
630
		$schema = $this->get_item_schema();
631
632
		// required arguments.
633
		if ( isset( $request['email'] ) && ! empty( $schema['properties']['email'] ) ) {
634
			$prepared_user->user_email = $request['email'];
635
		}
636
		if ( isset( $request['username'] ) && ! empty( $schema['properties']['username'] ) ) {
637
			$prepared_user->user_login = $request['username'];
638
		}
639
		if ( isset( $request['password'] ) && ! empty( $schema['properties']['password'] ) ) {
640
			$prepared_user->user_pass = $request['password'];
641
		}
642
643
		// optional arguments.
644
		if ( isset( $request['id'] ) ) {
645
			$prepared_user->ID = absint( $request['id'] );
646
		}
647 View Code Duplication
		if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
648
			$prepared_user->display_name = $request['name'];
649
		}
650 View Code Duplication
		if ( isset( $request['first_name'] ) && ! empty( $schema['properties']['first_name'] ) ) {
651
			$prepared_user->first_name = $request['first_name'];
652
		}
653 View Code Duplication
		if ( isset( $request['last_name'] ) && ! empty( $schema['properties']['last_name'] ) ) {
654
			$prepared_user->last_name = $request['last_name'];
655
		}
656 View Code Duplication
		if ( isset( $request['nickname'] ) && ! empty( $schema['properties']['nickname'] ) ) {
657
			$prepared_user->nickname = $request['nickname'];
658
		}
659 View Code Duplication
		if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
660
			$prepared_user->user_nicename = $request['slug'];
661
		}
662 View Code Duplication
		if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
663
			$prepared_user->description = $request['description'];
664
		}
665
666 View Code Duplication
		if ( isset( $request['url'] ) && ! empty( $schema['properties']['url'] ) ) {
667
			$prepared_user->user_url = $request['url'];
668
		}
669
670
		// setting roles will be handled outside of this function.
671
		if ( isset( $request['roles'] ) ) {
672
			$prepared_user->role = false;
673
		}
674
675
		/**
676
		 * Filter user data before inserting user via the REST API.
677
		 *
678
		 * @param object          $prepared_user User object.
679
		 * @param WP_REST_Request $request       Request object.
680
		 */
681
		return apply_filters( 'rest_pre_insert_user', $prepared_user, $request );
682
	}
683
684
	/**
685
	 * Determine if the current user is allowed to make the desired roles change.
686
	 *
687
	 * @param integer $user_id
688
	 * @param array   $roles
689
	 * @return WP_Error|boolean
690
	 */
691
	protected function check_role_update( $user_id, $roles ) {
692
		global $wp_roles;
693
694
		foreach ( $roles as $role ) {
695
696
			if ( ! isset( $wp_roles->role_objects[ $role ] ) ) {
697
				return new WP_Error( 'rest_user_invalid_role', sprintf( __( 'The role %s does not exist.' ), $role ), array( 'status' => 400 ) );
698
			}
699
700
			$potential_role = $wp_roles->role_objects[ $role ];
701
			// Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
702
			// Multisite super admins can freely edit their blog roles -- they possess all caps.
703 View Code Duplication
			if ( ! ( is_multisite() && current_user_can( 'manage_sites' ) ) && get_current_user_id() === $user_id && ! $potential_role->has_cap( 'edit_users' ) ) {
704
				return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give resource that role.' ), array( 'status' => rest_authorization_required_code() ) );
705
			}
706
707
			// The new role must be editable by the logged-in user.
708
709
			/** Include admin functions to get access to get_editable_roles() */
710
			require_once ABSPATH . 'wp-admin/includes/admin.php';
711
712
			$editable_roles = get_editable_roles();
713
			if ( empty( $editable_roles[ $role ] ) ) {
714
				return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give resource that role.' ), array( 'status' => 403 ) );
715
			}
716
		}
717
718
		return true;
719
720
	}
721
722
	/**
723
	 * Get the User's schema, conforming to JSON Schema
724
	 *
725
	 * @return array
726
	 */
727
	public function get_item_schema() {
728
		$schema = array(
729
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
730
			'title'      => 'user',
731
			'type'       => 'object',
732
			'properties' => array(
733
				'id'          => array(
734
					'description' => __( 'Unique identifier for the resource.' ),
735
					'type'        => 'integer',
736
					'context'     => array( 'embed', 'view', 'edit' ),
737
					'readonly'    => true,
738
				),
739
				'username'    => array(
740
					'description' => __( 'Login name for the resource.' ),
741
					'type'        => 'string',
742
					'context'     => array( 'edit' ),
743
					'required'    => true,
744
					'arg_options' => array(
745
						'sanitize_callback' => 'sanitize_user',
746
					),
747
				),
748
				'name'        => array(
749
					'description' => __( 'Display name for the resource.' ),
750
					'type'        => 'string',
751
					'context'     => array( 'embed', 'view', 'edit' ),
752
					'arg_options' => array(
753
						'sanitize_callback' => 'sanitize_text_field',
754
					),
755
				),
756
				'first_name'  => array(
757
					'description' => __( 'First name for the resource.' ),
758
					'type'        => 'string',
759
					'context'     => array( 'edit' ),
760
					'arg_options' => array(
761
						'sanitize_callback' => 'sanitize_text_field',
762
					),
763
				),
764
				'last_name'   => array(
765
					'description' => __( 'Last name for the resource.' ),
766
					'type'        => 'string',
767
					'context'     => array( 'edit' ),
768
					'arg_options' => array(
769
						'sanitize_callback' => 'sanitize_text_field',
770
					),
771
				),
772
				'email'       => array(
773
					'description' => __( 'The email address for the resource.' ),
774
					'type'        => 'string',
775
					'format'      => 'email',
776
					'context'     => array( 'edit' ),
777
					'required'    => true,
778
				),
779
				'url'         => array(
780
					'description' => __( 'URL of the resource.' ),
781
					'type'        => 'string',
782
					'format'      => 'uri',
783
					'context'     => array( 'embed', 'view', 'edit' ),
784
				),
785
				'description' => array(
786
					'description' => __( 'Description of the resource.' ),
787
					'type'        => 'string',
788
					'context'     => array( 'embed', 'view', 'edit' ),
789
					'arg_options' => array(
790
						'sanitize_callback' => 'wp_filter_post_kses',
791
					),
792
				),
793
				'link'        => array(
794
					'description' => __( 'Author URL to the resource.' ),
795
					'type'        => 'string',
796
					'format'      => 'uri',
797
					'context'     => array( 'embed', 'view', 'edit' ),
798
					'readonly'    => true,
799
				),
800
				'nickname'    => array(
801
					'description' => __( 'The nickname for the resource.' ),
802
					'type'        => 'string',
803
					'context'     => array( 'edit' ),
804
					'arg_options' => array(
805
						'sanitize_callback' => 'sanitize_text_field',
806
					),
807
				),
808
				'slug'        => array(
809
					'description' => __( 'An alphanumeric identifier for the resource.' ),
810
					'type'        => 'string',
811
					'context'     => array( 'embed', 'view', 'edit' ),
812
					'arg_options' => array(
813
						'sanitize_callback' => array( $this, 'sanitize_slug' ),
814
					),
815
				),
816
				'registered_date' => array(
817
					'description' => __( 'Registration date for the resource.' ),
818
					'type'        => 'string',
819
					'format'      => 'date-time',
820
					'context'     => array( 'edit' ),
821
					'readonly'    => true,
822
				),
823
				'roles'           => array(
824
					'description' => __( 'Roles assigned to the resource.' ),
825
					'type'        => 'array',
826
					'context'     => array( 'edit' ),
827
				),
828
				'password'        => array(
829
					'description' => __( 'Password for the resource (never included).' ),
830
					'type'        => 'string',
831
					'context'     => array(), // Password is never displayed
832
					'required'    => true,
833
				),
834
				'capabilities'    => array(
835
					'description' => __( 'All capabilities assigned to the resource.' ),
836
					'type'        => 'object',
837
					'context'     => array( 'edit' ),
838
					'readonly'    => true,
839
				),
840
				'extra_capabilities' => array(
841
					'description' => __( 'Any extra capabilities assigned to the resource.' ),
842
					'type'        => 'object',
843
					'context'     => array( 'edit' ),
844
					'readonly'    => true,
845
				),
846
			),
847
		);
848
849 View Code Duplication
		if ( get_option( 'show_avatars' ) ) {
850
			$avatar_properties = array();
851
852
			$avatar_sizes = rest_get_avatar_sizes();
853
			foreach ( $avatar_sizes as $size ) {
854
				$avatar_properties[ $size ] = array(
855
					'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
856
					'type'        => 'string',
857
					'format'      => 'uri',
858
					'context'     => array( 'embed', 'view', 'edit' ),
859
				);
860
			}
861
862
			$schema['properties']['avatar_urls']  = array(
863
				'description' => __( 'Avatar URLs for the resource.' ),
864
				'type'        => 'object',
865
				'context'     => array( 'embed', 'view', 'edit' ),
866
				'readonly'    => true,
867
				'properties'  => $avatar_properties,
868
			);
869
870
		}
871
872
		return $this->add_additional_fields_schema( $schema );
873
	}
874
875
	/**
876
	 * Get the query params for collections
877
	 *
878
	 * @return array
879
	 */
880
	public function get_collection_params() {
881
		$query_params = parent::get_collection_params();
882
883
		$query_params['context']['default'] = 'view';
884
885
		$query_params['exclude'] = array(
886
			'description'        => __( 'Ensure result set excludes specific ids.' ),
887
			'type'               => 'array',
888
			'default'            => array(),
889
			'sanitize_callback'  => 'wp_parse_id_list',
890
		);
891
		$query_params['include'] = array(
892
			'description'        => __( 'Limit result set to specific ids.' ),
893
			'type'               => 'array',
894
			'default'            => array(),
895
			'sanitize_callback'  => 'wp_parse_id_list',
896
		);
897
		$query_params['offset'] = array(
898
			'description'        => __( 'Offset the result set by a specific number of items.' ),
899
			'type'               => 'integer',
900
			'sanitize_callback'  => 'absint',
901
			'validate_callback'  => 'rest_validate_request_arg',
902
		);
903
		$query_params['order'] = array(
904
			'default'            => 'asc',
905
			'description'        => __( 'Order sort attribute ascending or descending.' ),
906
			'enum'               => array( 'asc', 'desc' ),
907
			'sanitize_callback'  => 'sanitize_key',
908
			'type'               => 'string',
909
			'validate_callback'  => 'rest_validate_request_arg',
910
		);
911
		$query_params['orderby'] = array(
912
			'default'            => 'name',
913
			'description'        => __( 'Sort collection by object attribute.' ),
914
			'enum'               => array(
915
				'id',
916
				'include',
917
				'name',
918
				'registered_date',
919
				'slug',
920
				'email',
921
				'url',
922
			),
923
			'sanitize_callback'  => 'sanitize_key',
924
			'type'               => 'string',
925
			'validate_callback'  => 'rest_validate_request_arg',
926
		);
927
		$query_params['slug']    = array(
928
			'description'        => __( 'Limit result set to resources with a specific slug.' ),
929
			'type'               => 'string',
930
			'validate_callback'  => 'rest_validate_request_arg',
931
		);
932
		$query_params['roles']   = array(
933
			'description'        => __( 'Limit result set to resources matching at least one specific role provided. Accepts csv list or single role.' ),
934
			'type'               => 'array',
935
			'sanitize_callback'  => 'wp_parse_slug_list',
936
		);
937
		return $query_params;
938
	}
939
}
940