' . 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); } ?>
✓
' . 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; }