Completed
Push — master ( f6d20e...4e14c4 )
by Mike
11:10
created

WC_REST_Customers_Controller::get_role_names()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 4
rs 10
1
<?php
2
/**
3
 * REST API Customers controller
4
 *
5
 * Handles requests to the /customers endpoint.
6
 *
7
 * @author   WooThemes
8
 * @category API
9
 * @package  WooCommerce/API
10
 * @since    2.6.0
11
 */
12
13
if ( ! defined( 'ABSPATH' ) ) {
14
	exit;
15
}
16
17
/**
18
 * REST API Customers controller class.
19
 *
20
 * @package WooCommerce/API
21
 * @extends WC_REST_Controller
22
 */
23
class WC_REST_Customers_Controller extends WC_REST_Controller {
24
25
	/**
26
	 * Endpoint namespace.
27
	 *
28
	 * @var string
29
	 */
30
	protected $namespace = 'wc/v1';
31
32
	/**
33
	 * Route base.
34
	 *
35
	 * @var string
36
	 */
37
	protected $rest_base = 'customers';
38
39
	/**
40
	 * Register the routes for customers.
41
	 */
42
	public function register_routes() {
43
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
44
			array(
45
				'methods'             => WP_REST_Server::READABLE,
46
				'callback'            => array( $this, 'get_items' ),
47
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
48
				'args'                => $this->get_collection_params(),
49
			),
50
			array(
51
				'methods'             => WP_REST_Server::CREATABLE,
52
				'callback'            => array( $this, 'create_item' ),
53
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
54
				'args'                => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
55
					'email' => array(
56
						'required' => true,
57
					),
58
					'username' => array(
59
						'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ),
60
					),
61
					'password' => array(
62
						'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ),
63
					),
64
				) ),
65
			),
66
			'schema' => array( $this, 'get_public_item_schema' ),
67
		) );
68
69
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
70
			array(
71
				'methods'             => WP_REST_Server::READABLE,
72
				'callback'            => array( $this, 'get_item' ),
73
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
74
				'args'                => array(
75
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
76
				),
77
			),
78
			array(
79
				'methods'             => WP_REST_Server::EDITABLE,
80
				'callback'            => array( $this, 'update_item' ),
81
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
82
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
83
			),
84
			array(
85
				'methods'             => WP_REST_Server::DELETABLE,
86
				'callback'            => array( $this, 'delete_item' ),
87
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
88
				'args'                => array(
89
					'force' => array(
90
						'default'     => false,
91
						'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
92
					),
93
					'reassign' => array(),
94
				),
95
			),
96
			'schema' => array( $this, 'get_public_item_schema' ),
97
		) );
98
99
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
100
			array(
101
				'methods'             => WP_REST_Server::EDITABLE,
102
				'callback'            => array( $this, 'batch_items' ),
103
				'permission_callback' => array( $this, 'batch_items_permissions_check' ),
104
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
105
			),
106
			'schema' => array( $this, 'get_public_batch_schema' ),
107
		) );
108
	}
109
110
	/**
111
	 * Check whether a given request has permission to read customers.
112
	 *
113
	 * @param  WP_REST_Request $request Full details about the request.
114
	 * @return WP_Error|boolean
115
	 */
116
	public function get_items_permissions_check( $request ) {
117
		if ( ! wc_rest_check_user_permissions( 'read' ) ) {
118
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
119
		}
120
		return true;
121
	}
122
123
	/**
124
	 * Check if a given request has access create customers.
125
	 *
126
	 * @param  WP_REST_Request $request Full details about the request.
127
	 * @return boolean
128
	 */
129
	public function create_item_permissions_check( $request ) {
130
		if ( ! wc_rest_check_user_permissions( 'create' ) ) {
131
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
132
		}
133
		return true;
134
	}
135
136
	/**
137
	 * Check if a given request has access to read a customer.
138
	 *
139
	 * @param  WP_REST_Request $request Full details about the request.
140
	 * @return WP_Error|boolean
141
	 */
142 View Code Duplication
	public function get_item_permissions_check( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
143
		$id = (int) $request['id'];
144
145
		if ( ! wc_rest_check_user_permissions( 'read', $id ) ) {
146
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
147
		}
148
		return true;
149
	}
150
151
	/**
152
	 * Check if a given request has access update a customer.
153
	 *
154
	 * @param  WP_REST_Request $request Full details about the request.
155
	 * @return boolean
156
	 */
