Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save xlplugins/e47cce3b09753104ca7e65f975b491a8 to your computer and use it in GitHub Desktop.

Select an option

Save xlplugins/e47cce3b09753104ca7e65f975b491a8 to your computer and use it in GitHub Desktop.
WFACP — Cart / checkout-page mismatch guard
<?php
/**
* Plugin Name: WFACP — Cart / checkout-page mismatch guard
* Description: Prevents wrong-product orders on FunnelKit (Aero) checkouts sharing one WooCommerce session. Stamps every Aero cart item with the checkout page that added it, detects foreign items during checkout render and self-corrects via one guarded reload. As a final safety net it blocks a mismatched order submission with a visible notice, then auto-reloads so the page loads its own product and the customer can place the order again.
* Version: 2.0.0
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'WFACP_Cart_Mismatch_Guard' ) ) {
class WFACP_Cart_Mismatch_Guard {
public function __construct() {
add_filter( 'woocommerce_add_cart_item', array( $this, 'stamp_cart_item' ), 10 );
add_filter( 'woocommerce_update_order_review_fragments', array( $this, 'flag_mismatch_fragment' ) );
add_action( 'woocommerce_after_checkout_validation', array( $this, 'block_mismatched_submission' ), 999, 2 );
add_action( 'wp_footer', array( $this, 'footer_script' ), 99 );
}
/**
* Remember which Aero checkout page added this item (gist idea, kept).
*/
public function stamp_cart_item( $cart_item ) {
if ( is_array( $cart_item ) && ( isset( $cart_item['_wfacp_product'] ) || isset( $cart_item['_wfacp_product_key'] ) ) && class_exists( 'WFACP_Common' ) ) {
$page_id = absint( WFACP_Common::get_id() );
if ( $page_id > 0 ) {
$cart_item['_wfacp_checkout_page_id'] = $page_id;
}
}
return $cart_item;
}
/**
* True when the cart holds an Aero item belonging to another checkout page.
*
* Pages without configured products (global/store checkout) never mismatch —
* they legitimately sell whatever cart arrives. Items without Aero markers
* (order bumps, upsells, gifts, custom additions) are never judged.
*/
private function cart_has_foreign_items( $page_id ) {
if ( ! class_exists( 'WFACP_Common' ) || ! function_exists( 'WC' ) || is_null( WC()->cart ) ) {
return false;
}
$page_id = absint( $page_id );
if ( $page_id < 1 ) {
return false;
}
$page_products = WFACP_Common::get_page_product( $page_id );
if ( empty( $page_products ) || ! is_array( $page_products ) ) {
return false;
}
$page_keys = array_map( 'strval', array_keys( $page_products ) );
foreach ( WC()->cart->get_cart_contents() as $cart_item ) {
if ( ! is_array( $cart_item ) ) {
continue;
}
// Primary: provenance stamp written by this guard.
if ( isset( $cart_item['_wfacp_checkout_page_id'] ) && absint( $cart_item['_wfacp_checkout_page_id'] ) > 0 ) {
if ( absint( $cart_item['_wfacp_checkout_page_id'] ) !== $page_id ) {
return true;
}
continue;
}
// Fallback for items added before this guard was deployed.
if ( ! empty( $cart_item['_wfacp_product_key'] ) && ! in_array( (string) $cart_item['_wfacp_product_key'], $page_keys, true ) ) {
return true;
}
}
return false;
}
/**
* Render-time detection: tell the page (via fragments) that its cart is foreign.
*/
public function flag_mismatch_fragment( $fragments ) {
if ( ! wp_doing_ajax() || ! class_exists( 'WFACP_Common' ) ) {
return $fragments;
}
if ( $this->cart_has_foreign_items( WFACP_Common::get_id() ) ) {
$fragments['wfacp_checkout_mismatch'] = 'yes';
}
return $fragments;
}
/**
* Final safety net: never create an order from a foreign cart.
*/
public function block_mismatched_submission( $data, $errors ) {
$page_id = isset( $_POST['_wfacp_post_id'] ) ? absint( wp_unslash( $_POST['_wfacp_post_id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WC checkout nonce verified upstream.
if ( $page_id < 1 && class_exists( 'WFACP_Common' ) ) {
$page_id = absint( WFACP_Common::get_id() );
}
if ( ! $this->cart_has_foreign_items( $page_id ) ) {
return;
}
if ( function_exists( 'wc_get_logger' ) ) {
wc_get_logger()->warning(
sprintf( 'Blocked mismatched order submission on checkout page %d.', $page_id ),
array( 'source' => 'wfacp-mismatch-guard' )
);
}
// Block with a visible reason; the footer JS reloads shortly after so
// the page re-claims its own product and the customer can order again.
$errors->add(
'wfacp_product_mismatch',
apply_filters(
'wfacp_product_mismatch_notice',
__( 'The product in your cart did not match this checkout, so the order was not placed. The page will refresh automatically to load the correct product — please place your order again.', 'woofunnels-aero-checkout' )
),
array( 'id' => 'wfacp_checkout_mismatch' )
);
}
/**
* Client side: self-correct with ONE guarded reload when a mismatch is
* reported (render-time or after a blocked submission), and refresh state
* when the page is restored from the back/forward cache.
*/
public function footer_script() {
if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
return;
}
?>
<script>
(function ($) {
var GUARD_KEY = 'wfacp_mm_reload_at';
function reloadOnce() {
var now = Date.now();
var last = 0;
try { last = parseInt(sessionStorage.getItem(GUARD_KEY), 10) || 0; } catch (e) {}
if (now - last < 30000) {
return; // never loop: at most one corrective reload per 30s
}
try { sessionStorage.setItem(GUARD_KEY, String(now)); } catch (e) {}
window.location.reload();
}
$(document.body).on('updated_checkout', function (e, data) {
if (data && data.fragments && data.fragments.wfacp_checkout_mismatch === 'yes') {
reloadOnce();
}
});
// Blocked submission: the notice explains why; give the customer a
// few seconds to read it, then reload so the page loads its own product.
$(document.body).on('checkout_error', function () {
setTimeout(function () {
var $err = $('li[data-id="wfacp_checkout_mismatch"], .woocommerce-error [data-id="wfacp_checkout_mismatch"]');
if ($err.length === 0) {
return;
}
setTimeout(reloadOnce, 4000);
}, 0);
});
// Restored from back/forward cache: state may be stale — resync.
window.addEventListener('pageshow', function (e) {
if (e.persisted) {
$(document.body).trigger('update_checkout');
}
});
})(jQuery);
</script>
<?php
}
}
new WFACP_Cart_Mismatch_Guard();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment