Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CodeIgniter AJAX file upload, $_FILE is empty when upload

Iam writing a script that uploads files via Drag and Drop using jQuery which reads the file on drop and adds it to an array of files like so:

var files = [];

$(document).ready(function() {
    jQuery.fn.dropbox = function(config) {
        var dragging = 0;
        var dragEnter = function(e) {
            e.stopPropagation();
            e.preventDefault();
            dragging++;
            $(".external-drop-indicator").fadeIn();
            $(".toast.toast-success").fadeIn().css("display", "inline-block");;
            return false;
        };
        var dragOver = function(e) {
            e.stopPropagation();
            e.preventDefault();
            return false;
        };
        var dragLeave = function(e) {
            e.stopPropagation();
            e.preventDefault();
            dragging--;
            if(dragging === 0) {
                $(".external-drop-indicator").fadeOut();
                $(".toast.toast-success").fadeOut();
            }
            return false;
        };
        var drop = function(e) {
            var dt = e.dataTransfer;
            var files_upload = dt.files;

            e.stopPropagation();
            e.preventDefault();

            if(files_upload && files_upload.length > 0 && config.onDrop !== undefined) {
                files.push(files_upload);
                config.onDrop(files_upload);
            }

            $(".external-drop-indicator").fadeOut();
            $(".toast.toast-success").fadeOut();

        };
        var applyDropbox = function(dropbox) {
            dropbox.addEventListener('dragenter', dragEnter, false);
            dropbox.addEventListener('dragover', dragOver, false);
            dropbox.addEventListener('dragleave', dragLeave, false);
            dropbox.addEventListener('drop', drop, false);
        };
        return this.each(function() {
            applyDropbox(this);
        });
    };
});

In summary what it does is, adds an extended jQuery function to enable drag and drop on a certain element of the website in which the function is applied.

Then I apply the extended functionality to the body for it to enable the file Drag and Drop functionality like so:

$(document).ready(function() {
    $('body').dropbox({
        onDrop: function(f) {
            $(f).each(function(idx, data) {
                var file_name = data.name;
                var extension = file_name.split('.');
                file_name = extension[0];
                extension = extension[1];
                if(extension == 'pdf' || extension == 'xls') {
                    showAjaxModal(base_url + 'index.php?modal/popup/file_create/' + folder_id + '/' + extension + '/' + file_name);
                } else {
                    $(".upload-area").append('<div class="alert alert-danger" style="display:inline-block;width:480px;"><strong>Error!</strong> File type is incorrect.</div>');
                }
            });
        }
    });
});

In summary what this does is adds the Drag and Drop functionality to the body and when a file is dropped it detects the extension of the file by splitting the name and the extension, so that I can verify that the extension of the file that was dropped is correct. Then it proceeds to show a modal to fill information about the file that is being uploaded for later submission, if the file extension is correct.

Then I proceed to fill the file information using CodeIgniter's function "form_open()" when the modal pops like so:

<?php echo form_open(base_url() . 'index.php?client/file/create/' . $param2, array('class' => 'form-horizontal form-groups-bordered validate ajax-upload', 'enctype' => 'multipart/form-data')); ?>

    <div class="col-md-4 file-info">
        <div class="icon-<?=($param3 == 'pdf' ? 'pdf' : 'document')?>"></div>
        <p>
            <?php echo $param4;?>
        </p>
    </div>

    <div class="col-md-8 new-file">

        <div class="form-group">
            <div class="col-sm-12">
                <div class="input-group">
                    <input type="text" class="form-control" name="tags" data-validate="required" data-message-required="Field is required" placeholder="File tags" value="" autofocus>
                </div>
            </div>
        </div>

        <div class="form-group">
            <div class="col-sm-12">
                <div class="input-group">
                    <input type="text" class="form-control" name="name" data-validate="required" data-message-required="Field is required" placeholder="File name" value="" autofocus>
                </div>
            </div>
        </div>

        <div class="form-group">
            <div class="col-sm-12">
                <div class="input-group">
                    <textarea rows="5" class="form-control" name="description" data-validate="required" data-message-required="Field is required" placeholder="File description" value="" autofocus></textarea>
                </div>
            </div>
        </div>

    </div>

    <div class="form-group">
        <div class="col-sm-offset-4 col-sm-7">
            <button type="submit" class="btn btn-info" id="submit-button">Upload</button>
            <span id="preloader-form"></span>
        </div>
    </div>

<?php echo form_close(); ?>

This basically creates a form which later will be submitted via Ajax.

Now I proceed to handle the file submission via jQuery for the information to be sent with the file or files that are being uploaded like so:

$(document).ready(function(e) {
    $('.ajax-upload').submit(function(e) {
        e.preventDefault();

        var $elm = $(this);

        var fd = new FormData();

        for(var i = 0; i < files.length; i++) {
            fd.append("file_" + i, files[i]);
        }

        var form_data = $(".ajax-upload").serializeArray();

        $.each(form_data, function(key, input) {
            fd.append(input.name, input.value);
        });

        var opts = {
            url: $elm.attr('action'),
            data: fd,
            cache: false,
            contentType: false,
            processData: false,
            type: 'POST',
            beforeSend: uValidate,
            success: showResponse
        };

        if(fd.fake) {
            opts.xhr = function() {
                var xhr = jQuery.ajaxSettings.xhr();
                xhr.send = xhr.sendAsBinary;
                return xhr;
            }
            opts.contentType = "multipart/form-data; boundary=" + fd.boundary;
            opts.data = fd.toString();
        }

        jQuery.ajax(opts);

        return false;
    });
});

Basically the form default action is overwritten and the files that were submitted on the previous code chunk for the drag and drop functionality are now appended to formData which later gets joined by the form data that was on the form I submit. Then the formData is sent via an AJAX call.

Now the controller looks like this, it handles the AJAX call and then executes the File Upload method on the model like so:

function file($param1 = '', $param2 = '', $param3 = 0) {
    if ($this->session->userdata('client_login') != 1) {
        $this->session->set_userdata('last_page', current_url());
        redirect(base_url(), 'refresh');
    }

    if ($param1 == 'create')
        $this->crud_model->create_file($param2);

    if ($param1 == 'edit')
        $this->crud_model->update_file($param2);

    if ($param1 == 'delete')
        $this->crud_model->delete_file($param2, $param3);

    $page_data['page_name'] = 'files';
    $page_data['page_title'] = 'Files List';
    $page_data['folder_id'] = $param3;
    $this->load->view('backend/index', $page_data);
}

Here is the model method:

function create_file($folder_id) {
    $data['name']         =   $this->input->post('name');
    $data['tags']         =   $this->input->post('tags');
    $data['description']  =   $this->input->post('description');
    $data['type']         =   'file';
    $data['folder_id']    =   $folder_id;
    $data['client_id']    =   $this->session->userdata('login_user_id');

    $config['upload_path'] = 'uploads/tmp/';
    $config['allowed_types'] = '*';
    $config['max_size'] = '100';

    $this->load->library('upload');
    $this->upload->initialize($config);

    var_dump($_FILES);
    die();

    foreach($_FILES as $field => $file)
    {
        //var_dump($_FILES); die();                
        // No problems with the file
        if($file['error'] == 0)
        {
            // So lets upload
            if ($this->upload->do_upload($field))
            {
                $data = $this->upload->data();
                echo $data['full_path'];
            }
            else
            {
                $errors = $this->upload->display_errors();
                var_dump($errors);
            }
        }
    }

    $this->db->insert('client_files' , $data);
}

So basically what happens is that the $_FILES array is empty, and the file doesn't get uploaded.

The "Request Payload" as viewed on Chrome's Developer Tools looks like this:

------WebKitFormBoundaryvVAxgIQd6qU8BtkF
Content-Disposition: form-data; name="file_0"

[object FileList]
------WebKitFormBoundaryvVAxgIQd6qU8BtkF
Content-Disposition: form-data; name="tags"

bsd
------WebKitFormBoundaryvVAxgIQd6qU8BtkF
Content-Disposition: form-data; name="name"

asd
------WebKitFormBoundaryvVAxgIQd6qU8BtkF
Content-Disposition: form-data; name="description"

asd

And the response I get from the var_dump() on the model is the following:

array(0) {
}

I have tried the solution on this question: Sending multipart/formdata with jQuery.ajax But no luck so far.

Any idea on what am I doing wrong and how to fix this issue? Thanks.

like image 981
Joscplan Avatar asked Jan 11 '16 22:01

Joscplan


1 Answers

The problem here is that I'm sending the array of files instead of the single files on the AJAX call, specifically in this part of the code:

for(var i = 0; i < files.length; i++) {
    fd.append("file_" + i, files[i]);
}

The solution was to append file by file to the formData instead of the array of files, something like this:

for(var i = 0; i < files[0].length; i++) {
    fd.append("file_" + i, files[i]);
}

This will append every single file of the files array instead of the array of files itself and it solves the problem.

In conclusion I was sending the array of files instead of the single files, the clue to this was the [object FileList] that the request was showing instead of the files information, which now makes a request like this:

Content-Disposition: form-data; name="file"; filename="exfile.pdf"
Content-Type: application/pdf
like image 151
Joscplan Avatar answered Oct 06 '22 00:10

Joscplan