27

I'm trying to accomplish a fairly simple task for my website, but I"m not sure exactly how to go about it. I want the user to be viewing a table, then click a button, at which point the user can save the contents of that table as a csv file. This request can sometimes be quite complicated so I generate a progress page to alert the user.

I have most things figured out except actually generating the csv file. (I use jQuery and PHP)

the jQuery code run on click:

hmis_query_csv_export: function(query_name) {
    $.uiLock('<p>Query Loading.</p><img src="/images/loading.gif" />')
    $.get({
        url: '/php_scripts/utils/csv_export.php',
        data: {query_name: query_name},
        success: function(data) {
            $.uiUnlock();
        }
    });}

the relevant PHP:

header("Content-type: text/x-csv");
header("Content-Disposition: attachment; filename=search_results.csv");
//
//Generate csv
//
echo $csvOutput
exit();

What this does is sends the text as the PHP file, but it's doesn't generate a download. What am I doing wrong?

The_Denominater
  • 985
  • 4
  • 9
  • 20

6 Answers6

48

If you are forcing a download, you can redirect the current page to the download link. Since the link will generate a download dialog, the current page (and its state) will be kept in place.

Basic approach:

$('a#query_name').click(function(){
    $('#wait-animation').show();
    document.location.href = '/php_scripts/utils/csv_export.php?query_name='+query_name;
    $('#wait-animation').hide();
});

More complicated:

$('a#query_name').click(function(){
    MyTimestamp = new Date().getTime(); // Meant to be global var
    $('#wait-animation').show();
    $.get('/php_scripts/utils/csv_export.php','timestamp='+MyTimestamp+'&query_name='query_name,function(){
        document.location.href = '/php_scripts/utils/csv_export.php?timestamp='+MyTimestamp+'&query_name='+query_name;
        $('#wait-animation').hide();
    });
});

At PHP script:

@header("Last-Modified: " . @gmdate("D, d M Y H:i:s",$_GET['timestamp']) . " GMT");
@header("Content-type: text/x-csv");
// If the file is NOT requested via AJAX, force-download
if(!isset($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) != 'xmlhttprequest') {
    header("Content-Disposition: attachment; filename=search_results.csv");
}
//
//Generate csv
//
echo $csvOutput
exit();

The URL for both requests must be the same to trick the browser not to start a new download at document.location.href, but to save the copy at the cache. I'm not totally sure about it, but seems pretty promising.

Ast Derek
  • 2,739
  • 1
  • 20
  • 28
  • Thanks a lot for the tips. I can get this to work very easily, but I'm wondering how I could use this while still implementing a screen lock and progress animation. I'm just worried that users might get confused since depending on the query this process can take up to 3 or 4 minutes. – The_Denominater Jul 27 '10 at 17:28
  • 1
    If it's going to take that long to create the CSV file, you may need to change directions slightly and instead use AJAX to kick off a PHP script that creates a temporary disk file; once the PHP script returns (with a filename) the AJAX callback function can do the redirect as Ast Derek shows. – godheadatomic Jul 27 '10 at 17:51
  • So your edit seems like it should work, but it seems like the statements are being run asynchronously. The third statement doesn't wait for the download to finish, so the wait screen appears then disappears instantaneously. Is there any way to force it to run synchronously? – The_Denominater Jul 27 '10 at 17:56
  • @godheadatomic I have artificial limitations in regards to that. The information I am relaying is confidential, and I'm not allowed to save it, even temporarily, on the web server(which is separate from the database). That limitation has caused me many problems so far.. – The_Denominater Jul 27 '10 at 17:59
  • 1
    The progress dialog is tricky. I had a similar problem here: http://stackoverflow.com/questions/1106377/detect-when-browser-receives-file-download but never really solved it. – JW. Jul 27 '10 at 18:04
  • The 'wait' animation/dialog is meant for the user to wait for the download to start, not for the download to finish. You won't be able to say if the download is finished or not, wihtout a more complicated approach – Ast Derek Jul 27 '10 at 18:10
  • Thanks for all the help, at this point I might just use the simple solution without progress notification, I'll just have to adequately warn the users in advance that the wait time is long. – The_Denominater Jul 27 '10 at 19:21
  • Ajax don't allow download file in progress. is that exactly? – huykon225 Dec 11 '17 at 09:24
