' . esc_html__('Ainbae Receipt Upload requires WooCommerce to be installed and active.', 'ainbae-receipt-upload-for-woocommerce') . '

'; }); return; } }); add_action('before_woocommerce_init', function () { if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); } }); // ───────────────────────────────────────────────────────────────────────────── // CONSTANTS // ───────────────────────────────────────────────────────────────────────────── define('AINBAE_BACS_VERSION', '2.1.3'); define('AINBAE_BACS_MAX_UPLOAD_SIZE', 5 * 1024 * 1024); define('AINBAE_BACS_RATE_LIMIT_MAX', 5); define('AINBAE_BACS_RATE_LIMIT_WINDOW', HOUR_IN_SECONDS); define('AINBAE_BACS_OPTION_KEY', 'ainbae_bacs_receipt_settings'); // ───────────────────────────────────────────────────────────────────────────── // SETTINGS HELPERS // ───────────────────────────────────────────────────────────────────────────── function ainbae_bacs_default_settings() { return array( 'whatsapp_enabled' => '1', 'whatsapp_number' => '1234567890', 'whatsapp_message_template' => '', 'require_receipt_before_order' => '0', 'color_card_bg' => '#f0f4f2', 'color_card_border' => '#d6e4dc', 'color_dropzone_bg' => '#ffffff', 'color_dropzone_border' => '#b0c8bc', 'color_icon' => '#0aa7ff', 'color_upload_btn_from' => '#0aa7ff', 'color_upload_btn_to' => '#0aa7ff', 'color_upload_btn_text' => '#ffffff', 'color_wa_btn_bg' => '#e6f9ee', 'color_wa_btn_border' => '#a8dfc0', 'color_wa_btn_text' => '#1a7a3c', 'color_heading' => '#1a1a1a', 'color_subtitle' => '#555555', 'color_hint' => '#888888', 'color_or_line' => '#d0ddd6', 'color_or_text' => '#999999', 'label_heading' => __('Verify Your Payment', 'ainbae-receipt-upload-for-woocommerce'), 'label_subtitle' => __('Please upload a screenshot of your transaction receipt, or send it directly via WhatsApp to process your order.', 'ainbae-receipt-upload-for-woocommerce'), 'label_dropzone' => __('Click to upload, or drag and drop your receipt file', 'ainbae-receipt-upload-for-woocommerce'), 'label_upload_btn' => __('Upload Receipt', 'ainbae-receipt-upload-for-woocommerce'), 'label_wa_btn' => __('Send Receipt via WhatsApp', 'ainbae-receipt-upload-for-woocommerce'), 'label_hint' => __('Allowed formats: JPG, PNG, PDF. Max size: 5 MB.', 'ainbae-receipt-upload-for-woocommerce'), 'card_border_radius' => '16', ); } function ainbae_bacs_setting($key) { $defaults = ainbae_bacs_default_settings(); $saved = get_option(AINBAE_BACS_OPTION_KEY, array()); $all = array_merge($defaults, (array) $saved); return $all[$key] ?? ($defaults[$key] ?? ''); } function ainbae_bacs_get_whatsapp_number() { $number = defined('BACS_WHATSAPP_NUMBER') ? BACS_WHATSAPP_NUMBER : ainbae_bacs_setting('whatsapp_number'); return apply_filters('ainbae_bacs_receipt_whatsapp_number', $number); } function ainbae_bacs_is_bacs_enabled() { if (! class_exists('WC_Payment_Gateways')) { return false; } $gateways = WC_Payment_Gateways::instance()->payment_gateways(); return isset($gateways['bacs']) && 'yes' === $gateways['bacs']->enabled; } // ───────────────────────────────────────────────────────────────────────────── // ADMIN — MENU & SAVE // ───────────────────────────────────────────────────────────────────────────── add_action('admin_menu', 'ainbae_bacs_register_menu'); function ainbae_bacs_register_menu() { add_submenu_page('woocommerce', __('Ainbae Receipt Upload Settings', 'ainbae-receipt-upload-for-woocommerce'), __('Upload Receipt', 'ainbae-receipt-upload-for-woocommerce'), 'manage_woocommerce', 'ainbae-receipt-settings', 'ainbae_bacs_render_settings_page'); } add_action('admin_init', 'ainbae_bacs_save_settings'); function ainbae_bacs_save_settings() { if ( ! isset($_POST['ainbae_bacs_settings_nonce']) || ! wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['ainbae_bacs_settings_nonce'])), 'ainbae_bacs_save_settings_action' ) || ! current_user_can('manage_woocommerce') || ! isset($_POST['ainbae_bacs_save_settings']) ) { return; } $defaults = ainbae_bacs_default_settings(); $sanitized = array(); $colour_keys = array('color_card_bg', 'color_card_border', 'color_dropzone_bg', 'color_dropzone_border', 'color_icon', 'color_upload_btn_from', 'color_upload_btn_to', 'color_upload_btn_text', 'color_wa_btn_bg', 'color_wa_btn_border', 'color_wa_btn_text', 'color_heading', 'color_subtitle', 'color_hint', 'color_or_line', 'color_or_text'); foreach ($colour_keys as $key) { $val = isset($_POST[$key]) ? sanitize_hex_color(wp_unslash($_POST[$key])) : ''; $sanitized[$key] = $val ?: $defaults[$key]; } foreach (array('label_heading', 'label_subtitle', 'label_dropzone', 'label_upload_btn', 'label_wa_btn', 'label_hint') as $key) { $sanitized[$key] = isset($_POST[$key]) ? sanitize_text_field(wp_unslash($_POST[$key])) : $defaults[$key]; } $sanitized['whatsapp_enabled'] = isset($_POST['whatsapp_enabled']) ? '1' : '0'; $number = isset($_POST['whatsapp_number']) ? sanitize_text_field(wp_unslash($_POST['whatsapp_number'])) : ''; $sanitized['whatsapp_number'] = $number ? preg_replace('/[^0-9]/', '', $number) : $defaults['whatsapp_number']; // WhatsApp message template (v2.0.0) $sanitized['whatsapp_message_template'] = isset($_POST['whatsapp_message_template']) ? sanitize_textarea_field(wp_unslash($_POST['whatsapp_message_template'])) : ''; // Checkout behaviour (v2.0.0) $sanitized['require_receipt_before_order'] = isset($_POST['require_receipt_before_order']) ? '1' : '0'; $sanitized['card_border_radius'] = isset($_POST['card_border_radius']) ? absint(wp_unslash($_POST['card_border_radius'])) : $defaults['card_border_radius']; update_option(AINBAE_BACS_OPTION_KEY, $sanitized); wp_safe_redirect(add_query_arg( array( 'page' => 'ainbae-receipt-settings', 'updated' => '1', '_wpnonce' => wp_create_nonce('ainbae_bacs_settings_updated'), ), admin_url('admin.php') )); exit; } // ───────────────────────────────────────────────────────────────────────────── // ENQUEUE ASSETS // ───────────────────────────────────────────────────────────────────────────── add_action('admin_enqueue_scripts', 'ainbae_bacs_enqueue_admin_assets'); function ainbae_bacs_enqueue_admin_assets($hook) { if (! current_user_can('manage_woocommerce')) { return; } if ('woocommerce_page_ainbae-receipt-settings' !== $hook) { return; } wp_enqueue_style('wp-color-picker'); wp_enqueue_script('wp-color-picker'); wp_enqueue_style('ainbae-bacs-admin-css', plugins_url('admin/css/admin.css', __FILE__), array(), AINBAE_BACS_VERSION); wp_enqueue_script('ainbae-bacs-admin-js', plugins_url('admin/js/admin.js', __FILE__), array('jquery', 'wp-color-picker'), AINBAE_BACS_VERSION, true); } /** * Ensure the public CSS and JS assets are enqueued. * * This is the single source of truth for public asset registration. It can be * called from the early `wp_enqueue_scripts` hook (for standard WooCommerce * pages) AND from the widget rendering function itself (for custom pages, * tracking pages, or any context where the widget is output via the developer * API). WordPress deduplicates enqueue calls, so calling this multiple times * is safe and results in assets being loaded only once. * * @since 2.1.3 */ function ainbae_bacs_ensure_public_assets() { static $localized = false; wp_enqueue_style('ainbae-bacs-public-css', plugins_url('public/css/public.css', __FILE__), array(), AINBAE_BACS_VERSION); wp_enqueue_script('ainbae-bacs-public-js', plugins_url('public/js/public.js', __FILE__), array('jquery'), AINBAE_BACS_VERSION, true); if (! $localized) { $localized = true; $on_checkout_page = function_exists('is_checkout') && is_checkout(); $checkout_feature = ainbae_bacs_setting('require_receipt_before_order') === '1'; wp_localize_script('ainbae-bacs-public-js', 'ainbaeBacsCheckout', array( 'enabled' => $on_checkout_page && $checkout_feature, 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('ainbae_bacs_checkout_upload'), 'heading' => ainbae_bacs_setting('label_heading'), 'subtitle' => __('Please upload your payment receipt to complete your order.', 'ainbae-receipt-upload-for-woocommerce'), 'dropzone' => ainbae_bacs_setting('label_dropzone'), 'upload_btn' => ainbae_bacs_setting('label_upload_btn'), 'hint' => ainbae_bacs_setting('label_hint'), 'uploading' => __('Uploading…', 'ainbae-receipt-upload-for-woocommerce'), 'success' => __('Receipt uploaded. Placing your order…', 'ainbae-receipt-upload-for-woocommerce'), 'success_redirect' => __('Receipt uploaded. Redirecting…', 'ainbae-receipt-upload-for-woocommerce'), 'err_size' => __('File is too large. Maximum size is 5 MB.', 'ainbae-receipt-upload-for-woocommerce'), 'err_type' => __('Invalid file type. Only JPG, PNG, and PDF are accepted.', 'ainbae-receipt-upload-for-woocommerce'), 'err_upload' => __('Upload failed. Please try again.', 'ainbae-receipt-upload-for-woocommerce'), 'err_required' => __('Please upload a payment receipt before placing your order.', 'ainbae-receipt-upload-for-woocommerce'), 'err_select_file' => __('Please select a receipt file to upload.', 'ainbae-receipt-upload-for-woocommerce'), // Dynamic CSS vars passed for modal theming 'colors' => array( 'card_bg' => ainbae_bacs_setting('color_card_bg'), 'card_border' => ainbae_bacs_setting('color_card_border'), 'card_radius' => absint(ainbae_bacs_setting('card_border_radius')), 'heading' => ainbae_bacs_setting('color_heading'), 'subtitle' => ainbae_bacs_setting('color_subtitle'), 'dropzone_bg' => ainbae_bacs_setting('color_dropzone_bg'), 'dropzone_border' => ainbae_bacs_setting('color_dropzone_border'), 'icon' => ainbae_bacs_setting('color_icon'), 'btn_from' => ainbae_bacs_setting('color_upload_btn_from'), 'btn_to' => ainbae_bacs_setting('color_upload_btn_to'), 'btn_text' => ainbae_bacs_setting('color_upload_btn_text'), 'hint' => ainbae_bacs_setting('color_hint'), ), )); } } add_action('wp_enqueue_scripts', 'ainbae_bacs_enqueue_public_assets'); function ainbae_bacs_enqueue_public_assets() { $on_order_page = function_exists('is_wc_endpoint_url') && (is_wc_endpoint_url('view-order') || is_wc_endpoint_url('order-received')); $on_checkout_page = function_exists('is_checkout') && is_checkout(); $checkout_feature = ainbae_bacs_setting('require_receipt_before_order') === '1'; if ($on_order_page || ($on_checkout_page && $checkout_feature)) { ainbae_bacs_ensure_public_assets(); } } // ───────────────────────────────────────────────────────────────────────────── // ADMIN — SETTINGS PAGE RENDER // ───────────────────────────────────────────────────────────────────────────── function ainbae_bacs_render_settings_page() { if (! current_user_can('manage_woocommerce')) wp_die(esc_html__('Access denied.', 'ainbae-receipt-upload-for-woocommerce')); $s = array(); foreach (array_keys(ainbae_bacs_default_settings()) as $key) { $s[$key] = ainbae_bacs_setting($key); } ?>

