0

I'm trying to return the results of a db_query as a raw text file. The php solution uses header() to instruct browsers to download content. I've attempted using header() in an AJAX callback function, but the following error code is always returned:

An AJAX HTTP error occurred.
HTTP Result Code: 200
Debugging information follows.
Path: /system/ajax
StatusText: OK
ResponseText: this is just a test

I'm looking for a way to save a php string value to a file client-side, ideally without requiring the file to first be saved on the server. Alternatively, if I could pass a value from an AJAX callback directly to javascript for post-processing, that would work too (haven't found an AJAX callback with this capability).

Here's my test code (that produces the above error):

<?php

function test_download_form($form, &$form_state) {

  $form['form_wrapper'] = array(
    '#prefix' => '<div id="form_wrapper">',
    '#suffix' => '</div>',
  );

  $form['form_wrapper']['download_submit'] = array(
    '#type' => 'button',
    '#value' => 'test download',
    '#id' => 'download_submit',
    '#ajax' => array(
      'callback' => 'test_download',
    ),
  );

  return $form;
}

function test_download($form, $form_state) {  
  $dateFile = rand(10000000, 99999999)."_test.txt";
  $dataString = "this is just a test";

  //header() and drupal_add_http_header() behave the same
  header('Content-Type: application/text');
  header('Content-Disposition: attachment; filename="'.$dateFile.'"');
/*  
  drupal_add_http_header('Content-Type', 'application/text; utf-8');
  drupal_add_http_header('Content-Disposition', 'attachment; filename="'.$dateFile.'"');
  drupal_send_headers();
*/
  echo $dataString;
  exit;
}
  • 5
    You can't force a file download from an AJAX request (it's a security thing). You either need to save the file and set location.href = '/path/to/file', or add a route which takes the string as a parameter and delivers the text file from memory, and set that instead (e.g. location.href = '/download-string?string=Foo';). You also might need to use application/octet-stream as the Content-Type header to get it working – Clive Aug 9 at 18:40
  • 1
    If it was me, and privacy wasn't a concern, I'd save it in a folder somewhere and run a cron job to clear out files older than an acceptable number of minutes. Yes there a char limits for a URL, but if you decide you don't want to save the file at all, I don't really see another option that isn't a variation/derivative – Clive Aug 9 at 19:04
  • 1
    Actually an alternative would be to save the text against a hash/identifier in the database or something, during the PHP part of the AJAX request, return that identifier to the browser, and use it in the URL instead of the full text. The download route would get the text from the DB by the ID in the query params, and stream it for download. – Clive Aug 9 at 19:07
  • 1
    I mean, if you don't need PHP, then yeah...don't use it :) You wouldn't need an AJAX request in that scenario, you would just have a link that, on click, base64 encodes the text and adds it to the href as a data URI. You wouldn't really need a cron job, this would be single-serving so the download script would delete the record after retrieving the text – Clive Aug 9 at 19:17
  • 1
    Once you have the string downloaded create the file in the browser, on the fly, mostly like @Clive suggested above: const blob = new Blob([plaintext], {type: 'application/octet-binary', endings: 'native'});link.attr('href', URL.createObjectURL(blob));const link = $('#your-link'); link.attr('href', URL.createObjectURL(blob));link.attr('download', 'file.txt'); As long as your file name contains the correct extension it does not matter type is binary. You can see this approach working here . – Rodrigo Panchiniak Fernandes Aug 13 at 13:30
0

I ended up making some modifications to the Drupal CSV module (code). The module code demonstrates that you can stream information directly to the client with header() designations. From what I was able to find, you aren't able to use form headers within form callbacks. An alternative method involved passing information with sessioning to an empty page (that doesn't have header restrictions). This had several undesirable consequences:

1.) Session information is saved temporarily server-side, increases time to load to client due to having to write twice.

2.) Finite memory allotment for sessions.

The solution below does not have these limitations:

<?php

function test_download_form($form, &$form_state) {

  $form['form_wrapper'] = array(
    '#prefix' => '<div id="form_wrapper">',
    '#suffix' => '</div>',
  );

  $form['form_wrapper']['download_submit'] = array(
    '#type' => 'submit',
    '#value' => 'test download',
    '#name' => 'retrieve',
  );

  return $form;
}

function test_download_form_submit($form, &$form_state) {
  $characters = 'ACGT';
  $randomString = '';
  $n = 50000000;

  for ($i = 0; $i < $n; $i++) { 
    $index = rand(0, strlen($characters) - 1); 
    $randomString .= $characters[$index]; 
  }

  //https://git.drupalcode.org/project/csv/blob/7.x-1.x/csv.module
  drupal_add_http_header('Content-Type', 'text; charset=utf-8');
  drupal_add_http_header('Content-Disposition', 'attachment; filename="test_header_method.txt"');
  $fp = fopen('php://output', 'w');
  fwrite($fp, $randomString);
  fclose($fp);
  drupal_exit();
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.