blob: 6abaddb658a551fd417b84eedb6fd78445ef7ad2 [file] [log] [blame]
swissChilif0cbdc32023-01-05 17:21:38 -05001<?php
2if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4}
5
6/**
7 * WC_Stripe_API class.
8 *
9 * Communicates with Stripe API.
10 */
11class WC_Stripe_API {
12
13 /**
14 * Stripe API Endpoint
15 */
16 const ENDPOINT = 'https://api.stripe.com/v1/';
17 const STRIPE_API_VERSION = '2019-09-09';
18
19 /**
20 * Secret API Key.
21 *
22 * @var string
23 */
24 private static $secret_key = '';
25
26 /**
27 * Set secret API Key.
28 *
29 * @param string $key
30 */
31 public static function set_secret_key( $secret_key ) {
32 self::$secret_key = $secret_key;
33 }
34
35 /**
36 * Get secret key.
37 *
38 * @return string
39 */
40 public static function get_secret_key() {
41 if ( ! self::$secret_key ) {
42 $options = get_option( 'woocommerce_stripe_settings' );
43 $secret_key = $options['secret_key'] ?? '';
44 $test_secret_key = $options['test_secret_key'] ?? '';
45
46 if ( isset( $options['testmode'] ) ) {
47 self::set_secret_key( 'yes' === $options['testmode'] ? $test_secret_key : $secret_key );
48 }
49 }
50 return self::$secret_key;
51 }
52
53 /**
54 * Generates the user agent we use to pass to API request so
55 * Stripe can identify our application.
56 *
57 * @since 4.0.0
58 * @version 4.0.0
59 */
60 public static function get_user_agent() {
61 $app_info = [
62 'name' => 'WooCommerce Stripe Gateway',
63 'version' => WC_STRIPE_VERSION,
64 'url' => 'https://woocommerce.com/products/stripe/',
65 'partner_id' => 'pp_partner_EYuSt9peR0WTMg',
66 ];
67
68 return [
69 'lang' => 'php',
70 'lang_version' => phpversion(),
71 'publisher' => 'woocommerce',
72 'uname' => 'Linux server 5.10.0-18-amd64 #1 SMP Debian 5.10.140-1 (2022-09-02) x86_64',
73 'application' => $app_info,
74 ];
75 }
76
77 /**
78 * Generates the headers to pass to API request.
79 *
80 * @since 4.0.0
81 * @version 4.0.0
82 */
83 public static function get_headers() {
84 $user_agent = self::get_user_agent();
85 $app_info = $user_agent['application'];
86
87 $headers = apply_filters(
88 'woocommerce_stripe_request_headers',
89 [
90 'Authorization' => 'Basic ' . base64_encode( self::get_secret_key() . ':' ),
91 'Stripe-Version' => self::STRIPE_API_VERSION,
92 ]
93 );
94
95 // These headers should not be overridden for this gateway.
96 $headers['User-Agent'] = $app_info['name'] . '/' . $app_info['version'] . ' (' . $app_info['url'] . ')';
97 $headers['X-Stripe-Client-User-Agent'] = wp_json_encode( $user_agent );
98
99 return $headers;
100 }
101
102 /**
103 * Send the request to Stripe's API
104 *
105 * @since 3.1.0
106 * @version 4.0.6
107 * @param array $request
108 * @param string $api
109 * @param string $method
110 * @param bool $with_headers To get the response with headers.
111 * @return stdClass|array
112 * @throws WC_Stripe_Exception
113 */
114 public static function request( $request, $api = 'charges', $method = 'POST', $with_headers = false ) {
115 WC_Stripe_Logger::log( "{$api} request: " . print_r( $request, true ) );
116
117 $headers = self::get_headers();
118 $idempotency_key = '';
119
120 if ( 'charges' === $api && 'POST' === $method ) {
121 $customer = ! empty( $request['customer'] ) ? $request['customer'] : '';
122 $source = ! empty( $request['source'] ) ? $request['source'] : $customer;
123 $idempotency_key = apply_filters( 'wc_stripe_idempotency_key', $request['metadata']['order_id'] . '-' . $source, $request );
124
125 $headers['Idempotency-Key'] = $idempotency_key;
126 }
127
128 $response = wp_safe_remote_post(
129 self::ENDPOINT . $api,
130 [
131 'method' => $method,
132 'headers' => $headers,
133 'body' => apply_filters( 'woocommerce_stripe_request_body', $request, $api ),
134 'timeout' => 70,
135 ]
136 );
137
138 if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
139 WC_Stripe_Logger::log(
140 'Error Response: ' . print_r( $response, true ) . PHP_EOL . PHP_EOL . 'Failed request: ' . print_r(
141 [
142 'api' => $api,
143 'request' => $request,
144 'idempotency_key' => $idempotency_key,
145 ],
146 true
147 )
148 );
149
150 throw new WC_Stripe_Exception( print_r( $response, true ), __( 'There was a problem connecting to the Stripe API endpoint.', 'woocommerce-gateway-stripe' ) );
151 }
152
153 if ( $with_headers ) {
154 return [
155 'headers' => wp_remote_retrieve_headers( $response ),
156 'body' => json_decode( $response['body'] ),
157 ];
158 }
159
160 return json_decode( $response['body'] );
161 }
162
163 /**
164 * Retrieve API endpoint.
165 *
166 * @since 4.0.0
167 * @version 4.0.0
168 * @param string $api
169 */
170 public static function retrieve( $api ) {
171 WC_Stripe_Logger::log( "{$api}" );
172
173 $response = wp_safe_remote_get(
174 self::ENDPOINT . $api,
175 [
176 'method' => 'GET',
177 'headers' => self::get_headers(),
178 'timeout' => 70,
179 ]
180 );
181
182 if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
183 WC_Stripe_Logger::log( 'Error Response: ' . print_r( $response, true ) );
184 return new WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'woocommerce-gateway-stripe' ) );
185 }
186
187 return json_decode( $response['body'] );
188 }
189
190 /**
191 * Send the request to Stripe's API with level 3 data generated
192 * from the order. If the request fails due to an error related
193 * to level3 data, make the request again without it to allow
194 * the payment to go through.
195 *
196 * @since 4.3.2
197 * @version 5.1.0
198 *
199 * @param array $request Array with request parameters.
200 * @param string $api The API path for the request.
201 * @param array $level3_data The level 3 data for this request.
202 * @param WC_Order $order The order associated with the payment.
203 *
204 * @return stdClass|array The response
205 */
206 public static function request_with_level3_data( $request, $api, $level3_data, $order ) {
207 // 1. Do not add level3 data if the array is empty.
208 // 2. Do not add level3 data if there's a transient indicating that level3 was
209 // not accepted by Stripe in the past for this account.
210 // 3. Do not try to add level3 data if merchant is not based in the US.
211 // https://stripe.com/docs/level3#level-iii-usage-requirements
212 // (Needs to be authenticated with a level3 gated account to see above docs).
213 if (
214 empty( $level3_data ) ||
215 get_transient( 'wc_stripe_level3_not_allowed' ) ||
216 'US' !== WC()->countries->get_base_country()
217 ) {
218 return self::request(
219 $request,
220 $api
221 );
222 }
223
224 // Add level 3 data to the request.
225 $request['level3'] = $level3_data;
226
227 $result = self::request(
228 $request,
229 $api
230 );
231
232 $is_level3_param_not_allowed = (
233 isset( $result->error )
234 && isset( $result->error->code )
235 && 'parameter_unknown' === $result->error->code
236 && isset( $result->error->param )
237 && 'level3' === $result->error->param
238 );
239
240 $is_level_3data_incorrect = (
241 isset( $result->error )
242 && isset( $result->error->type )
243 && 'invalid_request_error' === $result->error->type
244 );
245
246 if ( $is_level3_param_not_allowed ) {
247 // Set a transient so that future requests do not add level 3 data.
248 // Transient is set to expire in 3 months, can be manually removed if needed.
249 set_transient( 'wc_stripe_level3_not_allowed', true, 3 * MONTH_IN_SECONDS );
250 } elseif ( $is_level_3data_incorrect ) {
251 // Log the issue so we could debug it.
252 WC_Stripe_Logger::log(
253 'Level3 data sum incorrect: ' . PHP_EOL
254 . print_r( $result->error->message, true ) . PHP_EOL
255 . print_r( 'Order line items: ', true ) . PHP_EOL
256 . print_r( $order->get_items(), true ) . PHP_EOL
257 . print_r( 'Order shipping amount: ', true ) . PHP_EOL
258 . print_r( $order->get_shipping_total(), true ) . PHP_EOL
259 . print_r( 'Order currency: ', true ) . PHP_EOL
260 . print_r( $order->get_currency(), true )
261 );
262 }
263
264 // Make the request again without level 3 data.
265 if ( $is_level3_param_not_allowed || $is_level_3data_incorrect ) {
266 unset( $request['level3'] );
267 return self::request(
268 $request,
269 $api
270 );
271 }
272
273 return $result;
274 }
275}