157 View Code Duplication
	public function update_item_permissions_check( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
158
		$id = (int) $request['id'];
159
160
		if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) {
161
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
162
		}
163
		return true;
164
	}
165
166
	/**
167
	 * Check if a given request has access delete a customer.
168
	 *
169
	 * @param  WP_REST_Request $request Full details about the request.
170
	 * @return boolean
171
	 */
172 View Code Duplication
	public function delete_item_permissions_check( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
173
		$id = (int) $request['id'];
174
175
		if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) {
176
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
177
		}
178
		return true;
179
	}
180
181
	/**
182
	 * Check if a given request has access batch create, update and delete items.
183
	 *
184
	 * @param  WP_REST_Request $request Full details about the request.
185
	 * @return boolean
186
	 */
187
	public function batch_items_permissions_check( $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
188
		if ( ! wc_rest_check_user_permissions( 'batch' ) ) {
189
			return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
190
		}
191
		return true;
192
	}
193
194
	/**
195
	 * Get all customers.
196
	 *
197
	 * @param WP_REST_Request $request Full details about the request.
198
	 * @return WP_Error|WP_REST_Response
199
	 */
200
	public function get_items( $request ) {
201
		$prepared_args = array();
202
		$prepared_args['exclude'] = $request['exclude'];
203
		$prepared_args['include'] = $request['include'];
204
		$prepared_args['order']   = $request['order'];
205
		$prepared_args['number']  = $request['per_page'];
206 View Code Duplication
		if ( ! empty( $request['offset'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
207
			$prepared_args['offset'] = $request['offset'];
208
		} else {
209
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
210
		}
211
		$orderby_possibles = array(
212
			'id'              => 'ID',
213
			'include'         => 'include',
214
			'name'            => 'display_name',
215
			'registered_date' => 'registered',
216
		);
217
		$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
218
		$prepared_args['search']  = $request['search'];
219
220
		if ( '' !== $prepared_args['search'] ) {
221
			$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
222
		}
223
224
		// Filter by email.
225
		if ( ! empty( $request['email'] ) ) {
226
			$prepared_args['search']         = $request['email'];
227
			$prepared_args['search_columns'] = array( 'user_email' );
228
		}
229
230
		// Filter by role.
231
		if ( 'all' !== $request['role'] ) {
232
			$prepared_args['role'] = $request['role'];
233
		}
234
235
		/**
236
		 * Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
237
		 *
238
		 * @see https://developer.wordpress.org/reference/classes/wp_user_query/
239
		 *
240
		 * @param array           $prepared_args Array of arguments for WP_User_Query.
241
		 * @param WP_REST_Request $request       The current request.
242
		 */
243
		$prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request );
244
245
		$query = new WP_User_Query( $prepared_args );
246
247
		$users = array();
248
		foreach ( $query->results as $user ) {
249
			$data = $this->prepare_item_for_response( $user, $request );
250
			$users[] = $this->prepare_response_for_collection( $data );
251
		}
252
253
		$response = rest_ensure_response( $users );
254
255
		// Store pagation values for headers then unset for count query.
256
		$per_page = (int) $prepared_args['number'];
257
		$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
258
259
		$prepared_args['fields'] = 'ID';
260
261
		$total_users = $query->get_total();
262
		if ( $total_users < 1 ) {
263
			// Out-of-bounds, run the query again without LIMIT for total count.
264
			unset( $prepared_args['number'] );
265
			unset( $prepared_args['offset'] );
266
			$count_query = new WP_User_Query( $prepared_args );
267
			$total_users = $count_query->get_total();
268
		}
269
		$response->header( 'X-WP-Total', (int) $total_users );
270
		$max_pages = ceil( $total_users / $per_page );
271
		$response->header( 'X-WP-TotalPages', (int) $max_pages );
272
273
		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
274 View Code Duplication
		if ( $page > 1 ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
275
			$prev_page = $page - 1;
276
			if ( $prev_page > $max_pages ) {
277
				$prev_page = $max_pages;
278
			}
279
			$prev_link = add_query_arg( 'page', $prev_page, $base );
280
			$response->link_header( 'prev', $prev_link );
281
		}
282 View Code Duplication
		if ( $max_pages > $page ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
283
			$next_page = $page + 1;
284
			$next_link = add_query_arg( 'page', $next_page, $base );
285
			$response->link_header( 'next', $next_link );
286
		}
287
288
		return $response;
289
	}
290
291
	/**
292
	 * Create a single customer.
293
	 *
294
	 * @param WP_REST_Request $request Full details about the request.
295
	 * @return WP_Error|WP_REST_Response
296
	 */
297
	public function create_item( $request ) {
298
		try {
299
			if ( ! empty( $request['id'] ) ) {
300
				throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 );
301
			}
302
303
			// Sets the username.
304
			$request['username'] = ! empty( $request['username'] ) ? $request['username'] : '';
305
306
			// Sets the password.
307
			$request['password'] = ! empty( $request['password'] ) ? $request['password'] : '';
308
309
			// Create customer.
310
			$customer = new WC_Customer;
311
			$customer->set_username( $request['username'] );
312
			$customer->set_password( $request['password'] );
313
			$customer->set_email( $request['email'] );
314
			$customer->create();
315
316
			if ( ! $customer->get_id() ) {
317
				throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
318
			}
319
320
			$this->update_customer_meta_fields( $customer, $request );
321
			$customer->save();
322
323
			$user_data = get_user_by( 'id', $customer->get_id() );
324
			$this->update_additional_fields_for_object( $user_data, $request );
325
326
			/**
327
			 * Fires after a customer is created or updated via the REST API.
328
			 *
329
			 * @param WP_User         $user_data Data used to create the customer.
330
			 * @param WP_REST_Request $request   Request object.
331
			 * @param boolean         $creating  True when creating customer, false when updating customer.
332
			 */
333
			do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true );
334
335
			$request->set_param( 'context', 'edit' );
336
			$response = $this->prepare_item_for_response( $user_data, $request );
337
			$response = rest_ensure_response( $response );
338
			$response->set_status( 201 );
339
			$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) );
