MPaul
MPaul

Reputation: 2733

ColdFusion unable to read FormData sent by Ajax

I'm creating a file upload component that I can <cfinclude> onto any of my CFM pages which allows for both the standard file selection and Drag & Drop functionality.

When choosing a file and submitting the form, there is no problem since my ColdFusion code relies on the Form scope in order to retrieve the file.

In order to send files using the Drag & Drop feature, I'm using jQuery to send a request through Ajax sending the FormData based on the current form.

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

    // Get FormData based on form and append the dragged and dropped files 
    var ajaxData = new FormData($form.get(0));
    if (droppedFiles) {
        $.each(droppedFiles, function(i, file) {
            ajaxData.append($('input[type=file]').attr('name'), file);
        });
    }

    $.ajax({
        url: $form.attr('action'),
        type: $form.attr('method'),
        cache: false,
        contentType:false,
        processData: false,
        success: function(data) {
            if (data.SUCCESS) uploadSuccessful($form);
            else
            {
                console.error(data.ERROR);
            }
        },
        error: function(jqXHR, textStatus, errorThrown) {
            console.error('Error: File upload unsuccessful ' + errorThrown);
        },
        data: ajaxData,
        dataType: 'json',
        dataFilter: function(data, type){ 
            return data.substring(2, data.length); 
        }
    });
});

The HTML:

<form class="file-upload-container" method="post" action="upload.cfm" enctype="multipart/form-data">
    <label class="file-upload-input">
        <i class="fa fa-upload" aria-hidden="true" style="font-size:5em;"></i><br/><br/>
        <input type="file" name="attachment" id="attachment" />
    </label>
    <div class="file-upload-controls">
        <input type="button" id="cancelUpload" value="Cancel" class="btn btn-green" />
        <input type="submit" id="upload" value="Upload" class="btn btn-green" />
    </div>
</form>

The action of the <form> is to post to my upload.cfm page.

I first verify if the form posted had a element with the name "attachment":

<cfif structKeyExists(form, "attachment")>
    <!--- This always passes since I'm posting the form with the submit button --->
</cfif>

I then try to retrieve the filename so I can compare against the accepted file types, upload the file, rename the file, and insert an entry into my database. Where I am experiencing my issue is when I'm trying to get the filename from the posted FormData object (or even the entire File's contents..).

<cffunction name="GetTheFileName" access="public" returntype="string" output="false" >
    <cfargument name="fieldName" required="true" type="string" hint="Name of the Form field" />

    <cfset var tmpPartsArray = Form.getPartsArray() />

    <cfif IsDefined("tmpPartsArray")>
        <cfloop array="#tmpPartsArray#" index="local.tmpPart">
            <cfif local.tmpPart.isFile() AND local.tmpPart.getName() EQ arguments.fieldName> 
                <cfreturn LCase(local.tmpPart.getFileName()) />
            </cfif>
        </cfloop>
    </cfif>
    <cfreturn "" />
</cffunction>

The line Form.getPartsArray() returns an array, however the values within the array are empty. (e.g., FilePath:'',FileName:'')

This leads me to believe that FormData doesn't act in the same way as an actual form that is posted, regardless if ajax is posting the FormData as multipart/form-data.

Questions

  1. What am I missing in order to read the FormData object on the ColdFusion side in order to retrieve the FileName.

  2. How can the Form scope effectively utilize posted FormData as if it were an actual <form> being posted.

Research

This source indicates that I can read the file using Java's FileOutputStream. (Not an ideal solution since I also allow for the typical "Choose a file" which already utilizes the Form scope)

Upvotes: 5

Views: 1548

Answers (1)

MPaul
MPaul

Reputation: 2733

I have found a solution, albeit not the solution I was initially hoping for.

From my observations, ColdFusion does not treat the FormData object in the same way as an actual form post. This is clear when the Form scope doesn't contain the form data.

Instead I had to handle data posted with the Drag & Drop functionality in a different manner.

Following the suggested method for uploading chunks of files (source), I requested the upload.cfm page through ajax where the Content-Type matched the MimeType of the file to be uploaded, and the actual upload used the java.io.FileOutputStream class to write the file to disk.

JavaScript + AJAX:

var droppedFiles; // Assuming droppedFiles was populated during the 'drop' event

// Get the Form and FormData (although FormData won't be used when files are Dragged  & Dropped)
var $form = $('.file-upload-container');
var ajaxData = new FormData($(form).get(0));

var dragged = false;                
var filesToBeUploaded = document.getElementById("attachment");  
var file = filesToBeUploaded.files[0]; 
if (droppedFiles){file = droppedFiles;dragged=true;}

$.ajax({
    url: $form.attr('action'),
    type: $form.attr('method'),
    cache: false,
    contentType: dragged ? file.type : false,
    processData: false,
    success: function(data) {
        if (data.SUCCESS) { // Do stuff }
        else
        {
            // Do stuff
            console.error(data.ERROR);
        }
    },
    error: function(jqXHR, textStatus, errorThrown) {
        console.error('Error: File upload unsuccessful ' + errorThrown);
    },
    data: dragged ? file : ajaxData,
    dataType: 'json',
    dataFilter: function(data, type){ 
        return data.substring(2, data.length); 
    },
    headers: { 'X_FILE_NAME': dragged ? file.name : "", 'X_CONTENT_TYPE': dragged ? file.type : "" }
});

ColdFusion:

<cfset bUseFormData = false />

<cfset headerData = getHTTPRequestData().headers>
<cfif structKeyExists(headerData, "X_FILE_NAME") AND ("#headerData.X_FILE_NAME#" NEQ "")>
    <cfset bUseFormData = true />        
</cfif>

<cfif structKeyExists(form, "attachment") OR bUseFormData>

    <cfif bUseFormData>
        <cfset content = getHTTPRequestData().content>
        <cfset filePath = "#Application.AdminUploadPath#\" & "#headerData.X_CONTENT_TYPE#">
        <cfset fos = createObject("java", "java.io.FileOutputStream").init(filePath, true)>
        <cfset fos.write(content)>
        <cfset fos.close()>
    <cfelse>
        <cffile action="UPLOAD" filefield="form.attachment" destination="#Application.AdminUploadPath#">
    </cfif>

</cfif>

For simplicity's sake, I've excluded code validating file extension, MimeType, and error handling.

Upvotes: 1

Related Questions