PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260814
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260814
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 260814, at src/includes/classes/paypal-checkout-in.inc.php

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