<?php esc_attr_e('Ainbae Illustration', 'ainbae-receipt-upload-for-woocommerce'); ?>

✓

Direct bank transfer (BACS).', 'ainbae-receipt-upload-for-woocommerce'), esc_url(admin_url('admin.php?page=wc-settings&tab=checkout§ion=bacs')) ), array( 'a' => array( 'href' => array(), ), ) ); ?>

>

>

+ >
>

Supported variables:

{order_number} {order_total} {customer_name} {billing_email} {billing_phone} {site_name} {currency} {order_date}
array(__('Heading', 'ainbae-receipt-upload-for-woocommerce'), __('Main title at the top of the widget', 'ainbae-receipt-upload-for-woocommerce')), 'label_subtitle' => array(__('Subtitle', 'ainbae-receipt-upload-for-woocommerce'), __('Instruction text below the heading', 'ainbae-receipt-upload-for-woocommerce')), 'label_dropzone' => array(__('Drop Zone Text', 'ainbae-receipt-upload-for-woocommerce'), __('Text inside the file drop area', 'ainbae-receipt-upload-for-woocommerce')), 'label_upload_btn' => array(__('Upload Button', 'ainbae-receipt-upload-for-woocommerce'), __('Label on the upload button', 'ainbae-receipt-upload-for-woocommerce')), 'label_wa_btn' => array(__('WhatsApp Button', 'ainbae-receipt-upload-for-woocommerce'), __('Label on the WhatsApp button', 'ainbae-receipt-upload-for-woocommerce')), 'label_hint' => array(__('Hint Text', 'ainbae-receipt-upload-for-woocommerce'), __('Small text below the upload button', 'ainbae-receipt-upload-for-woocommerce')), ); foreach ($labels as $key => list($title, $desc)) : ?>

