Completed
Push — master ( f47a1d...37f03f )
by Claudio
28:06
created

WC_API_Customers::get_avatar_url()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 13
Ratio 100 %
Metric Value
dl 13
loc 13
rs 9.4285
nc 2
cc 3
eloc 6
nop 1
1
<?php
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 18 and the first side effect is on line 15.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * WooCommerce API Customers Class
4
 *
5
 * Handles requests to the /customers endpoint
6
 *
7
 * @author      WooThemes
8
 * @category    API
9
 * @package     WooCommerce/API
10
 * @since       2.1
11
 * @version     2.1
12
 */
13
14
if ( ! defined( 'ABSPATH' ) ) {
15
	exit; // Exit if accessed directly
16
}
17
18
class WC_API_Customers extends WC_API_Resource {
19
20
	/** @var string $base the route base */
21
	protected $base = '/customers';
22
23
	/** @var string $created_at_min for date filtering */
24
	private $created_at_min = null;
25
26
	/** @var string $created_at_max for date filtering */
27
	private $created_at_max = null;
28
29
	/**
30
	 * Setup class, overridden to provide customer data to order response
31
	 *
32
	 * @since 2.1
33
	 * @param WC_API_Server $server
34
	 * @return WC_API_Customers
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
35
	 */
36 View Code Duplication
	public function __construct( WC_API_Server $server ) {
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...
37
38
		parent::__construct( $server );
39
40
		// add customer data to order responses
41
		add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );
42
43
		// modify WP_User_Query to support created_at date filtering
44
		add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
45
	}
46
47
	/**
48
	 * Register the routes for this class
49
	 *
50
	 * GET /customers
51
	 * GET /customers/count
52
	 * GET /customers/<id>
53
	 * GET /customers/<id>/orders
54
	 *
55
	 * @since 2.1
56
	 * @param array $routes
57
	 * @return array
58
	 */
59
	public function register_routes( $routes ) {
60
61
		# GET /customers
62
		$routes[ $this->base ] = array(
63
			array( array( $this, 'get_customers' ),     WC_API_SERVER::READABLE ),
64
		);
65
66
		# GET /customers/count
67
		$routes[ $this->base . '/count'] = array(
68
			array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
69
		);
70
71
		# GET /customers/<id>
72
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
73
			array( array( $this, 'get_customer' ),  WC_API_SERVER::READABLE ),
74
		);
75
76
		# GET /customers/<id>/orders
77
		$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
78
			array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
79
		);
80
81
		return $routes;
82
	}
83
84
	/**
85
	 * Get all customers
86
	 *
87
	 * @since 2.1
88
	 * @param array $fields
89
	 * @param array $filter
90
	 * @param int $page
91
	 * @return array
92
	 */
93
	public function get_customers( $fields = null, $filter = array(), $page = 1 ) {
94
95
		$filter['page'] = $page;
96
97
		$query = $this->query_customers( $filter );
98
99
		$customers = array();
100
101
		foreach( $query->get_results() as $user_id ) {
102
103
			if ( ! $this->is_readable( $user_id ) )
104
				continue;
105
106
			$customers[] = current( $this->get_customer( $user_id, $fields ) );
0 ignored issues
show
Bug introduced by
It seems like $fields defined by parameter $fields on line 93 can also be of type array; however, WC_API_Customers::get_customer() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
107
		}
108
109
		$this->server->add_pagination_headers( $query );
110
111
		return array( 'customers' => $customers );
112
	}
113
114
	/**
115
	 * Get the customer for the given ID
116
	 *
117
	 * @since 2.1
118
	 * @param int $id the customer ID
119
	 * @param string $fields
120
	 * @return array
121
	 */
122
	public function get_customer( $id, $fields = null ) {
123
		global $wpdb;
124
125
		$id = $this->validate_request( $id, 'customer', 'read' );
126
127
		if ( is_wp_error( $id ) )
128
			return $id;
129
130
		$customer = new WP_User( $id );
131
132
		// get info about user's last order
133
		$last_order = $wpdb->get_row( "SELECT id, post_date_gmt
134
						FROM $wpdb->posts AS posts
135
						LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
136
						WHERE meta.meta_key = '_customer_user'
137
						AND   meta.meta_value = {$customer->ID}
138
						AND   posts.post_type = 'shop_order'
139
						AND   posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' )
140
					" );
141
142
		$customer_data = array(
143
			'id'               => $customer->ID,
144
			'created_at'       => $this->server->format_datetime( $customer->user_registered ),
145
			'email'            => $customer->user_email,
146
			'first_name'       => $customer->first_name,
147
			'last_name'        => $customer->last_name,
148
			'username'         => $customer->user_login,
149
			'last_order_id'    => is_object( $last_order ) ? $last_order->id : null,
150
			'last_order_date'  => is_object( $last_order ) ? $this->server->format_datetime( $last_order->post_date_gmt ) : null,
151
			'orders_count'     => (int) $customer->_order_count,
152
			'total_spent'      => wc_format_decimal( $customer->_money_spent, 2 ),
153
			'avatar_url'       => $this->get_avatar_url( $customer->customer_email ),
154
			'billing_address'  => array(
155
				'first_name' => $customer->billing_first_name,
156
				'last_name'  => $customer->billing_last_name,
157
				'company'    => $customer->billing_company,
158
				'address_1'  => $customer->billing_address_1,
159
				'address_2'  => $customer->billing_address_2,
160
				'city'       => $customer->billing_city,
161
				'state'      => $customer->billing_state,
162
				'postcode'   => $customer->billing_postcode,
163
				'country'    => $customer->billing_country,
164
				'email'      => $customer->billing_email,
165
				'phone'      => $customer->billing_phone,
166
			),
167
			'shipping_address' => array(
168
				'first_name' => $customer->shipping_first_name,
169
				'last_name'  => $customer->shipping_last_name,
170
				'company'    => $customer->shipping_company,
171
				'address_1'  => $customer->shipping_address_1,
172
				'address_2'  => $customer->shipping_address_2,
173
				'city'       => $customer->shipping_city,
174
				'state'      => $customer->shipping_state,
175
				'postcode'   => $customer->shipping_postcode,
176
				'country'    => $customer->shipping_country,
177
			),
178
		);
179
180
		return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
181
	}
182
183
	/**
184
	 * Get the total number of customers
185
	 *
186
	 * @since 2.1
187
	 * @param array $filter
188
	 * @return array
189
	 */
190 View Code Duplication
	public function get_customers_count( $filter = array() ) {
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...
191
192
		$query = $this->query_customers( $filter );
193
194
		if ( ! current_user_can( 'list_users' ) )
195
			return new WP_Error( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), array( 'status' => 401 ) );
196
197
		return array( 'count' => count( $query->get_results() ) );
198
	}
199
200
201
	/**
202
	 * Create a customer
203
	 *
204
	 * @TODO implement in 2.2 with woocommerce_create_new_customer()
205
	 * @param array $data
206
	 * @return array
207
	 */
208
	public function create_customer( $data ) {
0 ignored issues
show
Unused Code introduced by
The parameter $data 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...
209
210
		if ( ! current_user_can( 'create_users' ) )
211
			return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) );
212
213
		return array();
214
	}
215
216
	/**
217
	 * Edit a customer
218
	 *
219
	 * @TODO implement in 2.2
220
	 * @param int $id the customer ID
221
	 * @param array $data
222
	 * @return array
223
	 */
224 View Code Duplication
	public function edit_customer( $id, $data ) {
0 ignored issues
show
Unused Code introduced by
The parameter $data 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...
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...
225
226
		$id = $this->validate_request( $id, 'customer', 'edit' );
227
228
		if ( ! is_wp_error( $id ) )
229
			return $id;
230
231
		return $this->get_customer( $id );
232
	}
233
234
	/**
235
	 * Delete a customer
236
	 *
237
	 * @TODO enable along with PUT/POST in 2.2
238
	 * @param int $id the customer ID
239
	 * @return array
240
	 */