340
341
			return $response;
342
		} catch ( Exception $e ) {
343
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getErrorCode() does only exist in the following sub-classes of Exception: WC_CLI_Exception, WC_Data_Exception, WC_REST_Exception. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
344
		}
345
	}
346
347
	/**
348
	 * Get a single customer.
349
	 *
350
	 * @param WP_REST_Request $request Full details about the request.
351
	 * @return WP_Error|WP_REST_Response
352
	 */
353 View Code Duplication
	public function get_item( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
354
		$id        = (int) $request['id'];
355
		$user_data = get_userdata( $id );
356
357
		if ( empty( $id ) || empty( $user_data->ID ) ) {
358
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
359
		}
360
361
		$customer = $this->prepare_item_for_response( $user_data, $request );
362
		$response = rest_ensure_response( $customer );
363
364
		return $response;
365
	}
366
367
	/**
368
	 * Update a single user.
369
	 *
370
	 * @param WP_REST_Request $request Full details about the request.
371
	 * @return WP_Error|WP_REST_Response
372
	 */
373
	public function update_item( $request ) {
374
		try {
375
			$id       = (int) $request['id'];
376
			$customer = new WC_Customer( $id );
377
378
			if ( ! $customer->get_id() ) {
379
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 );
380
			}
381
382
			if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) {
383
				throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 );
384
			}
385
386
			if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) {
387
				throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 );
388
			}
389
390
			// Customer email.
391
			if ( isset( $request['email'] ) ) {
392
				$customer->set_email( sanitize_email( $request['email'] ) );
393
			}
394
395
			// Customer password.
396
			if ( isset( $request['password'] ) ) {
397
				$customer->set_password( wc_clean( $request['password'] ) );
398
			}
399
400
			$this->update_customer_meta_fields( $customer, $request );
401
			$customer->save();
402
403
			$user_data = get_userdata( $customer->get_id() );
404
			$this->update_additional_fields_for_object( $user_data, $request );
405
406
			/**
407
			 * Fires after a customer is created or updated via the REST API.
408
			 *
409
			 * @param WP_User         $customer  Data used to create the customer.
410
			 * @param WP_REST_Request $request   Request object.
411
			 * @param boolean         $creating  True when creating customer, false when updating customer.
412
			 */
413
			do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false );
414
415
			$request->set_param( 'context', 'edit' );
416
			$response = $this->prepare_item_for_response( $user_data, $request );
417
			$response = rest_ensure_response( $response );
418
			return $response;
419
		} catch ( Exception $e ) {
420
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getErrorCode() does only exist in the following sub-classes of Exception: WC_CLI_Exception, WC_Data_Exception, WC_REST_Exception. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
421
		}