get_billing_first_name() : ''; $billing_last = method_exists($order, 'get_billing_last_name') ? $order->get_billing_last_name() : ''; $customer_name = trim($billing_first . ' ' . $billing_last); if (empty($customer_name)) { $customer_name = __('Customer', 'ainbae-receipt-upload-for-woocommerce'); } $order_date = method_exists($order, 'get_date_created') && $order->get_date_created() ? $order->get_date_created()->date_i18n(get_option('date_format')) : ''; $variables = apply_filters('ainbae_bacs_whatsapp_template_variables', array( '{order_number}' => $order->get_order_number(), '{order_total}' => $order->get_total(), '{customer_name}' => $customer_name, '{billing_email}' => $order->get_billing_email(), '{billing_phone}' => $order->get_billing_phone(), '{site_name}' => get_bloginfo('name'), '{currency}' => $order->get_currency(), '{order_date}' => $order_date, ), $order); $message = str_replace(array_keys($variables), array_values($variables), $template); return apply_filters('ainbae_bacs_whatsapp_message', $message, $order); } // ───────────────────────────────────────────────────────────────────────────── // PRIVATE UPLOAD DIRECTORY // ───────────────────────────────────────────────────────────────────────────── function ainbae_bacs_get_private_upload_dir() { $upload_dir = wp_upload_dir(); $base = $upload_dir['basedir'] . '/ainbae-receipt-upload-for-woocommerce'; if (! file_exists($base)) { wp_mkdir_p($base); } $htaccess = $base . '/.htaccess'; if (! file_exists($htaccess)) { global $wp_filesystem; if (empty($wp_filesystem)) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } $wp_filesystem->put_contents( $htaccess, "# Block all direct HTTP access\n\nRequire all denied\n\n\nOrder deny,allow\nDeny from all\n\n", FS_CHMOD_FILE ); } $index = $base . '/index.php'; if (! file_exists($index)) { global $wp_filesystem; if (empty($wp_filesystem)) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } $wp_filesystem->put_contents($index, ' 'image/jpeg', 'png' => 'image/png', 'pdf' => 'application/pdf'); } function ainbae_bacs_current_user_can_access_order($order, $order_key = '') { $uid = get_current_user_id(); if ($uid && $uid === $order->get_customer_id()) return true; if (! empty($order_key) && hash_equals($order->get_order_key(), $order_key)) return true; return false; } function ainbae_bacs_rate_limit_exceeded($user_key) { $tk = 'ainbae_bacs_rl_' . md5($user_key); $count = (int) get_transient($tk); if ($count >= AINBAE_BACS_RATE_LIMIT_MAX) return true; set_transient($tk, $count + 1, AINBAE_BACS_RATE_LIMIT_WINDOW); return false; } // ───────────────────────────────────────────────────────────────────────────── // PART 1 — FRONTEND FORM // ───────────────────────────────────────────────────────────────────────────── add_action('woocommerce_order_details_before_order_table', 'ainbae_bacs_receipt_upload_form', 10, 1); // ───────────────────────────────────────────────────────────────────────────── // PUBLIC DEVELOPER API // ───────────────────────────────────────────────────────────────────────────── /** * Render the receipt upload widget for a given order. * * Public helper function that allows developers to embed the receipt upload * widget anywhere within WooCommerce — for example, on custom Thank You pages, * checkout customizations, theme templates, WooCommerce extensions, or custom * account pages. * * This function accepts either a WooCommerce Order object or an Order ID, * validates the input, and delegates entirely to the internal rendering * function. No widget code is duplicated. * * Usage: * * // Using a WC_Order object: * ainbae_render_receipt_upload( $order ); * * // Using an Order ID: * ainbae_render_receipt_upload( 123 ); * * // Inside a WooCommerce hook: * add_action( 'woocommerce_thankyou', function( $order_id ) { * ainbae_render_receipt_upload( $order_id ); * } ); * * @since 2.1.1 * * @param WC_Order|int $order WooCommerce Order object or a valid Order ID. * @return void */ function ainbae_render_receipt_upload($order) { if (is_numeric($order)) { $order = wc_get_order(absint($order)); } if (! $order instanceof WC_Order) { return; } ainbae_bacs_receipt_upload_form($order); } /** * Action hook: ainbae_receipt_upload_widget * * Fires the receipt upload widget for a given order. Themes and plugins can * trigger this hook to embed the widget without calling any internal functions * directly, making integration fully forward-compatible. * * The hook accepts either a WC_Order object or an Order ID (integer). * * Usage: * * // Using a WC_Order object: * do_action( 'ainbae_receipt_upload_widget', $order ); * * // Using an Order ID: * do_action( 'ainbae_receipt_upload_widget', 123 ); * * @since 2.1.1 * * @param WC_Order|int $order WooCommerce Order object or a valid Order ID. */ add_action('ainbae_receipt_upload_widget', function ($order) { ainbae_render_receipt_upload($order); }); // ───────────────────────────────────────────────────────────────────────────── // INTERNAL RENDERING // ───────────────────────────────────────────────────────────────────────────── /** * Internal: Render the receipt upload widget HTML for an order. * * Core rendering function. Checks payment method, order status, user access, * and whether a receipt has already been uploaded before outputting the widget. * * For public-facing usage, prefer ainbae_render_receipt_upload() or the * 'ainbae_receipt_upload_widget' action hook instead of calling this function * directly. * * @since 1.0.0 * * @param WC_Order $order A valid WooCommerce Order object. * @return void */ function ainbae_bacs_receipt_upload_form($order) { if ($order->get_payment_method() !== 'bacs') return; $status = $order->get_status(); // Statuses where the upload widget and all notices are irrelevant — bail immediately. if (in_array($status, array('completed', 'cancelled', 'refunded', 'failed', 'delivered'), true)) return; // Ensure required CSS and JS assets are available. On standard WooCommerce // pages (view-order, order-received) assets are already enqueued via the // wp_enqueue_scripts hook. On custom pages (e.g. custom Track Order pages) // this defensive call guarantees the widget renders with full styling and // JavaScript functionality. ainbae_bacs_ensure_public_assets(); if ($status === 'processing') { echo '
' . esc_html__('Payment Verified.', 'ainbae-receipt-upload-for-woocommerce') . ' ' . esc_html__('Your order is being processed.', 'ainbae-receipt-upload-for-woocommerce') . '
'; return; } $order_id = $order->get_id(); $order_key = $order->get_order_key(); if (! ainbae_bacs_current_user_can_access_order($order, $order_key)) return; if (get_post_meta($order_id, '_ainbae_bacs_receipt_path', true)) { if (WC()->session && WC()->session->get('ainbae_bacs_just_uploaded_' . $order_id)) { WC()->session->set('ainbae_bacs_just_uploaded_' . $order_id, false); // On checkout thank-you pages, WooCommerce does not print session notices. // We output the green success notice ourselves here. On my-account view-order pages, // we return empty and let WooCommerce print the queue notice. if (function_exists('is_wc_endpoint_url') && is_wc_endpoint_url('order-received')) { echo '
' . esc_html__('Receipt uploaded successfully.', 'ainbae-receipt-upload-for-woocommerce') . ' ' . esc_html__('Thank you! We will verify your payment shortly.', 'ainbae-receipt-upload-for-woocommerce') . '
'; return; } return; } // If the order is still on-hold the receipt is awaiting verification — show the standard notice. // For any other status (including store-defined custom statuses such as "Delivery Failed", // "Ready to Ship", "Refund Requested", etc.) the receipt has already been actioned by the // store, so we show a neutral informational notice instead of re-showing the upload prompt. if ($status === 'on-hold') { echo '
' . esc_html__('Payment Receipt Uploaded.', 'ainbae-receipt-upload-for-woocommerce') . ' ' . esc_html__('We will verify your payment shortly.', 'ainbae-receipt-upload-for-woocommerce') . '
'; } else { echo '
' . esc_html__('Payment Receipt Submitted.', 'ainbae-receipt-upload-for-woocommerce') . ' ' . esc_html__('Your receipt has been received and your order is being handled.', 'ainbae-receipt-upload-for-woocommerce') . '
'; } return; } $wa_enabled = ainbae_bacs_setting('whatsapp_enabled') === '1'; $wa_number = ainbae_bacs_get_whatsapp_number(); $wa_message = ainbae_bacs_get_whatsapp_message($order); $wa_link = 'https://wa.me/' . rawurlencode($wa_number) . '?text=' . rawurlencode($wa_message); // Inject settings as CSS custom properties $dynamic_styles = sprintf( '--bacs-card-bg: %s; --bacs-card-border: %s; --bacs-card-radius: %dpx; --bacs-heading: %s; --bacs-subtitle: %s; --bacs-dropzone-border: %s; --bacs-dropzone-bg: %s; --bacs-icon: %s; --bacs-btn-from: %s; --bacs-btn-to: %s; --bacs-btn-text: %s; --bacs-hint: %s; --bacs-or-text: %s; --bacs-or-line: %s; --bacs-wa-bg: %s; --bacs-wa-text: %s; --bacs-wa-border: %s;', esc_attr(ainbae_bacs_setting('color_card_bg')), esc_attr(ainbae_bacs_setting('color_card_border')), absint(ainbae_bacs_setting('card_border_radius')), esc_attr(ainbae_bacs_setting('color_heading')), esc_attr(ainbae_bacs_setting('color_subtitle')), esc_attr(ainbae_bacs_setting('color_dropzone_border')), esc_attr(ainbae_bacs_setting('color_dropzone_bg')), esc_attr(ainbae_bacs_setting('color_icon')), esc_attr(ainbae_bacs_setting('color_upload_btn_from')), esc_attr(ainbae_bacs_setting('color_upload_btn_to')), esc_attr(ainbae_bacs_setting('color_upload_btn_text')), esc_attr(ainbae_bacs_setting('color_hint')), esc_attr(ainbae_bacs_setting('color_or_text')), esc_attr(ainbae_bacs_setting('color_or_line')), esc_attr(ainbae_bacs_setting('color_wa_btn_bg')), esc_attr(ainbae_bacs_setting('color_wa_btn_text')), esc_attr(ainbae_bacs_setting('color_wa_btn_border')) ); ?>