241 View Code Duplication
	public function delete_customer( $id ) {
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...
242
243
		$id = $this->validate_request( $id, 'customer', 'delete' );
244
245
		if ( ! is_wp_error( $id ) )
246
			return $id;
247
248
		return $this->delete( $id, 'customer' );
249
	}
250
251
	/**
252
	 * Get the orders for a customer
253
	 *
254
	 * @since 2.1
255
	 * @param int $id the customer ID
256
	 * @param string $fields fields to include in response
257
	 * @return array
258
	 */
259 View Code Duplication
	public function get_customer_orders( $id, $fields = null ) {
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...
260
		global $wpdb;
261
262
		$id = $this->validate_request( $id, 'customer', 'read' );
263
264
		if ( is_wp_error( $id ) )
265
			return $id;
266
267
		$order_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id
268
						FROM $wpdb->posts AS posts
269
						LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
270
						WHERE meta.meta_key = '_customer_user'
271
						AND   meta.meta_value = '%s'
272
						AND   posts.post_type = 'shop_order'
273
						AND   posts.post_status = IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' )
274
					", $id ) );
275
276
		if ( empty( $order_ids ) )
277
			return array( 'orders' => array() );
278
279
		$orders = array();
280
281
		foreach ( $order_ids as $order_id ) {
282
			$orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) );
283
		}
284
285
		return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
286
	}
287
288
	/**
289
	 * Helper method to get customer user objects
290
	 *
291
	 * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
292
	 * pagination support
293
	 *
294
	 * @since 2.1
295
	 * @param array $args request arguments for filtering query
296
	 * @return WP_User_Query
297
	 */
298
	private function query_customers( $args = array() ) {
299
300
		// default users per page
301
		$users_per_page = get_option( 'posts_per_page' );
302
303
		// set base query arguments
304
		$query_args = array(
305
			'fields'  => 'ID',
306
			'role'    => 'customer',
307
			'orderby' => 'registered',
308
			'number'  => $users_per_page,
309
		);
310
311
		// search
312
		if ( ! empty( $args['q'] ) ) {
313
			$query_args['search'] = $args['q'];
314
		}
315
316
		// limit number of users returned
317 View Code Duplication
		if ( ! empty( $args['limit'] ) ) {
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...
318
319
			$query_args['number'] = absint( $args['limit'] );
320
321
			$users_per_page = absint( $args['limit'] );
322
		}
323
324
		// page
325
		$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;
326
327
		// offset
328 View Code Duplication
		if ( ! empty( $args['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...
329
			$query_args['offset'] = absint( $args['offset'] );
330
		} else {
331
			$query_args['offset'] = $users_per_page * ( $page - 1 );
332
		}
333
334
		// created date
335
		if ( ! empty( $args['created_at_min'] ) ) {
336
			$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
337
		}
338
339
		if ( ! empty( $args['created_at_max'] ) ) {
340
			$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
341
		}
342
343
		$query = new WP_User_Query( $query_args );
344
345
		// helper members for pagination headers
346
		$query->total_pages = ceil( $query->get_total() / $users_per_page );
347
		$query->page = $page;
348
349
		return $query;
350
	}
351
352
	/**
353
	 * Add customer data to orders
354
	 *
355
	 * @since 2.1
356
	 * @param $order_data
357
	 * @param $order
358
	 * @return array
359
	 */
360 View Code Duplication
	public function add_customer_data( $order_data, $order ) {
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...
361
362
		if ( 0 == $order->customer_user ) {
363
364
			// add customer data from order
365
			$order_data['customer'] = array(
366
				'id'               => 0,
367
				'email'            => $order->billing_email,
368
				'first_name'       => $order->billing_first_name,
369
				'last_name'        => $order->billing_last_name,
370
				'billing_address'  => array(
371
					'first_name' => $order->billing_first_name,
372
					'last_name'  => $order->billing_last_name,
373
					'company'    => $order->billing_company,
374
					'address_1'  => $order->billing_address_1,
375
					'address_2'  => $order->billing_address_2,
376
					'city'       => $order->billing_city,
377
					'state'      => $order->billing_state,
378
					'postcode'   => $order->billing_postcode,
379
					'country'    => $order->billing_country,
380
					'email'      => $order->billing_email,
381
					'phone'      => $order->billing_phone,
382
				),
383
				'shipping_address' => array(
384
					'first_name' => $order->shipping_first_name,
385
					'last_name'  => $order->shipping_last_name,
386
					'company'    => $order->shipping_company,
387
					'address_1'  => $order->shipping_address_1,
388
					'address_2'  => $order->shipping_address_2,
389
					'city'       => $order->shipping_city,
390
					'state'      => $order->shipping_state,
391
					'postcode'   => $order->shipping_postcode,
392
					'country'    => $order->shipping_country,
393
				),
394
			);
395
396
		} else {
397
398
			$order_data['customer'] = current( $this->get_customer( $order->customer_user ) );
399
		}
400
401
		return $order_data;
402
	}
403
404
	/**
405
	 * Modify the WP_User_Query to support filtering on the date the customer was created
406
	 *
407
	 * @since 2.1
408
	 * @param WP_User_Query $query
409
	 */
410 View Code Duplication
	public function modify_user_query( $query ) {
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...
411
412
		if ( $this->created_at_min )
413
			$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) );
414
415
		if ( $this->created_at_max )
416
			$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) );
