Browser Add-ons, JavaScript, Mozilla, Programming

Uploading form data and files with JavaScript (Mozilla)

One problem I stumble across occasionally in writing Firefox extensions is properly uploading form data that includes a file – that is, assembling the POST request in JavaScript while still maintaining the sanctity of any file or string data. You can’t just do this:

var request = "--boundary\r\n some text\r\n--boundary" + fileBytes + "\r\n--boundary--";

I had to spend a bit of time getting this just right in order to allow ScribeFire to upload media to Posterous, so I’m posting below the final solution at which I arrived; it was cobbled together from a dozen different examples I found around the Web (none of them solving the full problem), then lovingly massaged into the elegant function you see before you. With this function, you can pass in an array of fields and files, and the request will be crafted and returned to you, ready for upload.

Instructions for use are in the comment block at the top of the function.

function createPostRequest(args) {
  /**
   * Generates a POST request body for uploading.
   *
   * args is an associative array of the form fields.
   *
   * Example:
   * var args = { "field1": "abc", "field2" : "def", "fileField" : 
   *              { "file": theFile, "headers" : [ "X-Fake-Header: foo" ] } };
   * 
   * theFile is an nsILocalFile; the headers param for the file field is optional.
   *
   * This function returns an array like this:
   * { "requestBody" : uploadStream, "boundary" : BOUNDARY }
   * 
   * To upload:
   *
   * var postRequest = createPostRequest(args);
   * var req = new XMLHttpRequest();
   * req.open("POST", ...);
   * req.setRequestHeader("Content-Type","multipart/form-data; boundary="+postRequest.boundary);
   * req.setRequestHeader("Content-Length", (postRequest.requestBody.available()));
   * req.send(postRequest.requestBody);
   */
  
  function stringToStream(str) {
    function encodeToUtf8(oStr) {
      var utfStr = oStr;
      var uConv = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
                    .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
      uConv.charset = "UTF-8";
      utfStr = uConv.ConvertFromUnicode(oStr);

      return utfStr;
    }
    
    str = encodeToUtf8(str);
    
    var stream = Components.classes["@mozilla.org/io/string-input-stream;1"]
                   .createInstance(Components.interfaces.nsIStringInputStream);
    stream.setData(str, str.length);
    
    return stream;
  }
  
  function fileToStream(file) {
    var fpLocal  = Components.classes['@mozilla.org/file/local;1']
                     .createInstance(Components.interfaces.nsILocalFile);
    fpLocal.initWithFile(file);

    var finStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
                      .createInstance(Components.interfaces.nsIFileInputStream);                
    finStream.init(fpLocal, 1, 0, false);

    var bufStream = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
                      .createInstance(Components.interfaces.nsIBufferedInputStream);
    bufStream.init(finStream, 9000000);
    
    return bufStream;
  }
  
  var mimeSvc = Components.classes["@mozilla.org/mime;1"]
                  .getService(Components.interfaces.nsIMIMEService);
  const BOUNDARY = "---------------------------32191240128944"; 
  
  var streams = [];
  
  for (var i in args) {
    var buffer = "--" + BOUNDARY + "\r\n";
    buffer += "Content-Disposition: form-data; name=\"" + i + "\"";
    streams.push(stringToStream(buffer));
    
    if (typeof args[i] == "object") {
      buffer = "; filename=\"" + args[i].file.leafName + "\"";
      
      if ("headers" in args[i]) {
        if (args[i].headers.length > 0) {
          for (var q = 0; q < args[i].headers.length; q++){
            buffer += "\r\n" + args[i].headers[q];
          }
        }
      }
      
      var theMimeType = mimeSvc.getTypeFromFile(args[i].file);
      
      buffer += "\r\nContent-Type: " + theMimeType;
      buffer += "\r\n\r\n";
      
      streams.push(stringToStream(buffer));
      
      streams.push(fileToStream(args[i].file));
    }
    else {
      buffer = "\r\n\r\n";
      buffer += args[i];
      buffer += "\r\n";
      streams.push(stringToStream(buffer));
    }
  }
  
  var buffer = "--" + BOUNDARY + "--\r\n";
  streams.push(stringToStream(buffer));
  
  var uploadStream = Components.classes["@mozilla.org/io/multiplex-input-stream;1"]
                       .createInstance(Components.interfaces.nsIMultiplexInputStream);
  
  for (var i = 0; i < streams.length; i++) {
    uploadStream.appendStream(streams[i]);
  }
  
  return { "requestBody" : uploadStream, "boundary": BOUNDARY };
}
Standard

4 comments on “Uploading form data and files with JavaScript (Mozilla)

  1. CSBarbie says:

    Hi,
    I am going to try use this in my thunderbird extension and I am Trying to upload the file to a servlet. I am new to servlet programming so could you guide me how I could process this so I can and save the uploaded file at the server end. Thank you

Leave a Reply

Your email address will not be published. Required fields are marked *