Completed
Push — master ( 2c6ef6...281328 )
by Radoslav
02:07 queued 29s
created

WC_Stripe_Customer::set_default_source()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * WC_Stripe_Customer class.
8
 *
9
 * Represents a Stripe Customer.
10
 */
11
class WC_Stripe_Customer {
12
13
	/**
14
	 * Stripe customer ID
15
	 * @var string
16
	 */
17
	private $id = '';
18
19
	/**
20
	 * WP User ID
21
	 * @var integer
22
	 */
23
	private $user_id = 0;
24
25
	/**
26
	 * Data from API
27
	 * @var array
28
	 */
29
	private $customer_data = array();
30
31
	/**
32
	 * Constructor
33
	 * @param int $user_id The WP user ID
34
	 */
35
	public function __construct( $user_id = 0 ) {
36
		if ( $user_id ) {
37
			$this->set_user_id( $user_id );
38
			$this->set_id( $this->get_id_from_meta( $user_id ) );
39
		}
40
	}
41
42
	/**
43
	 * Get Stripe customer ID.
44
	 * @return string
45
	 */
46
	public function get_id() {
47
		return $this->id;
48
	}
49
50
	/**
51
	 * Set Stripe customer ID.
52
	 * @param [type] $id [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
53
	 */
54
	public function set_id( $id ) {
55
		// Backwards compat for customer ID stored in array format. (Pre 3.0)
56
		if ( is_array( $id ) && isset( $id['customer_id'] ) ) {
57
			$id = $id['customer_id'];
58
59
			$this->update_id_in_meta( $id );
60
		}
61
62
		$this->id = wc_clean( $id );
63
	}
64
65
	/**
66
	 * User ID in WordPress.
67
	 * @return int
68
	 */
69
	public function get_user_id() {
70
		return absint( $this->user_id );
71
	}
72
73
	/**
74
	 * Set User ID used by WordPress.
75
	 * @param int $user_id
76
	 */
77
	public function set_user_id( $user_id ) {
78
		$this->user_id = absint( $user_id );
79
	}
80
81
	/**
82
	 * Get user object.
83
	 * @return WP_User
84
	 */
85
	protected function get_user() {
86
		return $this->get_user_id() ? get_user_by( 'id', $this->get_user_id() ) : false;
87
	}
88
89
	/**
90
	 * Store data from the Stripe API about this customer
91
	 */
92
	public function set_customer_data( $data ) {
93
		$this->customer_data = $data;
94
	}
95
96
	/**
97
	 * Generates the customer request, used for both creating and updating customers.
98
	 *
99
	 * @param  array $args Additional arguments (optional).
100
	 * @return array
101
	 */
102
	protected function generate_customer_request( $args = array() ) {
103
		$billing_email = isset( $_POST['billing_email'] ) ? filter_var( $_POST['billing_email'], FILTER_SANITIZE_EMAIL ) : '';
104
		$user          = $this->get_user();
105
106
		if ( $user ) {
107
			$billing_first_name = get_user_meta( $user->ID, 'billing_first_name', true );
108
			$billing_last_name  = get_user_meta( $user->ID, 'billing_last_name', true );
109
110
			// If billing first name does not exists try the user first name.
111
			if ( empty( $billing_first_name ) ) {
112
				$billing_first_name = get_user_meta( $user->ID, 'first_name', true );
113
			}
114
115
			// If billing last name does not exists try the user last name.
116
			if ( empty( $billing_last_name ) ) {
117
				$billing_last_name = get_user_meta( $user->ID, 'last_name', true );
118
			}
119
120
			// translators: %1$s First name, %2$s Second name, %3$s Username.
121
			$description = sprintf( __( 'Name: %1$s %2$s, Username: %s', 'woocommerce-gateway-stripe' ), $billing_first_name, $billing_last_name, $user->user_login );
122
123
			$defaults = array(
124
				'email'       => $user->user_email,
125
				'description' => $description,
126
			);
127
		} else {
128
			$billing_first_name = isset( $_POST['billing_first_name'] ) ? filter_var( wp_unslash( $_POST['billing_first_name'] ), FILTER_SANITIZE_STRING ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
129
			$billing_last_name  = isset( $_POST['billing_last_name'] ) ? filter_var( wp_unslash( $_POST['billing_last_name'] ), FILTER_SANITIZE_STRING ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
130
131
			// translators: %1$s First name, %2$s Second name.
132
			$description = sprintf( __( 'Name: %1$s %2$s, Guest', 'woocommerce-gateway-stripe' ), $billing_first_name, $billing_last_name );
133
134
			$defaults = array(
135
				'email'       => $billing_email,
136
				'description' => $description,
137
			);
138
		}
139
140
		$metadata             = array();
141
		$defaults['metadata'] = apply_filters( 'wc_stripe_customer_metadata', $metadata, $user );
142
143
		return wp_parse_args( $args, $defaults );
144
	}
145
146
	/**
147
	 * Create a customer via API.
148
	 * @param array $args
149
	 * @return WP_Error|int
150
	 */
151
	public function create_customer( $args = array() ) {
152
		$args     = $this->generate_customer_request( $args );
153
		$response = WC_Stripe_API::request( apply_filters( 'wc_stripe_create_customer_args', $args ), 'customers' );
154
155
		if ( ! empty( $response->error ) ) {
156
			throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
157
		}
158
159
		$this->set_id( $response->id );
160
		$this->clear_cache();
161
		$this->set_customer_data( $response );
162
163
		if ( $this->get_user_id() ) {
164
			$this->update_id_in_meta( $response->id );
165
		}
166
167
		do_action( 'woocommerce_stripe_add_customer', $args, $response );
168
169
		return $response->id;
170
	}
171
172
	/**
173
	 * Updates the Stripe customer through the API.
174
	 *
175
	 * @param array $args Additional arguments for the request (optional).
176
	 */
177
	public function update_customer( $args = array() ) {
178
		if ( empty( $this->get_id() ) ) {
179
			throw new WC_Stripe_Exception( 'id_required_to_update_user', __( 'Attempting to update a Stripe customer without a customer ID.', 'woocommerce-gateway-stripe' ) );
180
		}
181
182
		$args     = $this->generate_customer_request( $args );
183
		$args     = apply_filters( 'wc_stripe_update_customer_args', $args );
184
		$response = WC_Stripe_API::request( $args, 'customers/' . $this->get_id() );
185
186
		if ( ! empty( $response->error ) ) {
187
			if ( $this->is_no_such_customer_error( $response->error ) ) {
188
				$message = $response->error->message;
189
190
				if ( ! preg_match( '/similar object exists/i', $message ) ) {
191
					$options  = get_option( 'woocommerce_stripe_settings' );
192
					$testmode = isset( $options['testmode'] ) && 'yes' === $options['testmode'];
193
194
					$message = sprintf(
195
						( $testmode
196
							// Translators: %s is a message, which states that no such customer exists, without a full stop at the end.
197
							? __( '%s. Was the customer created in live mode? ', 'woocommerce-gateway-stripe' )
198
							// Translators: %s is a message, which states that no such customer exists, without a full stop at the end.
199
							: __( '%s. Was the customer created in test mode? ', 'woocommerce-gateway-stripe' ) ),
200
						$message
201
					);
202
				}
203
204
				throw new WC_Stripe_Exception( print_r( $response, true ), $message );
205
			}
206
207
			throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
208
		}
209
210
		$this->clear_cache();
211
		$this->set_customer_data( $response );
212
213
		do_action( 'woocommerce_stripe_update_customer', $args, $response );
214
	}
215
216
	/**
217
	 * Checks to see if error is of invalid request
218
	 * error and it is no such customer.
219
	 *
220
	 * @since 4.1.2
221
	 * @param array $error
222
	 */
223
	public function is_no_such_customer_error( $error ) {
224
		return (
225
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
226
			'invalid_request_error' === $error->type &&
227
			preg_match( '/No such customer/i', $error->message )
228
		);
229
	}
230
231
	/**
232
	 * Add a source for this stripe customer.
233
	 * @param string $source_id
234
	 * @return WP_Error|int
235
	 */
236
	public function add_source( $source_id ) {
237
		if ( ! $this->get_id() ) {
238
			$this->set_id( $this->create_customer() );
239
		}
240
241
		$response = WC_Stripe_API::request(
242
			array(
243
				'source' => $source_id,
244
			),
245
			'customers/' . $this->get_id() . '/sources'
246
		);
247
248
		$wc_token = false;
249
250
		if ( ! empty( $response->error ) ) {
251
			// It is possible the WC user once was linked to a customer on Stripe
252
			// but no longer exists. Instead of failing, lets try to create a
253
			// new customer.
254
			if ( $this->is_no_such_customer_error( $response->error ) ) {
255
				$this->delete_id_from_meta();
256
				$this->create_customer();
257
				return $this->add_source( $source_id );
258
			} else {
259
				return $response;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $response; (stdClass|array) is incompatible with the return type documented by WC_Stripe_Customer::add_source of type WP_Error|integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
260
			}
261
		} elseif ( empty( $response->id ) ) {
262
			return new WP_Error( 'error', __( 'Unable to add payment source.', 'woocommerce-gateway-stripe' ) );
263
		}
264
265
		// Add token to WooCommerce.
266
		if ( $this->get_user_id() && class_exists( 'WC_Payment_Token_CC' ) ) {
267
			if ( ! empty( $response->type ) ) {
268
				switch ( $response->type ) {
269
					case 'alipay':
270
						break;
271
					case 'sepa_debit':
272
						$wc_token = new WC_Payment_Token_SEPA();
273
						$wc_token->set_token( $response->id );
274
						$wc_token->set_gateway_id( 'stripe_sepa' );
275
						$wc_token->set_last4( $response->sepa_debit->last4 );
276
						break;
277
					default:
278
						if ( 'source' === $response->object && 'card' === $response->type ) {
279
							$wc_token = new WC_Payment_Token_CC();
280
							$wc_token->set_token( $response->id );
281
							$wc_token->set_gateway_id( 'stripe' );
282
							$wc_token->set_card_type( strtolower( $response->card->brand ) );
283
							$wc_token->set_last4( $response->card->last4 );
284
							$wc_token->set_expiry_month( $response->card->exp_month );
285
							$wc_token->set_expiry_year( $response->card->exp_year );
286
						}
287
						break;
288
				}
289
			} else {
290
				// Legacy.
291
				$wc_token = new WC_Payment_Token_CC();
292
				$wc_token->set_token( $response->id );
293
				$wc_token->set_gateway_id( 'stripe' );
294
				$wc_token->set_card_type( strtolower( $response->brand ) );
295
				$wc_token->set_last4( $response->last4 );
296
				$wc_token->set_expiry_month( $response->exp_month );
297
				$wc_token->set_expiry_year( $response->exp_year );
298
			}
299
300
			$wc_token->set_user_id( $this->get_user_id() );
301
			$wc_token->save();
302
		}
303
304
		$this->clear_cache();
305
306
		do_action( 'woocommerce_stripe_add_source', $this->get_id(), $wc_token, $response, $source_id );
307
308
		return $response->id;
309
	}
310
311
	/**
312
	 * Get a customers saved sources using their Stripe ID.
313
	 *
314
	 * @param  string $customer_id
0 ignored issues
show
Bug introduced by
There is no parameter named $customer_id. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
315
	 * @return array
316
	 */
317
	public function get_sources() {
318
		if ( ! $this->get_id() ) {
319
			return array();
320
		}
321
322
		$sources = get_transient( 'stripe_sources_' . $this->get_id() );
323
324
		$response = WC_Stripe_API::request(
325
			array(
326
				'limit' => 100,
327
			),
328
			'customers/' . $this->get_id() . '/sources',
329
			'GET'
330
		);
331
332
		if ( ! empty( $response->error ) ) {
333
			return array();
334
		}
335
336
		if ( is_array( $response->data ) ) {
337
			$sources = $response->data;
338
		}
339
340
		return empty( $sources ) ? array() : $sources;
341
	}
342
343
	/**
344
	 * Delete a source from stripe.
345
	 * @param string $source_id
346
	 */
347
	public function delete_source( $source_id ) {
348
		if ( ! $this->get_id() ) {
349
			return false;
350
		}
351
352
		$response = WC_Stripe_API::request( array(), 'customers/' . $this->get_id() . '/sources/' . sanitize_text_field( $source_id ), 'DELETE' );
353
354
		$this->clear_cache();
355
356
		if ( empty( $response->error ) ) {
357
			do_action( 'wc_stripe_delete_source', $this->get_id(), $response );
358
359
			return true;
360
		}
361
362
		return false;
363
	}
364
365
	/**
366
	 * Set default source in Stripe
367
	 * @param string $source_id
368
	 */
369
	public function set_default_source( $source_id ) {
370
		$response = WC_Stripe_API::request(
371
			array(
372
				'default_source' => sanitize_text_field( $source_id ),
373
			),
374
			'customers/' . $this->get_id(),
375
			'POST'
376
		);
377
378
		$this->clear_cache();
379
380
		if ( empty( $response->error ) ) {
381
			do_action( 'wc_stripe_set_default_source', $this->get_id(), $response );
382
383
			return true;
384
		}
385
386
		return false;
387
	}
388
389
	/**
390
	 * Deletes caches for this users cards.
391
	 */
392
	public function clear_cache() {
393
		delete_transient( 'stripe_sources_' . $this->get_id() );
394
		delete_transient( 'stripe_customer_' . $this->get_id() );
395
		$this->customer_data = array();
396
	}
397
398
	/**
399
	 * Retrieves the Stripe Customer ID from the user meta.
400
	 *
401
	 * @param  int $user_id The ID of the WordPress user.
402
	 * @return string|bool  Either the Stripe ID or false.
403
	 */
404
	public function get_id_from_meta( $user_id ) {
405
		return get_user_option( '_stripe_customer_id', $user_id );
406
	}
407
408
	/**
409
	 * Updates the current user with the right Stripe ID in the meta table.
410
	 *
411
	 * @param string $id The Stripe customer ID.
412
	 */
413
	public function update_id_in_meta( $id ) {
414
		update_user_option( $this->get_user_id(), '_stripe_customer_id', $id, false );
415
	}
416
417
	/**
418
	 * Deletes the user ID from the meta table with the right key.
419
	 */
420
	public function delete_id_from_meta() {
421
		delete_user_option( $this->get_user_id(), '_stripe_customer_id', false );
422
	}
423
}
424