PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.26
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.26
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Modules / PaymentMethods / PayPalGateway / API / API.php

API.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.26, at app/Modules/PaymentMethods/PayPalGateway/API/API.php

435 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\PaymentMethods\PayPalGateway\API;
4
5 use FluentCart\App\Modules\PaymentMethods\PayPalGateway\PayPalSettingsBase;
6 use FluentCart\Framework\Support\Arr;
7
8 class API
9 {
10
11 private static $settings = null;
12 private const TEST_API_URL = 'https://api-m.sandbox.paypal.com';
13 private const LIVE_API_URL = 'https://api.paypal.com';
14
15 private const TEST_VERIFYING_URL = 'https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature';
16 private const LIVE_VERIFYING_URL = 'https://api-m.paypal.com/v1/notifications/verify-webhook-signature';
17
18 private static function getPayPalSettings()
19 {
20 if (!self::$settings) {
21 self::$settings = new PayPalSettingsBase();
22 }
23
24 return self::$settings;
25 }
26
27 public static function getAPIUrl($mode = 'test'): string
28 {
29 if ($mode === 'test') {
30 return self::TEST_API_URL;
31 }
32 return self::LIVE_API_URL;
33 }
34
35 protected static function getAuthAPI($mode = 'test')
36 {
37 if ($mode === 'live') {
38 return self::LIVE_API_URL . '/v1/oauth2/token';
39 }
40
41 return self::TEST_API_URL . '/v1/oauth2/token';
42 }
43
44 public static function validateCredentials($clientId, $clientSecret, $mode = 'test')
45 {
46 $result = self::getAccessToken($mode, [
47 'public_key' => $clientId,
48 'api_key' => $clientSecret
49 ]);
50
51 if (is_wp_error($result)) {
52 return $result;
53 }
54
55 return true;
56 }
57
58 /**
59 * @param string $path API path ex: checkout/orders (Required)
60 * @param string $version API version ex: v1, v2 (Optional)
61 * @param string $method HTTP method ex: GET, POST, DELETE (Optional)
62 * @param array $args API request arguments (Optional)
63 * @return mixed $response API response
64 * @throws \Exception if error occurs
65 */
66 public static function makeRequest($path, $version = 'v1', $method = 'POST', $args = [], $mode = '')
67 {
68 if (empty($path)) {
69 return new \WP_Error('invalid_path', esc_html__('API path is required', 'fluent-cart'));
70 }
71
72 $settings = self::getPayPalSettings();
73
74 if (!$mode) {
75 $mode = $settings->getMode();
76 }
77
78 $paypal_api_url = self::getAPIUrl($mode) . '/' . $version . '/' . $path;
79
80 $accessToken = self::getAccessToken($mode);
81
82 if (is_wp_error($accessToken)) {
83 return $accessToken;
84 }
85
86
87 //unset auth asertion headers, if platform app not connected
88 if ($settings->getProviderType() === 'api_keys') {
89 $headers = array(
90 'Authorization' => 'Bearer ' . $accessToken,
91 'Content-Type' => 'application/json',
92 'Accept' => 'application/json',
93 );
94 } else {
95 $authAssertion = static::generatePayPalAuthAssertion(
96 $settings->getPublicKey($mode),
97 static::getAccountId($settings, $mode)
98 );
99
100 $headers = array(
101 'Authorization' => 'Bearer ' . $accessToken,
102 'PayPal-Partner-Attribution-ID: FLUENTCART_SP_PPCP',
103 'Content-Type' => 'application/json',
104 'Accept' => 'application/json',
105 'PayPal-Auth-Assertion' => $authAssertion
106 );
107 }
108
109
110 if ('GET' === $method) {
111 // if args is not empty then append it to the url
112 if (!empty($args)) {
113 $paypal_api_url .= '?' . http_build_query($args);
114 }
115
116 return self::getRequest($paypal_api_url, $accessToken, $mode);
117 }
118
119 if ('POST' === $method) {
120 $headers['Prefer'] = 'return=representation';
121 }
122
123 $response = wp_remote_post($paypal_api_url, [
124 'headers' => $headers,
125 'method' => $method,
126 'body' => json_encode($args)
127 ]);
128
129 if (is_wp_error($response)) {
130 return new \WP_Error('general_error', $response->get_error_message(), $response);
131 }
132
133 $http_code = wp_remote_retrieve_response_code($response);
134 $body = json_decode(wp_remote_retrieve_body($response), true);
135
136 if ($http_code > 299) {
137 $code = 'general_error';
138 $message = 'PayPal General Error';
139 if (isset($body['error'])) {
140 $code = $body['error'];
141 }
142
143 if ($code === 'invalid_token') {
144 fluent_cart_update_option('_paypal_access_token_' . $mode, []);
145 }
146
147 if (!empty($body['message'])) {
148 $message = $body['message'];
149 if (isset($body['details'])) {
150 $message = Arr::get($body, 'details.0.issue', $message);
151 }
152 }
153
154 return new \WP_Error($code, $message, $body);
155 }
156
157 // it's success response with no content
158 if ($http_code == 204) {
159 return [
160 'status' => 'success',
161 'body' => 'No Content',
162 'code' => 204
163 ];
164 }
165
166 return $body;
167 }
168
169 public static function getResource($path, $data = [], $mode = '')
170 {
171 return self::makeRequest($path, 'v1', 'GET', $data, $mode);
172 }
173
174 public static function createResource($path, $data = [], $mode = '')
175 {
176 return self::makeRequest($path, 'v1', 'POST', $data, $mode);
177 }
178
179 public static function retrieveAccount($settings, $mode = '')
180 {
181 $paypalSettings = self::getPayPalSettings();
182 if (empty($mode)) {
183 $mode = $paypalSettings->getMode();
184 }
185
186 $settings = $paypalSettings->settings;
187
188 $clientId = Arr::get($settings, $mode . '_client_id');
189 $secretId = Arr::get($settings, $mode . '_client_secret');
190 $merchantId = Arr::get($settings, $mode . '_account_id');
191 $email = Arr::get($settings, $mode . '_email_address');
192 $accountType = Arr::get($settings, $mode . '_account_status');
193
194 if (!$clientId || !$secretId || !$merchantId) {
195 return false;
196 }
197
198 return [
199 'account_id' => $merchantId,
200 'display_name' => 'Merchant ID: ' . $merchantId,
201 'email' => $email,
202 'account_type' => $accountType
203 ];
204 }
205
206 public static function getRequest($url, $accessToken = null, $mode = '')
207 {
208 if (!$accessToken) {
209 $accessToken = self::getAccessToken($mode);
210
211 if (is_wp_error($accessToken)) {
212 return $accessToken;
213 }
214 }
215
216 $headers = array(
217 'Authorization' => 'Bearer ' . $accessToken,
218 'Content-Type' => 'application/json',
219 'Accept' => 'application/json',
220 'PayPal-Partner-Attribution-ID: FLUENTCART_SP_PPCP'
221 );
222
223 $response = wp_safe_remote_get($url, [
224 'headers' => $headers
225 ]);
226
227 if (is_wp_error($response)) {
228 return new \WP_Error('general_error', $response->get_error_message(), $response);
229 }
230
231 $http_code = wp_remote_retrieve_response_code($response);
232 $body = json_decode(wp_remote_retrieve_body($response), true);
233
234 if ($http_code == 200) {
235 return $body;
236 }
237
238 // it's success response with no content
239 if ($http_code == 204) {
240 return [
241 'status' => 'success',
242 'body' => 'No Content',
243 'code' => 204
244 ];
245 }
246
247 if ($http_code > 299) {
248 $code = 'general_error';
249 if (isset($body['error'])) {
250 $code = $body['error'];
251 }
252
253
254 if ($code === 'invalid_token' && $mode) {
255 fluent_cart_update_option('_paypal_access_token_' . $mode, []);
256 }
257
258 if (!empty($body['message'])) {
259 $message = $body['message'];
260 if (isset($body['details'])) {
261 $message = Arr::get($body, 'details.0.description');
262 }
263 }
264 return new \WP_Error($code, $message, $body);
265 }
266
267 $message = $body['message'] ?? 'PayPal General Error';
268
269 if (isset($body['details'])) {
270 $message = $body['details'][0]['issue'];
271 }
272
273 return new \WP_Error($http_code, $message, $body);
274 }
275
276 public static function createOrder($purchaseUnit)
277 {
278 return self::makeRequest('checkout/orders', 'v2', 'POST', [
279 'intent' => 'CAPTURE',
280 'purchase_units' => [$purchaseUnit],
281 'application_context' => ['shipping_preference' => 'NO_SHIPPING'],
282 ]);
283 }
284
285 public static function verifyPayment($paymentId)
286 {
287 return self::makeRequest('checkout/orders/' . $paymentId, 'v2', 'GET');
288 }
289
290 public function verifySubscription($subscriptionId, $mode = '')
291 {
292 return self::makeRequest('billing/subscriptions/' . $subscriptionId, 'v1', 'GET', [], $mode);
293 }
294
295 /**
296 * Retrieves PayPal access token using WP_HTTP.
297 *
298 * @param string $mode The PayPal mode (live/sandbox).
299 * @param array $args Additional arguments including public_key and api_key.
300 * @return string|\WP_Error Access token on success, WP_Error on failure.
301 */
302 private static function getAccessToken($mode = '', $args = [])
303 {
304 if (!$mode) {
305 $mode = (new PayPalSettingsBase())->getMode();
306 }
307
308 static $accessToken;
309
310 // Check for cached token
311 if (!$args) {
312 if ($accessToken) {
313 return $accessToken;
314 }
315
316 $existingToken = fluent_cart_get_option('_paypal_access_token_' . $mode);
317 if ($existingToken && isset($existingToken['expires_at']) && $existingToken['expires_at'] > time()) {
318 $accessToken = $existingToken['access_token'];
319 return $accessToken;
320 }
321 }
322
323 $apiUrl = self::getAuthAPI($mode);
324
325 // Prepare headers
326 $headers = [
327 'Accept' => 'application/json',
328 'Accept-Language' => 'en_US',
329 'PayPal-Partner-Attribution-ID' => 'FLUENTCART_SP_PPCP'
330 ];
331
332 // Prepare body
333 $body = [
334 'grant_type' => 'client_credentials'
335 ];
336
337 // Get credentials
338 $publicKey = !empty($args['public_key']) ? $args['public_key'] : self::getPayPalSettings()->getPublicKey($mode);
339 $apiKey = !empty($args['api_key']) ? $args['api_key'] : self::getPayPalSettings()->getApiKey($mode);
340
341 // Add Basic Auth header
342 $headers['Authorization'] = 'Basic ' . base64_encode($publicKey . ':' . $apiKey);
343
344 // Make HTTP request
345 $response = wp_remote_post($apiUrl, [
346 'headers' => $headers,
347 'body' => $body,
348 'timeout' => 30
349 ]);
350
351 // Check for WP_Error
352 if (is_wp_error($response)) {
353 return $response;
354 }
355
356 // Get response code and body
357 $http_code = wp_remote_retrieve_response_code($response);
358 $response_body = wp_remote_retrieve_body($response);
359
360 if ($http_code === 200) {
361 $response_data = json_decode($response_body, true);
362 $accessToken = $response_data['access_token'];
363
364 $data = [
365 'access_token' => $accessToken,
366 'expires_at' => time() + (int)$response_data['expires_in'] - 120 // Subtract 2 minutes
367 ];
368
369 fluent_cart_update_option('_paypal_access_token_' . $mode, $data);
370
371 return $accessToken;
372 }
373
374 $error = json_decode($response_body, true);
375 $errorMessage = $error['error_description'] ?? $error['error'] ?? esc_html__('Failed to retrieve access token from PayPal.', 'fluent-cart');
376
377 return new \WP_Error(
378 'access_token_error',
379 $errorMessage,
380 $error
381 );
382 }
383
384 /**
385 * Generate a PayPal-Auth-Assertion JWT header (unsigned, alg=none).
386 *
387 * @param string $clientId Your platform's REST API client ID
388 * @param string $sellerPayerId Seller's PayPal payer_id (preferred) or email
389 * @return string The PayPal‑Auth‑Assertion header value
390 */
391 private static function generatePayPalAuthAssertion($clientId, $sellerPayerId)
392 {
393 $header = ['alg' => 'none'];
394 $encodedHeader = rtrim(strtr(base64_encode(json_encode($header)), '+/', '-_'), '=');
395 $payload = [
396 'iss' => $clientId,
397 'payer_id' => $sellerPayerId
398 ];
399 $encodedPayload = rtrim(strtr(base64_encode(json_encode($payload)), '+/', '-_'), '=');
400 return "{$encodedHeader}.{$encodedPayload}.";
401 }
402
403 private static function getAccountId($settings, $mode)
404 {
405 return Arr::get($settings->settings, $mode . '_account_id');
406 }
407
408 public static function verifyWebhookSignature($body)
409 {
410 $verify_url = ((new PayPalSettingsBase())->getMode() === 'live')
411 ? self::LIVE_VERIFYING_URL
412 : self::TEST_VERIFYING_URL;
413
414 $accessToken = self::getAccessToken();
415
416 $args = array(
417 'headers' => array(
418 'Content-Type' => 'application/json',
419 'Authorization' => 'Bearer ' . $accessToken,
420 ),
421 'body' => json_encode($body),
422 'timeout' => 30,
423 'data_format' => 'body'
424 );
425
426 $response = wp_remote_post($verify_url, $args);
427
428 if (is_wp_error($response)) {
429 return $response;
430 }
431
432 return $response;
433 }
434 }
435