get_user_id() is the most reliable method of determining // whether the order belongs to a registered customer. $user_id = $order->get_user_id(); if ($user_id > 0) { return $order->get_view_order_url(); } return $order->get_checkout_order_received_url(); } /** * Common helper to validate and handle raw receipt file upload to private directory. * * @param array $file_input The $_FILES entry for the uploaded receipt file. * @param string $prefix Prefix to prepend to the unique UUID filename (e.g. 'checkout-'). * @return array|WP_Error Array of uploaded file data on success, or WP_Error on failure. */ function ainbae_bacs_handle_receipt_file_upload($file_input, $prefix = '') { if (empty($file_input) || ! isset($file_input['tmp_name']) || ! is_uploaded_file($file_input['tmp_name'])) { return new WP_Error('no_file', __('No file received or upload error.', 'ainbae-receipt-upload-for-woocommerce')); } if ((int) $file_input['error'] !== UPLOAD_ERR_OK) { $err_code = (int) $file_input['error']; /* translators: %d: file upload error code */ return new WP_Error('upload_error', sprintf(__('Upload error (code %d).', 'ainbae-receipt-upload-for-woocommerce'), $err_code)); } if ($file_input['size'] > AINBAE_BACS_MAX_UPLOAD_SIZE) { return new WP_Error('file_too_large', __('File is too large. Maximum size is 5 MB.', 'ainbae-receipt-upload-for-woocommerce')); } // Load WP upload helpers if (! function_exists('wp_handle_upload')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $private_dir = ainbae_bacs_get_private_upload_dir(); $uuid = wp_generate_uuid4(); $custom_upload_dir = function ($dirs) use ($private_dir) { $dirs['path'] = untrailingslashit($private_dir); $dirs['url'] = ''; $dirs['subdir'] = ''; return $dirs; }; add_filter('upload_dir', $custom_upload_dir); $uploaded = wp_handle_upload( $file_input, array( 'test_form' => false, 'mimes' => ainbae_bacs_allowed_mimes(), 'unique_filename_callback' => function ($dir, $name, $ext) use ($uuid, $prefix) { return $prefix . $uuid . $ext; }, ) ); remove_filter('upload_dir', $custom_upload_dir); if (isset($uploaded['error'])) { return new WP_Error('wp_upload_error', $uploaded['error']); } if (empty($uploaded['file']) || empty($uploaded['type'])) { return new WP_Error('invalid_type', __('Invalid file type. Only JPG, PNG, and PDF are accepted.', 'ainbae-receipt-upload-for-woocommerce')); } return $uploaded; } // ───────────────────────────────────────────────────────────────────────────── // PART 2 — PROCESS UPLOAD // ───────────────────────────────────────────────────────────────────────────── add_action('template_redirect', 'ainbae_bacs_process_receipt_upload'); function ainbae_bacs_process_receipt_upload() { if (! isset($_POST['submit_ainbae_bacs_receipt']) || ! isset($_FILES['ainbae_bacs_receipt_file'])) { return; } $order_id = isset($_POST['ainbae_bacs_order_id']) ? absint(wp_unslash($_POST['ainbae_bacs_order_id'])) : 0; if ( ! $order_id || ! isset($_POST['ainbae_bacs_receipt_nonce']) || ! wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['ainbae_bacs_receipt_nonce'])), 'ainbae_bacs_upload_receipt_' . $order_id ) ) { wc_add_notice(__('Security check failed. Please refresh the page and try again.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } $order = wc_get_order($order_id); if (! $order || $order->get_payment_method() !== 'bacs') { wc_add_notice(__('Invalid order.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } $order_key = isset($_POST['ainbae_bacs_order_key']) ? sanitize_text_field(wp_unslash($_POST['ainbae_bacs_order_key'])) : ''; if (! ainbae_bacs_current_user_can_access_order($order, $order_key)) { wc_add_notice(__('You do not have permission to upload a receipt for this order.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } if (in_array($order->get_status(), array('completed', 'processing', 'cancelled', 'refunded', 'failed', 'delivered'), true)) { wc_add_notice(__('A receipt cannot be uploaded for this order in its current status.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } if (get_post_meta($order_id, '_ainbae_bacs_receipt_path', true)) { wc_add_notice(__('A receipt has already been uploaded for this order.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown'; $actor = get_current_user_id() ? 'user_' . get_current_user_id() : 'ip_' . $ip; if (ainbae_bacs_rate_limit_exceeded($actor)) { wc_add_notice(__('Too many upload attempts. Please wait before trying again.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } // ── Handle upload using helper ──────────────────────────────────────────── // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $file_input = isset($_FILES['ainbae_bacs_receipt_file']) ? $_FILES['ainbae_bacs_receipt_file'] : null; $uploaded = ainbae_bacs_handle_receipt_file_upload($file_input); if (is_wp_error($uploaded)) { wc_add_notice($uploaded->get_error_message(), 'error'); return; } $dest = $uploaded['file']; // ── Persist receipt metadata ────────────────────────────────────────────── update_post_meta($order_id, '_ainbae_bacs_receipt_path', $dest); update_post_meta($order_id, '_ainbae_bacs_receipt_mime', sanitize_mime_type($uploaded['type'])); update_post_meta($order_id, '_ainbae_bacs_receipt_uploaded', current_time('mysql')); /* translators: %s: Date and time when the receipt was uploaded (e.g. 2025-01-17 14:30:00). */ $order->add_order_note(sprintf(__('Customer uploaded a bank transfer receipt on %s. Use the admin panel to view it securely.', 'ainbae-receipt-upload-for-woocommerce'), current_time('mysql'))); if (WC()->session) { WC()->session->set('ainbae_bacs_just_uploaded_' . $order_id, true); } wc_add_notice(__('Receipt uploaded successfully. Thank you! We will verify your payment shortly.', 'ainbae-receipt-upload-for-woocommerce'), 'success'); wp_safe_redirect(ainbae_bacs_get_upload_redirect_url($order)); exit; } // ───────────────────────────────────────────────────────────────────────────── // PART 3 — ADMIN ORDER PANEL & SECURE FILE VIEWER // ───────────────────────────────────────────────────────────────────────────── /** * Try to recover broken receipt metadata for an order. * If a file exists at the expected path but meta is missing/broken, rebuild it. * * @param int $order_id * @return string|false Recovered file path, or false. */ function ainbae_bacs_recover_receipt_meta($order_id) { $order = wc_get_order($order_id); if (! $order) return false; // Scan order notes for the upload note keyword $notes = wc_get_order_notes(array('order_id' => $order_id, 'type' => 'order')); $note_found = false; foreach ($notes as $note) { if (false !== strpos($note->content, 'bank transfer receipt')) { $note_found = true; break; } } if (! $note_found) return false; // Scan the private upload directory for any file associated with this order. // Files are UUID-named so we cannot match by order ID directly; // instead we look for any file and attempt to restore. $dir = ainbae_bacs_get_private_upload_dir(); if (! is_dir($dir)) return false; $files = glob($dir . '*'); if (empty($files)) return false; // Attempt to match by upload timestamp in notes vs file mtime. $recovered = false; foreach ($files as $file) { if (! is_file($file)) continue; $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); $mimes = array('jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'pdf' => 'application/pdf'); if (! isset($mimes[$ext])) continue; update_post_meta($order_id, '_ainbae_bacs_receipt_path', $file); update_post_meta($order_id, '_ainbae_bacs_receipt_mime', $mimes[$ext]); if (! get_post_meta($order_id, '_ainbae_bacs_receipt_uploaded', true)) { update_post_meta($order_id, '_ainbae_bacs_receipt_uploaded', gmdate('Y-m-d H:i:s', filemtime($file))); } $order->add_order_note(__('Receipt metadata recovered automatically by Ainbae Receipt Upload.', 'ainbae-receipt-upload-for-woocommerce')); if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log(sprintf('Ainbae Receipt Upload: Metadata recovered for order #%d. File: %s', $order_id, $file)); } $recovered = $file; break; } return $recovered; } add_action('woocommerce_admin_order_data_after_order_details', 'ainbae_bacs_display_receipt_in_admin', 10, 1); /** * Display receipt panel in WooCommerce admin order screen. * Uses a 5-priority detection system before showing "No receipt uploaded yet". * * Priority 1: Attachment ID * Priority 2: Stored file path (meta) * Priority 3: Stored receipt URL (meta) * Priority 4: Legacy metadata * Priority 5: Metadata recovery attempt * * @param WC_Order $order */ function ainbae_bacs_display_receipt_in_admin($order) { if ($order->get_payment_method() !== 'bacs') return; $order_id = $order->get_id(); $uploaded = get_post_meta($order_id, '_ainbae_bacs_receipt_uploaded', true); $receipt_found = false; $receipt_url = ''; echo '

