PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.20
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.20
1.6.6 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 All 49 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.20, at app/Modules/PaymentMethods/PayPalGateway/API/API.php

426 lines 13.2 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 verifyPayment($paymentId)
277 {
278 return self::makeRequest('checkout/orders/' . $paymentId, 'v2', 'GET');
279 }
280
281 public function verifySubscription($subscriptionId, $mode = '')
282 {
283 return self::makeRequest('billing/subscriptions/' . $subscriptionId, 'v1', 'GET', [], $mode);
284 }
285
286 /**
287 * Retrieves PayPal access token using WP_HTTP.
288 *
289 * @param string $mode The PayPal mode (live/sandbox).
290 * @param array $args Additional arguments including public_key and api_key.
291 * @return string|\WP_Error Access token on success, WP_Error on failure.
292 */
293 private static function getAccessToken($mode = '', $args = [])
294 {
295 if (!$mode) {
296 $mode = (new PayPalSettingsBase())->getMode();
297 }
298
299 static $accessToken;
300
301 // Check for cached token
302 if (!$args) {
303 if ($accessToken) {
304 return $accessToken;
305 }
306
307 $existingToken = fluent_cart_get_option('_paypal_access_token_' . $mode);
308 if ($existingToken && isset($existingToken['expires_at']) && $existingToken['expires_at'] > time()) {
309 $accessToken = $existingToken['access_token'];
310 return $accessToken;
311 }
312 }
313
314 $apiUrl = self::getAuthAPI($mode);
315
316 // Prepare headers
317 $headers = [
318 'Accept' => 'application/json',
319 'Accept-Language' => 'en_US',
320 'PayPal-Partner-Attribution-ID' => 'FLUENTCART_SP_PPCP'
321 ];
322
323 // Prepare body
324 $body = [
325 'grant_type' => 'client_credentials'
326 ];
327
328 // Get credentials
329 $publicKey = !empty($args['public_key']) ? $args['public_key'] : self::getPayPalSettings()->getPublicKey($mode);
330 $apiKey = !empty($args['api_key']) ? $args['api_key'] : self::getPayPalSettings()->getApiKey($mode);
331
332 // Add Basic Auth header
333 $headers['Authorization'] = 'Basic ' . base64_encode($publicKey . ':' . $apiKey);
334
335 // Make HTTP request
336 $response = wp_remote_post($apiUrl, [
337 'headers' => $headers,
338 'body' => $body,
339 'timeout' => 30
340 ]);
341
342 // Check for WP_Error
343 if (is_wp_error($response)) {
344 return $response;
345 }
346
347 // Get response code and body
348 $http_code = wp_remote_retrieve_response_code($response);
349 $response_body = wp_remote_retrieve_body($response);
350
351 if ($http_code === 200) {
352 $response_data = json_decode($response_body, true);
353 $accessToken = $response_data['access_token'];
354
355 $data = [
356 'access_token' => $accessToken,
357 'expires_at' => time() + (int)$response_data['expires_in'] - 120 // Subtract 2 minutes
358 ];
359
360 fluent_cart_update_option('_paypal_access_token_' . $mode, $data);
361
362 return $accessToken;
363 }
364
365 $error = json_decode($response_body, true);
366 $errorMessage = $error['error_description'] ?? $error['error'] ?? esc_html__('Failed to retrieve access token from PayPal.', 'fluent-cart');
367
368 return new \WP_Error(
369 'access_token_error',
370 $errorMessage,
371 $error
372 );
373 }
374
375 /**
376 * Generate a PayPal-Auth-Assertion JWT header (unsigned, alg=none).
377 *
378 * @param string $clientId Your platform's REST API client ID
379 * @param string $sellerPayerId Seller's PayPal payer_id (preferred) or email
380 * @return string The PayPal‑Auth‑Assertion header value
381 */
382 private static function generatePayPalAuthAssertion($clientId, $sellerPayerId)
383 {
384 $header = ['alg' => 'none'];
385 $encodedHeader = rtrim(strtr(base64_encode(json_encode($header)), '+/', '-_'), '=');
386 $payload = [
387 'iss' => $clientId,
388 'payer_id' => $sellerPayerId
389 ];
390 $encodedPayload = rtrim(strtr(base64_encode(json_encode($payload)), '+/', '-_'), '=');
391 return "{$encodedHeader}.{$encodedPayload}.";
392 }
393
394 private static function getAccountId($settings, $mode)
395 {
396 return Arr::get($settings->settings, $mode . '_account_id');
397 }
398
399 public static function verifyWebhookSignature($body)
400 {
401 $verify_url = ((new PayPalSettingsBase())->getMode() === 'live')
402 ? self::LIVE_VERIFYING_URL
403 : self::TEST_VERIFYING_URL;
404
405 $accessToken = self::getAccessToken();
406
407 $args = array(
408 'headers' => array(
409 'Content-Type' => 'application/json',
410 'Authorization' => 'Bearer ' . $accessToken,
411 ),
412 'body' => json_encode($body),
413 'timeout' => 30,
414 'data_format' => 'body'
415 );
416
417 $response = wp_remote_post($verify_url, $args);
418
419 if (is_wp_error($response)) {
420 return $response;
421 }
422
423 return $response;
424 }
425 }
426