
Hardened PHP replacement handlers for PeachPay Stripe capture and void AJAX endpoints, adding nonce verification, capability checks, and strict order and amount validation.
<?php
/**
* Hardened PeachPay Stripe payment capture and void AJAX handlers.
* By Taylor Christian Newsome
* Security controls
* - Authenticated AJAX requests only.
* - Nonce verification.
* - Strict order ID and amount validation.
* - Order-specific authorization.
* - Positive finite capture amounts.
* - Exception handling and server-side logging.
*
* Requires WordPress, WooCommerce, and the PeachPay Stripe integration.
*/
defined( 'ABSPATH' ) || exit;
/**
* Send a standardized JSON error and terminate execution.
*
* @param string $message Error message.
* @param int $status HTTP response status.
* @return void
*/
function peachpay_stripe_ajax_error( $message, $status = 400 ) {
wp_send_json_error(
array( 'message' => $message ),
$status
);
}
/**
* Verify an AJAX nonce.
*
* @param string $action Nonce action.
* @return void
*/
function peachpay_stripe_verify_ajax_nonce( $action ) {
if (
! isset( $_POST['security'] ) ||
! is_string( $_POST['security'] )
) {
peachpay_stripe_ajax_error(
'Invalid security token. Refresh the page and try again.',
403
);
}
$nonce = sanitize_text_field(
wp_unslash( $_POST['security'] )
);
if ( ! wp_verify_nonce( $nonce, $action ) ) {
peachpay_stripe_ajax_error(
'Invalid security token. Refresh the page and try again.',
403
);
}
}
/**
* Validate and retrieve a positive order ID from POST data.
*
* @return int
*/
function peachpay_stripe_get_validated_order_id() {
if (
! isset( $_POST['order_id'] ) ||
! is_string( $_POST['order_id'] ) &&
! is_int( $_POST['order_id'] )
) {
peachpay_stripe_ajax_error(
'A valid order_id is required.',
400
);
}
$raw_order_id = (string) wp_unslash( $_POST['order_id'] );
// Reject arrays, decimals, signs, whitespace, and other malformed IDs.
if (
'' === $raw_order_id ||
! ctype_digit( $raw_order_id ) ||
(int) $raw_order_id < 1
) {
peachpay_stripe_ajax_error(
'Invalid order_id.',
400
);
}
$order_id = absint( $raw_order_id );
if ( $order_id < 1 ) {
peachpay_stripe_ajax_error(
'Invalid order_id.',
400
);
}
return $order_id;
}
/**
* Retrieve an order and verify the current user is authorized to manage it.
*
* WooCommerce capability mappings can vary by version and configuration.
* Confirm that these capabilities match the permissions used by your store.
*
* @param int $order_id Order ID.
* @return WC_Order
*/
function peachpay_stripe_get_authorized_order( $order_id ) {
if ( ! function_exists( 'wc_get_order' ) ) {
peachpay_stripe_ajax_error(
'WooCommerce is unavailable.',
500
);
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
peachpay_stripe_ajax_error(
'Order not found.',
404
);
}
if (
! current_user_can( 'edit_shop_orders' ) ||
! current_user_can( 'edit_shop_order', $order_id )
) {
peachpay_stripe_ajax_error(
'Unauthorized to manage this order.',
403
);
}
return $order;
}
/**
* Validate a positive decimal capture amount.
*
* This validates the syntax and numeric range. It does not establish the
* Stripe payment intent's remaining capturable balance.
*
* @return float
*/
function peachpay_stripe_get_validated_capture_amount() {
if (
! isset( $_POST['amount_to_capture'] ) ||
! is_string( $_POST['amount_to_capture'] ) &&
! is_int( $_POST['amount_to_capture'] ) &&
! is_float( $_POST['amount_to_capture'] )
) {
peachpay_stripe_ajax_error(
'A valid amount_to_capture is required.',
400
);
}
$raw_amount = trim(
(string) wp_unslash( $_POST['amount_to_capture'] )
);
// Accept ordinary decimal notation only; reject exponent notation,
// signs, commas, currency symbols, and non-numeric input.
if (
'' === $raw_amount ||
! preg_match( '/^\d+(?:\.\d+)?$/D', $raw_amount )
) {
peachpay_stripe_ajax_error(
'Invalid amount to capture.',
400
);
}
// Reject excessively long values before converting to a float.
if ( strlen( $raw_amount ) > 32 ) {
peachpay_stripe_ajax_error(
'Invalid amount to capture.',
400
);
}
$amount = (float) $raw_amount;
if ( ! is_finite( $amount ) || $amount <= 0 ) {
peachpay_stripe_ajax_error(
'Invalid amount to capture.',
400
);
}
return $amount;
}
/**
* Log a Stripe operation failure without exposing internal details to clients.
*
* @param string $operation Operation name.
* @param int $order_id Order ID.
* @param Throwable $exception Exception thrown.
* @return void
*/
function peachpay_stripe_log_ajax_failure(
$operation,
$order_id,
$exception
) {
if ( function_exists( 'wc_get_logger' ) ) {
wc_get_logger()->error(
sprintf(
'Stripe %s failed for order %d. Exception type: %s.',
$operation,
(int) $order_id,
get_class( $exception )
),
array(
'source' => 'peachpay-stripe',
)
);
}
}
/**
* Handle an authenticated AJAX request to capture a Stripe payment.
*
* @return void
*/
function peachpay_stripe_handle_capture_payment() {
// 1. Verify request authenticity.
peachpay_stripe_verify_ajax_nonce(
'peachpay-stripe-capture-payment'
);
// 2. Validate the order ID and load an authorized order.
$order_id = peachpay_stripe_get_validated_order_id();
$order = peachpay_stripe_get_authorized_order(
$order_id
);
// 3. Validate the requested capture amount.
$raw_amount = peachpay_stripe_get_validated_capture_amount();
// 4. Confirm the required PeachPay integration is available.
if (
! class_exists( 'PeachPay_Stripe' ) ||
! is_callable( array( 'PeachPay_Stripe', 'format_amount' ) ) ||
! is_callable( array( 'PeachPay_Stripe', 'capture_payment' ) )
) {
peachpay_stripe_ajax_error(
'The Stripe payment integration is unavailable.',
500
);
}
try {
// 5. Format the amount according to PeachPay's currency conventions.
$amount_to_capture = PeachPay_Stripe::format_amount(
$raw_amount,
$order->get_currency()
);
if (
! is_numeric( $amount_to_capture ) ||
! is_finite( (float) $amount_to_capture ) ||
(float) $amount_to_capture <= 0
) {
peachpay_stripe_ajax_error(
'Invalid capture amount.',
400
);
}
/*
* IMPORTANT:
* capture_payment() must independently verify the Stripe PaymentIntent
* is capturable, enforce the remaining capturable balance, use the
* correct currency units, and handle duplicate requests safely.
*/
$result = PeachPay_Stripe::capture_payment(
$order,
$amount_to_capture
);
} catch ( Throwable $exception ) {
peachpay_stripe_log_ajax_failure(
'capture',
$order_id,
$exception
);
peachpay_stripe_ajax_error(
'Unable to capture the payment. Please check the payment status.',
500
);
}
// Preserve the existing PeachPay response format.
wp_send_json( $result );
}
/**
* Handle an authenticated AJAX request to void a Stripe payment.
*
* @return void
*/
function peachpay_stripe_handle_void_payment() {
// 1. Verify request authenticity.
peachpay_stripe_verify_ajax_nonce(
'peachpay-stripe-void-payment'
);
// 2. Validate the order ID and load an authorized order.
$order_id = peachpay_stripe_get_validated_order_id();
$order = peachpay_stripe_get_authorized_order(
$order_id
);
// 3. Confirm the required PeachPay integration is available.
if (
! class_exists( 'PeachPay_Stripe' ) ||
! is_callable( array( 'PeachPay_Stripe', 'void_payment' ) )
) {
peachpay_stripe_ajax_error(
'The Stripe payment integration is unavailable.',
500
);
}
try {
/*
* IMPORTANT:
* void_payment() must confirm the Stripe PaymentIntent is in a
* cancelable state. A payment that has already been captured cannot
* generally be voided; it may require a refund instead.
*/
$result = PeachPay_Stripe::void_payment( $order );
} catch ( Throwable $exception ) {
peachpay_stripe_log_ajax_failure(
'void',
$order_id,
$exception
);