' . 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_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', '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); } // ───────────────────────────────────────────────────────────────────────────── // 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']; $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(), '2.2.0'); wp_enqueue_script('ainbae-bacs-admin-js', plugins_url('admin/js/admin.js', __FILE__), array('jquery', 'wp-color-picker'), '2.2.0', true); } add_action('wp_enqueue_scripts', 'ainbae_bacs_enqueue_public_assets'); function ainbae_bacs_enqueue_public_assets() { if (function_exists('is_wc_endpoint_url') && (is_wc_endpoint_url('view-order') || is_wc_endpoint_url('order-received'))) { wp_enqueue_style('ainbae-bacs-public-css', plugins_url('public/css/public.css', __FILE__), array(), '2.2.0'); wp_enqueue_script('ainbae-bacs-public-js', plugins_url('public/js/public.js', __FILE__), array(), '2.2.0', true); } } // ───────────────────────────────────────────────────────────────────────────── // 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 Logo', 'ainbae-receipt-upload-for-woocommerce'); ?>

✓

>

+
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)) : ?>

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); function ainbae_bacs_receipt_upload_form($order) { if ($order->get_payment_method() !== 'bacs') return; $status = $order->get_status(); if (in_array($status, array('completed', 'cancelled', 'refunded', 'failed', 'delivered'), true)) return; 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)) { echo '
' . esc_html__('Payment Receipt Uploaded.', 'ainbae-receipt-upload-for-woocommerce') . ' ' . esc_html__('We will verify your payment shortly.', 'ainbae-receipt-upload-for-woocommerce') . '
'; return; } $wa_enabled = ainbae_bacs_setting('whatsapp_enabled') === '1'; $wa_number = ainbae_bacs_get_whatsapp_number(); /* translators: %1$s order number, %2$s currency, %3$s total */ $wa_message = sprintf( __("Hello, I am sharing the payment receipt for my recent order.\n\n *Order Number:* %1\$s\n *Amount:* %2\$s %3\$s\n\n Please find the receipt attached below.", 'ainbae-receipt-upload-for-woocommerce'), $order->get_order_number(), $order->get_currency(), $order->get_total() ); $wa_link = 'https://wa.me/' . esc_attr($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_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; } // ── Validate raw $_FILES entry ──────────────────────────────────────────── $file = $_FILES['ainbae_bacs_receipt_file']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput if ($file['error'] !== UPLOAD_ERR_OK) { /* translators: %d: File upload error code number. */ wc_add_notice(sprintf(__('Upload error (code %d).', 'ainbae-receipt-upload-for-woocommerce'), (int) $file['error']), 'error'); return; } if ($file['size'] > AINBAE_BACS_MAX_UPLOAD_SIZE) { wc_add_notice(__('File is too large. Maximum size is 5 MB.', 'ainbae-receipt-upload-for-woocommerce'), 'error'); return; } // ── Load WP upload helpers (needed on front-end) ────────────────────────── if (! function_exists('wp_handle_upload')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } // ── Temporarily redirect wp_handle_upload() to our private directory ────── $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'] = ''; // No public URL — private storage only. $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(), // Give the file our UUID-based name instead of the original filename. 'unique_filename_callback' => function ($dir, $name, $ext) use ($uuid) { return $uuid . $ext; }, ) ); remove_filter('upload_dir', $custom_upload_dir); // ── Handle upload result ────────────────────────────────────────────────── if (isset($uploaded['error'])) { // wp_handle_upload() already returns a translated error string. wc_add_notice($uploaded['error'], 'error'); return; } if (empty($uploaded['file']) || empty($uploaded['type'])) { wc_add_notice(__('Invalid file type. Only JPG, PNG, and PDF are accepted.', 'ainbae-receipt-upload-for-woocommerce'), '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'))); wc_add_notice(__('Receipt uploaded successfully. Thank you! We will verify your payment shortly.', 'ainbae-receipt-upload-for-woocommerce'), 'success'); wp_safe_redirect($order->get_view_order_url()); exit; } // ───────────────────────────────────────────────────────────────────────────── // PART 3 — ADMIN ORDER PANEL & SECURE FILE VIEWER // ───────────────────────────────────────────────────────────────────────────── add_action('woocommerce_admin_order_data_after_order_details', 'ainbae_bacs_display_receipt_in_admin', 10, 1); function ainbae_bacs_display_receipt_in_admin($order) { if ($order->get_payment_method() !== 'bacs') return; $path = get_post_meta($order->get_id(), '_ainbae_bacs_receipt_path', true); $uploaded = get_post_meta($order->get_id(), '_ainbae_bacs_receipt_uploaded', true); echo '

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

'; if ($path && file_exists($path)) { $url = wp_nonce_url(add_query_arg(array('action' => 'ainbae_bacs_view_receipt', 'order_id' => $order->get_id()), admin_url('admin-post.php')), 'ainbae_bacs_view_receipt_' . $order->get_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 { 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; }