' . 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.0'); 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); } 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)) { 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 ($on_checkout_page && $checkout_feature) { wp_localize_script('ainbae-bacs-public-js', 'ainbaeBacsCheckout', array( 'enabled' => true, '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'), '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'), // 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'), ), )); } else { wp_localize_script('ainbae-bacs-public-js', 'ainbaeBacsCheckout', array('enabled' => false)); } } } // ───────────────────────────────────────────────────────────────────────────── // 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); } ?>
✓
' . 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'))); } // File present? if ( ! isset( $_FILES['ainbae_bacs_checkout_receipt'], $_FILES['ainbae_bacs_checkout_receipt']['error'], $_FILES['ainbae_bacs_checkout_receipt']['size'], $_FILES['ainbae_bacs_checkout_receipt']['tmp_name'] ) || // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated ! is_uploaded_file($_FILES['ainbae_bacs_checkout_receipt']['tmp_name']) ) { wp_send_json_error(array('message' => __('No file received or upload error.', 'ainbae-receipt-upload-for-woocommerce'))); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated if ((int) $_FILES['ainbae_bacs_checkout_receipt']['error'] !== UPLOAD_ERR_OK) { $err_code = (int) $_FILES['ainbae_bacs_checkout_receipt']['error']; /* translators: %d: file upload error code */ wp_send_json_error(array('message' => sprintf(__('Upload error (code %d).', 'ainbae-receipt-upload-for-woocommerce'), $err_code))); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $file = $_FILES['ainbae_bacs_checkout_receipt']; // Size check if ($file['size'] > AINBAE_BACS_MAX_UPLOAD_SIZE) { wp_send_json_error(array('message' => __('File is too large. Maximum size is 5 MB.', 'ainbae-receipt-upload-for-woocommerce'))); } // 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, array( 'test_form' => false, 'mimes' => ainbae_bacs_allowed_mimes(), 'unique_filename_callback' => function ($dir, $name, $ext) use ($uuid) { return 'checkout-' . $uuid . $ext; }, ) ); remove_filter('upload_dir', $custom_upload_dir); if (isset($uploaded['error'])) { if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('Ainbae Receipt Upload (checkout): ' . $uploaded['error']); } wp_send_json_error(array('message' => $uploaded['error'])); } if (empty($uploaded['file']) || empty($uploaded['type'])) { wp_send_json_error(array('message' => __('Invalid file type. Only JPG, PNG, and PDF are accepted.', 'ainbae-receipt-upload-for-woocommerce'))); } // 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)); } // ── 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)); } }