Update 2024-04: Solved an issue
The following ultra lightweight plugin uses Ajax in a very secure way, to allow uploads in WooCommerce checkout page.
Note: This works only in classic checkout, but not in new Checkout Block.
When using Ajax, you can:
- Restrict/check the file size,
- Restrict to accepted file types only,
- check if the file has already been uploaded (but this is not really helpful as someone can upload a file, and re-upload an update file with the same name, without checking out yet).
- Hide completely all the sensitive data like the upload path, using WC Session to store it securely.
All uploaded files will go to a folder named "wc_checkout_uploads" located in WordPress main "uploads" directory. They will be included in a subfolder with the user ID as name (in 6 digits length).
For guests users, if checkout is enabled, the uploads have the same upload directory 000000 and a subdirectory based on the billing email.
See the "Complete Usage Examples" section (below after the plugin code) to:
- Display an upload field,
- Validate the field when it is required,
- Save the file URL and name as custom order meta data and displayed in the admin,
- Use that custom order meta data everywhere you like.
Here is the code of this lightweight plugin:
Main PHP file (add it to a folder named as you like): checkout_uploads.php
<?php
/*
Plugin Name: WooCommerce Checkout upload
Plugin URI: https://stackoverflow.com/a/76691778/3730754
Description: Add a input field type "file" for checkout (Ajax securely powered), and save the downloaded file URL and name as custom order metadata.
Version: 1.0
Author: LoicTheAztec
Author URI: https://stackoverflow.com/users/3730754/loictheaztec
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
register_activation_hook(__FILE__, 'wcu_plugin_activation');
function wcu_plugin_activation() {
// Make sure that WooCommerce plugin is active
if ( ! in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
$message = 'Requires WooCommerce plugin activated.';
echo $message;
trigger_error($message, E_USER_NOTICE);
}
}
// Enqueue JavaScript file and localize it
add_action( 'wp_enqueue_scripts', 'checkout_uploads_enqueue_scripts' );
function checkout_uploads_enqueue_scripts() {
if ( is_checkout() && ! is_wc_endpoint_url() ) {
wp_enqueue_script(
'checkout-uploads',
plugins_url( 'js/checkout_upload.js', __FILE__ ),
array('jquery'), false, true
);
wp_localize_script(
'checkout-uploads',
'checkout_uploads_params',
array(
'ajax_url' => admin_url( 'admin-ajax.php?action=checkout_upload&security='.wp_create_nonce('checkout_upload') ),
)
);
}
}
// ADd Input File type to WooCommerce form fields
add_filter( 'woocommerce_form_field', 'woocommerce_form_input_field_type_file', 10, 4 );
function woocommerce_form_input_field_type_file( $field, $key, $args, $value ){
if( $args['type'] === 'file' && is_checkout() && ! is_wc_endpoint_url() ){
if ( $args['required'] ) {
$args['class'][] = 'validate-required';
$required = ' <abbr class="required" title="' . esc_attr__( 'required', 'woocommerce' ) . '">*</abbr>';
} else {
$required = ' <span class="optional">(' . esc_html__( 'optional', 'woocommerce' ) . ')</span>';
}
$field = '';
$label_id = $args['id'];
$sort = $args['priority'] ? $args['priority'] : '';
$field_container = '<p class="form-row %1$s" id="%2$s" data-priority="' . esc_attr( $sort ) . '">%3$s</p>';
$max_size = isset($args['max_size']) ? 'data-max_size="' . intval( $args['max_size'] ) . '" ' : '';
$accept = isset($args['accept']) ? 'accept="' . esc_attr( $args['accept'] ) . '" ' : '';
$field .= sprintf( '<input type="%s" class="input-file %s" name="%s" id="%s" %s/>', esc_attr( $args['type'] ),
esc_attr( implode( ' ', $args['input_class'] ) ), esc_attr( $key ), esc_attr( $args['id'] ), $max_size . $accept );
if ( ! empty( $field ) ) {
$field_html = '<label for="' . esc_attr( $label_id ) . '" class="' . esc_attr( implode( ' ', $args['label_class'] ) ) . '">' . wp_kses_post( $args['label'] ) . $required . '</label>';
$field_html .= '<span class="woocommerce-input-wrapper">' . $field;
if ( $args['description'] ) {
$field_html .= '<span class="description" id="' . esc_attr( $args['id'] ) . '-description" aria-hidden="true">' . wp_kses_post( $args['description'] ) . '</span>';
}
$field_html .= '<span class="upload-response" style="display:none"></span></span>';
$container_class = esc_attr( implode( ' ', $args['class'] ) );
$container_id = esc_attr( $args['id'] ) . '_field';
$field = sprintf( $field_container, $container_class, $container_id, $field_html );
}
}
return $field;
}
// PHP Ajax responder
add_action( 'wp_ajax_checkout_upload', 'checkout_ajax_file_upload' );
add_action( 'wp_ajax_nopriv_checkout_upload', 'checkout_ajax_file_upload' );
function checkout_ajax_file_upload(){
check_ajax_referer('checkout_upload', 'security');
global $current_user;
if ( isset($_FILES['uploads']) ) {
if ( ! $current_user->ID && isset($_POST['email']) && ! empty($_POST['email']) ) {
// Generating a sub / subfolder (path) from billing email in '000000' guest directory
$user_path = '000000/'.substr(sanitize_title($_POST['email']), 0, 10); // For Guests
} else {
$user_path = str_pad($current_user->ID, 6, '0', STR_PAD_LEFT); // For logged in users
}
$upload_dir = wp_upload_dir();
$user_path = '/wc_checkout_uploads/' . $user_path;
$user_folder = $upload_dir['basedir'] . $user_path;
$user_url = $upload_dir['baseurl'] . $user_path;
if ( ! is_dir( $user_folder ) ) {
wp_mkdir_p( $user_folder );
chmod( $user_folder, 0777 );
}
$file_path = $user_folder . '/' . basename($_FILES['uploads']['name']);
$file_url = $user_url . '/' . basename( $_FILES['uploads']['name']);
if( move_uploaded_file($_FILES['uploads']['tmp_name'], $file_path)) {
// Save the file URL and the file name to WC Session
WC()->session->set('checkout_upload', array(
'file_url' => $file_url,
'file_name' => $_FILES['uploads']['name']
));
echo '<span style="color:green">' . __('Upload completed', 'woocommerce') . '</span><br>';
} else {
echo '<span style="color:red">' . __('Upload failed.') . '</span>';
}
}
wp_die();
}
The Javascript file located in a "js" subfolder: checkout_upload.js
jQuery( function($) {
if (typeof checkout_uploads_params === 'undefined') {
return false;
}
$('form.checkout').on( 'change', 'input[type=file]', function() {
const files = $(this).prop('files');
const email = $('input#billing_email').val();
if ( files.length ) {
const file = files[0];
const maxSize = $(this).data('max_size');
const formData = new FormData();
formData.append( 'uploads', file );
formData.append( 'email', email );
if ( maxSize > 0 && file.size > ( maxSize * 1024 ) ) {
const maxSizeText = 'This file is to heavy (' + parseInt(file.size / 1024) + ' ko)';
$( '.upload-response' ).html( maxSizeText ).css('color','red').fadeIn().delay(2000).fadeOut();
return;
}
$('form.checkout').block({message: null, overlayCSS:{background:"#fff",opacity: .6}});
$.ajax({
url: checkout_uploads_params.ajax_url,
type: 'POST',
data: formData,
contentType: false,
enctype: 'multipart/form-data',
processData: false,
success: function ( response ) {
$('form.checkout').unblock();
$( '.upload-response' ).html( response ).fadeIn().delay(2000).fadeOut();
},
error: function ( error ) {
$('form.checkout').unblock();
$( '.upload-response' ).html( error ).css('color','red').fadeIn().delay(2000).fadeOut();
}
});
}
});
});
End of the plugin code.
A new input field type "file" is now available for woocommerce form fields and has 2 additional optional arguments:
Complete Usage Examples:
1) Adding an upload field:
A) After the order notes (accepting only text / pdf files and limitting the download size).
add_action( 'woocommerce_after_order_notes', 'add_custom_checkout_field' );
function add_custom_checkout_field($checkout) {
echo '<div class="woocommerce-additional-fields__field-wrapper">';
woocommerce_form_field('certificate', array(
'type' => 'file',
'class' => array('form-row-wide'),
'label' => __('Upload Certificate', 'woocommerce'),
'required' => false,
'max_size' => '3072', // in ko (here 3 Mo size limit)
'accept' => '.img,.doc,.docx,.rtf,.txt', // text documents and pdf
), '');
echo '</div>';
}
This code goes in functions.php file of your active child theme (or active theme) or using Code Snippets plugin (recommended by WooCommerce).
B) In the billing (or shipping) address section for a specific user role (accepting only text / pdf files and limitting the download size).
Note: Here the field has "required" option enabled.
add_filter( 'woocommerce_checkout_fields', 'add_custom_billing_field' );
function add_custom_billing_field( $fields ) {
// Only for 'wholesale_customer' user role
if( ! current_user_can( 'wholesale_customer' ) ) return $fields;
$fields['billing']['billing_image'] = array(
'type' => 'file',
'label' => __('Upload your image', 'woocommerce'),
'class' => array('form-row-wide'),
'required' => true,
'max_size' => '5120', // in ko (here 5 Mo size limit)
'accept' => 'image/*', // Image files only
'priority' => 200,
);
return $fields;
}
For logged users only, you can use:
// Only for logged in users
if( ! is_user_logged_in() ) return $fields;
Important: When the field is required, and located in billing or shipping fields sections, add the following code, to avoid woocommerce stopping checkout (when the file has been uploaded):
// On billing or shipping section, when "upload" field is required
add_action( 'woocommerce_after_checkout_validation', 'custom_after_checkout_validation', 20, 2 );
function custom_after_checkout_validation($data, $errors) {
$field_key = 'billing_image'; // Here define the field key (or field ID)
$errors->remove($field_key.'_required'); // Remove unwanted error for input file
}
This code goes in functions.php file of your active child theme (or active theme) or using Code Snippets plugin (recommended by WooCommerce).
2) Validation to be used when the file is required:
// Required upload field validation
add_action( 'woocommerce_checkout_process', 'checkout_required_upload_validation' );
function checkout_required_upload_validation() {
$checkout_upload = WC()->session->get('checkout_upload');
if( empty( $checkout_upload ) ) {
wc_add_notice( __('Uploading your file is required in order to checkout.', 'woocommerce'), 'error' ); // Displays an error notice
}
}
This code goes in functions.php file of your active child theme (or active theme) or using Code Snippets plugin (recommended by WooCommerce).
3) Save the uploaded file URL and name:
// Save the uploaded file URL and name (array
add_action( 'woocommerce_checkout_create_order', 'save_checkout_uploaded_file', 10, 2 );
function save_checkout_uploaded_file( $order, $data ){
if( $checkout_upload = WC()->session->get('checkout_upload') ) {
$order->update_meta_data( '_checkout_upload', $checkout_upload ); // Save
}
WC()->session->__unset('checkout_upload'); // Remove session variable
}
4) Display the uploaded file URL and name:
A) In the admin, on order edit pages, after billing address:
// Display the uploaded file in admin orders
add_action('woocommerce_admin_order_data_after_billing_address', 'display_uploaded_file_in_admin_orders');
function display_uploaded_file_in_admin_orders( $order ){
if( $checkout_upload = $order->get_meta( '_checkout_upload' ) ) {
printf( '<p>%s <br><a href="%s">%s</a></p>',
__("File Upload:", 'woocommerce'),
$checkout_upload['file_url'],
$checkout_upload['file_name']
);
}
}
This code goes in functions.php file of your active child theme (or active theme) or using Code Snippets plugin (recommended by WooCommerce).
B) Everywhere needed with $order variable (the WC_Order object):
First, if needed, you can get the WC_Order object from the order ID like:
$order = wc_get_order( $order_id );
Then you will get the data using:
$upload = $order->get_meta('_checkout_upload');
Then, for a file, you can display it as a link like:
$upload = $order->get_meta('_checkout_upload');
printf( '<p>%s <br><a href="%s">%s</a></p>',
__("File Upload:", 'woocommerce'),
$upload['file_url'],
$upload['file_name']
);
Or for an image, you can display the image like:
$upload = $order->get_meta('_checkout_upload');
printf( '<p>%s <br><img src="%s" alt="%s" /><br><a href="%s">%s</a></p>',
__("Image Uploaded:", 'woocommerce'),
$upload['file_url'],
$upload['file_name'],
$upload['file_url'],
$upload['file_name']
);