PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260805
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260805
260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 120213 All 189 releases
s2member / src / includes / classes / paypal-checkout-in.inc.php

paypal-checkout-in.inc.php in s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions 260805, at src/includes/classes/paypal-checkout-in.inc.php

1,227 lines 50.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // @codingStandardsIgnoreFile
3 /**
4 * s2Member's PayPal Checkout (REST) handler.
5 *
6 * Server-side entrypoint for PayPal Checkout operations used by s2Member shortcodes:
7 * - Buy Now: create_order + capture_order (one-time payments).
8 * - Subscriptions (membership level): get_plan_id + confirm_subscription.
9 * - output="url|anchor": redirect/return flow (does not create orders on page load).
10 * - Optional: cancel_subscription (on-site cancel for logged-in users).
11 *
12 * Successful operations are proxied into s2Member's existing PayPal notify/return handlers,
13 * preserving legacy provisioning behavior (level/ccaps/EOT/etc.) without rewriting it.
14 *
15 * @package s2Member\PayPal
16 * @since 260101
17 */
18 if(!defined('WPINC')) // MUST have WordPress.
19 exit ('Do not access this file directly.');
20
21 if(!class_exists('c_ws_plugin__s2member_paypal_checkout_in'))
22 {
23 class c_ws_plugin__s2member_paypal_checkout_in
24 {
25 public static function paypal_checkout()
26 {
27 if(empty($_REQUEST['s2member_paypal_checkout']))
28 return;
29
30 @set_time_limit(0);
31 @ini_set('memory_limit', apply_filters('admin_memory_limit', WP_MAX_MEMORY_LIMIT));
32 @ini_set('display_errors', '0');
33
34 $op = !empty($_REQUEST['s2member_paypal_checkout_op']) ? strtolower(trim(stripslashes((string)$_REQUEST['s2member_paypal_checkout_op']))) : '';
35 $t = !empty($_REQUEST['s2member_paypal_checkout_t']) ? trim(stripslashes((string)$_REQUEST['s2member_paypal_checkout_t'])) : '';
36
37 $is_redirect_mode = in_array($op, array('redirect', 'return', 'cancel'), true);
38
39 $env_setting = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox() ? 'sandbox' : 'live';
40
41 if(!headers_sent())
42 {
43 nocache_headers();
44 if($is_redirect_mode)
45 header('Content-Type: text/html; charset=UTF-8');
46 else
47 header('Content-Type: application/json; charset=UTF-8');
48 }
49
50 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
51 'ppco' => 'checkout',
52 'env_setting' => $env_setting,
53 'event' => 'request',
54 'get' => $_GET,
55 'post' => $_POST,
56 'method' => !empty($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '',
57 'ip' => !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
58 'ua' => !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
59 'referer' => !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
60 ));
61
62 if(!$op || !$t)
63 {
64 echo wp_json_encode(array('error' => 'missing_op_or_token'));
65 exit();
66 }
67 $raw = c_ws_plugin__s2member_utils_encryption::decrypt($t);
68
69 //260204 Use the plugin's hardened unserialize routine:
70 // - PHP 7+: allowed_classes => false
71 // - PHP <7: blocks object payloads before calling unserialize()
72 $token = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($raw);
73
74 if(!is_array($token))
75 $token = false;
76
77 if(!$token || !is_array($token))
78 {
79 echo wp_json_encode(array('error' => 'invalid_token'));
80 exit();
81 }
82 if(!empty($token['exp']) && is_numeric($token['exp']) && time() > (int)$token['exp'])
83 {
84 echo wp_json_encode(array('error' => 'token_expired'));
85 exit();
86 }
87 if(empty($token['invoice']) || empty($token['ip']) || empty($token['item_number']) || empty($token['checksum']))
88 {
89 echo wp_json_encode(array('error' => 'token_incomplete'));
90 exit();
91 }
92 if($token['checksum'] !== md5($token['invoice'].$token['ip'].$token['item_number']))
93 {
94 echo wp_json_encode(array('error' => 'token_checksum_mismatch'));
95 exit();
96 }
97
98 if($token['ip'] !== c_ws_plugin__s2member_utils_ip::current())
99 {
100 //260414 PayPal Checkout browser returns can legitimately arrive with a different client IP; log it, but do not fail the token.
101 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
102 'ppco' => 'checkout',
103 'env_setting' => $env_setting,
104 'event' => 'token_ip_mismatch',
105 'token' => $token,
106 'ip' => c_ws_plugin__s2member_utils_ip::current(),
107 ));
108 }
109
110 $old__subscr_gateway = !empty($token['old__subscr_gateway']) ? (string)$token['old__subscr_gateway'] : '';
111 $old__subscr_id = !empty($token['old__subscr_id']) ? (string)$token['old__subscr_id'] : '';
112 $old__subscr_baid = !empty($token['old__subscr_baid']) ? (string)$token['old__subscr_baid'] : '';
113 $old__subscr_cid = !empty($token['old__subscr_cid']) ? (string)$token['old__subscr_cid'] : '';
114 $old__ipn_signup_vars = (!empty($token['old__ipn_signup_vars']) && is_array($token['old__ipn_signup_vars'])) ? $token['old__ipn_signup_vars'] : array(); //260408 Use the old context captured before the buyer left for PayPal.
115
116 // output="anchor|url" support: redirect-mode endpoints (GET).
117 if($op === 'redirect' || $op === 'return' || $op === 'cancel')
118 {
119 // NOTE: These endpoints are intended for output="anchor|url" shortcode formats.
120 // They redirect to PayPal approval URLs, then auto-POST into s2Member's existing PayPal notify + return handlers.
121
122 if($op === 'cancel')
123 {
124 $cancel = !empty($token['cancel']) ? (string)$token['cancel'] : home_url('/');
125 $cancel = wp_validate_redirect($cancel, home_url('/'));
126 wp_redirect($cancel);
127 exit();
128 }
129
130 $endpoint = home_url('/?s2member_paypal_checkout=1');
131 $return_url = $endpoint.'&s2member_paypal_checkout_op=return&s2member_paypal_checkout_t='.rawurlencode($t);
132 $cancel_url = $endpoint.'&s2member_paypal_checkout_op=cancel&s2member_paypal_checkout_t='.rawurlencode($t);
133
134 if($op === 'redirect')
135 {
136 $pp_token = $token;
137 $pp_token['return'] = $return_url;
138 $pp_token['cancel'] = $cancel_url;
139
140 if((!isset($pp_token['rr']) || (string)$pp_token['rr'] === '') || strtoupper((string)$pp_token['rr']) === 'BN')
141 {
142 $order = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_create($pp_token);
143
144 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
145 'ppco' => 'checkout',
146 'env_setting' => $env_setting,
147 'event' => 'redirect_order_create_response',
148 'order' => $order,
149 'token' => $token,
150 ));
151
152 $approve_url = '';
153 if(!empty($order['links']) && is_array($order['links']))
154 foreach($order['links'] as $link)
155 if(!empty($link['rel']) && !empty($link['href']))
156 {
157 $rel = strtolower((string)$link['rel']);
158 if($rel === 'approve' || $rel === 'payer-action' || $rel === 'approval_url')
159 $approve_url = (string)$link['href'];
160 }
161
162 if(!$approve_url)
163 {
164 echo 'order_approval_url_missing';
165 exit();
166 }
167
168 wp_redirect($approve_url);
169 exit();
170 }
171 else
172 {
173 $subscription = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_create($pp_token);
174
175 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
176 'ppco' => 'checkout',
177 'env_setting' => $env_setting,
178 'event' => 'redirect_subscription_create_response',
179 'subscription' => $subscription,
180 'token' => $token,
181 ));
182
183 $approve_url = '';
184 if(!empty($subscription['links']) && is_array($subscription['links']))
185 foreach($subscription['links'] as $link)
186 if(!empty($link['rel']) && !empty($link['href']) && strtolower((string)$link['rel']) === 'approve')
187 $approve_url = (string)$link['href'];
188
189 if(!$approve_url)
190 {
191 echo 'subscription_approval_url_missing';
192 exit();
193 }
194
195 wp_redirect($approve_url);
196 exit();
197 }
198 }
199
200 // Return URL: PayPal redirects here after approval.
201 if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
202 {
203 $order_id = !empty($_GET['token']) ? trim(stripslashes((string)$_GET['token'])) : '';
204 if(!$order_id)
205 {
206 echo 'missing_order_id';
207 exit();
208 }
209
210 $capture = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_capture($order_id, $token);
211
212 $cap0 = (!empty($capture['purchase_units'][0]['payments']['captures'][0]) && is_array($capture['purchase_units'][0]['payments']['captures'][0])) ? $capture['purchase_units'][0]['payments']['captures'][0] : array();
213 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
214 'ppco' => 'checkout',
215 'env_setting' => $env_setting,
216 'event' => 'capture_response',
217 'order_id' => $order_id,
218 'status' => !empty($capture['status']) ? (string)$capture['status'] : '',
219 'capture_id' => !empty($cap0['id']) ? (string)$cap0['id'] : '',
220 'amount' => !empty($cap0['amount']['value']) ? (string)$cap0['amount']['value'] : '',
221 'cc' => !empty($cap0['amount']['currency_code']) ? (string)$cap0['amount']['currency_code'] : '',
222 'payer' => !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '',
223 'capture' => $capture,
224 'token' => $token,
225 ));
226
227 if(empty($capture['status']) || strtoupper($capture['status']) !== 'COMPLETED')
228 {
229 echo 'order_capture_failed';
230 exit();
231 }
232
233 $payer_email = !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '';
234 $first_name = !empty($capture['payer']['name']['given_name']) ? (string)$capture['payer']['name']['given_name'] : '';
235 $last_name = !empty($capture['payer']['name']['surname']) ? (string)$capture['payer']['name']['surname'] : '';
236
237 $pu_amount = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['value']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['value'] : '';
238 $pu_cc = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code'] : '';
239 $pu_cap_id = !empty($capture['purchase_units'][0]['payments']['captures'][0]['id']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['id'] : '';
240
241 if(!$payer_email || !$pu_amount || !$pu_cc || !$pu_cap_id)
242 {
243 echo 'capture_missing_fields';
244 exit();
245 }
246
247 //260228 Normalize amount strings before comparison (e.g. 20 vs 20.00).
248 if(!empty($token['amount']) && number_format((float)$token['amount'], 2, '.', '') !== number_format((float)$pu_amount, 2, '.', ''))
249 {
250 echo 'amount_mismatch';
251 exit();
252 }
253 if(!empty($token['cc']) && strtoupper((string)$token['cc']) !== strtoupper((string)$pu_cc))
254 {
255 echo 'currency_mismatch';
256 exit();
257 }
258
259 $paypal = array(
260 'txn_type' => 'web_accept',
261 'payment_status' => 'Completed',
262 'txn_id' => $pu_cap_id,
263 'mc_gross' => $pu_amount,
264 'mc_currency' => $pu_cc,
265 'invoice' => (string)$token['invoice'],
266 'custom' => (string)$token['custom'],
267 'item_name' => (string)$token['item_name'],
268 'item_number' => (string)$token['item_number'],
269 'option_name1' => (string)$token['on0'],
270 'option_selection1' => (string)$token['os0'],
271 'option_name2' => (string)$token['on1'],
272 'option_selection2' => (string)$token['os1'],
273 'payer_email' => $payer_email,
274 'first_name' => $first_name,
275 'last_name' => $last_name,
276 );
277
278 $is_independent_ccaps_sale = (strpos((string)$token['item_number'], '*:') === 0);
279 $is_specific_post_page_sale = (strpos((string)$token['item_number'], 'sp:') === 0);
280 $can_cancel_old_subscr = (!$is_independent_ccaps_sale && !$is_specific_post_page_sale); //260407 Only membership replacement-style PPCO purchases should cancel an existing recurring subscription here.
281
282 $notify_url = home_url('/?s2member_paypal_notify=1');
283 $notify_post = array_merge($paypal, array(
284 's2member_paypal_proxy' => 'paypal',
285 's2member_paypal_proxy_use' => 'paypal_checkout',
286 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
287 ));
288 $notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);
289
290 if(!is_array($notify_r))
291 {
292 if($is_redirect_mode)
293 echo 'notify_proxy_failed';
294 else
295 {
296 if(!headers_sent())
297 status_header(500);
298
299 echo wp_json_encode(array('error' => 'notify_proxy_failed'));
300 }
301 exit();
302 }
303
304 //260407 Framework PPCO replacements need the same old-subscription cancellation behavior without affecting independent CCAPS or specific post/page purchases.
305 if($can_cancel_old_subscr && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $pu_cap_id), get_defined_vars())) //260406
306 c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); //260407
307
308 $return_url = (string)$token['return'];
309 $return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url);
310
311 $return_post = array_merge($paypal, array(
312 's2member_paypal_proxy' => 'paypal',
313 's2member_paypal_proxy_use' => 'paypal_checkout',
314 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
315 ));
316
317 // Auto-POST into s2Member's existing PayPal return handler.
318 echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /></head><body>';
319 echo '<form id="s2m_ppco_rtn" method="post" action="'.esc_attr($return_url).'">';
320 foreach($return_post as $k => $v)
321 echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr((string)$v).'" />';
322 echo '</form><script type="text/javascript">document.getElementById("s2m_ppco_rtn").submit();</script></body></html>';
323 exit();
324 }
325 else
326 {
327 $subscription_id = !empty($_GET['subscription_id']) ? trim(stripslashes((string)$_GET['subscription_id'])) : '';
328 if(!$subscription_id)
329 {
330 echo 'missing_subscription_id';
331 exit();
332 }
333
334 $subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscription_id));
335
336 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
337 'ppco' => 'checkout',
338 'env_setting' => $env_setting,
339 'event' => 'subscription_get_response',
340 'subscription_id' => $subscription_id,
341 'code' => !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0,
342 'body' => !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '',
343 'token' => $token,
344 ));
345
346 $subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0;
347 $subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '';
348
349 $subscription = array();
350 if($subscription_body)
351 $subscription = json_decode($subscription_body, true);
352
353 if(!is_array($subscription))
354 $subscription = array();
355
356 if($subscription_code < 200 || $subscription_code > 299 || empty($subscription['id']))
357 {
358 echo 'subscription_get_failed';
359 exit();
360 }
361
362 $custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : '';
363 if($custom_id && (string)$token['invoice'] && $custom_id !== (string)$token['invoice'])
364 {
365 echo 'subscription_custom_id_mismatch';
366 exit();
367 }
368
369 $subscriber_email = !empty($subscription['subscriber']['email_address']) ? (string)$subscription['subscriber']['email_address'] : '';
370 $first_name = !empty($subscription['subscriber']['name']['given_name']) ? (string)$subscription['subscriber']['name']['given_name'] : '';
371 $last_name = !empty($subscription['subscriber']['name']['surname']) ? (string)$subscription['subscriber']['name']['surname'] : '';
372
373 $paypal = array(
374 'txn_type' => 'subscr_signup',
375 'payment_status' => 'Completed',
376 'subscr_gateway' => 'paypal',
377
378 'txn_id' => $subscription_id,
379 'subscr_id' => $subscription_id,
380 'subscr_baid' => $subscription_id,
381 'subscr_cid' => $subscription_id,
382
383 'mc_gross' => (string)$token['amount'],
384 'mc_currency' => strtoupper((string)$token['cc']),
385
386 'period1' => (!empty($token['tp']) && !empty($token['tt'])) ? ((string)$token['tp'].' '.strtoupper((string)$token['tt'])) : '0 D',
387 'mc_amount1' => (!empty($token['tp']) && !empty($token['tt'])) ? (string)$token['ta'] : '0.00',
388
389 'period3' => ((string)$token['rp'].' '.strtoupper((string)$token['rt'])),
390 'mc_amount3' => (string)$token['amount'],
391 'recurring' => ((isset($token['rr']) && (string)$token['rr'] === '1') ? '1' : '0'),
392
393 'invoice' => (string)$token['invoice'],
394 'custom' => (string)$token['custom'],
395 'item_name' => (string)$token['item_name'],
396 'item_number' => (string)$token['item_number'],
397
398 'payer_email' => $subscriber_email,
399 'first_name' => $first_name,
400 'last_name' => $last_name,
401
402 'option_name1' => (string)$token['on0'],
403 'option_selection1' => (string)$token['os0'],
404 'option_name2' => (string)$token['on1'],
405 'option_selection2' => (string)$token['os1'],
406 );
407
408 //260406 Use the shared PayPal Checkout subscription-done option so checkout and webhooks agree on fallback suppression.
409 $option_ppco_subscr = 's2m_ppco_subscr_done_'.md5($subscription_id);
410 $option_ppco_subscr_time = (int)get_option($option_ppco_subscr, 0);
411
412 if($option_ppco_subscr_time > 0 && (time() - $option_ppco_subscr_time) >= DAY_IN_SECONDS)
413 {
414 delete_option($option_ppco_subscr);
415 $option_ppco_subscr_time = 0;
416 }
417
418 if(!$option_ppco_subscr_time)
419 {
420 if(!add_option($option_ppco_subscr, time(), '', 'no'))
421 update_option($option_ppco_subscr, time(), false);
422
423 $notify_url = home_url('/?s2member_paypal_notify=1');
424 $notify_post = array_merge($paypal, array(
425 's2member_paypal_proxy' => 'paypal',
426 's2member_paypal_proxy_use' => 'paypal_checkout',
427 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
428 ));
429 $notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);
430
431 if(!is_array($notify_r))
432 {
433 if($is_redirect_mode)
434 echo 'notify_proxy_failed';
435 else
436 {
437 if(!headers_sent())
438 status_header(500);
439
440 echo wp_json_encode(array('error' => 'notify_proxy_failed'));
441 }
442 exit();
443 }
444
445 //260407 Framework PPCO replacements can also replace subscriptions created by other gateways
446 if($old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $subscription_id), get_defined_vars())) //260406.
447 c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); //260407
448 }
449
450 $return_url2 = (string)$token['return'];
451 $return_url2 = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url2);
452
453 $return_post2 = array_merge($paypal, array(
454 's2member_paypal_proxy' => 'paypal',
455 's2member_paypal_proxy_use' => 'paypal_checkout',
456 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
457 ));
458
459 echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /></head><body>';
460 echo '<form id="s2m_ppco_rtn" method="post" action="'.esc_attr($return_url2).'">';
461 foreach($return_post2 as $k => $v)
462 echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr((string)$v).'" />';
463 echo '</form><script type="text/javascript">document.getElementById("s2m_ppco_rtn").submit();</script></body></html>';
464 exit();
465 }
466 }
467
468 if($op === 'get_plan_id')
469 {
470 if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
471 {
472 echo wp_json_encode(array('error' => 'not_subscription'));
473 exit();
474 }
475
476 $plan_id = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_plan_get_id($token);
477
478 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
479 'ppco' => 'checkout',
480 'env_setting' => $env_setting,
481 'event' => 'get_plan_id_response',
482 'plan_id' => $plan_id,
483 'token' => $token,
484 ));
485
486 if(!$plan_id)
487 {
488 echo wp_json_encode(array('error' => 'plan_create_failed'));
489 exit();
490 }
491
492 echo wp_json_encode(array('plan_id' => $plan_id));
493 exit();
494 }
495
496 if($op === 'confirm_subscription')
497 {
498 if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
499 {
500 echo wp_json_encode(array('error' => 'not_subscription'));
501 exit();
502 }
503 $subscription_id = !empty($_POST['subscription_id']) ? trim(stripslashes((string)$_POST['subscription_id'])) : '';
504
505 if(!$subscription_id)
506 {
507 echo wp_json_encode(array('error' => 'missing_subscription_id'));
508 exit();
509 }
510 $subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscription_id));
511
512 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
513 'ppco' => 'checkout',
514 'env_setting' => $env_setting,
515 'event' => 'subscription_get_response',
516 'subscription_id' => $subscription_id,
517 'subscription' => $subscription_r,
518 'token' => $token,
519 ));
520
521 $subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0;
522 $subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '';
523
524 $subscription = array();
525 if($subscription_body)
526 $subscription = json_decode($subscription_body, true);
527
528 if(!is_array($subscription))
529 $subscription = array();
530
531 if($subscription_code < 200 || $subscription_code > 299 || empty($subscription['id']))
532 {
533 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
534 'ppco' => 'checkout',
535 'env_setting' => $env_setting,
536 'event' => 'subscription_get_failed',
537 'subscription_id' => $subscription_id,
538 'code' => $subscription_code,
539 'body' => $subscription_body,
540 ));
541 echo wp_json_encode(array('error' => 'subscription_get_failed'));
542 exit();
543 }
544 $status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : '';
545
546 $is_single_cycle = (isset($token['rr']) && (string)$token['rr'] === '0');
547 $allow_expired_single_cycle = false;
548
549 // PayPal can complete a single-cycle subscription immediately, returning status=EXPIRED after payment.
550 if($is_single_cycle && $status === 'EXPIRED')
551 {
552 $lpv = '';
553 $lpc = '';
554
555 if(!empty($subscription['billing_info']['last_payment']['amount']['value']))
556 $lpv = (string)$subscription['billing_info']['last_payment']['amount']['value'];
557
558 if(!empty($subscription['billing_info']['last_payment']['amount']['currency_code']))
559 $lpc = strtoupper((string)$subscription['billing_info']['last_payment']['amount']['currency_code']);
560
561 if($lpv !== '' && $lpc !== '')
562 $allow_expired_single_cycle = true;
563 }
564
565 if($status && !in_array($status, array('ACTIVE', 'APPROVED', 'APPROVAL_PENDING'), true) && !$allow_expired_single_cycle)
566 {
567 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
568 'ppco' => 'checkout',
569 'env_setting' => $env_setting,
570 'event' => 'subscription_status_invalid',
571 'subscription_id' => $subscription_id,
572 'status' => $status,
573 ));
574
575 echo wp_json_encode(array('error' => 'subscription_status_invalid'));
576 exit();
577 }
578 $expected_plan_id = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_plan_get_id($token);
579 if($expected_plan_id && !empty($subscription['plan_id']) && (string)$subscription['plan_id'] !== (string)$expected_plan_id)
580 {
581 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
582 'ppco' => 'checkout',
583 'env_setting' => $env_setting,
584 'event' => 'plan_mismatch',
585 'subscription_id' => $subscription_id,
586 'expected' => $expected_plan_id,
587 'actual' => (string)$subscription['plan_id'],
588 ));
589 echo wp_json_encode(array('error' => 'plan_mismatch'));
590 exit();
591 }
592 $custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : '';
593 if($custom_id && $custom_id !== (string)$token['invoice'])
594 {
595 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
596 'ppco' => 'checkout',
597 'env_setting' => $env_setting,
598 'event' => 'subscription_custom_id_mismatch',
599 'subscription_id' => $subscription_id,
600 'expected' => (string)$token['invoice'],
601 'actual' => $custom_id,
602 ));
603 echo wp_json_encode(array('error' => 'subscription_custom_id_mismatch'));
604 exit();
605 }
606
607 $subscriber_email = !empty($subscription['subscriber']['email_address']) ? (string)$subscription['subscriber']['email_address'] : '';
608 $first_name = !empty($subscription['subscriber']['name']['given_name']) ? (string)$subscription['subscriber']['name']['given_name'] : '';
609 $last_name = !empty($subscription['subscriber']['name']['surname']) ? (string)$subscription['subscriber']['name']['surname'] : '';
610
611 $paypal = array(
612 'txn_type' => 'subscr_signup',
613 'payment_status' => 'Completed',
614 'subscr_gateway' => 'paypal',
615
616 'txn_id' => $subscription_id,
617 'subscr_id' => $subscription_id,
618 'subscr_baid' => $subscription_id,
619 'subscr_cid' => $subscription_id,
620
621 'mc_gross' => (string)$token['amount'],
622 'mc_currency' => strtoupper((string)$token['cc']),
623
624 'period1' => (!empty($token['tp']) && !empty($token['tt'])) ? ((string)$token['tp'].' '.strtoupper((string)$token['tt'])) : '0 D',
625 'mc_amount1' => (!empty($token['tp']) && !empty($token['tt'])) ? (string)$token['ta'] : '0.00',
626
627 'period3' => ((string)$token['rp'].' '.strtoupper((string)$token['rt'])),
628 'mc_amount3' => (string)$token['amount'],
629 'recurring' => ((isset($token['rr']) && (string)$token['rr'] === '1') ? '1' : '0'),
630
631 'invoice' => (string)$token['invoice'],
632 'custom' => (string)$token['custom'],
633 'item_name' => (string)$token['item_name'],
634 'item_number' => (string)$token['item_number'],
635
636 'payer_email' => $subscriber_email,
637 'first_name' => $first_name,
638 'last_name' => $last_name,
639
640 'option_name1' => (string)$token['on0'],
641 'option_selection1' => (string)$token['os0'],
642 'option_name2' => (string)$token['on1'],
643 'option_selection2' => (string)$token['os1'],
644 );
645
646 $ppco_dup_processed = false;
647 $option_ppco_subscr = 's2m_ppco_subscr_done_'.md5($subscription_id);
648 $option_ppco_subscr_time = (int)get_option($option_ppco_subscr, 0);
649
650 if($option_ppco_subscr_time > 0 && (time() - $option_ppco_subscr_time) >= DAY_IN_SECONDS)
651 {
652 delete_option($option_ppco_subscr);
653 $option_ppco_subscr_time = 0;
654 }
655
656 $ppco_dup_processed = ($option_ppco_subscr_time > 0);
657
658 if($ppco_dup_processed)
659 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
660 'ppco' => 'checkout',
661 'env_setting' => $env_setting,
662 'event' => 'duplicate_subscription_ignored',
663 'subscription_id' => $subscription_id,
664 'option' => $option_ppco_subscr,
665 ));
666
667 if(!$ppco_dup_processed)
668 {
669 if(!add_option($option_ppco_subscr, time(), '', 'no'))
670 update_option($option_ppco_subscr, time(), false);
671
672 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
673 'ppco' => 'checkout',
674 'env_setting' => $env_setting,
675 'event' => 'idempotency_subscription_set',
676 'subscription_id' => $subscription_id,
677 'option' => $option_ppco_subscr,
678 'expires_secs' => DAY_IN_SECONDS,
679 ));
680
681 $notify_url = home_url('/?s2member_paypal_notify=1');
682 $notify_post = array_merge($paypal, array(
683 's2member_paypal_proxy' => 'paypal',
684 's2member_paypal_proxy_use' => 'paypal_checkout',
685 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
686 ));
687 $notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);
688
689 if(!is_array($notify_r))
690 $notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
691
692 $notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
693 $notify_msg = !empty($notify_r['message']) ? (string)$notify_r['message'] : '';
694 $notify_body = !empty($notify_r['body']) ? $notify_r['body'] : '';
695
696 if($notify_code >= 200 && $notify_code <= 299)
697 {
698 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
699 'ppco' => 'checkout',
700 'env_setting' => $env_setting,
701 'event' => 'notify_proxy_response',
702 'subscription_id' => $subscription_id,
703 'url' => $notify_url,
704 'code' => $notify_code,
705 'message' => $notify_msg,
706 'body' => $notify_body,
707 ));
708
709 //260407 Framework PPCO AJAX replacements need the same gateway-aware old-subscription cancellation behavior.
710 if($old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $subscription_id), get_defined_vars())) //260406
711 c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); //260407
712 }
713 else
714 {
715 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
716 'ppco' => 'checkout',
717 'env_setting' => $env_setting,
718 'event' => 'notify_proxy_failed',
719 'subscription_id' => $subscription_id,
720 'url' => $notify_url,
721 'code' => $notify_code,
722 'message' => $notify_msg,
723 'body' => $notify_body,
724 ));
725 echo wp_json_encode(array('error' => 'notify_proxy_failed'));
726 exit();
727 }
728 }
729
730 $return_url = (string)$token['return'];
731 $return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url);
732
733 $return_post = array_merge($paypal, array(
734 's2member_paypal_proxy' => 'paypal',
735 's2member_paypal_proxy_use' => 'paypal_checkout',
736 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
737 ));
738
739 echo wp_json_encode(array(
740 'rtn_url' => $return_url,
741 'rtn_post' => $return_post,
742 ));
743 exit();
744 }
745
746 if($op === 'cancel_subscription')
747 {
748 if(!is_user_logged_in())
749 {
750 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
751 'ppco' => 'checkout',
752 'env_setting' => $env_setting,
753 'event' => 'cancel_subscription_not_logged_in',
754 'token' => $token,
755 ));
756
757 echo wp_json_encode(array('error' => 'not_logged_in'));
758 exit();
759 }
760 $user_id = (int)get_current_user_id();
761
762 $nonce = !empty($_POST['s2member_paypal_checkout_nonce']) ? trim(stripslashes((string)$_POST['s2member_paypal_checkout_nonce'])) : '';
763 if(!$nonce || !wp_verify_nonce($nonce, 's2m_ppco_cancel_'.$user_id))
764 {
765 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
766 'ppco' => 'checkout',
767 'env_setting' => $env_setting,
768 'event' => 'cancel_subscription_bad_nonce',
769 'user_id'=> $user_id,
770 ));
771
772 echo wp_json_encode(array('error' => 'bad_nonce'));
773 exit();
774 }
775
776 $token_user_id = !empty($token['user_id']) ? (int)$token['user_id'] : 0;
777 $token_subscr_id = !empty($token['subscr_id']) ? (string)$token['subscr_id'] : '';
778
779 if(!$token_user_id || $token_user_id !== $user_id || !$token_subscr_id)
780 {
781 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
782 'ppco' => 'checkout',
783 'env_setting' => $env_setting,
784 'event' => 'cancel_subscription_token_mismatch',
785 'user_id' => $user_id,
786 'token' => $token,
787 ));
788
789 echo wp_json_encode(array('error' => 'token_mismatch'));
790 exit();
791 }
792
793 $subscr_id = (string)get_user_option('s2member_subscr_id', $user_id);
794 if(!$subscr_id || $subscr_id !== $token_subscr_id)
795 {
796 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
797 'ppco' => 'checkout',
798 'env_setting' => $env_setting,
799 'event' => 'cancel_subscription_user_mismatch',
800 'user_id' => $user_id,
801 'user_subscr'=> $subscr_id,
802 'token_subscr'=> $token_subscr_id,
803 ));
804
805 echo wp_json_encode(array('error' => 'user_mismatch'));
806 exit();
807 }
808
809 $reason = !empty($_POST['reason']) ? trim(stripslashes((string)$_POST['reason'])) : 'Cancelled by subscriber.';
810 $reason = sanitize_text_field($reason);
811 if(!$reason)
812 $reason = 'Cancelled by subscriber.';
813
814 //260517 Get PayPal Checkout subscription details before cancelling locally.
815 $subscription = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_details($subscr_id);
816 $subscription_status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : '';
817 $next_billing_time = !empty($subscription['billing_info']['next_billing_time']) ? (string)$subscription['billing_info']['next_billing_time'] : '';
818 $next_billing_ts = ($next_billing_time) ? strtotime($next_billing_time) : 0;
819
820 if(!empty($subscription['__error']) || empty($subscription['id']) || (string)$subscription['id'] !== (string)$subscr_id || $subscription_status !== 'ACTIVE' || !$next_billing_ts || $next_billing_ts <= time())
821 {
822 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
823 'ppco' => 'checkout',
824 'env_setting'=> $env_setting,
825 'event' => 'cancel_subscription_details_unusable',
826 'user_id' => $user_id,
827 'subscr_id' => $subscr_id,
828 'status' => $subscription_status,
829 'next' => $next_billing_time,
830 'code' => !empty($subscription['__code']) ? (int)$subscription['__code'] : 0,
831 ));
832
833 echo wp_json_encode(array('error' => 'subscription_details_unusable'));
834 exit();
835 }
836
837 $r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_cancel($subscr_id, $reason);
838
839 $code = !empty($r['code']) ? (int)$r['code'] : 0;
840 $body = !empty($r['body']) ? (string)$r['body'] : '';
841
842 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
843 'ppco' => 'checkout',
844 'env_setting' => $env_setting,
845 'event' => 'cancel_subscription_response',
846 'user_id' => $user_id,
847 'subscr_id'=> $subscr_id,
848 'code' => $code,
849 'body' => $body,
850 ));
851
852 if($code === 204 || ($code >= 200 && $code <= 299))
853 {
854 // Immediately feed s2Member's existing cancel handler (webhooks may be missing in MVP sites).
855 $paypal = array(
856 'txn_type' => 'subscr_cancel',
857 'payment_status' => 'Completed',
858 'subscr_gateway' => 'paypal',
859
860 'txn_id' => $subscr_id,
861 'subscr_id' => $subscr_id,
862 'custom' => (string)get_user_option('s2member_custom', $user_id),
863
864 // Help legacy notify logic resolve user in some fallback cases.
865 'mp_id' => $subscr_id,
866 'recurring_payment_id' => $subscr_id,
867
868 //260517 Provide safe defaults when signup vars are missing.
869 'item_number' => (string)c_ws_plugin__s2member_user_access::user_access_level(wp_get_current_user()),
870 'item_name' => 'PayPal Checkout Subscription',
871
872 // Best-effort payer email for logs/fallback logic.
873 'payer_email' => (string)wp_get_current_user()->user_email,
874 );
875
876 //260517 Enrich with stored signup vars so legacy cancel handler can match and compute EOT.
877 if(($ipn_signup_vars = get_user_option('s2member_ipn_signup_vars', $user_id)) && is_array($ipn_signup_vars)
878 && !empty($ipn_signup_vars['subscr_id']) && (string)$ipn_signup_vars['subscr_id'] === (string)$subscr_id)
879 {
880 if(!empty($ipn_signup_vars['item_number']))
881 $paypal['item_number'] = (string)$ipn_signup_vars['item_number'];
882
883 if(!empty($ipn_signup_vars['item_name']))
884 $paypal['item_name'] = (string)$ipn_signup_vars['item_name'];
885
886 if(empty($paypal['period1']) && !empty($ipn_signup_vars['period1']))
887 $paypal['period1'] = (string)$ipn_signup_vars['period1'];
888
889 if(empty($paypal['period3']) && !empty($ipn_signup_vars['period3']))
890 $paypal['period3'] = (string)$ipn_signup_vars['period3'];
891 }
892
893 $notify_url = home_url('/?s2member_paypal_notify=1');
894 $notify_post = array_merge($paypal, array(
895 'proxy_user_id' => $user_id, //260517
896 'proxy_next_billing_time' => $next_billing_time, //260517
897 's2member_paypal_proxy' => 'paypal',
898 's2member_paypal_proxy_use' => 'paypal_checkout',
899 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
900 ));
901
902 $notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);
903
904 if(!is_array($notify_r))
905 $notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
906
907 $notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
908 if(!($notify_code >= 200 && $notify_code <= 299))
909 {
910 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
911 'ppco' => 'checkout',
912 'env_setting' => $env_setting,
913 'event' => 'cancel_subscription_notify_failed',
914 'user_id' => $user_id,
915 'subscr_id' => $subscr_id,
916 'notify_code' => $notify_code,
917 'notify_msg' => !empty($notify_r['message']) ? (string)$notify_r['message'] : '',
918 ));
919 }
920
921 echo wp_json_encode(array('ok' => 1));
922 exit();
923 }
924
925 echo wp_json_encode(array('error' => 'cancel_failed'));
926 exit();
927 }
928
929 if($op === 'create_order')
930 {
931 $order = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_create($token);
932
933 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
934 'ppco' => 'checkout',
935 'env_setting' => $env_setting,
936 'event' => 'create_order_response',
937 'order' => $order,
938 'token' => $token,
939 ));
940
941 if(empty($order['id']))
942 {
943 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
944 'ppco' => 'checkout',
945 'env_setting' => $env_setting,
946 'event' => 'order_create_failed',
947 'order' => $order,
948 'token' => $token,
949 ));
950
951 echo wp_json_encode(array('error' => 'order_create_failed'));
952 exit();
953 }
954 echo wp_json_encode(array('order_id' => $order['id']));
955 exit();
956 }
957 else if($op === 'capture_order')
958 {
959 $order_id = !empty($_POST['order_id']) ? trim(stripslashes((string)$_POST['order_id'])) : '';
960
961 if(!$order_id)
962 {
963 echo wp_json_encode(array('error' => 'missing_order_id'));
964 exit();
965 }
966 $capture = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_capture($order_id, $token);
967
968 $cap0 = (!empty($capture['purchase_units'][0]['payments']['captures'][0]) && is_array($capture['purchase_units'][0]['payments']['captures'][0])) ? $capture['purchase_units'][0]['payments']['captures'][0] : array();
969 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
970 'ppco' => 'checkout',
971 'env_setting' => $env_setting,
972 'event' => 'capture_response',
973 'order_id' => $order_id,
974 'status' => !empty($capture['status']) ? (string)$capture['status'] : '',
975 'capture_id' => !empty($cap0['id']) ? (string)$cap0['id'] : '',
976 'amount' => !empty($cap0['amount']['value']) ? (string)$cap0['amount']['value'] : '',
977 'cc' => !empty($cap0['amount']['currency_code']) ? (string)$cap0['amount']['currency_code'] : '',
978 'payer' => !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '',
979 'capture' => $capture,
980 'token' => $token,
981 ));
982
983 if(empty($capture['status']) || strtoupper($capture['status']) !== 'COMPLETED')
984 {
985 echo wp_json_encode(array('error' => 'order_capture_failed'));
986 exit();
987 }
988
989 /*
990 * Build PayPal-like variables to feed s2Member's existing IPN + Return handlers.
991 */
992 $payer_email = !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '';
993 $first_name = !empty($capture['payer']['name']['given_name']) ? (string)$capture['payer']['name']['given_name'] : '';
994 $last_name = !empty($capture['payer']['name']['surname']) ? (string)$capture['payer']['name']['surname'] : '';
995
996 $pu_amount = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['value']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['value'] : '';
997 $pu_cc = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code'] : '';
998 $pu_cap_id = !empty($capture['purchase_units'][0]['payments']['captures'][0]['id']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['id'] : '';
999
1000 if(!$payer_email || !$pu_amount || !$pu_cc || !$pu_cap_id)
1001 {
1002 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1003 'ppco' => 'checkout',
1004 'env_setting' => $env_setting,
1005 'event' => 'capture_missing_fields',
1006 'order_id' => $order_id,
1007 'capture' => $capture,
1008 'token' => $token,
1009 ));
1010
1011 echo wp_json_encode(array('error' => 'capture_missing_fields'));
1012 exit();
1013 }
1014
1015 // Extra safety: enforce token matches amount/currency/invoice/custom if provided.
1016 //260228 Normalize amount strings before comparison (e.g. 20 vs 20.00).
1017 if(!empty($token['amount']) && number_format((float)$token['amount'], 2, '.', '') !== number_format((float)$pu_amount, 2, '.', ''))
1018 {
1019 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1020 'ppco' => 'checkout',
1021 'env_setting' => $env_setting,
1022 'event' => 'amount_mismatch',
1023 'order_id' => $order_id,
1024 'token' => $token,
1025 'pu' => array('amount' => $pu_amount, 'cc' => $pu_cc),
1026 ));
1027 echo wp_json_encode(array('error' => 'amount_mismatch'));
1028 exit();
1029 }
1030 if(!empty($token['cc']) && strtoupper((string)$token['cc']) !== strtoupper((string)$pu_cc))
1031 {
1032 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1033 'ppco' => 'checkout',
1034 'env_setting' => $env_setting,
1035 'event' => 'currency_mismatch',
1036 'order_id' => $order_id,
1037 'token' => $token,
1038 'pu' => array('amount' => $pu_amount, 'cc' => $pu_cc),
1039 ));
1040 echo wp_json_encode(array('error' => 'currency_mismatch'));
1041 exit();
1042 }
1043 $cap_invoice_id = '';
1044 if(!empty($capture['purchase_units'][0]['invoice_id']))
1045 $cap_invoice_id = (string)$capture['purchase_units'][0]['invoice_id'];
1046 else if(!empty($capture['purchase_units'][0]['payments']['captures'][0]['invoice_id']))
1047 $cap_invoice_id = (string)$capture['purchase_units'][0]['payments']['captures'][0]['invoice_id'];
1048
1049 if($cap_invoice_id && $cap_invoice_id !== (string)$token['invoice'])
1050 {
1051 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1052 'ppco' => 'checkout',
1053 'env_setting' => $env_setting,
1054 'event' => 'invoice_mismatch',
1055 'order_id' => $order_id,
1056 'token' => $token,
1057 'invoice' => $cap_invoice_id,
1058 ));
1059 echo wp_json_encode(array('error' => 'invoice_mismatch'));
1060 exit();
1061 }
1062
1063 $cap_custom_id = '';
1064 if(!empty($capture['purchase_units'][0]['custom_id']))
1065 $cap_custom_id = (string)$capture['purchase_units'][0]['custom_id'];
1066 else if(!empty($capture['purchase_units'][0]['payments']['captures'][0]['custom_id']))
1067 $cap_custom_id = (string)$capture['purchase_units'][0]['payments']['captures'][0]['custom_id'];
1068
1069 if($cap_custom_id && !empty($token['custom']) && $cap_custom_id !== (string)$token['custom'])
1070 {
1071 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1072 'ppco' => 'checkout',
1073 'env_setting' => $env_setting,
1074 'event' => 'custom_mismatch',
1075 'order_id' => $order_id,
1076 'token' => $token,
1077 'custom' => array(
1078 'token' => !empty($token['custom']) ? $token['custom'] : '',
1079 'paypal' => $cap_custom_id,
1080 ),
1081 ));
1082 echo wp_json_encode(array('error' => 'custom_mismatch'));
1083 exit();
1084 }
1085
1086 $paypal = array(
1087 'txn_type' => 'web_accept',
1088 'payment_status' => 'Completed',
1089 'subscr_gateway' => 'paypal',
1090
1091 'txn_id' => $pu_cap_id,
1092 'subscr_id' => $pu_cap_id,
1093 'subscr_baid' => $pu_cap_id,
1094 'subscr_cid' => $pu_cap_id,
1095
1096 'mc_gross' => $pu_amount,
1097 'mc_currency' => strtoupper($pu_cc),
1098
1099 'invoice' => (string)$token['invoice'],
1100 'custom' => (string)$token['custom'],
1101 'item_name' => (string)$token['item_name'],
1102 'item_number' => (string)$token['item_number'],
1103
1104 'payer_email' => $payer_email,
1105 'first_name' => $first_name,
1106 'last_name' => $last_name,
1107
1108 // Preserve s2Member's tracking option fields.
1109 'option_name1' => (string)$token['on0'],
1110 'option_selection1' => (string)$token['os0'],
1111 'option_name2' => (string)$token['on1'],
1112 'option_selection2' => (string)$token['os1'],
1113 );
1114
1115 // Idempotency: prevent double-processing of the same PayPal capture ID.
1116 $ppco_dup_processed = false;
1117 if($pu_cap_id)
1118 {
1119 $transient_ppco_capture = 's2m_ppco_'.md5('s2member_transient_ppco_capture_'.$pu_cap_id);
1120 $ppco_dup_processed = (bool)get_transient($transient_ppco_capture);
1121
1122 if(!$ppco_dup_processed)
1123 {
1124 //260404 Keep PayPal Checkout dedupe/fallback transients below 30 days for object-cache compatibility.
1125 set_transient($transient_ppco_capture, time(), DAY_IN_SECONDS);
1126
1127 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1128 'ppco' => 'checkout',
1129 'env_setting' => $env_setting,
1130 'event' => 'idempotency_capture_set',
1131 'order_id' => $order_id,
1132 'txn_id' => $pu_cap_id,
1133 'transient' => $transient_ppco_capture,
1134 'expires_secs' => DAY_IN_SECONDS,
1135 ));
1136 }
1137 else
1138 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1139 'ppco' => 'checkout',
1140 'env_setting' => $env_setting,
1141 'event' => 'duplicate_capture_ignored',
1142 'order_id' => $order_id,
1143 'txn_id' => $pu_cap_id,
1144 ));
1145 }
1146
1147 if(!$ppco_dup_processed)
1148 {
1149 $is_independent_ccaps_sale = (strpos((string)$token['item_number'], '*:') === 0);
1150 $is_specific_post_page_sale = (strpos((string)$token['item_number'], 'sp:') === 0);
1151 $can_cancel_old_subscr = (!$is_independent_ccaps_sale && !$is_specific_post_page_sale); //260407 Only membership replacement-style PPCO purchases should cancel an existing recurring subscription here.
1152
1153 // 1) Fire the existing IPN handler via proxy (provisions access, emails, logs, etc).
1154 $notify_url = home_url('/?s2member_paypal_notify=1');
1155 $notify_post = array_merge($paypal, array(
1156 's2member_paypal_proxy' => 'paypal',
1157 's2member_paypal_proxy_use' => 'paypal_checkout',
1158 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
1159 ));
1160 $notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);
1161
1162 if(!is_array($notify_r))
1163 $notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
1164
1165 $notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
1166 $notify_msg = !empty($notify_r['message']) ? (string)$notify_r['message'] : '';
1167 $notify_body = !empty($notify_r['body']) ? $notify_r['body'] : '';
1168
1169 if($notify_code >= 200 && $notify_code <= 299)
1170 {
1171 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1172 'ppco' => 'checkout',
1173 'env_setting' => $env_setting,
1174 'event' => 'notify_proxy_response',
1175 'order_id' => $order_id,
1176 'txn_id' => $pu_cap_id,
1177 'url' => $notify_url,
1178 'code' => $notify_code,
1179 'message' => $notify_msg,
1180 'body' => $notify_body,
1181 ));
1182
1183 //260407 Framework PPCO AJAX replacements can also replace subscriptions created by other gateways without affecting independent CCAPS or specific post/page purchases.
1184 if($can_cancel_old_subscr && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $pu_cap_id), get_defined_vars())) //260406
1185 c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); //260407
1186 }
1187 else
1188 {
1189 c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
1190 'ppco' => 'checkout',
1191 'env_setting' => $env_setting,
1192 'event' => 'notify_proxy_failed',
1193 'order_id' => $order_id,
1194 'txn_id' => $pu_cap_id,
1195 'url' => $notify_url,
1196 'code' => $notify_code,
1197 'message' => $notify_msg,
1198 'body' => $notify_body,
1199 ));
1200 echo wp_json_encode(array('error' => 'notify_proxy_failed'));
1201 exit();
1202 }
1203 }
1204
1205 // 2) Send the user through the existing Return handler via POST (sets cookies, thank-you UX, reg tokens, etc).
1206 $return_url = (string)$token['return'];
1207 $return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url);
1208
1209 $return_post = array_merge($paypal, array(
1210 's2member_paypal_proxy' => 'paypal',
1211 's2member_paypal_proxy_use' => 'paypal_checkout',
1212 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
1213 ));
1214
1215 echo wp_json_encode(array(
1216 'rtn_url' => $return_url,
1217 'rtn_post' => $return_post,
1218 ));
1219 exit();
1220 }
1221
1222 echo wp_json_encode(array('error' => 'unknown_op'));
1223 exit();
1224 }
1225 }
1226 }
1227