Issues (90)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

lib/endpoints/class-wp-rest-users-controller.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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