2

I'm sorry, I'm pretty sure I've seen a similar question one day but I can't find it and I can't figure it out on my own.

I need to be able to save a JSON file locally, but the download attribute and the File API aren't browser-friendly enough so I decided to send the JSON string to the server and have the server send me the file with the right HTTP headers to make the browser download it.

But I don't want to leave the page. From what I can see, AJAX can't trigger file download (or I've been trying it wrong), so I'd have to use a hidden HTML form which would send the data through POST. How can I send POST data to a php file which will return a file to download without leaving the page?

What I've tried in AJAX:

sd.requestDownload = function() {
  if(sd.request && sd.request.readyState != 0) {
    return false; // there's already a request in progress
  }

  sd.request = sd.createRequest();

  sd.request.onreadystatechange = function() {
    if(sd.request.readyState == 4 && (sd.request.status == 200 || sd.request.status == 0)) {
      // download?
    } else if(sd.request.readyState < 4) {
      // show loading screen here
    }
  }

  sd.request.open("POST", "save.php", true);
  sd.request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  sd.request.send("json=" + encodeURIComponent(sd.saveToJSON()));
};

The PHP code:

<?PHP

session_start();

$file = '';

if(isset($_POST['json'])) {
  header('Content-Type: application/json; charset=utf-8');
  header('Content-Disposition: attachment; filename=plan.json');
  $file = $_POST['json'];
} else if(isset($_POST['file'])) {
  header('Content-Type: application/octet-stream; charset=utf-8');
  header('Content-Disposition: attachment; filename=plan');
  $file = $_POST['file'];
} else {
  http_response_code(400);
  $file = '400 ERROR: NO FILE RECEIVED';
}

header('Expires: Sun, 01 Jan 2015 00:00:00 GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', FALSE);
header('Pragma: no-cache');

echo htmlentities($file);
Domino
  • 4,988
  • 27
  • 51
  • If you navigate to a page that sends out headers to force a download, the user won't be navigated away from the current page. They will just be prompted for the download and stay on the same page. – Jonathan Kuhn Apr 23 '15 at 16:30

1 Answers1

0

I've figured something which works, but IE shows an error about missing a doctype in the debugger. My solution involves creating a form in JS without appending it to the page. If the HTTP header says that it's a file attachment, the page won't change. I've seen people talk about setting the target to an iframe or to "_blank" for extra saftey, but Chrome considers such calls to be popups, so it blocks them and won't send the POST request again if the user clicks on "show popup".

EDIT: it seems IE ignores the call to submit() if the form isn't on the page. I modified the code in consequence.

This question is what gave me the idea: JavaScript post request like a form submit

PHP

if(isset($_POST['filecontent'])) {
  header('Content-Type: application/octet-stream; charset=utf-8');
  if(isset($_POST['filename'])) {
    header('Content-Disposition: attachment; filename=' . preg_replace('/[^a-zA-Z0-9_\.]+/', "", $_POST['filename']));
  } else {
    header('Content-Disposition: attachment;');
  }
  $file = $_POST['filecontent'];
} else {
  http_response_code(400);
  $file = '400 ERROR: NO FILE RECEIVED';
}

header('Expires: Sun, 01 Jan 2015 00:00:00 GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', FALSE);
header('Pragma: no-cache');

echo $file;

JS

download = function(filename, filecontent) {
  var form = document.createElement("form");
  form.setAttribute("method", "post");
  form.setAttribute("action", "save.php");
  form.style.display = "none";
  var i = document.createElement("input");
  i.setAttribute("type", "hidden");
  i.setAttribute("name", "filename");
  i.setAttribute("value", filename);
  form.appendChild(i);
  i = document.createElement("input");
  i.setAttribute("type", "hidden");
  i.setAttribute("name", "filecontent");
  i.setAttribute("value", filecontent);
  form.appendChild(i);

  document.body.appendChild(form);
  form.submit();
  document.body.removeChild(form);
};
Community
  • 1
  • 1
Domino
  • 4,988
  • 27
  • 51