--- a/s2member/s2member.php
+++ b/s2member/s2member.php
@@ -20,8 +20,8 @@
*/
/* -- This section for WordPress parsing. ------------------------------------------------------------------------------
-Version: 260127
-Stable tag: 260127
+Version: 260215
+Stable tag: 260215
SSL Compatible: yes
bbPress Compatible: yes
@@ -36,7 +36,7 @@
Authorize.Net Compatible: yes w/s2Member Pro
ClickBank Compatible: yes w/s2Member Pro
-Tested up to: 7.0-alpha-61539
+Tested up to: 7.0-alpha-61642
Requires at least: 4.2
Requires PHP: 5.6.2
@@ -77,7 +77,7 @@
*
* @var string
*/
-${__FILE__}['tmp'] = '260127'; //version//
+${__FILE__}['tmp'] = '260215'; //version//
if(!defined('WS_PLUGIN__S2MEMBER_VERSION'))
define('WS_PLUGIN__S2MEMBER_VERSION', ${__FILE__}['tmp']);
/**
--- a/s2member/src/includes/classes/paypal-checkout-in.inc.php
+++ b/s2member/src/includes/classes/paypal-checkout-in.inc.php
@@ -36,11 +36,14 @@
$is_redirect_mode = in_array($op, array('redirect', 'return', 'cancel'), true);
- nocache_headers();
- if($is_redirect_mode)
- header('Content-Type: text/html; charset=UTF-8');
- else
- header('Content-Type: application/json; charset=UTF-8');
+ if(!headers_sent())
+ {
+ nocache_headers();
+ if($is_redirect_mode)
+ header('Content-Type: text/html; charset=UTF-8');
+ else
+ header('Content-Type: application/json; charset=UTF-8');
+ }
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
'ppco' => 'checkout',
@@ -55,27 +58,37 @@
if(!$op || !$t)
{
- echo json_encode(array('error' => 'missing_op_or_token'));
+ echo wp_json_encode(array('error' => 'missing_op_or_token'));
exit();
}
- if(!($token = @unserialize(c_ws_plugin__s2member_utils_encryption::decrypt($t))) || !is_array($token))
+ $raw = c_ws_plugin__s2member_utils_encryption::decrypt($t);
+
+ //260204 Use the plugin's hardened unserialize routine:
+ // - PHP 7+: allowed_classes => false
+ // - PHP <7: blocks object payloads before calling unserialize()
+ $token = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($raw);
+
+ if(!is_array($token))
+ $token = false;
+
+ if(!$token || !is_array($token))
{
- echo json_encode(array('error' => 'invalid_token'));
+ echo wp_json_encode(array('error' => 'invalid_token'));
exit();
}
if(!empty($token['exp']) && is_numeric($token['exp']) && time() > (int)$token['exp'])
{
- echo json_encode(array('error' => 'token_expired'));
+ echo wp_json_encode(array('error' => 'token_expired'));
exit();
}
if(empty($token['invoice']) || empty($token['ip']) || empty($token['item_number']) || empty($token['checksum']))
{
- echo json_encode(array('error' => 'token_incomplete'));
+ echo wp_json_encode(array('error' => 'token_incomplete'));
exit();
}
if($token['checksum'] !== md5($token['invoice'].$token['ip'].$token['item_number']))
{
- echo json_encode(array('error' => 'token_checksum_mismatch'));
+ echo wp_json_encode(array('error' => 'token_checksum_mismatch'));
exit();
}
@@ -88,7 +101,7 @@
'ip' => c_ws_plugin__s2member_utils_ip::current(),
));
- echo json_encode(array('error' => 'token_ip_mismatch'));
+ echo wp_json_encode(array('error' => 'token_ip_mismatch'));
exit();
}
@@ -100,7 +113,9 @@
if($op === 'cancel')
{
- wp_redirect(!empty($token['cancel']) ? (string)$token['cancel'] : home_url('/'));
+ $cancel = !empty($token['cancel']) ? (string)$token['cancel'] : home_url('/');
+ $cancel = wp_validate_redirect($cancel, home_url('/'));
+ wp_redirect($cancel);
exit();
}
@@ -258,7 +273,15 @@
if(!is_array($notify_r))
{
- echo 'notify_proxy_failed';
+ if($is_redirect_mode)
+ echo 'notify_proxy_failed';
+ else
+ {
+ if(!headers_sent())
+ status_header(500);
+
+ echo wp_json_encode(array('error' => 'notify_proxy_failed'));
+ }
exit();
}
@@ -377,7 +400,15 @@
if(!is_array($notify_r))
{
- echo 'notify_proxy_failed';
+ if($is_redirect_mode)
+ echo 'notify_proxy_failed';
+ else
+ {
+ if(!headers_sent())
+ status_header(500);
+
+ echo wp_json_encode(array('error' => 'notify_proxy_failed'));
+ }
exit();
}
}
@@ -404,7 +435,7 @@
{
if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
{
- echo json_encode(array('error' => 'not_subscription'));
+ echo wp_json_encode(array('error' => 'not_subscription'));
exit();
}
@@ -419,11 +450,11 @@
if(!$plan_id)
{
- echo json_encode(array('error' => 'plan_create_failed'));
+ echo wp_json_encode(array('error' => 'plan_create_failed'));
exit();
}
- echo json_encode(array('plan_id' => $plan_id));
+ echo wp_json_encode(array('plan_id' => $plan_id));
exit();
}
@@ -431,14 +462,14 @@
{
if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
{
- echo json_encode(array('error' => 'not_subscription'));
+ echo wp_json_encode(array('error' => 'not_subscription'));
exit();
}
$subscription_id = !empty($_POST['subscription_id']) ? trim(stripslashes((string)$_POST['subscription_id'])) : '';
if(!$subscription_id)
{
- echo json_encode(array('error' => 'missing_subscription_id'));
+ echo wp_json_encode(array('error' => 'missing_subscription_id'));
exit();
}
$subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscription_id));
@@ -470,7 +501,7 @@
'code' => $subscription_code,
'body' => $subscription_body,
));
- echo json_encode(array('error' => 'subscription_get_failed'));
+ echo wp_json_encode(array('error' => 'subscription_get_failed'));
exit();
}
$status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : '';
@@ -503,7 +534,7 @@
'status' => $status,
));
- echo json_encode(array('error' => 'subscription_status_invalid'));
+ echo wp_json_encode(array('error' => 'subscription_status_invalid'));
exit();
}
$expected_plan_id = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_plan_get_id($token);
@@ -516,7 +547,7 @@
'expected' => $expected_plan_id,
'actual' => (string)$subscription['plan_id'],
));
- echo json_encode(array('error' => 'plan_mismatch'));
+ echo wp_json_encode(array('error' => 'plan_mismatch'));
exit();
}
$custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : '';
@@ -529,7 +560,7 @@
'expected' => (string)$token['invoice'],
'actual' => $custom_id,
));
- echo json_encode(array('error' => 'subscription_custom_id_mismatch'));
+ echo wp_json_encode(array('error' => 'subscription_custom_id_mismatch'));
exit();
}
@@ -604,6 +635,9 @@
));
$notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);
+ if(!is_array($notify_r))
+ $notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
+
$notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
$notify_msg = !empty($notify_r['message']) ? (string)$notify_r['message'] : '';
$notify_body = !empty($notify_r['body']) ? $notify_r['body'] : '';
@@ -629,7 +663,7 @@
'message' => $notify_msg,
'body' => $notify_body,
));
- echo json_encode(array('error' => 'notify_proxy_failed'));
+ echo wp_json_encode(array('error' => 'notify_proxy_failed'));
exit();
}
}
@@ -643,7 +677,7 @@
's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
));
- echo json_encode(array(
+ echo wp_json_encode(array(
'rtn_url' => $return_url,
'rtn_post' => $return_post,
));
@@ -660,7 +694,7 @@
'token' => $token,
));
- echo json_encode(array('error' => 'not_logged_in'));
+ echo wp_json_encode(array('error' => 'not_logged_in'));
exit();
}
$user_id = (int)get_current_user_id();
@@ -674,7 +708,7 @@
'user_id'=> $user_id,
));
- echo json_encode(array('error' => 'bad_nonce'));
+ echo wp_json_encode(array('error' => 'bad_nonce'));
exit();
}
@@ -690,7 +724,7 @@
'token' => $token,
));
- echo json_encode(array('error' => 'token_mismatch'));
+ echo wp_json_encode(array('error' => 'token_mismatch'));
exit();
}
@@ -705,7 +739,7 @@
'token_subscr'=> $token_subscr_id,
));
- echo json_encode(array('error' => 'user_mismatch'));
+ echo wp_json_encode(array('error' => 'user_mismatch'));
exit();
}
@@ -739,10 +773,31 @@
'txn_id' => $subscr_id,
'subscr_id' => $subscr_id,
- // Helps some older cancel paths/logs; not required for user resolution.
+ // Help legacy notify logic resolve user in some fallback cases.
+ 'mp_id' => $subscr_id,
+ 'recurring_payment_id' => $subscr_id,
+
+ // Best-effort payer email for logs/fallback logic.
'payer_email' => (string)wp_get_current_user()->user_email,
);
+ // Enrich with stored signup vars so legacy cancel handler can match and compute EOT.
+ if(($ipn_signup_vars = get_user_option('s2member_ipn_signup_vars', $user_id)) && is_array($ipn_signup_vars)
+ && !empty($ipn_signup_vars['subscr_id']) && (string)$ipn_signup_vars['subscr_id'] === (string)$subscr_id)
+ {
+ if(empty($paypal['item_number']) && !empty($ipn_signup_vars['item_number']))
+ $paypal['item_number'] = (string)$ipn_signup_vars['item_number'];
+
+ if(empty($paypal['item_name']) && !empty($ipn_signup_vars['item_name']))
+ $paypal['item_name'] = (string)$ipn_signup_vars['item_name'];
+
+ if(empty($paypal['period1']) && !empty($ipn_signup_vars['period1']))
+ $paypal['period1'] = (string)$ipn_signup_vars['period1'];
+
+ if(empty($paypal['period3']) && !empty($ipn_signup_vars['period3']))
+ $paypal['period3'] = (string)$ipn_signup_vars['period3'];
+ }
+
$notify_url = home_url('/?s2member_paypal_notify=1');
$notify_post = array_merge($paypal, array(
's2member_paypal_proxy' => 'paypal',
@@ -752,6 +807,9 @@
$notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);
+ if(!is_array($notify_r))
+ $notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
+
$notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
if(!($notify_code >= 200 && $notify_code <= 299))
{
@@ -765,11 +823,11 @@
));
}
- echo json_encode(array('ok' => 1));
+ echo wp_json_encode(array('ok' => 1));
exit();
}
- echo json_encode(array('error' => 'cancel_failed'));
+ echo wp_json_encode(array('error' => 'cancel_failed'));
exit();
}
@@ -793,10 +851,10 @@
'token' => $token,
));
- echo json_encode(array('error' => 'order_create_failed'));
+ echo wp_json_encode(array('error' => 'order_create_failed'));
exit();
}
- echo json_encode(array('order_id' => $order['id']));
+ echo wp_json_encode(array('order_id' => $order['id']));
exit();
}
else if($op === 'capture_order')
@@ -805,7 +863,7 @@
if(!$order_id)
{
- echo json_encode(array('error' => 'missing_order_id'));
+ echo wp_json_encode(array('error' => 'missing_order_id'));
exit();
}
$capture = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_capture($order_id, $token);
@@ -826,7 +884,7 @@
if(empty($capture['status']) || strtoupper($capture['status']) !== 'COMPLETED')
{
- echo json_encode(array('error' => 'order_capture_failed'));
+ echo wp_json_encode(array('error' => 'order_capture_failed'));
exit();
}
@@ -851,7 +909,7 @@
'token' => $token,
));
- echo json_encode(array('error' => 'capture_missing_fields'));
+ echo wp_json_encode(array('error' => 'capture_missing_fields'));
exit();
}
@@ -865,7 +923,7 @@
'token' => $token,
'pu' => array('amount' => $pu_amount, 'cc' => $pu_cc),
));
- echo json_encode(array('error' => 'amount_mismatch'));
+ echo wp_json_encode(array('error' => 'amount_mismatch'));
exit();
}
if(!empty($token['cc']) && strtoupper((string)$token['cc']) !== strtoupper((string)$pu_cc))
@@ -877,7 +935,7 @@
'token' => $token,
'pu' => array('amount' => $pu_amount, 'cc' => $pu_cc),
));
- echo json_encode(array('error' => 'currency_mismatch'));
+ echo wp_json_encode(array('error' => 'currency_mismatch'));
exit();
}
$cap_invoice_id = '';
@@ -895,7 +953,7 @@
'token' => $token,
'invoice' => $cap_invoice_id,
));
- echo json_encode(array('error' => 'invoice_mismatch'));
+ echo wp_json_encode(array('error' => 'invoice_mismatch'));
exit();
}
@@ -917,7 +975,7 @@
'paypal' => $cap_custom_id,
),
));
- echo json_encode(array('error' => 'custom_mismatch'));
+ echo wp_json_encode(array('error' => 'custom_mismatch'));
exit();
}
@@ -990,6 +1048,9 @@
));
$notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);
+ if(!is_array($notify_r))
+ $notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
+
$notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
$notify_msg = !empty($notify_r['message']) ? (string)$notify_r['message'] : '';
$notify_body = !empty($notify_r['body']) ? $notify_r['body'] : '';
@@ -1017,7 +1078,7 @@
'message' => $notify_msg,
'body' => $notify_body,
));
- echo json_encode(array('error' => 'notify_proxy_failed'));
+ echo wp_json_encode(array('error' => 'notify_proxy_failed'));
exit();
}
}
@@ -1032,14 +1093,14 @@
's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
));
- echo json_encode(array(
+ echo wp_json_encode(array(
'rtn_url' => $return_url,
'rtn_post' => $return_post,
));
exit();
}
- echo json_encode(array('error' => 'unknown_op'));
+ echo wp_json_encode(array('error' => 'unknown_op'));
exit();
}
}
--- a/s2member/src/includes/classes/paypal-utilities.inc.php
+++ b/s2member/src/includes/classes/paypal-utilities.inc.php
@@ -559,6 +559,11 @@
&& ($ipn_signup_var_item_number = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("item_number", FALSE, $array["mp_id"])))
$_item_number = trim($ipn_signup_var_item_number); // Found w/ a Billing Agreement ID.
+ //260213 Backfill from stored IPN Signup Vars using recurring_payment_id/subscr_id (PayPal may omit item_number on cancellations).
+ else if(is_array($array = $array_or_string) && (!empty($array["recurring_payment_id"]) || !empty($array["subscr_id"]))
+ && ($ipn_signup_var_item_number = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("item_number", FALSE, ((!empty($array["recurring_payment_id"])) ? $array["recurring_payment_id"] : $array["subscr_id"]))))
+ $_item_number = trim($ipn_signup_var_item_number); // Found w/ a Subscription ID.
+
else if(is_string($string = $array_or_string) && !empty($string)) $_item_number = trim($string);
if(!empty($_item_number) && preg_match($GLOBALS["WS_PLUGIN__"]["s2member"]["c"]["membership_item_number_w_or_wo_level_regex"], $_item_number))
@@ -597,6 +602,11 @@
&& ($ipn_signup_var_item_name = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("item_name", FALSE, $array["mp_id"])))
$item_name = trim($ipn_signup_var_item_name); // Found w/ a Billing Agreement ID.
+ //260213 Backfill from stored IPN Signup Vars using recurring_payment_id/subscr_id (PayPal may omit item_name on cancellations).
+ else if(is_array($array = $array_or_string) && (!empty($array["recurring_payment_id"]) || !empty($array["subscr_id"]))
+ && ($ipn_signup_var_item_name = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("item_name", FALSE, ((!empty($array["recurring_payment_id"])) ? $array["recurring_payment_id"] : $array["subscr_id"]))))
+ $item_name = trim($ipn_signup_var_item_name); // Found w/ a Subscription ID.
+
else if(is_string($string = $array_or_string) && !empty($string)) $item_name = trim($string);
return apply_filters("ws_plugin__s2member_paypal_pro_item_name", ((!empty($item_name)) ? $item_name : false), get_defined_vars());
@@ -635,6 +645,11 @@
&& ($ipn_signup_var_period1 = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("period1", FALSE, $array["mp_id"])))
$_period1 = trim($ipn_signup_var_period1); // Found w/ a Billing Agreement ID.
+ //260213 Backfill from stored IPN Signup Vars using recurring_payment_id/subscr_id (PayPal may omit period1 on cancellations).
+ else if(is_array($array = $array_or_string) && (!empty($array["recurring_payment_id"]) || !empty($array["subscr_id"]))
+ && ($ipn_signup_var_period1 = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("period1", FALSE, ((!empty($array["recurring_payment_id"])) ? $array["recurring_payment_id"] : $array["subscr_id"]))))
+ $_period1 = trim($ipn_signup_var_period1); // Found w/ a Subscription ID.
+
else if(is_string($string = $array_or_string) && !empty($string)) $_period1 = trim($string);
if /* Were we able to get a `period1` string? */(!empty($_period1))
@@ -691,6 +706,11 @@
&& ($ipn_signup_var_period3 = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("period3", FALSE, $array["mp_id"])))
$_period3 = trim($ipn_signup_var_period3); // Found w/ a Billing Agreement ID.
+ //260213 Backfill from stored IPN Signup Vars using recurring_payment_id/subscr_id (PayPal may omit period3 on cancellations).
+ else if(is_array($array = $array_or_string) && (!empty($array["recurring_payment_id"]) || !empty($array["subscr_id"]))
+ && ($ipn_signup_var_period3 = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("period3", FALSE, ((!empty($array["recurring_payment_id"])) ? $array["recurring_payment_id"] : $array["subscr_id"]))))
+ $_period3 = trim($ipn_signup_var_period3); // Found w/ a Subscription ID.
+
else if(is_string($string = $array_or_string) && !empty($string)) $_period3 = trim($string);
if /* Were we able to get a `period3` string? */(!empty($_period3))
@@ -846,19 +866,22 @@
$r = c_ws_plugin__s2member_utils_urls::remote($url, $body, $args, true);
+ if(!is_array($r))
+ $r = array('code' => 0, 'message' => 'request_failed', 'headers' => array(), 'body' => '');
+
if(!isset($r['code']) || (int)$r['code'] !== 200)
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'oauth',
- 'event' => 'token_failed',
- 'env' => self::paypal_checkout_is_sandbox() ? 'sandbox' : 'live',
- 'url' => $url,
- 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
- 'message' => !empty($r['message']) ? (string)$r['message'] : '',
- 'body' => !empty($r['body']) ? $r['body'] : '',
+ 'ppco' => 'oauth',
+ 'event' => 'token_failed',
+ 'env_setting' => self::paypal_checkout_is_sandbox() ? 'sandbox' : 'live',
+ 'url' => $url,
+ 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
+ 'message' => !empty($r['message']) ? (string)$r['message'] : '',
+ 'body' => !empty($r['body']) ? $r['body'] : '',
));
$data = array();
- if(!empty($r['body']))
+ if(!empty($r['body']) && is_string($r['body']))
$data = json_decode($r['body'], true);
if(!empty($data['access_token']) && !empty($data['expires_in']))
@@ -898,9 +921,9 @@
$ok = ($token) ? true : false;
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'checkout',
- 'event' => $ok ? 'creds_test_ok' : 'creds_test_failed',
- 'env' => $env,
+ 'ppco' => 'checkout',
+ 'event' => $ok ? 'creds_test_ok' : 'creds_test_failed',
+ 'env_setting' => $env,
));
$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
@@ -972,10 +995,10 @@
delete_transient($transient);
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'checkout',
- 'event' => 'cleared_cache',
- 'env' => $env,
- 'cred_id' => $cred_id,
+ 'ppco' => 'checkout',
+ 'event' => 'cleared_cache',
+ 'env_setting' => $env,
+ 'cred_id' => $cred_id,
));
$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
@@ -1010,17 +1033,24 @@
);
if($body !== null)
- $args['body'] = is_string($body) ? $body : json_encode($body);
+ {
+ $encoded = is_string($body) ? $body : wp_json_encode($body);
+ $args['body'] = ($encoded !== false) ? $encoded : '{}';
+ }
$r = c_ws_plugin__s2member_utils_urls::remote($url, false, $args, true);
+ if(!is_array($r))
+ $r = array('code' => 0, 'message' => 'request_failed', 'headers' => array(), 'body' => '');
+
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'api_request',
- 'method' => $method,
- 'path' => $path,
- 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
- 'message' => !empty($r['message']) ? (string)$r['message'] : '',
- 'body' => !empty($r['body']) ? $r['body'] : '',
+ 'ppco' => 'api_request',
+ 'env_setting' => self::paypal_checkout_is_sandbox() ? 'sandbox' : 'live',
+ 'method' => $method,
+ 'path' => $path,
+ 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
+ 'message' => !empty($r['message']) ? (string)$r['message'] : '',
+ 'body' => !empty($r['body']) ? $r['body'] : '',
));
return $r;
@@ -1110,7 +1140,7 @@
$r = self::paypal_checkout_api_request('POST', '/v2/checkout/orders', $body, $headers);
$data = array();
- if(!empty($r['body']))
+ if(!empty($r['body']) && is_string($r['body']))
$data = json_decode($r['body'], true);
return is_array($data) ? $data : array();
@@ -1174,7 +1204,7 @@
$r = self::paypal_checkout_api_request('POST', '/v2/checkout/orders/'.$order_id.'/capture', (object)array(), $headers);
$data = array();
- if(!empty($r['body']))
+ if(!empty($r['body']) && is_string($r['body']))
$data = json_decode($r['body'], true);
return is_array($data) ? $data : array();
@@ -1226,7 +1256,7 @@
$r = self::paypal_checkout_api_request('POST', '/v1/billing/subscriptions', $body, $headers);
$data = array();
- if(!empty($r['body']))
+ if(!empty($r['body']) && is_string($r['body']))
$data = json_decode($r['body'], true);
return is_array($data) ? $data : array();
@@ -1391,27 +1421,27 @@
);
$headers = array(
- 'PayPal-Request-Id' => 's2m-ppco-plan-'.md5($env.'|'.$plan_key.'|'.md5(json_encode($body))),
+ 'PayPal-Request-Id' => 's2m-ppco-plan-'.md5($env.'|'.$plan_key.'|'.md5((string)wp_json_encode($body))),
);
$r = self::paypal_checkout_api_request('POST', '/v1/billing/plans', $body, $headers);
$data = array();
- if(!empty($r['body']))
+ if(!empty($r['body']) && is_string($r['body']))
$data = json_decode($r['body'], true);
$plan_id = !empty($data['id']) ? (string)$data['id'] : '';
if(!$plan_id)
{
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'plan',
- 'event' => 'plan_create_failed',
- 'env' => $env,
- 'plan_key'=> $plan_key,
- 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
- 'message' => !empty($r['message']) ? (string)$r['message'] : '',
- 'body' => !empty($r['body']) ? (string)$r['body'] : '',
- 'request' => $body,
+ 'ppco' => 'plan',
+ 'event' => 'plan_create_failed',
+ 'env_setting' => $env,
+ 'plan_key' => $plan_key,
+ 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
+ 'message' => !empty($r['message']) ? (string)$r['message'] : '',
+ 'body' => !empty($r['body']) ? (string)$r['body'] : '',
+ 'request' => $body,
));
return '';
}
@@ -1446,12 +1476,12 @@
$GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["paypal_checkout_cache"] = (!empty($options['paypal_checkout_cache']) && is_array($options['paypal_checkout_cache'])) ? $options['paypal_checkout_cache'] : array();
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'plan',
- 'event' => 'plan_cached',
- 'env' => $env,
- 'cred_id' => $cred_id,
- 'plan_key'=> $plan_key,
- 'plan_id' => $plan_id,
+ 'ppco' => 'plan',
+ 'event' => 'plan_cached',
+ 'env_setting' => $env,
+ 'cred_id' => $cred_id,
+ 'plan_key' => $plan_key,
+ 'plan_id' => $plan_id,
));
return $plan_id;
@@ -1507,19 +1537,19 @@
$r = self::paypal_checkout_api_request('POST', '/v1/catalogs/products', $body, $headers);
$data = array();
- if(!empty($r['body']))
+ if(!empty($r['body']) && is_string($r['body']))
$data = json_decode($r['body'], true);
$product_id = !empty($data['id']) ? (string)$data['id'] : '';
if(!$product_id)
{
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'product',
- 'event' => 'product_create_failed',
- 'env' => $env,
- 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
- 'message' => !empty($r['message']) ? (string)$r['message'] : '',
- 'body' => !empty($r['body']) ? (string)$r['body'] : '',
+ 'ppco' => 'product',
+ 'event' => 'product_create_failed',
+ 'env_setting' => $env,
+ 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
+ 'message' => !empty($r['message']) ? (string)$r['message'] : '',
+ 'body' => !empty($r['body']) ? (string)$r['body'] : '',
));
return '';
}
@@ -1554,10 +1584,10 @@
$GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["paypal_checkout_cache"] = (!empty($options['paypal_checkout_cache']) && is_array($options['paypal_checkout_cache'])) ? $options['paypal_checkout_cache'] : array();
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'product',
- 'event' => 'product_cached',
- 'env' => $env,
- 'cred_id' => $cred_id,
+ 'ppco' => 'product',
+ 'event' => 'product_cached',
+ 'env_setting' => $env,
+ 'cred_id' => $cred_id,
'product_key' => $product_key,
'product_id' => $product_id,
));
@@ -1592,10 +1622,6 @@
*/
public static function paypal_checkout_verify_webhook_signature($event, $raw_body, $headers = array())
{
- $webhook_id = self::paypal_checkout_webhook_id();
- if(!$webhook_id)
- return false;
-
$tx_id = !empty($headers['paypal-transmission-id']) ? $headers['paypal-transmission-id'] : '';
$tx_time = !empty($headers['paypal-transmission-time']) ? $headers['paypal-transmission-time'] : '';
$tx_sig = !empty($headers['paypal-transmission-sig']) ? $headers['paypal-transmission-sig'] : '';
@@ -1605,6 +1631,19 @@
if(!$tx_id || !$tx_time || !$tx_sig || !$cert || !$algo)
return false;
+ //260205 Detect sandbox vs live from the cert URL.
+ $orig_sandbox = self::paypal_checkout_is_sandbox();
+ $cert_is_sandbox = (strpos((string)$cert, 'sandbox') !== false);
+
+ $GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $cert_is_sandbox ? '1' : '0';
+
+ $webhook_id = self::paypal_checkout_webhook_id();
+ if(!$webhook_id)
+ {
+ $GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
+ return false;
+ }
+
$body = array(
'transmission_id' => $tx_id,
'transmission_time' => $tx_time,
@@ -1617,9 +1656,20 @@
$r = self::paypal_checkout_api_request('POST', '/v1/notifications/verify-webhook-signature', $body);
if(empty($r['code']) || (int)$r['code'] !== 200 || empty($r['body']))
+ {
+ $GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
+ return false;
+ }
+
+ if(!is_string($r['body']))
+ {
+ $GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
return false;
+ }
$data = json_decode($r['body'], true);
+
+ $GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
return !empty($data['verification_status']) && $data['verification_status'] === 'SUCCESS';
}
@@ -1691,12 +1741,36 @@
self::paypal_checkout_webhook_store_id($existing_id);
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'webhook',
- 'event' => 'updated_webhook',
- 'env' => $env,
- 'id' => $existing_id,
- 'url' => $url,
- 'code' => (int)$r['code'],
+ 'ppco' => 'webhook',
+ 'event' => 'updated_webhook',
+ 'env_setting' => $env,
+ 'id' => $existing_id,
+ 'url' => $url,
+ 'code' => (int)$r['code'],
+ ));
+
+ $GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
+ return array('id' => $existing_id, 'op' => 'updated', 'env' => $env);
+ }
+
+ //260205 PayPal may return 400 when there is no change; treat as success.
+ $no_change = false;
+ if(!empty($r['body']) && is_string($r['body']))
+ {
+ $d = json_decode($r['body'], true);
+ $no_change = !empty($d['name']) && $d['name'] === 'WEBHOOK_PATCH_REQUEST_NO_CHANGE';
+ }
+ if($no_change)
+ {
+ self::paypal_checkout_webhook_store_id($existing_id);
+
+ c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
+ 'ppco' => 'webhook',
+ 'event' => 'updated_webhook_no_change',
+ 'env_setting' => $env,
+ 'id' => $existing_id,
+ 'url' => $url,
+ 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
));
$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
@@ -1704,14 +1778,14 @@
}
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'webhook',
- 'event' => 'update_webhook_failed',
- 'env' => $env,
- 'id' => $existing_id,
- 'url' => $url,
- 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
- 'message' => !empty($r['message']) ? (string)$r['message'] : '',
- 'body' => !empty($r['body']) ? (string)$r['body'] : '',
+ 'ppco' => 'webhook',
+ 'event' => 'update_webhook_failed',
+ 'env_setting' => $env,
+ 'id' => $existing_id,
+ 'url' => $url,
+ 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
+ 'message' => !empty($r['message']) ? (string)$r['message'] : '',
+ 'body' => !empty($r['body']) ? (string)$r['body'] : '',
));
}
@@ -1722,38 +1796,64 @@
$r = self::paypal_checkout_api_request('POST', '/v1/notifications/webhooks', $body);
$id = '';
- if(!empty($r['code']) && (int)$r['code'] === 201 && !empty($r['body']))
+ if(!empty($r['code']) && (int)$r['code'] === 201 && !empty($r['body']) && is_string($r['body']))
{
$data = json_decode($r['body'], true);
if(!empty($data['id']))
$id = (string)$data['id'];
}
+ $adopted_existing = false;
+
+ //260205 If URL already exists, lookup existing webhook by URL and adopt its ID.
+ if(!$id && !empty($r['code']) && (int)$r['code'] === 400 && !empty($r['body']) && is_string($r['body']))
+ {
+ $d = json_decode($r['body'], true);
+ if(!empty($d['name']) && $d['name'] === 'WEBHOOK_URL_ALREADY_EXISTS')
+ {
+ $lr = self::paypal_checkout_api_request('GET', '/v1/notifications/webhooks');
+ if(!empty($lr['code']) && (int)$lr['code'] === 200 && !empty($lr['body']) && is_string($lr['body']))
+ {
+ $ld = json_decode($lr['body'], true);
+ if(!empty($ld['webhooks']) && is_array($ld['webhooks']))
+ {
+ foreach($ld['webhooks'] as $_wh)
+ if(!empty($_wh['url']) && (string)$_wh['url'] === $url && !empty($_wh['id']))
+ {
+ $id = (string)$_wh['id'];
+ $adopted_existing = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
if($id)
{
self::paypal_checkout_webhook_store_id($id);
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'webhook',
- 'event' => 'created_webhook',
- 'env' => $env,
- 'id' => $id,
- 'url' => $url,
- 'code' => (int)$r['code'],
+ 'ppco' => 'webhook',
+ 'event' => $adopted_existing ? 'adopted_webhook' : 'created_webhook',
+ 'env_setting' => $env,
+ 'id' => $id,
+ 'url' => $url,
+ 'code' => $adopted_existing ? 200 : (int)$r['code'],
));
$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
- return array('id' => $id, 'op' => 'created', 'env' => $env);
+ return array('id' => $id, 'op' => $adopted_existing ? 'adopted' : 'created', 'env' => $env);
}
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'webhook',
- 'event' => 'create_webhook_failed',
- 'env' => $env,
- 'url' => $url,
- 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
- 'message' => !empty($r['message']) ? (string)$r['message'] : '',
- 'body' => !empty($r['body']) ? (string)$r['body'] : '',
+ 'ppco' => 'webhook',
+ 'event' => 'create_webhook_failed',
+ 'env_setting' => $env,
+ 'url' => $url,
+ 'code' => !empty($r['code']) ? (int)$r['code'] : 0,
+ 'message' => !empty($r['message']) ? (string)$r['message'] : '',
+ 'body' => !empty($r['body']) ? (string)$r['body'] : '',
));
$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
--- a/s2member/src/includes/classes/paypal-webhook-in.inc.php
+++ b/s2member/src/includes/classes/paypal-webhook-in.inc.php
@@ -38,15 +38,17 @@
if(!empty($_GET['s2member_paypal_webhook_test']) && current_user_can('manage_options')
&& !empty($_GET['_wpnonce']) && wp_verify_nonce((string)$_GET['_wpnonce'], 's2member_ppco_webhook_test'))
{
- $env = (!empty($_GET['ppco_webhook_env']) && $_GET['ppco_webhook_env'] === 'sandbox') ? 'sandbox' : 'live';
+ $env_site = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox() ? 'sandbox' : 'live';
+ $env_webhook = (!empty($_GET['ppco_webhook_env']) && $_GET['ppco_webhook_env'] === 'sandbox') ? 'sandbox' : 'live';
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'webhook',
- 'env' => $env,
- 'event' => 'endpoint_test_ok',
- 'host' => !empty($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : '',
- 'uri' => !empty($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '',
- 'ssl' => is_ssl() ? '1' : '0',
+ 'ppco' => 'webhook',
+ 'env_setting' => $env_site,
+ 'env_webhook' => $env_webhook,
+ 'event' => 'endpoint_test_ok',
+ 'host' => !empty($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : '',
+ 'uri' => !empty($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '',
+ 'ssl' => is_ssl() ? '1' : '0',
));
status_header(200);
@@ -56,7 +58,8 @@
'SUCCESS',
'',
's2Member PayPal Webhook Endpoint (reachability test)',
- 'Environment: '.$env,
+ 'Environment setting: '.$env_site,
+ 'Environment webhook: '.$env_webhook,
'SSL: '.(is_ssl() ? 'yes' : 'no'),
'Host: '.(!empty($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : ''),
'URI: '.(!empty($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : ''),
@@ -78,16 +81,6 @@
$raw_body = file_get_contents('php://input');
$event = json_decode((string)$raw_body, true);
- if(!is_array($event) || empty($event['id']) || empty($event['event_type']))
- {
- c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
- 'ppco' => 'webhook',
- 'event' => 'invalid_payload',
- ));
- status_header(400);
- exit();
- }
-
$headers = array();
if(function_exists('getallheaders'))
foreach((array)getallheaders() as $_k => $_v)
@@ -104,11 +97,35 @@
if(empty($headers[$_key]) && !empty($_SERVER[$_server]))
$headers[$_key] = (string)$_SERVER[$_server];
+ //260206 Detect environment from inbound PayPal cert URL.
+ $cert_url = !empty($headers['paypal-cert-url']) ? (string)$headers['paypal-cert-url'] : '';
+ $env_site = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox() ? 'sandbox' : 'live';
+
+ $cert_host = $cert_url ? (string)parse_url($cert_url, PHP_URL_HOST) : '';
+ $env_webhook = 'unknown';
+
+ if($cert_host && preg_match('/(^|.)paypal.com$/i', $cert_host))
+ $env_webhook = (stripos($cert_host, 'sandbox') !== false || strpos($cert_url, 'sandbox') !== false) ? 'sandbox' : 'live';
+
+ if(!is_array($event) || empty($event['id']) || empty($event['event_type']))
+ {
+ c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
+ 'ppco' => 'webhook',
+ 'env_setting' => $env_site,
+ 'env_webhook' => $env_webhook,
+ 'event' => 'invalid_payload',
+ ));
+ status_header(400);
+ exit();
+ }
+
$verified = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_verify_webhook_signature($event, $raw_body, $headers);
if(!$verified)
{
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
'ppco' => 'webhook',
+ 'env_setting'=> $env_site,
+ 'env_webhook'=> $env_webhook,
'event' => 'signature_failed',
'event_id' => (string)$event['id'],
'event_type' => (string)$event['event_type'],
@@ -130,6 +147,8 @@
{
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
'ppco' => 'webhook',
+ 'env_setting'=> $env_site,
+ 'env_webhook'=> $env_webhook,
'event' => 'duplicate_event',
'action' => 'ignored',
'note' => 'Duplicate webhook delivery (event_id already processed).',
@@ -168,6 +187,8 @@
// Ignore other BILLING.SUBSCRIPTION.* events for MVP.
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
'ppco' => 'webhook',
+ 'env_setting'=> $env_site,
+ 'env_webhook'=> $env_webhook,
'event' => 'ignored',
'event_id' => $event_id,
'event_type' => $event_type,
@@ -186,6 +207,28 @@
// Best-effort payer email for logs/fallback logic.
if(!empty($resource['subscriber']['email_address']))
$paypal['payer_email'] = (string)$resource['subscriber']['email_address'];
+
+ // Enrich lifecycle events with stored signup vars so legacy notify handlers can match and set EOT properly.
+ //!!! TO-DO: Deduplicate signup-vars enrichment logic (also used in PayPal Checkout proxy confirm flow).
+ if(!empty($paypal['txn_type']) && $subscr_id
+ && in_array($paypal['txn_type'], array('subscr_cancel', 'subscr_eot', 'subscr_failed', 'recurring_payment_suspended_due_to_max_failed_payment'), true)
+ && ($user_id = c_ws_plugin__s2member_utils_users::get_user_id_with($subscr_id))
+ && is_array($ipn_signup_vars = get_user_option('s2member_ipn_signup_vars', $user_id))
+ && !empty($ipn_signup_vars['subscr_id']) && (string)$ipn_signup_vars['subscr_id'] === (string)$subscr_id
+ )
+ {
+ if(empty($paypal['item_number']) && !empty($ipn_signup_vars['item_number']))
+ $paypal['item_number'] = (string)$ipn_signup_vars['item_number'];
+
+ if(empty($paypal['item_name']) && !empty($ipn_signup_vars['item_name']))
+ $paypal['item_name'] = (string)$ipn_signup_vars['item_name'];
+
+ if(empty($paypal['period1']) && !empty($ipn_signup_vars['period1']))
+ $paypal['period1'] = (string)$ipn_signup_vars['period1'];
+
+ if(empty($paypal['period3']) && !empty($ipn_signup_vars['period3']))
+ $paypal['period3'] = (string)$ipn_signup_vars['period3'];
+ }
}
// Recurring payment events (PayPal often emits PAYMENT.SALE.COMPLETED for subscription payments).
@@ -233,6 +276,8 @@
// Ignore for MVP.
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
'ppco' => 'webhook',
+ 'env_setting'=> $env_site,
+ 'env_webhook'=> $env_webhook,
'event' => 'ignored',
'event_id' => $event_id,
'event_type' => $event_type,
@@ -255,6 +300,8 @@
{
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
'ppco' => 'webhook',
+ 'env_setting'=> $env_site,
+ 'env_webhook'=> $env_webhook,
'event' => 'duplicate_txn',
'action' => 'ignored',
'note' => 'Duplicate webhook delivery (txn_id already processed).',
@@ -281,16 +328,21 @@
'timeout' => 20,
), true);
+ if(!is_array($r))
+ $r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
+
$code = !empty($r['code']) ? (int)$r['code'] : 0;
if($code >= 200 && $code <= 299)
{
- set_transient($event_id_transient, time(), 315569260);
+ set_transient($event_id_transient, time(), 31556952);
if($txn_transient)
- set_transient($txn_transient, time(), 315569260);
+ set_transient($txn_transient, time(), 31556952);
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
'ppco' => 'webhook',
+ 'env_setting'=> $env_site,
+ 'env_webhook'=> $env_webhook,
'event' => 'notify_proxy_response',
'event_id' => $event_id,
'event_type' => $event_type,
@@ -304,6 +356,8 @@
else
c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
'ppco' => 'webhook',
+ 'env_setting'=> $env_site,
+ 'env_webhook'=> $env_webhook,
'event' => 'notify_proxy_failed',
'event_id' => $event_id,
'event_type' => $event_type,
--- a/s2member/src/includes/classes/registrations.inc.php
+++ b/s2member/src/includes/classes/registrations.inc.php
@@ -73,6 +73,9 @@
*/
public static function generate_password($password = '')
{
+ if (did_action('retrieve_password') || did_action('login_form_rp') || did_action('login_form_resetpass')) //260214
+ return $password;
+
static $did_generate_password = false; // Once only.
foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
@@ -81,7 +84,7 @@
$ci = $GLOBALS['WS_PLUGIN__']['s2member']['o']['ruris_case_sensitive'] ? '' : 'i';
- if(!$did_generate_password && !is_admin() && (preg_match('//wp-login.php/'.$ci, $_SERVER['REQUEST_URI']) || (c_ws_plugin__s2member_utils_conds::bp_is_installed() && bp_is_register_page())))
+ if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_reg_password']) && !$did_generate_password && !is_admin() && ((preg_match('//wp-login.php/'.$ci, $_SERVER['REQUEST_URI']) && did_action('login_form_register')) || (c_ws_plugin__s2member_utils_conds::bp_is_installed() && bp_is_register_page()))) //260214
{
$GLOBALS['ws_plugin__s2member_custom_wp_login_bp_password'] = false; // Initialize.
--- a/s2member/src/includes/classes/sc-paypal-button-in.inc.php
+++ b/s2member/src/includes/classes/sc-paypal-button-in.inc.php
@@ -69,6 +69,9 @@
$force_notify_url_scheme = apply_filters("ws_plugin__s2member_during_sc_paypal_button_force_notify_url_scheme", null, get_defined_vars ());
$force_return_url_scheme = apply_filters("ws_plugin__s2member_during_sc_paypal_button_force_return_url_scheme", null, get_defined_vars ());
+ // PayPal Checkout SDK memoization (per request; shared across all button variants).
+ static $ppco_sdks = array();
+
foreach(array_keys(get_defined_vars())as$__v)$__refs[$__v]=&$$__v;
do_action("ws_plugin__s2member_before_sc_paypal_button_after_shortcode_atts", get_defined_vars ());
unset($__refs, $__v);
@@ -79,16 +82,17 @@
// - output="button": on-site cancel via REST API (logged-in users only).
// - output="anchor|url": link to PayPal subscription management UI (sandbox/live aware).
// Falls back to legacy PayPal cancellation flow when user is not logged in or has no subscription id.
- if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled() && in_array($attr["output"], array("button", "anchor", "url"), true) && is_user_logged_in() && get_user_option('s2member_subscr_id', (int)get_current_user_id()))
+ if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled() && in_array($attr["output"], array("button", "anchor", "url"), true) && is_user_logged_in())
{
$user_id = (int)get_current_user_id();
$subscr_id = (string)get_user_option('s2member_subscr_id', $user_id);
- if($subscr_id)
- {
- $ppco_sandbox = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
- $pp_manage_url = ($ppco_sandbox) ? 'https://www.sandbox.paypal.com/myaccount/autopay/connect/' : 'https://www.paypal.com/myaccount/autopay/connect/';
+ $ppco_sandbox = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
+ $pp_manage_url = ($ppco_sandbox) ? 'https://www.sandbox.paypal.com/myaccount/autopay/connect/' : 'https://www.paypal.com/myaccount/autopay/connect/';
+ // output="url|anchor": always link to PayPal subscription management UI.
+ if(in_array($attr["output"], array("url", "anchor"), true))
+ {
// output="url": return the PayPal management URL.
if($attr["output"] === "url")
{
@@ -116,6 +120,26 @@
$code = c_ws_plugin__s2member_sc_paypal_button_e::sc_paypal_button_encryption ($code, get_defined_vars ());
return apply_filters("ws_plugin__s2member_sc_paypal_button", $code, get_defined_vars ());
}
+ }
+
+ if($subscr_id)
+ {
+
+ // output="button": if Paid Subscr. ID is not a subscription id, fall back to PayPal management UI.
+ if($attr["output"] === "button" && !preg_match('/^I-[A-Z0-9]+$/', $subscr_id))
+ {
+ $default_image = "https://www.paypal.com/" . (($attr["lang"]) ? $attr["lang"] : _x ("en_US", "s2member-front paypal-button-lang-code", "s2member")) . "/i/btn/btn_unsubscribe_LG.gif";
+ $img_src = ($attr["image"] && $attr["image"] !== "default") ? $attr["image"] : $default_image;
+
+ $code = $_code = '<a href="'.esc_attr($pp_manage_url).'" target="_blank" rel="nofollow noopener"><img src="'.esc_attr($img_src).'" style="width:auto; height:auto; border:0;" alt="PayPal" /></a>';
+
+ foreach(array_keys(get_defined_vars())as$__v)$__refs[$__v]=&$$__v;
+ do_action("ws_plugin__s2member_during_sc_paypal_cancellation_button", get_defined_vars ());
+ unset($__refs, $__v);
+
+ $code = c_ws_plugin__s2member_sc_paypal_button_e::sc_paypal_button_encryption ($code, get_defined_vars ());
+ return apply_filters("ws_plugin__s2member_sc_paypal_button", $code, get_defined_vars ());
+ }
// output="button": REST cancel (modern-looking HTML button).
$ppco_endpoint = home_url("/?s2member_paypal_checkout=1", $force_notify_url_scheme);
@@ -235,8 +259,6 @@
// Uses server-side create + server-side capture, then posts into existing IPN + Return handlers.
if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
{
- static $ppco_sdks = array();
-
$ppco_sandbox = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
$ppco_client_id = (string)$GLOBALS["WS_PLUGIN__"]["s2member"]["o"][($ppco_sandbox) ? "paypal_checkout_sandbox_client_id" : "paypal_checkout_client_id"];
@@ -341,6 +363,7 @@
if($ppco_sdk_just_loaded)
{
+ $ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
$code .= '<script id="'.esc_attr($ppco_sdk_id).'" data-namespace="'.esc_attr($ppco_sdk_ns).'" src="'.esc_attr($ppco_sdk_src).'"></script>'."n";
}
@@ -447,8 +470,6 @@
// Uses server-side create + server-side capture, then posts into existing IPN + Return handlers.
if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
{
- static $ppco_sdks = array();
-
$ppco_sandbox = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
$ppco_client_id = (string)$GLOBALS["WS_PLUGIN__"]["s2member"]["o"][($ppco_sandbox) ? "paypal_checkout_sandbox_client_id" : "paypal_checkout_client_id"];
@@ -553,6 +574,7 @@
if($ppco_sdk_just_loaded)
{
+ $ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
$code .= '<script id="'.esc_attr($ppco_sdk_id).'" data-namespace="'.esc_attr($ppco_sdk_ns).'" src="'.esc_attr($ppco_sdk_src).'"></script>'."n";
}
@@ -673,8 +695,6 @@
// Uses server-side create + server-side capture/confirm, then posts into existing IPN + Return handlers.
if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
{
- static $ppco_sdks = array();
-
$ppco_sandbox = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
$ppco_client_id = (string)$GLOBALS["WS_PLUGIN__"]["s2member"]["o"][($ppco_sandbox) ? "paypal_checkout_sandbox_client_id" : "paypal_checkout_client_id"];
@@ -787,6 +807,7 @@
if($ppco_sdk_just_loaded)
{
+ $ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
$code .= '<script id="'.esc_attr($ppco_sdk_id).'" data-namespace="'.esc_attr($ppco_sdk_ns).'" src="'.esc_attr($ppco_sdk_src).'"></script>'."n";
}
--- a/s2member/src/includes/classes/utils-gets.inc.php
+++ b/s2member/src/includes/classes/utils-gets.inc.php
@@ -234,7 +234,7 @@
if(!is_object($user) || empty($user->ID)) // No ``$user`` object? Maybe not logged-in?.
$singular_ids[] = (int)$r->post_id; // It's NOT available. There is no ``$user``.
- else if(is_array($ccaps = @unserialize($r->meta_value))) // Make sure we unserialize.
+ else if(is_array($ccaps = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($r->meta_value))) // Make sure we unserialize.
{
foreach($ccaps as $ccap) // Test for Custom Capability Restrictions now.
if(strlen($ccap) && !$user->has_cap('access_s2member_ccap_'.$ccap))
--- a/s2member/src/includes/classes/utils-logs.inc.php
+++ b/s2member/src/includes/classes/utils-logs.inc.php
@@ -220,6 +220,7 @@
'/paypal-api/' => array('short' => 'PayPal API communication.', 'long' => 'This log file records all communication between s2Member and PayPal APIs. Such as PayPal Button Encryption and PayPal Pro API calls that process transactions. This log file may be used (in some scenarios), even if you're running a PayPal Payments Pro (Payflow Edition) account. See also: gateway-core-ipn.log (s2Member's core processor).'),
'/paypal-payflow-api/' => array('short' => 'PayPal Pro (PayFlow Edition) API communication.', 'long' => 'This log file records all communication between s2Member and the PayPal Payments Pro (PayFlow Edition) APIs. This log file is only used if you operate a PayPal Payments Pro (PayFlow Edition) account; i.e., only if you integrate s2Member Pro with Payflow for Recurring Billing. See also: gateway-core-ipn.log (s2Member's core processor).'),
+ '/paypal-checkout/' => array('short' => 'PayPal Checkout (REST/JS SDK) communication.', 'long' => 'This log file records communication related to s2Member's PayPal Checkout integration (REST API requests, JS SDK/checkout flow diagnostics, and webhook/notify proxy details). See also: gateway-core-ipn.log (s2Member's core processor).'),
'/authnet-api/' => array('short' => 'Authorize.Net API communication.', 'long' => 'This log file records all communication between s2Member and Authorize.Net APIs (for both AIM and ARB integrations). See also: gateway-core-ipn.log (s2Member's core processor).'),
'/authnet-arb/' => array('short' => 'Authorize.Net ARB Subscription status checks.', 'long' => 'This log file records s2Member's Authorize.Net ARB Subscription status checks. s2Member polls the ARB service periodically to check the status of existing Members (e.g., to see if billing is still active or not).'),
--- a/s2member/src/includes/classes/utils-users.inc.php
+++ b/s2member/src/includes/classes/utils-users.inc.php
@@ -183,6 +183,10 @@
}
}
//250426 Fall back to a makeshift ipn_signup_vars array.
+ //!!! TO-DO: Gateway enrichment for missing term details (period1/period3) when signup vars are missing.
+ // PayPal Checkout subs: use REST Subscriptions API.
+ // Legacy PayPal recurring profiles: use legacy NVP/Payflow APIs.
+ // Do not guess term values in fallback vars (risk of incorrect EOT decisions).
if (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['ipn_signup_vars_fallback'])) {
$userdata = get_userdata((int)$user_id);
$ipn_signup_vars = array(
@@ -478,7 +482,52 @@
if($check_gateway) switch($subscr_gateway) // A bit different for each payment gateway.
{
- case 'paypal': // PayPal (PayPal Pro only).
+ case 'paypal': // PayPal (legacy Pro NVP/Payflow + PayPal Checkout REST).
+
+ //260213 PayPal Checkout subscriptions: use REST Subscriptions API for reconciliation (NVP GetRecurringPaymentsProfileDetails returns 11592).
+ if(!empty($ipn_signup_vars['s2member_paypal_proxy_use']) && $ipn_signup_vars['s2member_paypal_proxy_use'] === 'paypal_checkout')
+ {
+ $ppco_enabled = !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_enable']) && (string)$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_enable'] !== '0';
+
+ $ppco_creds = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_creds();
+
+ if(!$ppco_enabled || empty($ppco_creds['client_id']) || empty($ppco_creds['secret']))
+ return array_merge($empty_response, array(
+ 'debug' => 'PayPal Checkout subscription reconciliation skipped (disabled or missing REST API credentials).',
+ ));
+
+ $subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscr_id));
+
+ $subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0;
+ $subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '';
+
+ $subscription = array();
+ if($subscription_body)
+ $subscription = json_decode($subscription_body, true);
+
+ if(!is_array($subscription))
+ $subscription = array();
+
+ if(!($subscription_code >= 200 && $subscription_code <= 299 && !empty($subscription['id'])))
+ return array_merge($empty_response, array(
+ 'debug' => 'PayPal Checkout subscription reconciliation failed (REST subscription lookup unsuccessful); skipping legacy PayPal status checks.',
+ ));
+
+ $status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : '';
+ $next = !empty($subscription['billing_info']['next_billing_time']) ? (string)$subscription['billing_info']['next_billing_time'] : '';
+
+ if($status && $status !== 'ACTIVE')
+ return array('type' => 'fixed', 'time' => $auto_eot_time, 'tense' => $auto_eot_time <= $now ? 'past' : 'future',
+ 'debug' => 'This is the estimated EOT time. PayPal Checkout says this subscription is no longer active, and thus, access should be terminated at this time.');
+
+ if($next && ($time = strtotime($next)) > $now)
+ return array('type' => 'next', 'time' => $time, 'tense' => $time <= $now ? 'past' : 'future',
+ 'debug' => 'PayPal Checkout says this is the next payment time.');
+
+ return array_merge($empty_response, array(
+ 'debug' => 'PayPal Checkout says this subscription is active; no next billing time was returned.',
+ ));
+ }
if(!c_ws_plugin__s2member_utils_conds::pro_is_installed()
|| !class_exists('c_ws_plugin__s2member_pro_paypal_utilities')
--- a/s2member/src/includes/menu-pages/paypal-ops.inc.php
+++ b/s2member/src/includes/menu-pages/paypal-ops.inc.php
@@ -34,28 +34,12 @@
$ppco_creds_notice = '';
$ppco_cache_notice = '';
- $ppco_flash_key = 's2member_ppco_notice_' . get_current_user_id();
-
- if(empty($_GET['s2member_ppco_webhook']) && empty($_GET['s2member_ppco_creds_test']) && empty($_GET['s2member_ppco_clear_cache']))
- if(($ppco_flash = get_transient($ppco_flash_key)))
- {
- if(!empty($ppco_flash['webhook']))
- $ppco_webhook_notice = (string)$ppco_flash['webhook'];
-
- if(!empty($ppco_flash['creds']))
- $ppco_creds_notice = (string)$ppco_flash['creds'];
-
- if(!empty($ppco_flash['cache']))
- $ppco_cache_notice = (string)$ppco_flash['cache'];
-
- delete_transient($ppco_flash_key);
- }
-
- if(!empty($_GET['s2member_ppco_webhook']) && empty($_GET['s2member_ppco_creds_test']) && empty($_GET['s2member_ppco_clear_cache']) && current_user_can('manage_options'))
+ $ppco_r = !empty($_POST) ? $_POST : $_GET;
+ if(!empty($ppco_r['s2member_ppco_webhook']) && empty($ppco_r['s2member_ppco_creds_test']) && empty($ppco_r['s2member_ppco_clear_cache']) && current_user_can('manage_options'))
{
- if(!empty($_GET['_wpnonce']) && wp_verify_nonce((string)$_GET['_wpnonce'], 's2member_ppco_webhook'))
+ if(!empty($ppco_r['_wpnonce']) && wp_verify_nonce((string)$ppco_r[