init_hooks(); } /** * Initialize hooks. */ private function init_hooks() { add_action( 'wp_ajax_afridirect_upload_proof', array( $this, 'upload_proof' ) ); add_action( 'wp_ajax_nopriv_afridirect_upload_proof', array( $this, 'upload_proof' ) ); // For guest orders. add_action( 'wp_ajax_afridirect_approve_order', array( $this, 'approve_order' ) ); add_action( 'wp_ajax_afridirect_reject_order', array( $this, 'reject_order' ) ); add_action( 'wp_ajax_afridirect_get_order_review', array( $this, 'get_order_review' ) ); // Standalone payment handlers. add_action( 'wp_ajax_afridirect_create_payment', array( $this, 'create_payment' ) ); add_action( 'wp_ajax_nopriv_afridirect_create_payment', array( $this, 'create_payment' ) ); add_action( 'wp_ajax_afridirect_approve_payment', array( $this, 'approve_payment' ) ); add_action( 'wp_ajax_afridirect_reject_payment', array( $this, 'reject_payment' ) ); } /** * Handle proof upload (for both WooCommerce orders and standalone payments). */ public function upload_proof() { $logger = afridirect_get_logger(); $user_id = get_current_user_id(); // Verify nonce with safe check that returns JSON instead of exiting. if ( ! check_ajax_referer( 'afridirect-payment-nonce', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'afridirect-payment' ) ), 403 ); } // Check if this is a standalone payment or WooCommerce order. $payment_id = isset( $_POST['payment_id'] ) ? absint( wp_unslash( $_POST['payment_id'] ) ) : 0; // Handle standalone payment upload. if ( $payment_id ) { $this->handle_standalone_upload( $payment_id ); return; } // Handle WooCommerce order upload (existing flow). if ( ! isset( $_POST['order_id'] ) ) { if ( $logger ) { $logger->error( 'AfriDirect upload failed: Invalid order ID', array( 'source' => 'afridirect-payment', 'user_id' => $user_id, 'order_id_post' => 'not set', ) ); } wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'afridirect-payment' ) ), 400 ); } $order_id = absint( wp_unslash( $_POST['order_id'] ) ); if ( ! $order_id ) { if ( $logger ) { $logger->error( 'AfriDirect upload failed: Invalid order ID', array( 'source' => 'afridirect-payment', 'user_id' => $user_id, 'order_id_post' => isset( $_POST['order_id'] ) ? sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : 'not set', ) ); } wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'afridirect-payment' ) ), 400 ); } if ( ! class_exists( 'WooCommerce' ) ) { wp_send_json_error( array( 'message' => __( 'WooCommerce is required for order uploads.', 'afridirect-payment' ) ) ); } // Check if file was uploaded. if ( ! isset( $_FILES['proof_file'] ) || empty( $_FILES['proof_file'] ) || ! is_array( $_FILES['proof_file'] ) ) { $logger->error( 'AfriDirect upload failed: No file in request', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, ) ); wp_send_json_error( array( 'message' => __( 'No file uploaded. Please select a file.', 'afridirect-payment' ) ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File arrays cannot be sanitized like text; validation happens below. $file = isset( $_FILES['proof_file'] ) ? wp_unslash( $_FILES['proof_file'] ) : array(); // Validate file array structure. if ( ! isset( $file['error'] ) || ! isset( $file['name'] ) || ! isset( $file['tmp_name'] ) || ! isset( $file['size'] ) ) { wp_send_json_error( array( 'message' => __( 'Invalid file upload structure.', 'afridirect-payment' ) ) ); } // Validate tmp_name exists and error is 0. if ( empty( $file['tmp_name'] ) || ! file_exists( $file['tmp_name'] ) ) { wp_send_json_error( array( 'message' => __( 'Invalid file upload.', 'afridirect-payment' ) ) ); } // Check for upload errors. $error_code = (int) $file['error']; if ( $error_code !== UPLOAD_ERR_OK ) { $error_messages = array( UPLOAD_ERR_INI_SIZE => __( 'File exceeds upload_max_filesize directive in php.ini.', 'afridirect-payment' ), UPLOAD_ERR_FORM_SIZE => __( 'File exceeds MAX_FILE_SIZE directive in HTML form.', 'afridirect-payment' ), UPLOAD_ERR_PARTIAL => __( 'File was only partially uploaded.', 'afridirect-payment' ), UPLOAD_ERR_NO_FILE => __( 'No file was uploaded.', 'afridirect-payment' ), UPLOAD_ERR_NO_TMP_DIR => __( 'Missing temporary folder.', 'afridirect-payment' ), UPLOAD_ERR_CANT_WRITE => __( 'Failed to write file to disk.', 'afridirect-payment' ), UPLOAD_ERR_EXTENSION => __( 'File upload stopped by extension.', 'afridirect-payment' ), ); $error_msg = isset( $error_messages[ $error_code ] ) ? $error_messages[ $error_code ] : __( 'Unknown upload error.', 'afridirect-payment' ); $file_name = isset( $file['name'] ) ? sanitize_file_name( wp_unslash( $file['name'] ) ) : 'unknown'; $file_size = isset( $file['size'] ) ? absint( $file['size'] ) : 0; if ( $logger ) { $logger->error( 'AfriDirect upload failed: Upload error ' . $error_code . ' - ' . $error_msg, array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'error_code' => $error_code, 'file_name' => $file_name, 'file_size' => $file_size, ) ); } wp_send_json_error( array( 'message' => $error_msg ) ); } // Sanitize filename. $sanitized_filename = sanitize_file_name( wp_unslash( $file['name'] ) ); // Check file size (5MB max). $file_size = absint( $file['size'] ); $max_size = 5 * 1024 * 1024; // 5MB in bytes. if ( $file_size > $max_size ) { $file_size_mb = round( $file_size / 1024 / 1024, 2 ); if ( $logger ) { $logger->error( 'AfriDirect upload failed: File too large', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'file_name' => $sanitized_filename, 'file_size' => $file_size, 'file_size_mb' => $file_size_mb, 'max_size' => $max_size, ) ); } /* translators: %.2f: File size in megabytes */ wp_send_json_error( array( 'message' => sprintf( __( 'File size (%.2f MB) exceeds 5MB limit.', 'afridirect-payment' ), $file_size_mb ) ) ); } // Validate file type using wp_check_filetype_and_ext. $allowed_mimes = array( 'jpg|jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'pdf' => 'application/pdf', ); $filetype_check = wp_check_filetype_and_ext( $file['tmp_name'], $sanitized_filename, $allowed_mimes ); $allowed_types = array( 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf' ); $allowed_extensions = array( 'jpg', 'jpeg', 'png', 'webp', 'pdf' ); if ( empty( $filetype_check['ext'] ) || empty( $filetype_check['type'] ) ) { if ( $logger ) { $logger->error( 'AfriDirect upload failed: Invalid file type (no ext/type)', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'file_name' => $sanitized_filename, ) ); } wp_send_json_error( array( 'message' => __( 'Invalid file type. Allowed: JPG, JPEG, PNG, WEBP, PDF.', 'afridirect-payment' ) ) ); } // Validate extension and mime type. if ( ! in_array( strtolower( $filetype_check['ext'] ), $allowed_extensions, true ) ) { if ( $logger ) { $logger->error( 'AfriDirect upload failed: Invalid file extension', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'file_name' => $sanitized_filename, 'file_ext' => $filetype_check['ext'], ) ); } wp_send_json_error( array( 'message' => __( 'Invalid file type. Allowed: JPG, JPEG, PNG, WEBP, PDF.', 'afridirect-payment' ) ) ); } if ( ! in_array( $filetype_check['type'], $allowed_types, true ) ) { if ( $logger ) { $logger->error( 'AfriDirect upload failed: Invalid mime type', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'file_name' => $sanitized_filename, 'mime_type' => $filetype_check['type'], ) ); } wp_send_json_error( array( 'message' => __( 'Invalid file type. Allowed: JPG, JPEG, PNG, WEBP, PDF.', 'afridirect-payment' ) ) ); } // Check order exists. $order = wc_get_order( $order_id ); if ( ! $order ) { if ( $logger ) { $logger->error( 'AfriDirect upload failed: Order not found', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, ) ); } wp_send_json_error( array( 'message' => __( 'Order not found.', 'afridirect-payment' ) ) ); } // Check order ownership or admin capability. $order_user_id = $order->get_user_id(); // Allow if user owns order OR is admin, OR if the order key matches. The key is the secret // the customer received in the redirect URL, so it also covers guest orders and customers who // logged in to a different account after checkout. $is_authorized = false; if ( $user_id && ( $user_id === $order_user_id || current_user_can( 'manage_woocommerce' ) ) ) { $is_authorized = true; } elseif ( isset( $_POST['order_key'] ) ) { $order_key = sanitize_text_field( wp_unslash( $_POST['order_key'] ) ); if ( '' !== $order_key && hash_equals( (string) $order->get_order_key(), $order_key ) ) { $is_authorized = true; } } if ( ! $is_authorized ) { if ( $logger ) { $logger->error( 'AfriDirect upload failed: Unauthorized access', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'order_user_id' => $order_user_id, 'has_order_key' => isset( $_POST['order_key'] ), 'is_admin' => current_user_can( 'manage_woocommerce' ), ) ); } wp_send_json_error( array( 'message' => __( 'You do not have permission to upload proof for this order.', 'afridirect-payment' ) ) ); } // Check uploads directory is writable. $upload_dir = wp_upload_dir(); if ( ! $upload_dir['error'] && ! wp_is_writable( $upload_dir['basedir'] ) ) { if ( $logger ) { $logger->error( 'AfriDirect upload failed: Uploads directory not writable', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'upload_dir' => $upload_dir['basedir'], 'upload_dir_error' => $upload_dir['error'], ) ); } wp_send_json_error( array( 'message' => __( 'Upload directory is not writable. Please contact the site administrator.', 'afridirect-payment' ) ) ); } // Sanitize file data before passing to handler. $sanitized_file = array( 'name' => $sanitized_filename, 'type' => $filetype_check['type'], 'tmp_name' => $file['tmp_name'], 'error' => $error_code, 'size' => $file_size, ); // Handle upload. $proof_handler = AfriDirect_Payment_Proof_Handler::instance(); $result = $proof_handler->handle_upload( $order_id, $sanitized_file, false ); if ( is_wp_error( $result ) ) { $error_message = $result->get_error_message(); $error_code = $result->get_error_code(); if ( $logger ) { $logger->error( 'AfriDirect upload failed: ' . $error_message, array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'error_code' => $error_code, 'file_name' => isset( $file['name'] ) ? sanitize_file_name( wp_unslash( $file['name'] ) ) : 'unknown', 'file_size' => isset( $file['size'] ) ? absint( $file['size'] ) : 0, 'file_type' => isset( $file['type'] ) ? sanitize_mime_type( $file['type'] ) : 'unknown', ) ); } wp_send_json_error( array( 'message' => $error_message ) ); } // If order was rejected, reset to proof status. if ( 'adp-rejected' === $order->get_status() ) { $order->update_status( 'adp-proof', __( 'New proof uploaded after rejection.', 'afridirect-payment' ) ); $order->save(); } if ( $logger ) { $logger->info( 'AfriDirect upload successful', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'attachment_id' => $result['attachment_id'] ) ); } wp_send_json_success( array( 'message' => __( 'Proof uploaded successfully.', 'afridirect-payment' ), 'file_url' => $result['file_url'], 'attachment_id' => $result['attachment_id'], 'redirect' => true, ) ); } /** * Handle standalone payment upload. * * @param int $payment_id Payment ID. */ private function handle_standalone_upload( $payment_id ) { $logger = afridirect_get_logger(); $user_id = get_current_user_id(); // Verify nonce with safe check that returns JSON instead of exiting. // Note: Nonce was already verified in upload_proof(), but we verify again here for extra security. if ( ! check_ajax_referer( 'afridirect-payment-nonce', 'nonce', false ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Nonce verification failed', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, ) ); } wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'afridirect-payment' ) ), 403 ); } // Get payment key for verification. $payment_key = isset( $_POST['payment_key'] ) ? sanitize_text_field( wp_unslash( $_POST['payment_key'] ) ) : ''; if ( empty( $payment_key ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Missing payment key', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, ) ); } wp_send_json_error( array( 'message' => __( 'Invalid payment key.', 'afridirect-payment' ) ) ); } // Verify payment and key. $payment = AfriDirect_Payments::get_payment( $payment_id ); if ( ! $payment ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Payment not found', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, ) ); } wp_send_json_error( array( 'message' => __( 'Payment not found.', 'afridirect-payment' ) ) ); } // Payments started from a payment request have a hard time limit. The customer can start again from the same link. if ( ! empty( $payment['request_id'] ) && 'pending_proof' === $payment['status'] && $payment['expires_at'] > 0 && time() > $payment['expires_at'] && AfriDirect_Payments::verify_key( $payment_id, $payment_key ) ) { wp_send_json_error( array( 'message' => __( 'The time for this payment has run out. Please open your payment request link again to start a new payment.', 'afridirect-payment' ) ) ); } if ( ! AfriDirect_Payments::verify_key( $payment_id, $payment_key ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Invalid payment key', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, ) ); } wp_send_json_error( array( 'message' => __( 'Invalid payment key.', 'afridirect-payment' ) ) ); } // Validate and sanitize $_FILES['proof_file'] - do not treat as trusted. if ( ! isset( $_FILES['proof_file'] ) || empty( $_FILES['proof_file'] ) || ! is_array( $_FILES['proof_file'] ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: No file uploaded', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, ) ); } wp_send_json_error( array( 'message' => __( 'No file uploaded. Please select a file.', 'afridirect-payment' ) ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File arrays cannot be sanitized like text; validation happens below. $file = isset( $_FILES['proof_file'] ) ? wp_unslash( $_FILES['proof_file'] ) : array(); // Validate required keys exist. if ( ! isset( $file['error'] ) || ! isset( $file['name'] ) || ! isset( $file['tmp_name'] ) || ! isset( $file['size'] ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Invalid file upload structure', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, ) ); } wp_send_json_error( array( 'message' => __( 'Invalid file upload structure.', 'afridirect-payment' ) ) ); } // Validate tmp_name exists and error is 0. if ( empty( $file['tmp_name'] ) || ! file_exists( $file['tmp_name'] ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Invalid file upload', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, ) ); } wp_send_json_error( array( 'message' => __( 'Invalid file upload.', 'afridirect-payment' ) ) ); } // Cast error to int and require UPLOAD_ERR_OK. $error_code = (int) $file['error']; if ( $error_code !== UPLOAD_ERR_OK ) { $error_messages = array( UPLOAD_ERR_INI_SIZE => __( 'File exceeds upload_max_filesize directive in php.ini.', 'afridirect-payment' ), UPLOAD_ERR_FORM_SIZE => __( 'File exceeds MAX_FILE_SIZE directive in HTML form.', 'afridirect-payment' ), UPLOAD_ERR_PARTIAL => __( 'File was only partially uploaded.', 'afridirect-payment' ), UPLOAD_ERR_NO_FILE => __( 'No file was uploaded.', 'afridirect-payment' ), UPLOAD_ERR_NO_TMP_DIR => __( 'Missing temporary folder.', 'afridirect-payment' ), UPLOAD_ERR_CANT_WRITE => __( 'Failed to write file to disk.', 'afridirect-payment' ), UPLOAD_ERR_EXTENSION => __( 'File upload stopped by extension.', 'afridirect-payment' ), ); $error_msg = isset( $error_messages[ $error_code ] ) ? $error_messages[ $error_code ] : __( 'Unknown upload error.', 'afridirect-payment' ); $file_name = isset( $file['name'] ) ? sanitize_file_name( wp_unslash( $file['name'] ) ) : 'unknown'; $file_size = isset( $file['size'] ) ? absint( $file['size'] ) : 0; if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Upload error ' . $error_code . ' - ' . $error_msg, array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, 'error_code' => $error_code, 'file_name' => $file_name, 'file_size' => $file_size, ) ); } wp_send_json_error( array( 'message' => $error_msg ) ); } // Sanitize filename. $sanitized_filename = sanitize_file_name( wp_unslash( $file['name'] ) ); // Check file size (5MB max). $sanitized_filesize = absint( $file['size'] ); $max_size = 5 * 1024 * 1024; // 5MB in bytes. if ( $sanitized_filesize > $max_size ) { $file_size_mb = round( $sanitized_filesize / 1024 / 1024, 2 ); if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: File too large', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, 'file_name' => $sanitized_filename, 'file_size' => $sanitized_filesize, 'file_size_mb' => $file_size_mb, 'max_size' => $max_size, ) ); } /* translators: %.2f: File size in megabytes */ wp_send_json_error( array( 'message' => sprintf( __( 'File size (%.2f MB) exceeds 5MB limit.', 'afridirect-payment' ), $file_size_mb ) ) ); } // Validate file type using wp_check_filetype_and_ext. $allowed_mimes = array( 'jpg|jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'pdf' => 'application/pdf', ); $filetype_check = wp_check_filetype_and_ext( $file['tmp_name'], $sanitized_filename, $allowed_mimes ); $allowed_types = array( 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf' ); $allowed_extensions = array( 'jpg', 'jpeg', 'png', 'webp', 'pdf' ); if ( empty( $filetype_check['ext'] ) || empty( $filetype_check['type'] ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Invalid file type (no ext/type)', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, 'file_name' => $sanitized_filename, ) ); } wp_send_json_error( array( 'message' => __( 'Invalid file type. Allowed: JPG, JPEG, PNG, WEBP, PDF.', 'afridirect-payment' ) ) ); } // Validate extension and mime type. if ( ! in_array( strtolower( $filetype_check['ext'] ), $allowed_extensions, true ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Invalid file extension', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, 'file_name' => $sanitized_filename, 'file_ext' => $filetype_check['ext'], ) ); } wp_send_json_error( array( 'message' => __( 'Invalid file type. Allowed: JPG, JPEG, PNG, WEBP, PDF.', 'afridirect-payment' ) ) ); } if ( ! in_array( $filetype_check['type'], $allowed_types, true ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Invalid mime type', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, 'file_name' => $sanitized_filename, 'mime_type' => $filetype_check['type'], ) ); } wp_send_json_error( array( 'message' => __( 'Invalid file type. Allowed: JPG, JPEG, PNG, WEBP, PDF.', 'afridirect-payment' ) ) ); } // Check uploads directory is writable. $upload_dir = wp_upload_dir(); if ( ! $upload_dir['error'] && ! wp_is_writable( $upload_dir['basedir'] ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: Upload directory not writable', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, 'upload_dir' => $upload_dir['basedir'], ) ); } wp_send_json_error( array( 'message' => __( 'Upload directory is not writable. Please contact the site administrator.', 'afridirect-payment' ) ) ); } // Sanitize file data before passing to handler. $sanitized_file = array( 'name' => $sanitized_filename, 'type' => $filetype_check['type'], 'tmp_name' => $file['tmp_name'], 'error' => $error_code, 'size' => $sanitized_filesize, ); // Handle upload. $proof_handler = AfriDirect_Payment_Proof_Handler::instance(); $result = $proof_handler->handle_upload( $payment_id, $sanitized_file, true ); if ( is_wp_error( $result ) ) { if ( $logger ) { $logger->error( 'AfriDirect standalone upload failed: ' . $result->get_error_message(), array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, 'error_code' => $result->get_error_code(), 'file_name' => $sanitized_filename, ) ); } wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } // If payment was rejected, reset status. if ( 'rejected' === $payment['status'] ) { AfriDirect_Payments::update_status( $payment_id, 'needs_review' ); } // Get receipt URL. $receipt_url = AfriDirect_Payment_Receipts::get_receipt_url( $payment_id ); if ( $logger ) { $logger->info( 'AfriDirect standalone upload successful', array( 'source' => 'afridirect-payment', 'payment_id' => $payment_id, 'user_id' => $user_id, 'attachment_id' => $result['attachment_id'], ) ); } wp_send_json_success( array( 'message' => __( 'Proof uploaded successfully.', 'afridirect-payment' ), 'file_url' => $result['file_url'], 'attachment_id' => $result['attachment_id'], 'receipt_url' => $receipt_url, 'redirect' => false, // Don't reload, just show success. ) ); } /** * Approve order. */ public function approve_order() { // Check capabilities. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'afridirect-payment' ) ) ); } // Verify nonce. check_ajax_referer( 'afridirect-payment-admin-nonce', 'nonce' ); // Get order ID. if ( ! isset( $_POST['order_id'] ) ) { wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'afridirect-payment' ) ), 400 ); } $order_id = absint( wp_unslash( $_POST['order_id'] ) ); if ( ! $order_id ) { wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'afridirect-payment' ) ) ); } if ( ! function_exists( 'wc_get_order' ) ) { wp_send_json_error( array( 'message' => __( 'WooCommerce is required for this action.', 'afridirect-payment' ) ) ); } $order = wc_get_order( $order_id ); if ( ! $order ) { wp_send_json_error( array( 'message' => __( 'Order not found.', 'afridirect-payment' ) ) ); } // Get admin notes. $notes = isset( $_POST['notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : ''; // Save notes. if ( $notes ) { $order->update_meta_data( '_afridirect_admin_note', $notes ); } // Generate receipt number if not exists. $receipt_number = $order->get_meta( '_afridirect_receipt_no' ); if ( ! $receipt_number ) { $receipt_generator = AfriDirect_Payment_Receipt_Generator::instance(); $receipt_number = $receipt_generator->generate_receipt_number( $order_id ); $order->update_meta_data( '_afridirect_receipt_no', $receipt_number ); } // Set review status meta. $order->update_meta_data( '_afridirect_review_status', 'approved' ); // Update order status (configurable via filter, default: processing). $approve_status = apply_filters( 'afridirect_woo_approve_status', 'processing' ); $order->update_status( $approve_status, __( 'AfriDirect proof approved', 'afridirect-payment' ) ); // Add order note. $order->add_order_note( __( 'AfriDirect proof approved', 'afridirect-payment' ) ); $order->save(); // Email the customer. WooCommerce sends nothing when an order moves from a custom status to Processing. AfriDirect_Payment_Email_Handler::instance()->send_approved_email( $order_id, $order ); do_action( 'afridirect_order_approved', $order_id ); wp_send_json_success( array( 'message' => __( 'Order approved successfully.', 'afridirect-payment' ), ) ); } /** * Reject order. */ public function reject_order() { // Check capabilities. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'afridirect-payment' ) ) ); } // Verify nonce. check_ajax_referer( 'afridirect-payment-admin-nonce', 'nonce' ); // Get order ID. if ( ! isset( $_POST['order_id'] ) ) { wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'afridirect-payment' ) ), 400 ); } $order_id = absint( wp_unslash( $_POST['order_id'] ) ); if ( ! $order_id ) { wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'afridirect-payment' ) ) ); } if ( ! function_exists( 'wc_get_order' ) ) { wp_send_json_error( array( 'message' => __( 'WooCommerce is required for this action.', 'afridirect-payment' ) ) ); } $order = wc_get_order( $order_id ); if ( ! $order ) { wp_send_json_error( array( 'message' => __( 'Order not found.', 'afridirect-payment' ) ) ); } // Get rejection reason (required). $reason = isset( $_POST['reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['reason'] ) ) : ''; if ( empty( $reason ) ) { wp_send_json_error( array( 'message' => __( 'Rejection reason is required.', 'afridirect-payment' ) ) ); } // Save rejection reason. $order->update_meta_data( '_afridirect_admin_note', $reason ); // Set review status meta. $order->update_meta_data( '_afridirect_review_status', 'declined' ); // Update order status (configurable via filter, default: failed). $decline_status = apply_filters( 'afridirect_woo_decline_status', 'failed' ); $order->update_status( $decline_status, __( 'AfriDirect proof declined', 'afridirect-payment' ) ); // Add order note. $order->add_order_note( __( 'AfriDirect proof declined', 'afridirect-payment' ) . ': ' . $reason ); $order->save(); // Email the customer the reason. The generic WooCommerce "unsuccessful" email is switched off for these orders. AfriDirect_Payment_Email_Handler::instance()->send_rejected_email( $order_id, $order ); do_action( 'afridirect_order_rejected', $order_id ); wp_send_json_success( array( 'message' => __( 'Order rejected successfully.', 'afridirect-payment' ), ) ); } /** * Get order review content. */ public function get_order_review() { // Check capabilities. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'afridirect-payment' ) ) ); } // Verify nonce. check_ajax_referer( 'afridirect-payment-admin-nonce', 'nonce' ); // Get order ID. if ( ! isset( $_POST['order_id'] ) ) { wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'afridirect-payment' ) ), 400 ); } $order_id = absint( wp_unslash( $_POST['order_id'] ) ); if ( ! $order_id ) { wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'afridirect-payment' ) ) ); } if ( ! function_exists( 'wc_get_order' ) ) { wp_send_json_error( array( 'message' => __( 'WooCommerce is required for this action.', 'afridirect-payment' ) ) ); } $order = wc_get_order( $order_id ); if ( ! $order ) { wp_send_json_error( array( 'message' => __( 'Order not found.', 'afridirect-payment' ) ) ); } // Get review content. $admin_dashboard = AfriDirect_Payment_Admin_Dashboard::instance(); $html = $admin_dashboard->get_order_review_modal( $order ); wp_send_json_success( array( 'html' => $html, ) ); } /** * Create standalone payment. */ public function create_payment() { // Verify nonce. check_ajax_referer( 'afridirect_create_payment', 'afridirect_nonce' ); // Check honeypot field. if ( ! empty( $_POST['website'] ) ) { wp_send_json_error( array( 'message' => __( 'Spam detected.', 'afridirect-payment' ) ) ); } // Rate limiting check. if ( ! $this->check_rate_limit() ) { wp_send_json_error( array( 'message' => __( 'Too many requests. Please wait a few minutes and try again.', 'afridirect-payment' ) ) ); } // Get form ID. $form_id = isset( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0; if ( ! $form_id ) { wp_send_json_error( array( 'message' => __( 'Invalid form ID.', 'afridirect-payment' ) ) ); } // Get form data. $form = AfriDirect_Payment_Forms_Helper::get_form( $form_id ); if ( ! $form ) { wp_send_json_error( array( 'message' => __( 'Form not found.', 'afridirect-payment' ) ) ); } // Get amount. $amount = isset( $_POST['amount'] ) ? floatval( $_POST['amount'] ) : 0; if ( $amount <= 0 ) { wp_send_json_error( array( 'message' => __( 'Invalid amount.', 'afridirect-payment' ) ) ); } // Validate amount based on form mode. if ( 'fixed' === $form['mode'] ) { if ( abs( $amount - $form['fixed_amount'] ) > 0.01 ) { wp_send_json_error( array( 'message' => __( 'Amount does not match the fixed amount.', 'afridirect-payment' ) ) ); } } else { // Variable mode. if ( $form['min_amount'] > 0 && $amount < $form['min_amount'] ) { /* translators: %s: Minimum payment amount */ wp_send_json_error( array( 'message' => sprintf( __( 'Amount must be at least %s.', 'afridirect-payment' ), number_format( $form['min_amount'], 2 ) ) ) ); } if ( $form['max_amount'] > 0 && $amount > $form['max_amount'] ) { /* translators: %s: Maximum payment amount */ wp_send_json_error( array( 'message' => sprintf( __( 'Amount must not exceed %s.', 'afridirect-payment' ), number_format( $form['max_amount'], 2 ) ) ) ); } } // Get payment method. $method_id = isset( $_POST['method_id'] ) ? sanitize_key( $_POST['method_id'] ) : ''; // Normalize method IDs for comparison. $allowed_method_ids = AfriDirect_Payment_Forms_Helper::normalize_method_ids( $form['allowed_method_ids'] ); // If no methods selected, allow all methods. if ( empty( $allowed_method_ids ) ) { $all_methods = AfriDirect_Payment_Methods::get_all(); $allowed_method_ids = array_keys( $all_methods ); } if ( empty( $method_id ) || ! in_array( $method_id, $allowed_method_ids, true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid payment method.', 'afridirect-payment' ) ) ); } $method = AfriDirect_Payment_Methods::get( $method_id ); if ( ! $method ) { wp_send_json_error( array( 'message' => __( 'Payment method not found.', 'afridirect-payment' ) ) ); } // Get payer information. $payer_name = ''; $payer_email = ''; $payer_phone = ''; if ( $form['require_name'] ) { $payer_name = isset( $_POST['payer_name'] ) ? sanitize_text_field( wp_unslash( $_POST['payer_name'] ) ) : ''; if ( empty( $payer_name ) ) { wp_send_json_error( array( 'message' => __( 'Name is required.', 'afridirect-payment' ) ) ); } } else { $payer_name = isset( $_POST['payer_name'] ) ? sanitize_text_field( wp_unslash( $_POST['payer_name'] ) ) : ''; } if ( $form['require_email'] ) { $payer_email = isset( $_POST['payer_email'] ) ? sanitize_email( wp_unslash( $_POST['payer_email'] ) ) : ''; if ( empty( $payer_email ) || ! is_email( $payer_email ) ) { wp_send_json_error( array( 'message' => __( 'Valid email is required.', 'afridirect-payment' ) ) ); } } else { $payer_email = isset( $_POST['payer_email'] ) ? sanitize_email( wp_unslash( $_POST['payer_email'] ) ) : ''; } if ( $form['require_phone'] ) { $payer_phone = isset( $_POST['payer_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['payer_phone'] ) ) : ''; if ( empty( $payer_phone ) ) { wp_send_json_error( array( 'message' => __( 'Phone is required.', 'afridirect-payment' ) ) ); } } else { $payer_phone = isset( $_POST['payer_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['payer_phone'] ) ) : ''; } // Create payment. // Determine countdown timer for proof upload (standalone payments). // Priority: form timer > method timer > default 30 minutes. $timer_minutes = 0; $form_timer_minutes = absint( get_post_meta( $form_id, '_afridirect_timer_minutes', true ) ); if ( $form_timer_minutes >= 1 ) { $timer_minutes = $form_timer_minutes; } elseif ( isset( $method['timer_minutes'] ) && '' !== $method['timer_minutes'] ) { $timer_minutes = absint( $method['timer_minutes'] ); } if ( $timer_minutes <= 0 ) { $timer_minutes = 30; // Sensible fallback. } // Deadlines are stored as real (UTC) Unix time: the countdown in the browser compares them with Date.now(). $expires_at = time() + ( $timer_minutes * MINUTE_IN_SECONDS ); $payment_data = array( 'form_id' => $form_id, 'amount' => $amount, 'currency' => $form['currency'], 'method_id' => $method_id, 'method_label' => $method['label'], 'payer_name' => $payer_name, 'payer_email' => $payer_email, 'payer_phone' => $payer_phone, 'timer_minutes'=> $timer_minutes, 'expires_at' => $expires_at, ); $payment_id = AfriDirect_Payments::create_payment( $payment_data ); if ( is_wp_error( $payment_id ) ) { wp_send_json_error( array( 'message' => $payment_id->get_error_message() ) ); } // Get payment to retrieve access key. $payment = AfriDirect_Payments::get_payment( $payment_id ); if ( ! $payment ) { wp_send_json_error( array( 'message' => __( 'Failed to create payment.', 'afridirect-payment' ) ) ); } // Build redirect URL from the Payment Pending page (created on activation / // admin_init). If the option is missing (e.g. admin_init has not run since // this version was installed), create the page now so the customer is never // left on the form. $pending_page_id = (int) get_option( 'afridirect_pending_page_id', 0 ); if ( ! $pending_page_id && function_exists( 'afridirect_payment' ) ) { afridirect_payment()->ensure_pages(); $pending_page_id = (int) get_option( 'afridirect_pending_page_id', 0 ); } $pending_base = $pending_page_id ? get_permalink( $pending_page_id ) : ''; if ( ! $pending_base ) { $pending_base = home_url( '/' ); } $redirect_url = add_query_arg( array( 'pid' => $payment_id, 'key' => rawurlencode( $payment['access_key'] ), ), $pending_base ); // Email the customer a link back to their payment page. do_action( 'afridirect_payment_created', $payment_id ); wp_send_json_success( array( 'payment_id' => $payment_id, 'redirect_url' => $redirect_url, 'message' => $form['success_message'], ) ); } /** * Check rate limit (max 5 submissions per 10 minutes per IP). * * @return bool */ private function check_rate_limit() { $ip = $this->get_client_ip(); $transient_key = 'afridirect_rate_limit_' . md5( $ip ); $submissions = get_transient( $transient_key ); if ( false === $submissions ) { $submissions = 0; } if ( $submissions >= 5 ) { return false; } set_transient( $transient_key, $submissions + 1, 10 * MINUTE_IN_SECONDS ); return true; } /** * Get client IP address. * * @return string */ private function get_client_ip() { $ip_keys = array( 'HTTP_CF_CONNECTING_IP', // Cloudflare. 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', ); foreach ( $ip_keys as $key ) { if ( ! empty( $_SERVER[ $key ] ) ) { $ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); // Handle comma-separated IPs (from proxies). if ( strpos( $ip, ',' ) !== false ) { $ip = trim( explode( ',', $ip )[0] ); } if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } } } return '0.0.0.0'; } /** * Approve standalone payment. */ public function approve_payment() { // Check capabilities. if ( ! current_user_can( AfriDirect_Payment_Admin_Menu::instance()->get_capability() ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'afridirect-payment' ) ) ); } // Verify nonce. check_ajax_referer( 'afridirect_payment_actions', 'nonce' ); // Get payment ID. $payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0; if ( ! $payment_id ) { wp_send_json_error( array( 'message' => __( 'Invalid payment ID.', 'afridirect-payment' ) ) ); } $payment = AfriDirect_Payments::get_payment( $payment_id ); if ( ! $payment ) { wp_send_json_error( array( 'message' => __( 'Payment not found.', 'afridirect-payment' ) ) ); } // Update payment status. AfriDirect_Payments::update_status( $payment_id, 'approved' ); // Generate receipt number if not exists. $receipt_number = get_post_meta( $payment_id, '_afridirect_receipt_no', true ); if ( ! $receipt_number ) { $receipt_number = 'ADP-' . $payment_id . '-' . time(); update_post_meta( $payment_id, '_afridirect_receipt_no', $receipt_number ); } // Trigger email notification. do_action( 'afridirect_payment_approved', $payment_id ); wp_send_json_success( array( 'message' => __( 'Payment approved successfully.', 'afridirect-payment' ), ) ); } /** * Reject standalone payment. */ public function reject_payment() { // Check capabilities. if ( ! current_user_can( AfriDirect_Payment_Admin_Menu::instance()->get_capability() ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized access.', 'afridirect-payment' ) ) ); } // Verify nonce. check_ajax_referer( 'afridirect_payment_actions', 'nonce' ); // Get payment ID. $payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0; if ( ! $payment_id ) { wp_send_json_error( array( 'message' => __( 'Invalid payment ID.', 'afridirect-payment' ) ) ); } $payment = AfriDirect_Payments::get_payment( $payment_id ); if ( ! $payment ) { wp_send_json_error( array( 'message' => __( 'Payment not found.', 'afridirect-payment' ) ) ); } // Get rejection reason (required). $reason = isset( $_POST['reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['reason'] ) ) : ''; if ( empty( $reason ) ) { wp_send_json_error( array( 'message' => __( 'Rejection reason is required.', 'afridirect-payment' ) ) ); } // Save rejection reason. update_post_meta( $payment_id, '_afridirect_rejection_reason', $reason ); // Update payment status. AfriDirect_Payments::update_status( $payment_id, 'rejected' ); // Trigger email notification. do_action( 'afridirect_payment_rejected', $payment_id, $reason ); wp_send_json_success( array( 'message' => __( 'Payment rejected successfully.', 'afridirect-payment' ), ) ); } }