422
	}
423
424
	/**
425
	 * Delete a single customer.
426
	 *
427
	 * @param WP_REST_Request $request Full details about the request.
428
	 * @return WP_Error|WP_REST_Response
429
	 */
430
	public function delete_item( $request ) {
431
		$id       = (int) $request['id'];
432
		$reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
433
		$force    = isset( $request['force'] ) ? (bool) $request['force'] : false;
434
435
		// We don't support trashing for this type, error out.
436
		if ( ! $force ) {
437
			return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
438
		}
439
440
		$user_data = get_userdata( $id );
441
		if ( ! $user_data ) {
442
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) );
443
		}
444
445
		if ( ! empty( $reassign ) ) {
446
			if ( $reassign === $id || ! get_userdata( $reassign ) ) {
447
				return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) );
448
			}
449
		}
450
451
		$request->set_param( 'context', 'edit' );
452
		$response = $this->prepare_item_for_response( $user_data, $request );
453
454
		/** Include admin customer functions to get access to wp_delete_user() */
455
		require_once ABSPATH . 'wp-admin/includes/user.php';
456
457
		$customer = new WC_Customer( $id );
458
459
		if ( ! is_null( $reassign ) ) {
460
			$result = $customer->delete_and_reassign( $reassign );
461
		} else {
462
			$result = $customer->delete();
463
		}
464
465
		if ( ! $result ) {
466
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
467
		}
468
469
		/**
470
		 * Fires after a customer is deleted via the REST API.
471
		 *
472
		 * @param WP_User          $user_data User data.
473
		 * @param WP_REST_Response $response  The response returned from the API.
474
		 * @param WP_REST_Request  $request   The request sent to the API.
475
		 */
476
		do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request );
477
478
		return $response;
479
	}
480
481
	/**
482
	 * Prepare a single customer output for response.
483
	 *
484
	 * @param WP_User           $user_data User object.
485
	 * @param WP_REST_Request   $request   Request object.
486
	 * @return WP_REST_Response $response  Response data.
487
	 */
488
	public function prepare_item_for_response( $user_data, $request ) {
489
		$customer    = new WC_Customer( $user_data->ID );
490
		$data        = $customer->get_data();
491
		$format_date = array( 'date_created', 'date_modified' );
492
493
		// Format date values.
494 View Code Duplication
		foreach ( $format_date as $key ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
495
			$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : null;
496
		}
497
498
		// Remove unwanted CRUD data.
499
		unset( $data['role'] );
500
501
		// Additional non-crud data.
502
		$data['last_order']   = null;
503
		$data['orders_count'] = $customer->get_order_count();
504
		$data['total_spent']  = $customer->get_total_spent();
505
		$data['avatar_url']   = $customer->get_avatar_url();
506
507
		if ( $last_order_data = $customer->get_last_order() ) {
508
			$data['last_order'] = array(
509
				'id'   => $last_order_data->get_id(),
510
				'date' => $last_order_data->get_date_created() ? wc_rest_prepare_date_response( $last_order_data->get_date_created() ) : null,
511
			);
512
		}
513
514
		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
515
		$data     = $this->add_additional_fields_to_object( $data, $request );
516
		$data     = $this->filter_response_by_context( $data, $context );
517
		$response = rest_ensure_response( $data );
518
		$response->add_links( $this->prepare_links( $user_data ) );
519
520
		/**
521
		 * Filter customer data returned from the REST API.
522
		 *
523
		 * @param WP_REST_Response $response   The response object.
524
		 * @param WP_User          $user_data  User object used to create response.
525
		 * @param WP_REST_Request  $request    Request object.
526
		 */
527
		return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request );
528
	}
529
530
	/**
531
	 * Update customer meta fields.
532
	 *
533
	 * @param WC_Customer $customer
534
	 * @param WP_REST_Request $request
535
	 */