' . esc_html__('Bank Transfer Receipt', 'ainbae-receipt-upload-for-woocommerce') . '

'; // ── Priority 1: Attachment ID ──────────────────────────────────────────── $attachment_id = get_post_meta($order_id, '_ainbae_bacs_receipt_attachment_id', true); if ($attachment_id && wp_attachment_is_image($attachment_id)) { $att_url = wp_get_attachment_url($attachment_id); if ($att_url) { $receipt_url = $att_url; $receipt_found = true; if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log(sprintf('Ainbae Receipt Upload: Admin display resolved via attachment ID for order #%d.', $order_id)); } } } // ── Priority 2: Stored file path ───────────────────────────────────────── if (! $receipt_found) { $path = get_post_meta($order_id, '_ainbae_bacs_receipt_path', true); if ($path && file_exists($path) && is_file($path)) { $receipt_found = true; // Use the secure admin-post endpoint (nonce-authenticated, admin only) } elseif ($path) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log(sprintf('Ainbae Receipt Upload: Path meta found but file missing for order #%d. Path: %s', $order_id, $path)); } } } // ── Priority 3: Stored receipt URL ─────────────────────────────────────── if (! $receipt_found) { $meta_url = get_post_meta($order_id, '_ainbae_bacs_receipt_url', true); if ($meta_url) { $receipt_url = $meta_url; $receipt_found = true; } } // ── Priority 4: Legacy metadata ────────────────────────────────────────── if (! $receipt_found) { $legacy_path = get_post_meta($order_id, '_ainbae_bacs_receipt_file', true); if ($legacy_path && file_exists($legacy_path) && is_file($legacy_path)) { // Migrate legacy meta to current key update_post_meta($order_id, '_ainbae_bacs_receipt_path', $legacy_path); $receipt_found = true; if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log(sprintf('Ainbae Receipt Upload: Legacy meta migrated for order #%d.', $order_id)); } } } // ── Priority 5: Metadata recovery attempt ──────────────────────────────── if (! $receipt_found) { $recovered = ainbae_bacs_recover_receipt_meta($order_id); if ($recovered) { $receipt_found = true; } } // ── Render ──────────────────────────────────────────────────────────────── if ($receipt_found) { if ($receipt_url) { // Direct URL (attachment or stored URL) echo '

