init_hooks(); } /** * Initialize hooks. */ private function init_hooks() { // Handled via AJAX. } /** * Handle proof upload (for WooCommerce orders or standalone payments). * * @param int $order_id Order ID (or payment_id for standalone). * @param array $file File data. * @param bool $is_standalone Whether this is a standalone payment. * @return array|WP_Error */ public function handle_upload( $order_id, $file, $is_standalone = false ) { $user_id = get_current_user_id(); $logger = afridirect_get_logger(); if ( $is_standalone ) { return $this->handle_standalone_upload( $order_id, $file ); } // WooCommerce order handling. if ( ! class_exists( 'WooCommerce' ) ) { return new WP_Error( 'woocommerce_required', __( 'WooCommerce is required for order uploads.', 'afridirect-payment' ) ); } $order = wc_get_order( $order_id ); if ( ! $order ) { if ( $logger ) { $logger->error( 'AfriDirect handle_upload failed: Invalid order', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, ) ); } return new WP_Error( 'invalid_order', __( 'Invalid order.', 'afridirect-payment' ) ); } // Check if order belongs to current user. if ( get_current_user_id() !== $order->get_user_id() && ! current_user_can( 'manage_woocommerce' ) ) { if ( $logger ) { $logger->error( 'AfriDirect handle_upload failed: Unauthorized access', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'order_user_id' => $order->get_user_id(), ) ); } return new WP_Error( 'unauthorized', __( 'Unauthorized access.', 'afridirect-payment' ) ); } // Validate file. $validation = $this->validate_file( $file ); if ( is_wp_error( $validation ) ) { if ( $logger ) { $logger->error( 'AfriDirect handle_upload failed: File validation failed', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'error_code' => $validation->get_error_code(), 'error_message' => $validation->get_error_message(), 'file_name' => isset( $file['name'] ) ? $file['name'] : 'unknown', 'file_size' => isset( $file['size'] ) ? $file['size'] : 0, ) ); } return $validation; } // Load the WordPress filesystem API and use it immediately below. require_once ABSPATH . 'wp-admin/includes/file.php'; // Prepare upload. $upload = wp_handle_upload( $file, array( 'test_form' => false ) ); if ( isset( $upload['error'] ) ) { $error_msg = $upload['error']; if ( $logger ) { $logger->error( 'AfriDirect handle_upload failed: wp_handle_upload error', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'upload_error' => $error_msg, 'file_name' => isset( $file['name'] ) ? $file['name'] : 'unknown', 'file_size' => isset( $file['size'] ) ? $file['size'] : 0, ) ); } return new WP_Error( 'upload_failed', $error_msg ); } // Create attachment. $attachment_data = array( 'post_mime_type' => $upload['type'], 'post_title' => sanitize_file_name( 'proof-order-' . $order_id . '-' . time() ), 'post_content' => '', 'post_status' => 'private', // Private so not publicly accessible. ); $attachment_id = wp_insert_attachment( $attachment_data, $upload['file'] ); if ( is_wp_error( $attachment_id ) ) { $error_msg = $attachment_id->get_error_message(); if ( $logger ) { $logger->error( 'AfriDirect handle_upload failed: Failed to create attachment', array( 'source' => 'afridirect-payment', 'order_id' => $order_id, 'user_id' => $user_id, 'error_code' => $attachment_id->get_error_code(), 'error_message' => $error_msg, 'upload_file' => isset( $upload['file'] ) ? $upload['file'] : 'unknown', ) ); } return new WP_Error( 'attachment_failed', __( 'Failed to create attachment.', 'afridirect-payment' ) ); } // Load the WordPress image API and use it immediately below. require_once ABSPATH . 'wp-admin/includes/image.php'; $attach_data = wp_generate_attachment_metadata( $attachment_id, $upload['file'] ); wp_update_attachment_metadata( $attachment_id, $attach_data ); // Save file info to order meta. $file_url = wp_get_attachment_url( $attachment_id ); $order->update_meta_data( '_afridirect_proof_url', $file_url ); $order->update_meta_data( '_afridirect_proof_attachment_id', $attachment_id ); $order->update_meta_data( '_afridirect_proof_uploaded_at', time() ); // Update order status. $order->update_status( 'adp-proof', __( 'Payment proof uploaded by customer.', 'afridirect-payment' ) ); $order->save(); // Trigger email notification. do_action( 'afridirect_payment_uploaded', $order_id ); return array( 'success' => true, 'file_url' => $file_url, 'attachment_id' => $attachment_id, ); } /** * Validate uploaded file. * * @param array $file File data. * @return bool|WP_Error */ private function validate_file( $file ) { $logger = afridirect_get_logger(); // Check if file was uploaded. if ( ! isset( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) { $logger->error( 'AfriDirect validate_file failed: Invalid file upload', array( 'source' => 'afridirect-payment', 'has_tmp_name' => isset( $file['tmp_name'] ), 'is_uploaded_file' => isset( $file['tmp_name'] ) ? is_uploaded_file( $file['tmp_name'] ) : false, 'file_name' => isset( $file['name'] ) ? $file['name'] : 'unknown', ) ); return new WP_Error( 'invalid_file', __( 'Invalid file upload.', 'afridirect-payment' ) ); } // Check file size (5MB max). $max_size = 5 * 1024 * 1024; // 5MB in bytes. if ( $file['size'] > $max_size ) { $file_size_mb = round( $file['size'] / 1024 / 1024, 2 ); $logger->error( 'AfriDirect validate_file failed: File too large', array( 'source' => 'afridirect-payment', 'file_name' => isset( $file['name'] ) ? $file['name'] : 'unknown', 'file_size' => $file['size'], 'file_size_mb' => $file_size_mb, 'max_size' => $max_size, ) ); /* translators: %.2f: File size in megabytes */ return new WP_Error( 'file_too_large', sprintf( __( 'File size (%.2f MB) exceeds 5MB limit.', 'afridirect-payment' ), $file_size_mb ) ); } // Check file type. $allowed_types = array( 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf' ); $allowed_extensions = array( 'jpg', 'jpeg', 'png', 'webp', 'pdf' ); $file_type = wp_check_filetype( $file['name'] ); $mime_type = $file['type']; // Validate both extension and mime type. if ( ! in_array( $file_type['ext'], $allowed_extensions, true ) ) { $logger->error( 'AfriDirect validate_file failed: Invalid file extension', array( 'source' => 'afridirect-payment', 'file_name' => isset( $file['name'] ) ? $file['name'] : 'unknown', 'file_ext' => $file_type['ext'], 'allowed_extensions' => $allowed_extensions, ) ); return new WP_Error( 'invalid_file_type', __( 'Invalid file type. Allowed: JPG, JPEG, PNG, WEBP, PDF.', 'afridirect-payment' ) ); } if ( ! in_array( $mime_type, $allowed_types, true ) ) { $logger->error( 'AfriDirect validate_file failed: Invalid mime type', array( 'source' => 'afridirect-payment', 'file_name' => isset( $file['name'] ) ? $file['name'] : 'unknown', 'mime_type' => $mime_type, 'allowed_types' => $allowed_types, ) ); return new WP_Error( 'invalid_file_type', __( 'Invalid file type. Allowed: JPG, JPEG, PNG, WEBP, PDF.', 'afridirect-payment' ) ); } // Additional security check for images. if ( strpos( $mime_type, 'image/' ) === 0 ) { $image_info = @getimagesize( $file['tmp_name'] ); if ( false === $image_info ) { $logger->error( 'AfriDirect validate_file failed: Invalid image file', array( 'source' => 'afridirect-payment', 'file_name' => isset( $file['name'] ) ? $file['name'] : 'unknown', 'mime_type' => $mime_type, ) ); return new WP_Error( 'invalid_image', __( 'Invalid image file.', 'afridirect-payment' ) ); } } return true; } /** * Get proof file URL for order. * * @param int $order_id Order ID. * @return string|false */ public function get_proof_url( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return false; } return $order->get_meta( '_afridirect_proof_url' ); } /** * Get proof file path for order. * * @param int $order_id Order ID. * @return string|false */ public function get_proof_path( $order_id ) { if ( ! class_exists( 'WooCommerce' ) ) { return false; } $order = wc_get_order( $order_id ); if ( ! $order ) { return false; } return $order->get_meta( '_afridirect_payment_file_path' ); } /** * Handle standalone payment upload. * * @param int $payment_id Payment ID. * @param array $file File data. * @return array|WP_Error */ private function handle_standalone_upload( $payment_id, $file ) { $payment = AfriDirect_Payments::get_payment( $payment_id ); if ( ! $payment ) { return new WP_Error( 'invalid_payment', __( 'Invalid payment.', 'afridirect-payment' ) ); } // Validate file. $validation = $this->validate_file( $file ); if ( is_wp_error( $validation ) ) { return $validation; } // Load the WordPress filesystem API and use it immediately below. require_once ABSPATH . 'wp-admin/includes/file.php'; // Prepare upload. $upload = wp_handle_upload( $file, array( 'test_form' => false ) ); if ( isset( $upload['error'] ) ) { return new WP_Error( 'upload_failed', $upload['error'] ); } // Create attachment. $attachment_data = array( 'post_mime_type' => $upload['type'], 'post_title' => sanitize_file_name( 'proof-payment-' . $payment_id . '-' . time() ), 'post_content' => '', 'post_status' => 'private', // Private so not publicly accessible. ); $attachment_id = wp_insert_attachment( $attachment_data, $upload['file'] ); if ( is_wp_error( $attachment_id ) ) { return new WP_Error( 'attachment_failed', __( 'Failed to create attachment.', 'afridirect-payment' ) ); } // Load the WordPress image API and use it immediately below. require_once ABSPATH . 'wp-admin/includes/image.php'; $attach_data = wp_generate_attachment_metadata( $attachment_id, $upload['file'] ); wp_update_attachment_metadata( $attachment_id, $attach_data ); // Save file info to payment meta. $file_url = wp_get_attachment_url( $attachment_id ); update_post_meta( $payment_id, '_afridirect_proof_url', $file_url ); update_post_meta( $payment_id, '_afridirect_proof_attachment_id', $attachment_id ); update_post_meta( $payment_id, '_afridirect_proof_uploaded_at', time() ); // Update payment status. AfriDirect_Payments::update_status( $payment_id, 'needs_review' ); // Trigger email notification. do_action( 'afridirect_payment_proof_uploaded', $payment_id ); return array( 'success' => true, 'file_url' => $file_url, 'attachment_id' => $attachment_id, ); } }