536
	protected function update_customer_meta_fields( $customer, $request ) {
537
		$schema = $this->get_item_schema();
538
539
		// Meta data
540
		if ( isset( $request['meta_data'] ) ) {
541
			if ( is_array( $request['meta_data'] ) ) {
542
				foreach ( $request['meta_data'] as $meta ) {
543
					$coupon->update_meta_data( $meta['key'], $meta['value'], $meta['id'] );
544
				}
545
			}
546
		}
547
548
		// Customer first name.
549
		if ( isset( $request['first_name'] ) ) {
550
			$customer->set_first_name( wc_clean( $request['first_name'] ) );
551
		}
552
553
		// Customer last name.
554
		if ( isset( $request['last_name'] ) ) {
555
			$customer->set_last_name( wc_clean( $request['last_name'] ) );
556
		}
557
558
		// Customer billing address.
559 View Code Duplication
		if ( isset( $request['billing'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
560
			foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) {
561
				if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
562
					$customer->{"set_billing_{$field}"}( $request['billing'][ $field ]  );
563
				}
564
			}
565
		}
566
567
		// Customer shipping address.
568 View Code Duplication
		if ( isset( $request['shipping'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
569
			foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) {
570
				if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
571
					$customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ]  );
572
				}
573
			}
574
		}
575
	}
576
577
	/**
578
	 * Prepare links for the request.
579
	 *
580
	 * @param WP_User $customer Customer object.
581
	 * @return array Links for the given customer.
582
	 */
583 View Code Duplication
	protected function prepare_links( $customer ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
584
		$links = array(
585
			'self' => array(
586
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ),
587
			),
588
			'collection' => array(
589
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
590
			),
591
		);
592
593
		return $links;
594
	}
595
596
	/**
597
	 * Get the Customer's schema, conforming to JSON Schema.
598
	 *
599
	 * @return array
600
	 */