5

The best way to accomplish this is to use a Data URI as follows:

  1. Make the AJAX call to the server as per normal
  2. Generate the CSV on the server-side
  3. Return the data (either bare or inside a JSON structure)
  4. Create a Data URI in Javascript using the returned data
  5. Set window.location.href to the Data URI

See this link for instructions (paragraph #3, specifically): http://en.wikipedia.org/wiki/Data_URI_scheme

This way, you don't need to save any files on the server, and you also don't need to use iframes or hidden form elements or any such hacks.

Aquarelle
  • 8,864
  • 1
  • 17
  • 11
5

EDIT I just tried this with a 10MB file and it seems that val() is too slow to insert the data. Hurrumph.


Okay, so I gave this one another go. This may or may not be completely insane! The idea is to make an AJAX request to create the data, then use the callback to insert the data into a hidden form on the current page which has an action of a third "download" page; after the insertion, the form is automatically submitted, the download page sends headers and echoes the POST, and et voila, download.

All the while, on the original page you've got an indication that the file is being prepared, and when it finishes the indicator is updated.

NOTE: this test code isn't tested extensively, and has no real security checks (or any at all) put in place. I tested it with a 1.5MB CSV file I had laying about and it was reasonably snappy.

Index.html

<a id="downloadlink" href="#">Click Me</a>
<div id="wait"></div>
<form id="hiddenform" method="POST" action="download.php">
    <input type="hidden" id="filedata" name="data" value="">
</form>

test.js

$(document).ready(function(){
  $("#downloadlink").click(function(){       // click the link to download
      lock();                                // start indicator
      $.get("create.php",function(filedata){ // AJAX call returns with CSV file data
          $("#filedata").val(filedata);      // insert into the hidden form
          unlock();                          // update indicator
          $("#hiddenform").submit();         // submit the form data to the download page
      });
  });

  function lock(){
      $("#wait").text("Creating File...");
  }

  function unlock(){
      $("#wait").text("Done");
  }
});

create.php

<?php
//create $data
print $data;
?>

download.php

<?php
header("Pragma: public");
header("Expires: 0"); 
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Content-Type: text/x-csv");
header("Content-Disposition: attachment;filename=\"search_results.csv\""); 

if($_POST['data']){
    print $_POST['data'];
}
?>
1

I don't think you can make the browser download using a AJAX/JS request. Try using a hidden iframe that navigates to the page which generates the CSV

Bob Fincheimer
  • 17,978
  • 1
  • 29
  • 54
0

Well the point of using AJAX is to avoid a visible reload of the page. If you want a download, you want the opposite,- a brand new request from the browser. I'd say, just create a simple button pointing to your php page.

Nicolas78
  • 5,124
  • 1
  • 23
  • 41
  • Ok, if I do that how would I integrate a loading animation and a UI lock into it? Wouldn't that be a reason to use AJAX? Also, I'm pretty new to this and I'm not sure the best way to do that while integrating the GET parameter into the page request. – The_Denominater Jul 27 '10 at 17:19
  • @The_Denominator use an iframe –  Jul 27 '10 at 17:30
0

To echo and expand on what others have said, you can't really send the file using AJAX. One of the reasons for this is (and someone correct me if I'm wrong on this, please) that the page you're currently on already has sent its content headers; you can't send them again to the same window, even with an AJAX request (which is what your PHP file is attempting to do).

What I've done before in projects is to simply provide a link (with target="_blank" or javascript redirect) to a separate download PHP page. If you're using Apache, check out mod_xsendfile as well.