| 1 |
<?php |
| 2 |
/** |
| 3 |
* Stripe Helper functions for SureForms Payments. |
| 4 |
* |
| 5 |
* @package sureforms |
| 6 |
* @since 2.0.0 |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace SRFM\Inc\Payments\Stripe; |
| 10 |
|
| 11 |
use SRFM\Inc\Database\Tables\Payments; |
| 12 |
use SRFM\Inc\Payments\Payment_Helper; |
| 13 |
use SRFM_Pro\Admin\Licensing; |
| 14 |
|
| 15 |
if ( ! defined( 'ABSPATH' ) ) { |
| 16 |
exit; |
| 17 |
} |
| 18 |
|
| 19 |
/** |
| 20 |
* Stripe Helper functions for SureForms Payments. |
| 21 |
* |
| 22 |
* @since 2.0.0 |
| 23 |
*/ |
| 24 |
class Stripe_Helper { |
| 25 |
/** |
| 26 |
* Static cache for webhook verification results during the same request. |
| 27 |
* |
| 28 |
* @since 2.0.0 |
| 29 |
* @var array<string, bool> |
| 30 |
*/ |
| 31 |
private static $webhook_verification_cache = []; |
| 32 |
|
| 33 |
/** |
| 34 |
* Check if Stripe is connected. |
| 35 |
* |
| 36 |
* @since 2.0.0 |
| 37 |
* @return bool True if Stripe is connected, false otherwise. |
| 38 |
*/ |
| 39 |
public static function is_stripe_connected() { |
| 40 |
$payment_settings = self::get_all_stripe_settings(); |
| 41 |
return is_array( $payment_settings ) && isset( $payment_settings['stripe_connected'] ) && is_bool( $payment_settings['stripe_connected'] ) ? $payment_settings['stripe_connected'] : false; |
| 42 |
} |
| 43 |
|
| 44 |
/** |
| 45 |
* Get the current Stripe mode (test or live). |
| 46 |
* |
| 47 |
* @since 2.0.0 |
| 48 |
* @return string The current payment mode ('test' or 'live'). |
| 49 |
*/ |
| 50 |
public static function get_stripe_mode() { |
| 51 |
return Payment_Helper::get_payment_mode(); |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* Check if webhook is configured. |
| 56 |
* |
| 57 |
* Checks if webhooks are properly configured based on the current payment mode. |
| 58 |
* Can optionally verify the webhook connection with Stripe API. |
| 59 |
* |
| 60 |
* @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode. |
| 61 |
* @param bool $verify Whether to verify with Stripe API. Default false (checks local settings only). |
| 62 |
* @since 2.0.0 |
| 63 |
* @return bool True if webhook is configured, false otherwise. |
| 64 |
*/ |
| 65 |
public static function is_webhook_configured( $mode = null, $verify = false ) { |
| 66 |
// Get current payment mode. |
| 67 |
$payment_mode = is_string( $mode ) && in_array( $mode, [ 'test', 'live' ], true ) ? $mode : self::get_stripe_mode(); |
| 68 |
|
| 69 |
// Get webhook settings. |
| 70 |
$payment_settings = self::get_all_stripe_settings(); |
| 71 |
|
| 72 |
if ( ! is_array( $payment_settings ) ) { |
| 73 |
return false; |
| 74 |
} |
| 75 |
|
| 76 |
// Check webhook secret exists based on mode. |
| 77 |
$webhook_secret_key = 'webhook_' . $payment_mode . '_secret'; |
| 78 |
$has_secret = ! empty( $payment_settings[ $webhook_secret_key ] ); |
| 79 |
|
| 80 |
// If no secret found, webhook is not configured. |
| 81 |
if ( ! $has_secret ) { |
| 82 |
return false; |
| 83 |
} |
| 84 |
|
| 85 |
// If verification is not requested, return true (secret exists). |
| 86 |
if ( ! $verify ) { |
| 87 |
return true; |
| 88 |
} |
| 89 |
|
| 90 |
// Verify with Stripe API (returns boolean). |
| 91 |
return self::verify_webhook_connection( $payment_mode ); |
| 92 |
} |
| 93 |
|
| 94 |
/** |
| 95 |
* Get Stripe secret key for the specified mode. |
| 96 |
* |
| 97 |
* @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode. |
| 98 |
* @since 2.0.0 |
| 99 |
* @return string The secret key for the specified mode, or empty string if not found. |
| 100 |
*/ |
| 101 |
public static function get_stripe_secret_key( $mode = null ) { |
| 102 |
$payment_settings = self::get_all_stripe_settings(); |
| 103 |
|
| 104 |
if ( null === $mode ) { |
| 105 |
$mode = self::get_stripe_mode(); |
| 106 |
} |
| 107 |
|
| 108 |
return is_array( $payment_settings ) && isset( $payment_settings[ 'stripe_' . $mode . '_secret_key' ] ) && is_string( $payment_settings[ 'stripe_' . $mode . '_secret_key' ] ) ? $payment_settings[ 'stripe_' . $mode . '_secret_key' ] : ''; |
| 109 |
} |
| 110 |
|
| 111 |
/** |
| 112 |
* Get Stripe publishable key for the specified mode. |
| 113 |
* |
| 114 |
* @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode. |
| 115 |
* @since 2.0.0 |
| 116 |
* @return string The publishable key for the specified mode, or empty string if not found. |
| 117 |
*/ |
| 118 |
public static function get_stripe_publishable_key( $mode = null ) { |
| 119 |
if ( null === $mode ) { |
| 120 |
$mode = self::get_stripe_mode(); |
| 121 |
} |
| 122 |
|
| 123 |
$payment_settings = self::get_all_stripe_settings(); |
| 124 |
|
| 125 |
return is_array( $payment_settings ) && isset( $payment_settings[ 'stripe_' . $mode . '_publishable_key' ] ) && is_string( $payment_settings[ 'stripe_' . $mode . '_publishable_key' ] ) ? $payment_settings[ 'stripe_' . $mode . '_publishable_key' ] : ''; |
| 126 |
} |
| 127 |
|
| 128 |
/** |
| 129 |
* Get the default currency from payment settings. |
| 130 |
* |
| 131 |
* @since 2.0.0 |
| 132 |
* @return string The currency code (e.g., 'USD'). |
| 133 |
*/ |
| 134 |
public static function get_currency() { |
| 135 |
return Payment_Helper::get_currency(); |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* Get the Stripe settings page URL. |
| 140 |
* |
| 141 |
* This returns the URL to the SureForms Stripe settings page in the admin. |
| 142 |
* As of now, the URL is: |
| 143 |
* http://localhost:10008/wp-admin/admin.php?page=sureforms_form_settings&tab=payments-settings&subpage=payment-methods&gateway=stripe |
| 144 |
* The site URL is dynamic and will adapt to the current WordPress installation. |
| 145 |
* |
| 146 |
* @since 2.0.0 |
| 147 |
* @return string The URL to the Stripe settings page. |
| 148 |
*/ |
| 149 |
public static function get_stripe_settings_url() { |
| 150 |
return admin_url( 'admin.php?page=sureforms_form_settings&tab=payments-settings&subpage=payment-methods&gateway=stripe' ); |
| 151 |
} |
| 152 |
|
| 153 |
/** |
| 154 |
* Make a request to the Stripe API. |
| 155 |
* |
| 156 |
* @param string $endpoint The API endpoint to call. |
| 157 |
* @param string $method The HTTP method (GET, POST, PUT, PATCH, DELETE). Default 'POST'. |
| 158 |
* @param array<mixed> $data The data to send with the request. Default empty array. |
| 159 |
* @param string $resource_id The resource ID to append to the endpoint. Default empty string. |
| 160 |
* @param array<mixed> $extra_args Additional arguments to pass to the request. Default empty array. |
| 161 |
* @since 2.0.0 |
| 162 |
* @return array<mixed> Response array with 'success' boolean and either 'data' or 'error' key. |
| 163 |
*/ |
| 164 |
public static function stripe_api_request( $endpoint, $method = 'POST', $data = [], $resource_id = '', $extra_args = [] ) { |
| 165 |
if ( ! self::is_stripe_connected() ) { |
| 166 |
return [ |
| 167 |
'success' => false, |
| 168 |
'error' => [ |
| 169 |
'code' => 'stripe_not_connected', |
| 170 |
'message' => __( 'Stripe is not connected.', 'sureforms' ), |
| 171 |
'type' => 'auth', |
| 172 |
'raw_response' => null, |
| 173 |
], |
| 174 |
]; |
| 175 |
} |
| 176 |
|
| 177 |
$payment_mode = (string) self::get_stripe_mode(); |
| 178 |
|
| 179 |
if ( ! empty( $extra_args ) && is_array( $extra_args ) ) { |
| 180 |
$payment_mode = isset( $extra_args['mode'] ) && is_string( $extra_args['mode'] ) && in_array( $extra_args['mode'], [ 'test', 'live' ], true ) ? $extra_args['mode'] : $payment_mode; |
| 181 |
} |
| 182 |
|
| 183 |
$secret_key = (string) self::get_stripe_secret_key( $payment_mode ); |
| 184 |
|
| 185 |
if ( empty( $secret_key ) ) { |
| 186 |
return [ |
| 187 |
'success' => false, |
| 188 |
'error' => [ |
| 189 |
'code' => 'missing_secret_key', |
| 190 |
'message' => sprintf( |
| 191 |
/* translators: %s: payment mode (test/live) */ |
| 192 |
__( 'Stripe %s secret key is missing.', 'sureforms' ), |
| 193 |
$payment_mode |
| 194 |
), |
| 195 |
'type' => 'auth', |
| 196 |
'raw_response' => null, |
| 197 |
], |
| 198 |
]; |
| 199 |
} |
| 200 |
|
| 201 |
$url = 'https://api.stripe.com/v1/' . $endpoint; |
| 202 |
if ( ! empty( $resource_id ) ) { |
| 203 |
$url .= '/' . $resource_id; |
| 204 |
} |
| 205 |
|
| 206 |
$headers = [ |
| 207 |
'Authorization' => 'Bearer ' . $secret_key, |
| 208 |
'Content-Type' => 'application/x-www-form-urlencoded', |
| 209 |
]; |
| 210 |
|
| 211 |
$args = [ |
| 212 |
'method' => $method, |
| 213 |
'headers' => $headers, |
| 214 |
'timeout' => 30, |
| 215 |
]; |
| 216 |
|
| 217 |
if ( ! empty( $data ) && in_array( $method, [ 'POST', 'PUT', 'PATCH' ], true ) ) { |
| 218 |
$args['body'] = http_build_query( self::flatten_stripe_data( $data ) ); |
| 219 |
} elseif ( ! empty( $data ) && 'GET' === $method ) { |
| 220 |
$url .= '?' . http_build_query( self::flatten_stripe_data( $data ) ); |
| 221 |
} |
| 222 |
|
| 223 |
$response = wp_remote_request( $url, $args ); |
| 224 |
|
| 225 |
if ( is_wp_error( $response ) ) { |
| 226 |
$error_message = $response->get_error_message(); |
| 227 |
return [ |
| 228 |
'success' => false, |
| 229 |
'error' => [ |
| 230 |
'code' => $response->get_error_code(), |
| 231 |
'message' => sprintf( |
| 232 |
/* translators: %s: network error message */ |
| 233 |
__( 'Network error: %s', 'sureforms' ), |
| 234 |
$error_message |
| 235 |
), |
| 236 |
'type' => 'network', |
| 237 |
'raw_response' => $response, |
| 238 |
], |
| 239 |
]; |
| 240 |
} |
| 241 |
|
| 242 |
$body = wp_remote_retrieve_body( $response ); |
| 243 |
$code = wp_remote_retrieve_response_code( $response ); |
| 244 |
|
| 245 |
// Try to decode the response body. |
| 246 |
$decoded_body = json_decode( $body, true ); |
| 247 |
if ( json_last_error() !== JSON_ERROR_NONE ) { |
| 248 |
return [ |
| 249 |
'success' => false, |
| 250 |
'error' => [ |
| 251 |
'code' => 'invalid_response', |
| 252 |
'message' => __( 'Invalid response format from Stripe API.', 'sureforms' ), |
| 253 |
'type' => 'invalid_response', |
| 254 |
'raw_response' => $body, |
| 255 |
], |
| 256 |
]; |
| 257 |
} |
| 258 |
|
| 259 |
if ( $code >= 400 ) { |
| 260 |
$stripe_error = is_array( $decoded_body ) && isset( $decoded_body['error'] ) && is_array( $decoded_body['error'] ) ? $decoded_body['error'] : []; |
| 261 |
$error_code = isset( $stripe_error['code'] ) ? (string) $stripe_error['code'] : 'unknown_error'; |
| 262 |
$error_message = isset( $stripe_error['message'] ) ? (string) $stripe_error['message'] : 'Unknown Stripe API error'; |
| 263 |
$error_type = isset( $stripe_error['type'] ) ? (string) $stripe_error['type'] : 'api_error'; |
| 264 |
|
| 265 |
return [ |
| 266 |
'success' => false, |
| 267 |
'error' => [ |
| 268 |
'code' => $error_code, |
| 269 |
'message' => $error_message, |
| 270 |
'type' => 'stripe_api', |
| 271 |
'stripe_error_type' => $error_type, |
| 272 |
'http_status' => $code, |
| 273 |
'raw_response' => $decoded_body, |
| 274 |
], |
| 275 |
]; |
| 276 |
} |
| 277 |
|
| 278 |
// Success case - return the decoded response with success indicator. |
| 279 |
return [ |
| 280 |
'success' => true, |
| 281 |
'data' => $decoded_body, |
| 282 |
]; |
| 283 |
} |
| 284 |
|
| 285 |
/** |
| 286 |
* Retrieve the middleware base URL for Stripe API communication. |
| 287 |
* |
| 288 |
* By default, returns the production middleware URL that securely proxies requests |
| 289 |
* between the plugin and Stripe's API. |
| 290 |
* |
| 291 |
* Developers working in local or staging environments can override the SRFM_MIDDLEWARE_BASE_URL |
| 292 |
* constant (for example, set it to "http://sureforms-payments-middleware.test") to point |
| 293 |
* to a locally running payments middleware app (e.g., http://sureforms-payments-middleware.test/payments/stripe/). |
| 294 |
* |
| 295 |
* You can also modify the return value or use a filter hook to customize the URL as needed |
| 296 |
* for testing, debugging, or customizing payment flows during development. |
| 297 |
* |
| 298 |
* @since 2.0.0 |
| 299 |
* @return string The middleware base URL. |
| 300 |
*/ |
| 301 |
public static function middle_ware_base_url() { |
| 302 |
return SRFM_MIDDLEWARE_BASE_URL . 'payments/stripe/'; |
| 303 |
} |
| 304 |
|
| 305 |
/** |
| 306 |
* Get currency symbol. |
| 307 |
* |
| 308 |
* @param string $currency Currency code. |
| 309 |
* @return string |
| 310 |
* @since 2.0.0 |
| 311 |
*/ |
| 312 |
public static function get_currency_symbol( $currency ) { |
| 313 |
return Payment_Helper::get_currency_symbol( $currency ); |
| 314 |
} |
| 315 |
|
| 316 |
/** |
| 317 |
* Check if currency is zero-decimal. |
| 318 |
* |
| 319 |
* @param string $currency Currency code. |
| 320 |
* @since 2.0.0 |
| 321 |
* @return bool True if zero-decimal currency. |
| 322 |
*/ |
| 323 |
public static function is_zero_decimal_currency( $currency ) { |
| 324 |
return Payment_Helper::is_zero_decimal_currency( $currency ); |
| 325 |
} |
| 326 |
|
| 327 |
/** |
| 328 |
* Convert amount to Stripe's smallest currency unit. |
| 329 |
* |
| 330 |
* For two-decimal currencies (USD, EUR, etc.): multiplies by 100 |
| 331 |
* For zero-decimal currencies (JPY, KRW, etc.): returns as-is |
| 332 |
* |
| 333 |
* @param float|string|int $amount Amount in major currency unit (can contain commas). |
| 334 |
* @param string $currency Currency code. |
| 335 |
* @since 2.0.0 |
| 336 |
* @return int Amount in smallest currency unit (cents for 2-decimal, whole for 0-decimal). |
| 337 |
*/ |
| 338 |
public static function amount_to_stripe_format( $amount, $currency ) { |
| 339 |
$amount = self::clean_amount( $amount ); |
| 340 |
return self::is_zero_decimal_currency( $currency ) |
| 341 |
? (int) round( $amount ) |
| 342 |
: (int) round( $amount * 100 ); |
| 343 |
} |
| 344 |
|
| 345 |
/** |
| 346 |
* Convert amount from Stripe's smallest currency unit to major unit. |
| 347 |
* |
| 348 |
* For two-decimal currencies (USD, EUR, etc.): divides by 100 |
| 349 |
* For zero-decimal currencies (JPY, KRW, etc.): returns as-is |
| 350 |
* |
| 351 |
* @param int|string|float $amount Amount in smallest currency unit (can contain commas). |
| 352 |
* @param string $currency Currency code. |
| 353 |
* @since 2.0.0 |
| 354 |
* @return float Amount in major currency unit. |
| 355 |
*/ |
| 356 |
public static function amount_from_stripe_format( $amount, $currency ) { |
| 357 |
$amount = self::clean_amount( $amount ); |
| 358 |
return self::is_zero_decimal_currency( $currency ) |
| 359 |
? $amount |
| 360 |
: $amount / 100; |
| 361 |
} |
| 362 |
|
| 363 |
/** |
| 364 |
* Generate unique payment ID using base36 encoding and random string, always 14 characters. |
| 365 |
* |
| 366 |
* Format: {base36_encoded_id}{random_chars} |
| 367 |
* Example: 3F7B9A1E4C7D2A (exactly 14 chars) |
| 368 |
* |
| 369 |
* @param int $auto_increment_id The database auto-increment ID. |
| 370 |
* @since 2.0.0 |
| 371 |
* @return string Generated unique payment ID (always 14 characters). |
| 372 |
*/ |
| 373 |
public static function generate_unique_payment_id( $auto_increment_id ) { |
| 374 |
// Convert the auto-increment ID to base36. |
| 375 |
$encoded_id = base_convert( (string) $auto_increment_id, 10, 36 ); |
| 376 |
// Calculate the length of random part needed to make the ID exactly 14 chars. |
| 377 |
$random_length = 14 - strlen( $encoded_id ); |
| 378 |
if ( $random_length < 1 ) { |
| 379 |
$random_length = 1; // Always leave at least 1 random char for collision prevention. |
| 380 |
} |
| 381 |
// Generate random part using only valid base36 (alphanumeric) chars. |
| 382 |
// bin2hex gives 2 chars per byte, so we need ceil($random_length / 2) bytes. |
| 383 |
$bytes_needed = max( 1, (int) ceil( $random_length / 2 ) ); // Ensure at least 1 byte. |
| 384 |
$random_bytes = bin2hex( random_bytes( $bytes_needed ) ); |
| 385 |
$random_part = substr( $random_bytes, 0, $random_length ); |
| 386 |
$unique_id = strtoupper( $encoded_id . $random_part ); |
| 387 |
// Ensure exactly 14 chars. |
| 388 |
return substr( $unique_id, 0, 14 ); |
| 389 |
} |
| 390 |
|
| 391 |
/** |
| 392 |
* Get the SureForms Pro License Key. |
| 393 |
* |
| 394 |
* @param bool $allow_remote_check Whether a license-cache miss may block on the |
| 395 |
* SureCart API. Pass false from front-end request |
| 396 |
* paths - see is_pro_license_active(). |
| 397 |
* @since 2.0.0 |
| 398 |
* @since 2.12.8 Added the $allow_remote_check parameter. |
| 399 |
* @return string The SureForms Pro License Key. |
| 400 |
*/ |
| 401 |
public static function get_license_key( $allow_remote_check = true ) { |
| 402 |
$licensing = self::get_licensing_instance(); |
| 403 |
if ( ! $licensing || |
| 404 |
! method_exists( $licensing, 'licensing_setup' ) || ! method_exists( $licensing->licensing_setup(), 'settings' ) ) { |
| 405 |
return ''; |
| 406 |
} |
| 407 |
// Check if the SureForms Pro license is active. |
| 408 |
$is_license_active = self::is_pro_license_active( $allow_remote_check ); |
| 409 |
// If the license is active, get the license key. |
| 410 |
$license_setup = $licensing->licensing_setup(); |
| 411 |
return ! empty( $is_license_active ) && is_object( $license_setup ) && method_exists( $license_setup, 'settings' ) ? $license_setup->settings()->license_key : ''; |
| 412 |
} |
| 413 |
|
| 414 |
/** |
| 415 |
* Check if the SureForms Pro license is active. |
| 416 |
* |
| 417 |
* On a cache miss the Pro licensing check makes blocking wp_remote_request calls |
| 418 |
* with a 30 second timeout each. That is fine on an admin screen, but this helper |
| 419 |
* is also reached from wp_ajax_nopriv_ checkout handlers, where an unauthenticated |
| 420 |
* visitor would wait on SureCart before their payment intent is created - and a |
| 421 |
* single slow response would pin the cached verdict for the whole site. |
| 422 |
* |
| 423 |
* Pass false from those paths to read the last known status instead. |
| 424 |
* |
| 425 |
* @param bool $allow_remote_check Whether a cache miss may block on the SureCart API. |
| 426 |
* @since 2.0.0 |
| 427 |
* @since 2.12.8 Added the $allow_remote_check parameter. |
| 428 |
* @return bool|string True if the SureForms Pro license is active, false otherwise. |
| 429 |
*/ |
| 430 |
public static function is_pro_license_active( $allow_remote_check = true ) { |
| 431 |
$licensing = self::get_licensing_instance(); |
| 432 |
if ( ! $licensing || ! method_exists( $licensing, 'is_license_active' ) |
| 433 |
) { |
| 434 |
return ''; |
| 435 |
} |
| 436 |
// Check if the SureForms Pro license is active. |
| 437 |
return $licensing->is_license_active( $allow_remote_check ); |
| 438 |
} |
| 439 |
|
| 440 |
/** |
| 441 |
* Get the webhook URL for Stripe. |
| 442 |
* |
| 443 |
* Returns the dynamic webhook URL based on the site's REST API endpoint. |
| 444 |
* Example: http://localhost:10008/wp-json/sureforms/webhook |
| 445 |
* |
| 446 |
* @param string $mode The payment mode ('test' or 'live'). Default is 'test'. |
| 447 |
* |
| 448 |
* @since 2.0.0 |
| 449 |
* @return string The webhook URL. |
| 450 |
*/ |
| 451 |
public static function get_webhook_url( $mode = 'test' ) { |
| 452 |
return 'test' === $mode ? rest_url( 'sureforms/webhook_test' ) : rest_url( 'sureforms/webhook_live' ); |
| 453 |
} |
| 454 |
|
| 455 |
/** |
| 456 |
* Verify webhook connection with Stripe. |
| 457 |
* |
| 458 |
* Checks if the webhook endpoint exists and is enabled in Stripe |
| 459 |
* based on the current payment mode. Uses static cache for same request. |
| 460 |
* |
| 461 |
* @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode. |
| 462 |
* @since 2.0.0 |
| 463 |
* @return bool True if webhook is enabled, false otherwise. |
| 464 |
*/ |
| 465 |
public static function verify_webhook_connection( $mode = null ) { |
| 466 |
// Get current payment mode. |
| 467 |
$payment_mode = is_string( $mode ) && in_array( $mode, [ 'test', 'live' ], true ) ? $mode : self::get_stripe_mode(); |
| 468 |
|
| 469 |
// Check static cache first to avoid repeated API calls in same request. |
| 470 |
$cache_key = 'webhook_' . $payment_mode; |
| 471 |
if ( isset( self::$webhook_verification_cache[ $cache_key ] ) ) { |
| 472 |
return self::$webhook_verification_cache[ $cache_key ]; |
| 473 |
} |
| 474 |
|
| 475 |
// Get webhook settings. |
| 476 |
$payment_settings = self::get_all_stripe_settings(); |
| 477 |
|
| 478 |
if ( ! is_array( $payment_settings ) ) { |
| 479 |
self::$webhook_verification_cache[ $cache_key ] = false; |
| 480 |
return false; |
| 481 |
} |
| 482 |
|
| 483 |
// Get webhook ID based on mode. |
| 484 |
$webhook_id_key = 'webhook_' . $payment_mode . '_id'; |
| 485 |
$webhook_id = isset( $payment_settings[ $webhook_id_key ] ) && is_string( $payment_settings[ $webhook_id_key ] ) ? $payment_settings[ $webhook_id_key ] : ''; |
| 486 |
|
| 487 |
if ( empty( $webhook_id ) ) { |
| 488 |
self::$webhook_verification_cache[ $cache_key ] = false; |
| 489 |
return false; |
| 490 |
} |
| 491 |
|
| 492 |
// Make API request to verify webhook. |
| 493 |
$response = self::stripe_api_request( 'webhook_endpoints', 'GET', [], $webhook_id, [ 'mode' => $payment_mode ] ); |
| 494 |
|
| 495 |
// If API call failed (webhook not found, deleted, or error), clear webhook data. |
| 496 |
if ( ! $response['success'] ) { |
| 497 |
self::clear_webhook_data( $payment_mode, $payment_settings ); |
| 498 |
self::$webhook_verification_cache[ $cache_key ] = false; |
| 499 |
return false; |
| 500 |
} |
| 501 |
|
| 502 |
// Check webhook status and mode match. |
| 503 |
$webhook_data = $response['data']; |
| 504 |
$is_enabled = isset( $webhook_data['status'] ) && 'enabled' === $webhook_data['status']; |
| 505 |
|
| 506 |
// Verify the livemode matches the current mode. |
| 507 |
$webhook_livemode = isset( $webhook_data['livemode'] ) && is_bool( $webhook_data['livemode'] ) ? $webhook_data['livemode'] : false; |
| 508 |
$expected_livemode = 'live' === $payment_mode; |
| 509 |
$mode_matches = $webhook_livemode === $expected_livemode; |
| 510 |
|
| 511 |
// Webhook is connected only if enabled and mode matches. |
| 512 |
$is_connected = $is_enabled && $mode_matches; |
| 513 |
|
| 514 |
// If webhook is not connected, clear the webhook data from settings. |
| 515 |
if ( ! $is_connected ) { |
| 516 |
self::clear_webhook_data( $payment_mode, $payment_settings ); |
| 517 |
} |
| 518 |
|
| 519 |
// Cache result for this request. |
| 520 |
self::$webhook_verification_cache[ $cache_key ] = $is_connected; |
| 521 |
|
| 522 |
return $is_connected; |
| 523 |
} |
| 524 |
|
| 525 |
/** |
| 526 |
* Check if any transaction is present in the payments table. |
| 527 |
* |
| 528 |
* @since 2.0.0 |
| 529 |
* @return bool True if at least one transaction exists, false otherwise. |
| 530 |
*/ |
| 531 |
public static function is_transaction_present() { |
| 532 |
global $wpdb; |
| 533 |
|
| 534 |
// Get payments table name. |
| 535 |
$payments_table = Payments::get_instance()->get_tablename(); |
| 536 |
|
| 537 |
if ( empty( $payments_table ) || ! is_string( $payments_table ) ) { |
| 538 |
return false; |
| 539 |
} |
| 540 |
|
| 541 |
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 542 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table query to check transaction existence; table name from get_tablename() and validated (not user input); cannot be parameterized with prepare(). |
| 543 |
$count = $wpdb->get_var( |
| 544 |
"SELECT COUNT(*) FROM {$payments_table} LIMIT 1" |
| 545 |
); |
| 546 |
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 547 |
|
| 548 |
return ! empty( $count ) && absint( $count ) > 0; |
| 549 |
} |
| 550 |
|
| 551 |
/** |
| 552 |
* Get all Stripe settings from srfm_options. |
| 553 |
* |
| 554 |
* Retrieves the complete Stripe settings array from the nested structure: |
| 555 |
* srfm_options -> payment_settings -> stripe |
| 556 |
* |
| 557 |
* @since 2.0.0 |
| 558 |
* @return array<string, mixed> The Stripe settings array, or default settings if not found. |
| 559 |
*/ |
| 560 |
public static function get_all_stripe_settings() { |
| 561 |
$stripe_settings = Payment_Helper::get_gateway_settings( 'stripe' ); |
| 562 |
|
| 563 |
// Return default settings if empty. |
| 564 |
return ! empty( $stripe_settings ) ? $stripe_settings : self::get_default_stripe_settings(); |
| 565 |
} |
| 566 |
|
| 567 |
/** |
| 568 |
* Update all Stripe settings in srfm_options. |
| 569 |
* |
| 570 |
* Stores the complete Stripe settings array in the nested structure: |
| 571 |
* srfm_options -> payment_settings -> stripe |
| 572 |
* |
| 573 |
* @param array<string, mixed> $settings The Stripe settings array to save. |
| 574 |
* @since 2.0.0 |
| 575 |
* @return bool True on success, false on failure. |
| 576 |
*/ |
| 577 |
public static function update_all_stripe_settings( $settings ) { |
| 578 |
if ( ! is_array( $settings ) ) { |
| 579 |
return false; |
| 580 |
} |
| 581 |
|
| 582 |
return Payment_Helper::update_gateway_settings( 'stripe', $settings ); |
| 583 |
} |
| 584 |
|
| 585 |
/** |
| 586 |
* Get a specific Stripe setting value by key. |
| 587 |
* |
| 588 |
* @param string $key The setting key to retrieve. |
| 589 |
* @param mixed $default The default value to return if key doesn't exist. |
| 590 |
* @since 2.0.0 |
| 591 |
* @return mixed The setting value or default if not found. |
| 592 |
*/ |
| 593 |
public static function get_stripe_setting( $key, $default = '' ) { |
| 594 |
if ( ! is_string( $key ) || empty( $key ) ) { |
| 595 |
return $default; |
| 596 |
} |
| 597 |
|
| 598 |
$settings = self::get_all_stripe_settings(); |
| 599 |
|
| 600 |
return $settings[ $key ] ?? $default; |
| 601 |
} |
| 602 |
|
| 603 |
/** |
| 604 |
* Update a specific Stripe setting value by key. |
| 605 |
* |
| 606 |
* @param string $key The setting key to update. |
| 607 |
* @param mixed $value The value to set. |
| 608 |
* @since 2.0.0 |
| 609 |
* @return bool True on success, false on failure. |
| 610 |
*/ |
| 611 |
public static function update_stripe_setting( $key, $value ) { |
| 612 |
if ( ! is_string( $key ) || empty( $key ) ) { |
| 613 |
return false; |
| 614 |
} |
| 615 |
|
| 616 |
$settings = self::get_all_stripe_settings(); |
| 617 |
$settings[ $key ] = $value; |
| 618 |
|
| 619 |
return self::update_all_stripe_settings( $settings ); |
| 620 |
} |
| 621 |
|
| 622 |
/** |
| 623 |
* Get default Stripe settings structure. |
| 624 |
* |
| 625 |
* Note: currency and payment_mode are now stored in global settings. |
| 626 |
* |
| 627 |
* @since 2.0.0 |
| 628 |
* @return array<string, mixed> Default Stripe settings array. |
| 629 |
*/ |
| 630 |
public static function get_default_stripe_settings() { |
| 631 |
return [ |
| 632 |
'stripe_connected' => false, |
| 633 |
'stripe_account_id' => '', |
| 634 |
'stripe_account_email' => '', |
| 635 |
'stripe_live_publishable_key' => '', |
| 636 |
'stripe_live_secret_key' => '', |
| 637 |
'stripe_test_publishable_key' => '', |
| 638 |
'stripe_test_secret_key' => '', |
| 639 |
'payment_mode' => 'test', |
| 640 |
'webhook_test_secret' => '', |
| 641 |
'webhook_test_url' => '', |
| 642 |
'webhook_test_id' => '', |
| 643 |
'webhook_live_secret' => '', |
| 644 |
'webhook_live_url' => '', |
| 645 |
'webhook_live_id' => '', |
| 646 |
'account_name' => '', |
| 647 |
]; |
| 648 |
} |
| 649 |
|
| 650 |
/** |
| 651 |
* Get Stripe Connect URL |
| 652 |
* |
| 653 |
* @since 2.0.0 |
| 654 |
* @return \WP_REST_Response |
| 655 |
*/ |
| 656 |
public static function get_stripe_connect_url() { |
| 657 |
// Stripe client ID from checkout-plugins-stripe-woo. |
| 658 |
$client_id = 'ca_KOXfLe7jv1m4L0iC4KNEMc5fT8AXWWuL'; |
| 659 |
|
| 660 |
// Use the same redirect URI pattern as checkout-plugins-stripe-woo. |
| 661 |
$redirect_url = admin_url( 'admin.php?page=sureforms_form_settings&tab=payments-settings&subpage=payment-methods&gateway=stripe' ); |
| 662 |
$nonce = wp_create_nonce( 'stripe-connect' ); |
| 663 |
$redirect_with_nonce = add_query_arg( 'srfm_stripe_connect_nonce', $nonce, $redirect_url ); |
| 664 |
|
| 665 |
// Store our own callback data. |
| 666 |
set_transient( 'srfm_stripe_connect_nonce_' . get_current_user_id(), $nonce, HOUR_IN_SECONDS ); |
| 667 |
|
| 668 |
// Create state parameter exactly like checkout-plugins-stripe-woo. |
| 669 |
$state_param = wp_json_encode( |
| 670 |
[ |
| 671 |
'redirect' => $redirect_with_nonce, |
| 672 |
] |
| 673 |
); |
| 674 |
$state = ''; |
| 675 |
if ( is_string( $state_param ) ) { |
| 676 |
$state = base64_encode( $state_param ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode |
| 677 |
} |
| 678 |
|
| 679 |
$connect_url = add_query_arg( |
| 680 |
[ |
| 681 |
'response_type' => 'code', |
| 682 |
'client_id' => $client_id, |
| 683 |
'stripe_landing' => 'login', |
| 684 |
'always_prompt' => 'true', |
| 685 |
'scope' => 'read_write', |
| 686 |
'state' => $state, |
| 687 |
], |
| 688 |
'https://connect.stripe.com/oauth/authorize' |
| 689 |
); |
| 690 |
|
| 691 |
return rest_ensure_response( [ 'url' => $connect_url ] ); |
| 692 |
} |
| 693 |
|
| 694 |
/** |
| 695 |
* Get the Stripe account ID. |
| 696 |
* |
| 697 |
* @since 2.5.1 |
| 698 |
* @return string The Stripe account ID. |
| 699 |
*/ |
| 700 |
public static function get_stripe_account_id() { |
| 701 |
$account = self::get_stripe_setting( 'stripe_account_id' ); |
| 702 |
if ( empty( $account ) || ! is_string( $account ) ) { |
| 703 |
return ''; |
| 704 |
} |
| 705 |
return $account; |
| 706 |
} |
| 707 |
|
| 708 |
/** |
| 709 |
* Send payment data to middleware intersect endpoint. |
| 710 |
* |
| 711 |
* @param string $charge_id Stripe charge ID (ch_xxx format). |
| 712 |
* @param string $secret_key Stripe secret key. |
| 713 |
* @param string $stripe_account_id Stripe account ID (optional). |
| 714 |
* @param string $plugin_name Plugin name (default: 'SureForms'). |
| 715 |
* @since 2.5.1 |
| 716 |
* @return void |
| 717 |
*/ |
| 718 |
public static function intersect_payment( $charge_id, $secret_key = '', $stripe_account_id = '', $plugin_name = 'SureForms' ) { |
| 719 |
// Validate charge ID format (must be ch_xxx). |
| 720 |
if ( empty( $charge_id ) || ! preg_match( '/^ch_[a-zA-Z0-9]+$/', $charge_id ) ) { |
| 721 |
return; |
| 722 |
} |
| 723 |
|
| 724 |
if ( empty( $secret_key ) ) { |
| 725 |
return; |
| 726 |
} |
| 727 |
|
| 728 |
// Prepare request data. |
| 729 |
$request_data = [ |
| 730 |
'plugin_name' => $plugin_name, |
| 731 |
'secret_key' => $secret_key, |
| 732 |
'transaction_id' => $charge_id, |
| 733 |
'account_id' => $stripe_account_id, |
| 734 |
]; |
| 735 |
|
| 736 |
// Encode and send to middleware. |
| 737 |
$request_body = wp_json_encode( $request_data ); |
| 738 |
$request_body = is_string( $request_body ) ? $request_body : ''; |
| 739 |
$request_body = base64_encode( $request_body ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode |
| 740 |
|
| 741 |
if ( empty( $request_body ) ) { |
| 742 |
return; |
| 743 |
} |
| 744 |
|
| 745 |
// Send to middleware intersect endpoint. |
| 746 |
wp_remote_post( |
| 747 |
self::middle_ware_base_url() . 'payment/intersect', |
| 748 |
[ |
| 749 |
'timeout' => 30, |
| 750 |
'body' => $request_body, |
| 751 |
'headers' => [ |
| 752 |
'Content-Type' => 'application/json', |
| 753 |
], |
| 754 |
] |
| 755 |
); |
| 756 |
} |
| 757 |
|
| 758 |
/** |
| 759 |
* Clear webhook data from settings for a specific mode. |
| 760 |
* |
| 761 |
* Removes webhook_secret, webhook_id, and webhook_url for the specified mode. |
| 762 |
* |
| 763 |
* @param string $mode The payment mode ('test' or 'live'). |
| 764 |
* @param array<string, mixed> $payment_settings The payment settings array. |
| 765 |
* @since 2.0.0 |
| 766 |
* @return void |
| 767 |
*/ |
| 768 |
private static function clear_webhook_data( $mode, $payment_settings ) { |
| 769 |
$updated_settings = $payment_settings; |
| 770 |
|
| 771 |
if ( 'live' === $mode ) { |
| 772 |
$updated_settings['webhook_live_secret'] = ''; |
| 773 |
$updated_settings['webhook_live_id'] = ''; |
| 774 |
$updated_settings['webhook_live_url'] = ''; |
| 775 |
} else { |
| 776 |
$updated_settings['webhook_test_secret'] = ''; |
| 777 |
$updated_settings['webhook_test_id'] = ''; |
| 778 |
$updated_settings['webhook_test_url'] = ''; |
| 779 |
} |
| 780 |
|
| 781 |
self::update_all_stripe_settings( $updated_settings ); |
| 782 |
} |
| 783 |
|
| 784 |
/** |
| 785 |
* Clean up amount to float. |
| 786 |
* |
| 787 |
* Removes commas, spaces, and ensures a numeric float value. |
| 788 |
* |
| 789 |
* @param float|string|int $amount Amount to clean up. |
| 790 |
* @since 2.0.0 |
| 791 |
* @return float Clean float value. |
| 792 |
*/ |
| 793 |
private static function clean_amount( $amount ) { |
| 794 |
if ( is_string( $amount ) ) { |
| 795 |
$amount = str_replace( [ ',', ' ' ], '', $amount ); |
| 796 |
} |
| 797 |
return is_numeric( $amount ) ? (float) $amount : 0.0; |
| 798 |
} |
| 799 |
|
| 800 |
/** |
| 801 |
* Flattens a multidimensional array into a single-level array using Stripe's bracket notation. |
| 802 |
* |
| 803 |
* This is useful for preparing data to be sent to the Stripe API, which expects |
| 804 |
* nested parameters to be formatted as key[subkey]=value. |
| 805 |
* |
| 806 |
* @param array<mixed> $data The multidimensional array to flatten. |
| 807 |
* @param string $prefix (Optional) The prefix for nested keys. Default is an empty string. |
| 808 |
* @since 2.0.0 |
| 809 |
* @return array<mixed> The flattened array with bracket notation keys. |
| 810 |
*/ |
| 811 |
private static function flatten_stripe_data( $data, $prefix = '' ) { |
| 812 |
$result = []; |
| 813 |
|
| 814 |
foreach ( $data as $key => $value ) { |
| 815 |
$new_key = $prefix ? $prefix . '[' . $key . ']' : $key; |
| 816 |
|
| 817 |
if ( is_array( $value ) ) { |
| 818 |
$result = array_merge( $result, self::flatten_stripe_data( $value, $new_key ) ); |
| 819 |
} else { |
| 820 |
$result[ $new_key ] = $value; |
| 821 |
} |
| 822 |
} |
| 823 |
|
| 824 |
return $result; |
| 825 |
} |
| 826 |
|
| 827 |
/** |
| 828 |
* Get the Licensing Instance. |
| 829 |
* |
| 830 |
* @since 2.0.0 |
| 831 |
* @return object|null The Licensing Instance. |
| 832 |
*/ |
| 833 |
private static function get_licensing_instance() { |
| 834 |
if ( ! class_exists( 'SRFM_Pro\Admin\Licensing' ) ) { |
| 835 |
return null; |
| 836 |
} |
| 837 |
return Licensing::get_instance(); |
| 838 |
} |
| 839 |
} |
| 840 |
|