601
	public function get_item_schema() {
602
		$schema = array(
603
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
604
			'title'      => 'customer',
605
			'type'       => 'object',
606
			'properties' => array(
607
				'id' => array(
608
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
609
					'type'        => 'integer',
610
					'context'     => array( 'view', 'edit' ),
611
					'readonly'    => true,
612
				),
613
				'date_created' => array(
614
					'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ),
615
					'type'        => 'date-time',
616
					'context'     => array( 'view', 'edit' ),
617
					'readonly'    => true,
618
				),
619
				'date_modified' => array(
620
					'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ),
621
					'type'        => 'date-time',
622
					'context'     => array( 'view', 'edit' ),
623
					'readonly'    => true,
624
				),
625
				'email' => array(
626
					'description' => __( 'The email address for the customer.', 'woocommerce' ),
627
					'type'        => 'string',
628
					'format'      => 'email',
629
					'context'     => array( 'view', 'edit' ),
630
				),
631
				'first_name' => array(
632
					'description' => __( 'Customer first name.', 'woocommerce' ),
633
					'type'        => 'string',
634
					'context'     => array( 'view', 'edit' ),
635
					'arg_options' => array(
636
						'sanitize_callback' => 'sanitize_text_field',
637
					),
638
				),
639
				'last_name' => array(
640
					'description' => __( 'Customer last name.', 'woocommerce' ),
641
					'type'        => 'string',
642
					'context'     => array( 'view', 'edit' ),
643
					'arg_options' => array(
644
						'sanitize_callback' => 'sanitize_text_field',
645
					),
646
				),
647
				'username' => array(
648
					'description' => __( 'Customer login name.', 'woocommerce' ),
649
					'type'        => 'string',
650
					'context'     => array( 'view', 'edit' ),
651
					'arg_options' => array(
652
						'sanitize_callback' => 'sanitize_user',
653
					),
654
				),
655
				'password' => array(
656
					'description' => __( 'Customer password.', 'woocommerce' ),
657
					'type'        => 'string',
658
					'context'     => array( 'edit' ),
659
				),
660
				'last_order' => array(
661
					'description' => __( 'Last order data.', 'woocommerce' ),
662
					'type'        => 'array',
663
					'context'     => array( 'view', 'edit' ),
664
					'readonly'    => true,
665
					'properties'  => array(
666
						'id' => array(
667
							'description' => __( 'Last order ID.', 'woocommerce' ),
668
							'type'        => 'integer',
669
							'context'     => array( 'view', 'edit' ),
670
							'readonly'    => true,
671
						),
672
						'date' => array(
673
							'description' => __( 'UTC DateTime of the customer last order.', 'woocommerce' ),
674
							'type'        => 'date-time',
675
							'context'     => array( 'view', 'edit' ),
676
							'readonly'    => true,
677
						),
678
					),
679
				),
680
				'orders_count' => array(
681
					'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ),
682
					'type'        => 'integer',
683
					'context'     => array( 'view', 'edit' ),
684
					'readonly'    => true,
685
				),
686
				'total_spent' => array(
687
					'description' => __( 'Total amount spent.', 'woocommerce' ),
688
					'type'        => 'string',
689
					'context'     => array( 'view', 'edit' ),
690
					'readonly'    => true,
691
				),
692
				'avatar_url' => array(
693
					'description' => __( 'Avatar URL.', 'woocommerce' ),
694
					'type'        => 'string',
695
					'context'     => array( 'view', 'edit' ),
696
					'readonly'    => true,
697
				),
698
				'billing' => array(
699
					'description' => __( 'List of billing address data.', 'woocommerce' ),
700
					'type'        => 'array',
701
					'context'     => array( 'view', 'edit' ),
702
					'properties' => array(
703
						'first_name' => array(
704
							'description' => __( 'First name.', 'woocommerce' ),
705
							'type'        => 'string',
706
							'context'     => array( 'view', 'edit' ),
707
						),
708
						'last_name' => array(
709
							'description' => __( 'Last name.', 'woocommerce' ),
710
							'type'        => 'string',
711
							'context'     => array( 'view', 'edit' ),
712
						),
713
						'company' => array(
714
							'description' => __( 'Company name.', 'woocommerce' ),
715
							'type'        => 'string',
716
							'context'     => array( 'view', 'edit' ),
717
						),
718
						'address_1' => array(
719
							'description' => __( 'Address line 1.', 'woocommerce' ),
720
							'type'        => 'string',
721
							'context'     => array( 'view', 'edit' ),
722
						),
723
						'address_2' => array(
724
							'description' => __( 'Address line 2.', 'woocommerce' ),
725
							'type'        => 'string',
726
							'context'     => array( 'view', 'edit' ),
727
						),
728
						'city' => array(
729
							'description' => __( 'City name.', 'woocommerce' ),
730
							'type'        => 'string',
731
							'context'     => array( 'view', 'edit' ),
732
						),
733
						'state' => array(
734
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
735
							'type'        => 'string',
736
							'context'     => array( 'view', 'edit' ),
737
						),
738
						'postcode' => array(
739
							'description' => __( 'Postal code.', 'woocommerce' ),
740
							'type'        => 'string',
741
							'context'     => array( 'view', 'edit' ),
742
						),
743
						'country' => array(
744
							'description' => __( 'ISO code of the country.', 'woocommerce' ),
745
							'type'        => 'string',
746
							'context'     => array( 'view', 'edit' ),
747
						),
748
						'email' => array(
749
							'description' => __( 'Email address.', 'woocommerce' ),
750
							'type'        => 'string',
751
							'format'      => 'email',
752
							'context'     => array( 'view', 'edit' ),
753
						),
754
						'phone' => array(
755
							'description' => __( 'Phone number.', 'woocommerce' ),
756
							'type'        => 'string',
757
							'context'     => array( 'view', 'edit' ),
758
						),
759
					),
760
				),
761
				'shipping' => array(
762
					'description' => __( 'List of shipping address data.', 'woocommerce' ),
763
					'type'        => 'array',
764
					'context'     => array( 'view', 'edit' ),
765
					'properties' => array(
766
						'first_name' => array(
767
							'description' => __( 'First name.', 'woocommerce' ),
768
							'type'        => 'string',
769
							'context'     => array( 'view', 'edit' ),
770
						),
771
						'last_name' => array(
772
							'description' => __( 'Last name.', 'woocommerce' ),
773
							'type'        => 'string',
774
							'context'     => array( 'view', 'edit' ),
775
						),
776
						'company' => array(
777
							'description' => __( 'Company name.', 'woocommerce' ),
778
							'type'        => 'string',
779
							'context'     => array( 'view', 'edit' ),
780
						),
781
						'address_1' => array(
782
							'description' => __( 'Address line 1.', 'woocommerce' ),
783
							'type'        => 'string',
784
							'context'     => array( 'view', 'edit' ),
785
						),
786
						'address_2' => array(
787
							'description' => __( 'Address line 2.', 'woocommerce' ),
788
							'type'        => 'string',
789
							'context'     => array( 'view', 'edit' ),
790
						),
791
						'city' => array(
792
							'description' => __( 'City name.', 'woocommerce' ),
793
							'type'        => 'string',
794
							'context'     => array( 'view', 'edit' ),
795
						),
796
						'state' => array(
797
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
798
							'type'        => 'string',
799
							'context'     => array( 'view', 'edit' ),
800
						),
801
						'postcode' => array(
802
							'description' => __( 'Postal code.', 'woocommerce' ),
803
							'type'        => 'string',
804
							'context'     => array( 'view', 'edit' ),
805
						),
806
						'country' => array(
807
							'description' => __( 'ISO code of the country.', 'woocommerce' ),
808
							'type'        => 'string',
809
							'context'     => array( 'view', 'edit' ),
810
						),
811
					),
812
				),
813
				'is_paying_customer' => array(
814
					'description' => __( 'Is the customer a paying customer?', 'woocommerce' ),
815
					'type'        => 'bool',
816
					'context'     => array( 'view', 'edit' ),
817
					'readonly'    => true,
818
				),
819
				'meta_data' => array(
820
					'description' => __( 'Order meta data.', 'woocommerce' ),
821
					'type'        => 'array',
822
					'context'     => array( 'view', 'edit' ),
823
					'properties'  => array(
824
						'id' => array(
825
							'description' => __( 'Meta ID.', 'woocommerce' ),
826
							'type'        => 'int',
827
							'context'     => array( 'view', 'edit' ),
828
							'readonly'    => true,
829
						),
830
						'key' => array(
831
							'description' => __( 'Meta key.', 'woocommerce' ),
832
							'type'        => 'string',
833
							'context'     => array( 'view', 'edit' ),
834
						),
835
						'value' => array(
836
							'description' => __( 'Meta value.', 'woocommerce' ),
837
							'type'        => 'string',
838
							'context'     => array( 'view', 'edit' ),
839
						),
840
					),
841
				),
842
			),
843
		);
844
845
		return $this->add_additional_fields_schema( $schema );
846
	}