417
	}
418
419
	/**
420
	 * Wrapper for @see get_avatar() which doesn't simply return
421
	 * the URL so we need to pluck it from the HTML img tag
422
	 *
423
	 * @since 2.1
424
	 * @param string $email the customer's email
425
	 * @return string the URL to the customer's avatar
426
	 */
427 View Code Duplication
	private function get_avatar_url( $email ) {
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...
428
429
		$avatar_html = get_avatar( $email );
430
431
		// Get the URL of the avatar from the provided HTML
432
		preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches );
433
434
		if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) {
435
			return esc_url_raw( $matches[1] );
436
		}
437
438
		return null;
439
	}
440
441
	/**
442
	 * Validate the request by checking:
443
	 *
444
	 * 1) the ID is a valid integer
445
	 * 2) the ID returns a valid WP_User
446
	 * 3) the current user has the proper permissions
447
	 *
448
	 * @since 2.1
449
	 * @see WC_API_Resource::validate_request()
450
	 * @param string|int $id the customer ID
451
	 * @param string $type the request type, unused because this method overrides the parent class
452
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
453
	 * @return int|WP_Error valid user ID or WP_Error if any of the checks fails
454
	 */
455
	protected function validate_request( $id, $type, $context ) {
456
457
		$id = absint( $id );
458
459
		// validate ID
460
		if ( empty( $id ) )
461
			return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) );
462
463
		// non-existent IDs return a valid WP_User object with the user ID = 0
464
		$customer = new WP_User( $id );
465
466
		if ( 0 === $customer->ID )
467
			return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) );
468
469
		// validate permissions
470
		switch ( $context ) {
471
472
			case 'read':
473
				if ( ! current_user_can( 'list_users' ) )
474
					return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) );
475
				break;
476
477
			case 'edit':
478
				if ( ! current_user_can( 'edit_users' ) )
479
					return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) );
480
				break;
481
482
			case 'delete':
483
				if ( ! current_user_can( 'delete_users' ) )
484
					return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) );
485
				break;
486
		}
487
488
		return $id;
489
	}
490
491
	/**
492
	 * Check if the current user can read users
493
	 *
494
	 * @since 2.1
495
	 * @see WC_API_Resource::is_readable()
496
	 * @param int|WP_Post $post unused
497
	 * @return bool true if the current user can read users, false otherwise
498
	 */
499
	protected function is_readable( $post ) {
500
501
		return current_user_can( 'list_users' );
502
	}
503
504
}
505