2

I have a custom theme with a front-end form which is used to create a post using Ajax, and it works except for the file uploads.

The form looks like this...

<form class="job-form" id="job-form" method="post" enctype="multipart/form-data">

    <input type="text" name="job_title" id="job_title" />
    <input type="text" name="job_description" id="job_description" />
    <input type="file" name="job_files[]" id="job_files" multiple="multiple"/>

    <button>Submit</button>
</form>

...and this JS...

$('#job-form').on('submit', function(e) {
    e.preventDefault();

    $.post( inputs.ajaxurl, {
        action : 'add_job',
        nonce : inputs.nonce,
        post : $(this).serialize()
    },
    function(response) {
        console.log(response);
        ResponseSuccess(response);
    }); 

    return false;

});

..and PHP to add everything to the database:

add_action( 'wp_ajax_add_job', 'wp_ajax_add_job' );
add_action( 'wp_ajax_nopriv_add_job', 'add_job' );
function add_job() {

    $params = array();
    parse_str($_POST["post"], $params);

    if(isset($_POST["job-form"])) {

        $job_title          = sanitize_text_field($params['job_title']);
        $job_description    = sanitize_text_field($params['job_description']);

        $args = array(
            'post_title'    => $job_title,
            'post_status'   => 'publish',
            'post_type'     => 'jobs',
            'post_author'   => 1
        );
        
        $result     = wp_insert_post( $args );
        $post_id    = $result;
    
        add_post_meta( $post_id, "job_title" , $job_title);
        add_post_meta( $post_id, "job_description" , $job_description);

        /* If attachments were attached, upload them */
        if($_FILES) {
            require_once(ABSPATH . "wp-admin" . '/includes/image.php');
            require_once(ABSPATH . "wp-admin" . '/includes/file.php');
            require_once(ABSPATH . "wp-admin" . '/includes/media.php');

            $files = $_FILES["job_files"];  
            foreach ($files['name'] as $key => $value) {            
                if ($files['name'][$key]) { 
                    $file = array( 
                        'name'      => $files['name'][$key],
                        'type'      => $files['type'][$key], 
                        'tmp_name'  => $files['tmp_name'][$key], 
                        'error'     => $files['error'][$key],
                        'size'      => $files['size'][$key]
                    ); 
                    $_FILES = array ("job_files" => $file); 
                    foreach ($_FILES as $file => $array) {              
                        $new_upload = media_handle_upload($file,$post_id); 
                    }
                } 
            }
        }

        ajaxStatus('job-created', __('Job created.', 'jobs-theme'), $post_id);
            
    }
}

When submitting the form, the post gets created along with the meta keys and meta values without any dramas, but the file attachments are not working. What am I doing wrong?

User_FTW
  • 504
  • 1
  • 16
  • 44
  • https://wordpress.stackexchange.com/questions/198781/wordpress-ajax-file-upload-frontend – Momin IqbalAhmed Feb 19 '21 at 10:31
  • Where is the field named `job-form`? You look for it in the PHP code, but there is no such field either in the HTML or JS. The *form* has the ID `job-form`, but that shouldn’t make its way to the server. – user3840170 Feb 19 '21 at 13:02

2 Answers2

3

As jQuery documentation for serialize explains,

Data from file select elements is not serialized.

The reason is that serialize encodes form elements into the application/x-www-form-urlencoded format, which can only accommodate plaintext string values, and not file uploads. To be able to upload files, you will have to submit the whole form in the multipart/form-data format. One way to accomplish it is to use the FormData object; jQuery doesn’t directly support it, but it can accomodate it easily.

Here’s a minimal modification of your code to make this work:

$('#job-form').on('submit', function(e) {
    e.preventDefault();

    // take the data from the form
    const formData = new FormData(this);
    // append further fields
    formData.append('action', 'add_job');
    formData.append('nonce', inputs.nonce);

    $.ajax( inputs.ajaxurl, {
        type: 'POST',
        data: formData,
        contentType: false, // prevent jQuery from using an incorrect Content-type header
        processData: false, // prevent jQuery from mangling the form data
        success: function(response) {
            console.log(response);
            ResponseSuccess(response);
        }
    }); 

    return false;
});

The contentType and processData properties need to be set to prevent jQuery from mangling the data and adding incorrect headers.

Because I had to modify the actually-submitted form structure to be able to handle file uploads, the form code and the receiver script will also have to be modified, for example like this:

    <form class="job-form" id="job-form" method="post" enctype="multipart/form-data">

        <input type="text" name="post[job_title]" id="job_title" />
        <input type="text" name="post[job_description]" id="job_description" />
        <input type="file" name="job_files[]" id="job_files" multiple="multiple"/>

        <button>Submit</button>
    </form>

And in the receiving script:

    # ...
    function add_job() {

        $params = $_POST['post'];

        if(isset($_POST["job-form"])) {
    # ...

Here I am taking advantage of PHP’s built-in structural form parsing, where fields with names of the form a[b] generate associative arrays that can be accessed as $_POST['a']['b'] on the PHP side.

user3840170
  • 26,597
  • 4
  • 30
  • 62
  • I think it's close. I am now seeing the meta key called `job_files` with this meta value: ```a:1:{s:10:"job_files";a:5:{s:4:"name";a:3:{i:0;s:8:"pic1.jpg";i:1;s:8:"pic2.jpg";i:2;s:8:"pic3.jpg";}s:4:"type";a:3:{i:0;s:10:"image/jpeg";i:1;s:10:"image/jpeg";i:2;s:10:"image/jpeg";}s:8:"tmp_name";a:3:{i:0;s:21:"C:xampptmpphp706A.tmp";i:1;s:21:"C:xampptmpphp706B.tmp";i:2;s:21:"C:xampptmpphp707C.tmp";}s:5:"error";a:3:{i:0;i:0;i:1;i:0;i:2;i:0;}s:4:"size";a:3:{i:0;i:246961;i:1;i:4684937;i:2;i:1132984;}}}``` But curiously no files are actually being uploaded to the wp-content directory. – User_FTW Feb 19 '21 at 12:47
  • @User_FTW The only strange thing I see is that backslashes appear to be missing from file system paths embedded in the string you gave, and although it looks like output from `serialize`, it cannot be actually `unserialize`d back (the length counter of one string is wrong) – user3840170 Feb 19 '21 at 20:04
  • @User_FTW [`media_handle_upload`](https://developer.wordpress.org/reference/functions/media_handle_upload/) can return a `WP_Error`, which you don’t check for. Can you add some code to check it? And does the response contain a `X-WP-Upload-Attachment-ID` header? – user3840170 Feb 20 '21 at 11:59
  • What are you seeing, if any, in the $_FILES[...]['error'] field(s)? – sativay Feb 24 '21 at 12:31
-1

You need to rewrite your ajax request code just like below.


var $form = $("#yourFormSelector");
$.ajax({
    type: 'post',
    url: 'url',
    data: new FormData($form[0]),
    contentType: false,
    processData: false,
    success: function(response) {
        //Your response handler code...
    }
});