847
848
	/**
849
	 * Get role names.
850
	 *
851
	 * @return array
852
	 */
853
	protected function get_role_names() {
854
		global $wp_roles;
855
		return array_keys( $wp_roles->role_names );
856
	}
857
858
	/**
859
	 * Get the query params for collections.
860
	 *
861
	 * @return array
862
	 */
863
	public function get_collection_params() {
864
		$params = parent::get_collection_params();
865
866
		$params['context']['default'] = 'view';
867
868
		$params['exclude'] = array(
869
			'description'        => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
870
			'type'               => 'array',
871
			'default'            => array(),
872
			'sanitize_callback'  => 'wp_parse_id_list',
873
		);
874
		$params['include'] = array(
875
			'description'        => __( 'Limit result set to specific IDs.', 'woocommerce' ),
876
			'type'               => 'array',
877
			'default'            => array(),
878
			'sanitize_callback'  => 'wp_parse_id_list',
879
		);
880
		$params['offset'] = array(
881
			'description'        => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
882
			'type'               => 'integer',
883
			'sanitize_callback'  => 'absint',
884
			'validate_callback'  => 'rest_validate_request_arg',
885
		);
886
		$params['order'] = array(
887
			'default'            => 'asc',
888
			'description'        => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
889
			'enum'               => array( 'asc', 'desc' ),
890
			'sanitize_callback'  => 'sanitize_key',
891
			'type'               => 'string',
892
			'validate_callback'  => 'rest_validate_request_arg',
893
		);
894
		$params['orderby'] = array(
895
			'default'            => 'name',
896
			'description'        => __( 'Sort collection by object attribute.', 'woocommerce' ),
897
			'enum'               => array(
898
				'id',
899
				'include',
900
				'name',
901
				'registered_date',
902
			),
903
			'sanitize_callback'  => 'sanitize_key',
904
			'type'               => 'string',
905
			'validate_callback'  => 'rest_validate_request_arg',
906
		);
907
		$params['email'] = array(
908
			'description'        => __( 'Limit result set to resources with a specific email.', 'woocommerce' ),
909
			'type'               => 'string',
910
			'format'             => 'email',
911
			'validate_callback'  => 'rest_validate_request_arg',
912
		);
913
		$params['role'] = array(
914
			'description'        => __( 'Limit result set to resources with a specific role.', 'woocommerce' ),
915
			'type'               => 'string',
916
			'default'            => 'customer',
917
			'enum'               => array_merge( array( 'all' ), $this->get_role_names() ),
918
			'validate_callback'  => 'rest_validate_request_arg',
919
		);
920
		return $params;
921
	}
922
}
923