64

I am trying to fake a file upload without actually using a file input from the user. The file's content will be dynamically generated from a string.

Is this possible? Have anyone ever done this before? Are there examples/theory available?

To clarify, I know how to upload a file using AJAX techniques using a hidden iframe and friends - the problem is uploading a file that is not in the form.

I am using ExtJS, but jQuery is feasible as well since ExtJS can plug into it (ext-jquery-base).

LiraNuna
  • 64,916
  • 15
  • 117
  • 140
  • This seems like the wrong solution to your problem (if you have control of the server-side). If the file's content will be generated from a string, why not just POST that string and create the file on the server (using PHP or whatever)? If you are uploading a file to a 3rd party destination, then ignore this comment. – Jonathan Julian Feb 04 '10 at 18:00
  • @JonathanJulian, no matter what, this usecase smells of real hack-value -), awesome trick! – JWL Jun 07 '12 at 13:22

7 Answers7

47

If you don't need support for older browsers, you can use the FormData Object, which is part of the File API:

const formData = new FormData();
const blob = new Blob(['Lorem ipsum'], { type: 'plain/text' });
formData.append('file', blob, 'readme.txt');

const request = new XMLHttpRequest();
request.open('POST', 'http://example.org/upload');
request.send(formData);

File API is supported by all current browsers (IE10+)

Josa
  • 716
  • 6
  • 10
  • 2
    I avoid writing my own XMLHttpRequests. This is definitely my preferred answer! – Chris Jun 02 '16 at 19:00
  • 1
    This should be the accepted answer - I spent 8 hours combing through various posts, and this is what worked, and in very few lines of code. – domoarigato Oct 05 '16 at 09:14
36

Why not just use XMLHttpRequest() with POST?

function beginQuoteFileUnquoteUpload(data)
{
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "http://www.mysite.com/myuploadhandler.php", true);
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhr.onreadystatechange = function ()
    {
        if (xhr.readyState == 4 && xhr.status == 200)
            alert("File uploaded!");
    }
    xhr.send("filedata="+encodeURIComponent(data));
}

The handler script at the server just writes the file data to a file.

EDIT
File upload is still a http post with a different content type. You can use this content type and separate your content with boundaries:

function beginQuoteFileUnquoteUpload(data)
{
    // Define a boundary, I stole this from IE but you can use any string AFAIK
    var boundary = "---------------------------7da24f2e50046";
    var xhr = new XMLHttpRequest();
    var body = '--' + boundary + '\r\n'
             // Parameter name is "file" and local filename is "temp.txt"
             + 'Content-Disposition: form-data; name="file";'
             + 'filename="temp.txt"\r\n'
             // Add the file's mime-type
             + 'Content-type: plain/text\r\n\r\n'
             + data + '\r\n'
             + boundary + '--';

    xhr.open("POST", "http://www.mysite.com/myuploadhandler.php", true);
    xhr.setRequestHeader(
        "Content-type", "multipart/form-data; boundary="+boundary

    );
    xhr.onreadystatechange = function ()
    {
        if (xhr.readyState == 4 && xhr.status == 200)
            alert("File uploaded!");
    }
    xhr.send(body);
}

If you want to send additional data, you just separate each section with a boundary and describe the content-disposition and content-type headers for each section. Each header is separated by a newline and the body is separated from the headers by an additional newline. Naturally, uploading binary data in this fashion would be slightly more difficult :-)

Further edit: forgot to mention, make sure whatever boundary string isn't in the text "file" that you're sending, otherwise it will be treated as a boundary.

LiraNuna
  • 64,916
  • 15
  • 117
  • 140
Andy E
  • 338,112
  • 86
  • 474
  • 445
  • Because the server will not recognize it as an uploaded 'file'. – LiraNuna Feb 04 '10 at 09:30
  • I think he wants to know how to generate `data`. – Luca Matteis Feb 04 '10 at 09:31
  • @LiraNuna: Why does that matter if you're generating the content from a string? Can't it just recognize it as a string and write it? – Andy E Feb 04 '10 at 09:32
  • 1
    That would require me to change server-side code, which is in my case, impossible (remote service). – LiraNuna Feb 04 '10 at 09:33
  • Maybe I'm doing something bad, but I just can't get the server to recognize the request as valid POST. – LiraNuna Feb 04 '10 at 21:47
  • 1
    @Andy: It's okay, I had to read the RFC several times to get it working! – LiraNuna Feb 05 '10 at 19:42
  • 1
    Just two minor corrections (I found a web application not accepting the above format): Contenty-Type should have a capital "T" and the last line of "body" should have two more dashes at the beginning: + '--' + boundary + '--'; – sowdust Nov 19 '19 at 22:17
13

Just sharing the final result, which works - and has clean way of adding/removing parameters without hardcoding anything.

var boundary = '-----------------------------' +
            Math.floor(Math.random() * Math.pow(10, 8));

    /* Parameters go here */
var params = {
    file: {
        type: 'text/plain',
        filename: Path.utils.basename(currentTab.id),
        content: GET_CONTENT() /* File content goes here */
    },
    action: 'upload',
    overwrite: 'true',
    destination: '/'
};

var content = [];
for(var i in params) {
    content.push('--' + boundary);

    var mimeHeader = 'Content-Disposition: form-data; name="'+i+'"; ';
    if(params[i].filename)
        mimeHeader += 'filename="'+ params[i].filename +'";';
    content.push(mimeHeader);

    if(params[i].type)
        content.push('Content-Type: ' + params[i].type);

    content.push('');
    content.push(params[i].content || params[i]);
};

    /* Use your favorite toolkit here */
    /* it should still work if you can control headers and POST raw data */
Ext.Ajax.request({
    method: 'POST',
    url: 'www.example.com/upload.php',
    jsonData: content.join('\r\n'),
    headers: {
        'Content-Type': 'multipart/form-data; boundary=' + boundary,
        'Content-Length': content.length
    }
});

This was tested to work on all modern browsers, including but not limited to:

  • IE6+
  • FF 1.5+
  • Opera 9+
  • Chrome 1.0+
  • Safari 3.0+
LiraNuna
  • 64,916
  • 15
  • 117
  • 140
  • +1 Nice solution. But I think is something wrong with your algorithm. Why you use a `for in` for the params object? It seams like it's prepared for more than one file but the second file how will be named in the object? Where are `action`, `overwrite`, and `destination` used? and how they not break the code inside the `for in`? – Mariano Desanze Sep 03 '10 at 19:21
  • @Protron: The reason I use `for( in )` is to get the keys from the description object. The code will detect if `filename` is set on a nested object (that describes a file to upload). The other parameters (`overwrite`, `action`, `destination`) are just extra parameters passed as if you used a form. – LiraNuna Sep 04 '10 at 03:38
  • 2
    @LiraNuna, I see all you guys getting all magical about the `-----------------------------`, the only requirement by the MIME spec (see RFC 1341, sec 7.2.1) is that the the boundary commence with `--`followed by a valid token (see RFC 1341 sec.4). Hope this helps others know their freedom too :-) – JWL Jun 07 '12 at 13:37
  • 2
    Hi, this code isn't quite correct. Content-Length is incorrectly calculated - it doesn't include the '\r\n' in the array join. Also technically this doesn't do the boundary correctly. It should be '--boundary' initially, then 'boundary' between parts and 'boundary--' afterwards. With these fixes it appears to work OK for me against Tomcat/JBoss. Great work :-) – Swannie Oct 17 '12 at 16:14
7

A file upload it's just a POST request with that file content properly encoded and with an special multipart/formdata header. You need to use that <input type=file /> because your browser security forbid you to access user disk directly.

As you don't need to read user disk, YES, you can fake it using Javascript. It will be just a XMLHttpRequest. To forge an "authentic" upload request, you can install Fiddler and inspect your outgoing request.

You'll need to encode that file correctly, so this link can be very useful: RFC 2388: Returning Values from Forms: multipart/form-data

Valentin Rocher
  • 11,667
  • 45
  • 59
Rubens Farias
  • 57,174
  • 8
  • 131
  • 162
  • What should go in that request then? how is that protocol defined? how to fake it? – LiraNuna Feb 04 '10 at 09:37
  • that isn't a protocol, it's just a regular HTTP request; I updated my answer – Rubens Farias Feb 04 '10 at 09:40
  • I didn't use Fiddler (Linux user here), but Firebug does show how it should look. This brings me one step closer. I am upvoting as it is helpful, but not yet selecting the answer. – LiraNuna Feb 04 '10 at 09:49
6

Easy way to imitate "fake" file upload with jQuery:

var fd = new FormData();
var file = new Blob(['file contents'], {type: 'plain/text'});

fd.append('formFieldName', file, 'fileName.txt');

$.ajax({
  url: 'http://example.com/yourAddress',
  method: 'post',
  data: fd,
  processData: false,        //this...
  contentType: false         //and this is for formData type
});
ixpl0
  • 748
  • 5
  • 15
4

I just caught this POST_DATA string with the Firefox TamperData addon. I submitted a form with one type="file" field named "myfile" and a submit button named "btn-submit" with value "Upload". The contents of the uploaded file are

Line One
Line Two
Line Three

So here is the POST_DATA string:

-----------------------------192642264827446\r\n
Content-Disposition: form-data;    \n
name="myfile"; filename="local-file-name.txt"\r\n
Content-Type: text/plain\r\n
\r\n
Line \n
One\r\n
Line Two\r\n
Line Three\r\n
\r\n
-----------------------------192642264827446\n
\r\n
Content-Disposition: form-data; name="btn-submit"\r\n
\r\n
Upload\n
\r\n
-----------------------------192642264827446--\r\n

I'm not sure what the number means (192642264827446), but that should not be too hard to find out.

John La Rooy
  • 295,403
  • 53
  • 369
  • 502
Tom Bartel
  • 2,283
  • 1
  • 15
  • 18
  • I reformatted the POST_DATA to make it easier to read, the 192642264827446 looks like a boundary marker – John La Rooy Feb 04 '10 at 10:16
  • Thanks, gnibbler. Yeah, I thought it might be something like a boundary marker, probably just some random number. – Tom Bartel Feb 04 '10 at 11:13
  • 1
    Yeah, it's a boundary marker. If you check the `multipart/form-data` header, the boundary will follow it. The random number at the end is to avoid any conflictions with the data being sent. – Andy E Feb 04 '10 at 12:14
3

https://stackoverflow.com/a/2198524/2914587 worked for me, after I added an extra '--' before the final boundary in the payload:

var body = '--' + boundary + '\r\n'
         // Parameter name is "file" and local filename is "temp.txt"
         + 'Content-Disposition: form-data; name="file";'
         + 'filename="temp.txt"\r\n'
         // Add the file's mime-type
         + 'Content-type: plain/text\r\n\r\n'
         + data + '\r\n'
         + '--' + boundary + '--';
Community
  • 1
  • 1