' . esc_html__('View Uploaded Receipt', 'ainbae-receipt-upload-for-woocommerce') . ''; } else { // Secure admin-post endpoint for private file $url = wp_nonce_url( add_query_arg( array('action' => 'ainbae_bacs_view_receipt', 'order_id' => $order_id), admin_url('admin-post.php') ), 'ainbae_bacs_view_receipt_' . $order_id ); echo '

' . esc_html__('View Uploaded Receipt', 'ainbae-receipt-upload-for-woocommerce') . ''; } if ($uploaded) { echo '  ' . esc_html__('Uploaded:', 'ainbae-receipt-upload-for-woocommerce') . ' ' . esc_html($uploaded) . ''; } echo '

'; } else { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log(sprintf('Ainbae Receipt Upload: All receipt checks failed for order #%d. Showing "No receipt" message.', $order_id)); } echo '

' . esc_html__('No receipt uploaded yet.', 'ainbae-receipt-upload-for-woocommerce') . '

'; } } add_action('admin_post_ainbae_bacs_view_receipt', 'ainbae_bacs_serve_receipt_to_admin'); function ainbae_bacs_serve_receipt_to_admin() { if (! current_user_can('manage_woocommerce')) { wp_die(esc_html__('Access denied.', 'ainbae-receipt-upload-for-woocommerce'), 403); } $order_id = isset($_GET['order_id']) ? absint(wp_unslash($_GET['order_id'])) : 0; if (! $order_id || ! check_admin_referer('ainbae_bacs_view_receipt_' . $order_id)) { wp_die(esc_html__('Invalid request.', 'ainbae-receipt-upload-for-woocommerce'), 400); } $path = get_post_meta($order_id, '_ainbae_bacs_receipt_path', true); $mime = get_post_meta($order_id, '_ainbae_bacs_receipt_mime', true); // ── Validate path exists ────────────────────────────────────────────── if (! $path || ! file_exists($path) || ! is_file($path)) { wp_die(esc_html__('Receipt not found.', 'ainbae-receipt-upload-for-woocommerce'), 404); } // ── Path traversal protection ───────────────────────────────────────── $real_path = realpath($path); $real_dir = realpath(ainbae_bacs_get_private_upload_dir()); // Legacy: receipts uploaded by v1.0.1 were stored in wp-content/bacs-receipts-private/. $upload_info = wp_upload_dir(); $content_base = dirname($upload_info['basedir']); $real_dir_legacy = realpath($content_base . '/bacs-receipts-private'); $in_current = $real_dir && $real_path && strpos($real_path, $real_dir) === 0; $in_legacy = $real_dir_legacy && $real_path && strpos($real_path, $real_dir_legacy) === 0; if (false === $real_path || (! $in_current && ! $in_legacy)) { wp_die(esc_html__('Access denied.', 'ainbae-receipt-upload-for-woocommerce'), 403); } // ── MIME type validation ────────────────────────────────────────────── if (! $mime || ! in_array($mime, ainbae_bacs_allowed_mimes(), true)) { wp_die(esc_html__('Invalid file type.', 'ainbae-receipt-upload-for-woocommerce'), 400); } // ── Serve file ──────────────────────────────────────────────────────── $ext = pathinfo($real_path, PATHINFO_EXTENSION); $file_size = filesize($real_path); if (false === $file_size || 0 === $file_size) { wp_die(esc_html__('Could not read the receipt file.', 'ainbae-receipt-upload-for-woocommerce'), 500); } // WordPress (and plugins) may have buffered output during admin-post.php // initialization. Any buffered HTML prepended to binary data will corrupt // the image/PDF stream. Discard every active output buffer before serving. while (ob_get_level() > 0) { ob_end_clean(); } nocache_headers(); header('Content-Type: ' . sanitize_mime_type($mime)); header('Content-Length: ' . $file_size); header('Content-Disposition: inline; filename="receipt-order-' . $order_id . '.' . esc_attr($ext) . '"'); header('X-Content-Type-Options: nosniff'); // Stream the binary file directly — readfile() outputs straight to the // client without loading data into a PHP variable, avoiding any // escaping concerns for binary content (images / PDFs). // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Binary streaming to browser; WP_Filesystem has no streaming equivalent. readfile($real_path); exit; } // ───────────────────────────────────────────────────────────────────────────── // PART 4 — SETTINGS PAGE: WHATSAPP TEMPLATE & CHECKOUT BEHAVIOUR CARDS (v2.0.0) // ───────────────────────────────────────────────────────────────────────────── // NOTE: The new fields are injected into the existing settings page render // function via the helper below. The render function calls // ainbae_bacs_render_whatsapp_extra_fields() and // ainbae_bacs_render_checkout_behaviour_card() at the appropriate points. // Because the render function is defined earlier in this file and is not easily // split, the two new setting blocks are added inline via output buffering // hooks on a custom action fired from inside the render function. // ───────────────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────────────── // PART 5 — CHECKOUT UPLOAD AJAX HANDLERS (v2.0.0 — Feature 2) // ───────────────────────────────────────────────────────────────────────────── // Return a fresh nonce for the checkout upload form. add_action('wp_ajax_ainbae_bacs_get_checkout_nonce', 'ainbae_bacs_ajax_get_checkout_nonce'); add_action('wp_ajax_nopriv_ainbae_bacs_get_checkout_nonce', 'ainbae_bacs_ajax_get_checkout_nonce'); function ainbae_bacs_ajax_get_checkout_nonce() { wp_send_json_success(array('nonce' => wp_create_nonce('ainbae_bacs_checkout_upload'))); } // Handle pre-checkout receipt upload. add_action('wp_ajax_ainbae_bacs_checkout_upload', 'ainbae_bacs_ajax_checkout_upload'); add_action('wp_ajax_nopriv_ainbae_bacs_checkout_upload', 'ainbae_bacs_ajax_checkout_upload'); function ainbae_bacs_ajax_checkout_upload() { // Nonce check if ( ! isset($_POST['nonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'ainbae_bacs_checkout_upload') ) { wp_send_json_error(array('message' => __('Security check failed.', 'ainbae-receipt-upload-for-woocommerce'))); } // Rate limiting $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown'; $actor = get_current_user_id() ? 'user_' . get_current_user_id() : 'ip_' . $ip; if (ainbae_bacs_rate_limit_exceeded($actor)) { wp_send_json_error(array('message' => __('Too many upload attempts. Please wait before trying again.', 'ainbae-receipt-upload-for-woocommerce'))); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $file_input = isset($_FILES['ainbae_bacs_checkout_receipt']) ? $_FILES['ainbae_bacs_checkout_receipt'] : null; $uploaded = ainbae_bacs_handle_receipt_file_upload($file_input, 'checkout-'); if (is_wp_error($uploaded)) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('Ainbae Receipt Upload (checkout): ' . $uploaded->get_error_message()); } wp_send_json_error(array('message' => $uploaded->get_error_message())); } // Store temp data in WooCommerce session $token = wp_generate_uuid4(); WC()->session->set('ainbae_bacs_checkout_receipt_' . $token, array( 'path' => $uploaded['file'], 'mime' => sanitize_mime_type($uploaded['type']), 'time' => current_time('mysql'), )); wp_send_json_success(array('token' => $token)); } // Handle post-checkout receipt upload. add_action('wp_ajax_ainbae_bacs_order_upload', 'ainbae_bacs_ajax_order_upload'); add_action('wp_ajax_nopriv_ainbae_bacs_order_upload', 'ainbae_bacs_ajax_order_upload'); function ainbae_bacs_ajax_order_upload() { // Nonce check $order_id = isset($_POST['ainbae_bacs_order_id']) ? absint(wp_unslash($_POST['ainbae_bacs_order_id'])) : 0; if ( ! $order_id || ! isset($_POST['nonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'ainbae_bacs_upload_receipt_' . $order_id) ) { wp_send_json_error(array('message' => __('Security check failed.', 'ainbae-receipt-upload-for-woocommerce'))); } $order = wc_get_order($order_id); if (! $order || $order->get_payment_method() !== 'bacs') { wp_send_json_error(array('message' => __('Invalid order.', 'ainbae-receipt-upload-for-woocommerce'))); } $order_key = isset($_POST['ainbae_bacs_order_key']) ? sanitize_text_field(wp_unslash($_POST['ainbae_bacs_order_key'])) : ''; if (! ainbae_bacs_current_user_can_access_order($order, $order_key)) { wp_send_json_error(array('message' => __('You do not have permission to upload a receipt for this order.', 'ainbae-receipt-upload-for-woocommerce'))); } if (in_array($order->get_status(), array('completed', 'processing', 'cancelled', 'refunded', 'failed', 'delivered'), true)) { wp_send_json_error(array('message' => __('A receipt cannot be uploaded for this order in its current status.', 'ainbae-receipt-upload-for-woocommerce'))); } if (get_post_meta($order_id, '_ainbae_bacs_receipt_path', true)) { wp_send_json_error(array('message' => __('A receipt has already been uploaded for this order.', 'ainbae-receipt-upload-for-woocommerce'))); } // Rate limiting $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown'; $actor = get_current_user_id() ? 'user_' . get_current_user_id() : 'ip_' . $ip; if (ainbae_bacs_rate_limit_exceeded($actor)) { wp_send_json_error(array('message' => __('Too many upload attempts. Please wait before trying again.', 'ainbae-receipt-upload-for-woocommerce'))); } // File input check // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $file_input = isset($_FILES['ainbae_bacs_receipt_file']) ? $_FILES['ainbae_bacs_receipt_file'] : null; $uploaded = ainbae_bacs_handle_receipt_file_upload($file_input); if (is_wp_error($uploaded)) { wp_send_json_error(array('message' => $uploaded->get_error_message())); } $dest = $uploaded['file']; // Persist receipt metadata update_post_meta($order_id, '_ainbae_bacs_receipt_path', $dest); update_post_meta($order_id, '_ainbae_bacs_receipt_mime', sanitize_mime_type($uploaded['type'])); update_post_meta($order_id, '_ainbae_bacs_receipt_uploaded', current_time('mysql')); /* translators: %s: Date and time when the receipt was uploaded (e.g. 2025-01-17 14:30:00). */ $order->add_order_note(sprintf(__('Customer uploaded a bank transfer receipt on %s. Use the admin panel to view it securely.', 'ainbae-receipt-upload-for-woocommerce'), current_time('mysql'))); if (WC()->session) { WC()->session->set('ainbae_bacs_just_uploaded_' . $order_id, true); } // Add WC notice so that when user redirects, they see the success message wc_add_notice(__('Receipt uploaded successfully. Thank you! We will verify your payment shortly.', 'ainbae-receipt-upload-for-woocommerce'), 'success'); wp_send_json_success(array( 'message' => __('Receipt uploaded successfully. Thank you! We will verify your payment shortly.', 'ainbae-receipt-upload-for-woocommerce'), 'redirect_url' => ainbae_bacs_get_upload_redirect_url($order), )); } // ── Render checkout hidden token input ─────────────────────────────────────── add_action('woocommerce_review_order_before_submit', 'ainbae_bacs_checkout_hidden_token_field'); function ainbae_bacs_checkout_hidden_token_field() { if (ainbae_bacs_setting('require_receipt_before_order') === '1') { echo ''; } } // ── Validate receipt token at checkout ──────────────────────────────────────── add_action('woocommerce_checkout_process', 'ainbae_bacs_checkout_require_receipt'); function ainbae_bacs_checkout_require_receipt() { if (ainbae_bacs_setting('require_receipt_before_order') !== '1') { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce verifies its own checkout nonce before firing woocommerce_checkout_process. $payment_method = isset($_POST['payment_method']) ? sanitize_text_field(wp_unslash($_POST['payment_method'])) : ''; if ($payment_method !== 'bacs') { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce verifies its own checkout nonce before firing woocommerce_checkout_process. $token = isset($_POST['ainbae_bacs_checkout_token']) ? sanitize_text_field(wp_unslash($_POST['ainbae_bacs_checkout_token'])) : ''; if (! $token) { $token = isset($_COOKIE['ainbae_bacs_checkout_token']) ? sanitize_text_field(wp_unslash($_COOKIE['ainbae_bacs_checkout_token'])) : ''; } if (! $token) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('Ainbae Receipt Upload (Classic validation error): No receipt token found.'); } wc_add_notice(__('Please upload a payment receipt before placing your order.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } if (! WC()->session) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('Ainbae Receipt Upload (Classic validation error): Session not initialized.'); } wc_add_notice(__('Session error. Please refresh the page and try again.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } $data = WC()->session->get('ainbae_bacs_checkout_receipt_' . $token); if (empty($data) || empty($data['path']) || ! file_exists($data['path'])) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('Ainbae Receipt Upload (Classic validation error): Receipt file not found in session.'); } wc_add_notice(__('Your uploaded receipt could not be found. Please upload again.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); } } // ── Attach receipt to order after it is created (Classic Checkout) ─────────── add_action('woocommerce_checkout_order_created', 'ainbae_bacs_attach_checkout_receipt', 10, 1); function ainbae_bacs_attach_checkout_receipt($order) { if (ainbae_bacs_setting('require_receipt_before_order') !== '1') { return; } if ($order->get_payment_method() !== 'bacs') { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in woocommerce_checkout_process $token = isset($_POST['ainbae_bacs_checkout_token']) ? sanitize_text_field(wp_unslash($_POST['ainbae_bacs_checkout_token'])) : ''; if (! $token) { $token = isset($_COOKIE['ainbae_bacs_checkout_token']) ? sanitize_text_field(wp_unslash($_COOKIE['ainbae_bacs_checkout_token'])) : ''; } if (! $token || ! WC()->session) { return; } $data = WC()->session->get('ainbae_bacs_checkout_receipt_' . $token); if (empty($data) || empty($data['path']) || ! file_exists($data['path'])) { return; } $order_id = $order->get_id(); update_post_meta($order_id, '_ainbae_bacs_receipt_path', $data['path']); update_post_meta($order_id, '_ainbae_bacs_receipt_mime', $data['mime']); update_post_meta($order_id, '_ainbae_bacs_receipt_uploaded', $data['time']); /* translators: %s: date and time of upload */ $order->add_order_note(sprintf(__('Customer uploaded a bank transfer receipt on %s (at checkout). Use the admin panel to view it securely.', 'ainbae-receipt-upload-for-woocommerce'), $data['time'])); // Clear session token and cookie WC()->session->__unset('ainbae_bacs_checkout_receipt_' . $token); if (isset($_COOKIE['ainbae_bacs_checkout_token'])) { setcookie('ainbae_bacs_checkout_token', '', time() - 3600, '/'); } } // ── WooCommerce Blocks / Store API Checkout validation & attachment ───────── add_action('woocommerce_store_api_checkout_order_processed', 'ainbae_bacs_store_api_checkout_validation', 10, 1); function ainbae_bacs_store_api_checkout_validation($order) { if (ainbae_bacs_setting('require_receipt_before_order') !== '1') { return; } if ($order->get_payment_method() !== 'bacs') { return; } $token = isset($_COOKIE['ainbae_bacs_checkout_token']) ? sanitize_text_field(wp_unslash($_COOKIE['ainbae_bacs_checkout_token'])) : ''; if (! $token) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('Ainbae Receipt Upload (Blocks validation error): No receipt token found in cookie.'); } ainbae_bacs_throw_store_api_error(__('Please upload a payment receipt before placing your order.', 'ainbae-receipt-upload-for-woocommerce')); } if (! WC()->session) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('Ainbae Receipt Upload (Blocks validation error): Session not initialized.'); } ainbae_bacs_throw_store_api_error(__('Session error. Please refresh the page and try again.', 'ainbae-receipt-upload-for-woocommerce')); } $data = WC()->session->get('ainbae_bacs_checkout_receipt_' . $token); if (empty($data) || empty($data['path']) || ! file_exists($data['path'])) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('Ainbae Receipt Upload (Blocks validation error): Receipt file not found in session.'); } ainbae_bacs_throw_store_api_error(__('Your uploaded receipt could not be found. Please upload again.', 'ainbae-receipt-upload-for-woocommerce')); } // Attach receipt data to order $order_id = $order->get_id(); update_post_meta($order_id, '_ainbae_bacs_receipt_path', $data['path']); update_post_meta($order_id, '_ainbae_bacs_receipt_mime', $data['mime']); update_post_meta($order_id, '_ainbae_bacs_receipt_uploaded', $data['time']); /* translators: %s: date and time of upload */ $order->add_order_note(sprintf(__('Customer uploaded a bank transfer receipt on %s (at checkout). Use the admin panel to view it securely.', 'ainbae-receipt-upload-for-woocommerce'), $data['time'])); // Clear session token & cookie WC()->session->__unset('ainbae_bacs_checkout_receipt_' . $token); if (isset($_COOKIE['ainbae_bacs_checkout_token'])) { setcookie('ainbae_bacs_checkout_token', '', time() - 3600, '/'); } } function ainbae_bacs_throw_store_api_error($message) { if (class_exists('Automattic\WooCommerce\StoreApi\Exceptions\RouteException')) { throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'ainbae_bacs_checkout_error', esc_html($message), 400 ); } else { throw new \Exception(esc_html($message)); } }