Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I would like to add a "Click to download" button to one of my WordPress plugins, and I'm not sure which hook to use. So far, hooking 'admin_init' to this code seems to work:

 header("Content-type: application/x-msdownload");
 header("Content-Disposition: attachment; filename=data.csv");
 header("Pragma: no-cache");
 header("Expires: 0");
 echo 'data';
 exit();

This seems to work, but I just want to see if there is a best practice out there.

Thanks, Dave

share|improve this question

2 Answers 2

up vote 19 down vote accepted

Hi @Dave Morris:

If I understand you correctly you want to have a URL something like the following whose response to the browser will be the content you generate, i.e. your .CSV file and no generated content from WordPress?

http://example.com/download/data.csv

I think you are looking for the 'template_redirect' hook. You can find 'template_redirect' in /wp-includes/template-loader.php which is a file all WordPress developers should become familiar with; it's short and sweet and routes every non-admin page load so be sure to take a look at it.

Just add the following to your theme's functions.php file or in another file that you include in functions.php:

add_action('template_redirect','yoursite_template_redirect');
function yoursite_template_redirect() {
  if ($_SERVER['REQUEST_URI']=='/downloads/data.csv') {
    header("Content-type: application/x-msdownload",true,200);
    header("Content-Disposition: attachment; filename=data.csv");
    header("Pragma: no-cache");
    header("Expires: 0");
    echo 'data';
    exit();
  }
}

Note the test for the '/downloads/data.csv' URL by inspecting $_SERVER['REQUEST_URI']. Also note the added ,true,200 to your header() call where you set the Content-type; this is because WordPress will have set the 404 "Not Found" status code because it doesn't recognize the URL. It's no problem though as the true tells header() to replace the 404 WordPress had set and to use the HTTP 200 "Okay" status code instead.

And here's what it looks like in FireFox (Note the screenshot doesn't have a /downloads/ virtual directory because after taking and annotating the screenshot it just seemed like a good idea to add a '/downloads/' virtual directory):

Screenshot of a download URL for a CSV file

UPDATE

If you want the download to be handled from a URL that is prefixed with /wp-admin/ to give the user the visual indication that it is protected by a login you can do that as well; the description of one way follows.

I encapsulated into a class this time, called DownloadCSV, and to created a user "capability" called 'download_csv' for the 'administrator' role (read about Roles and Capabilities here) You could just piggyback off of the predefined 'export' role if you like and if so just search & replace 'download_csv' with 'export' and remove the register_activation_hook() call and the activate() function. By the way, the need for a activation hook is one reason why I moved this to a plugin instead of keeping in the theme's functions.php file.*

I also added a "Download CSV" menu option off the "Tools" menu using add_submenu_page() and linked it to the 'download_csv' capability.

Lastly I chose the 'plugins_loaded' hook because it was the earliest appropriate hook I could use. You could use 'admin_init' but that hook is run much later (1130th hook call vs. the 3rd hook call) so why let WordPress do more throw-away work than it needs to? (I used my Instrument Hooks plugin to figure out which hook to use.)

In the hook I check to ensure my URL starts with /wp-admin/tools.php by inspecting the $pagenow variable, I verify that current_user_can('download_csv') and if that passes then I test $_GET['download'] to see if it contains data.csv; if yes we run practically the same code as before. I also remove the ,true,200 from the call to header() in the previous example because here WordPress knows it is a good URL so didn't set the 404 status yet. So here is your code:

<?php
/*
Plugin Name: Download CSV
Author: Mike Schinkel
Author URI: http://mikeschinkel.com
 */
if (!class_exists('DownloadCSV')) {
  class DownloadCSV {
    static function on_load() {
      add_action('plugins_loaded',array(__CLASS__,'plugins_loaded'));
      add_action('admin_menu',array(__CLASS__,'admin_menu'));
      register_activation_hook(__FILE__,array(__CLASS__,'activate'));
    }
    static function activate() {
      $role = get_role('administrator');
      $role->add_cap('download_csv');
    }
    static function admin_menu() {
      add_submenu_page('tools.php',    // Parent Menu
        'Download CSV',                // Page Title
        'Download CSV',                // Menu Option Label
        'download_csv',                // Capability
        'tools.php?download=data.csv');// Option URL relative to /wp-admin/
    }
    static function plugins_loaded() {
      global $pagenow;
      if ($pagenow=='tools.php' && 
          current_user_can('download_csv') && 
          isset($_GET['download'])  && 
          $_GET['download']=='data.csv') {
        header("Content-type: application/x-msdownload");
        header("Content-Disposition: attachment; filename=data.csv");
        header("Pragma: no-cache");
        header("Expires: 0");
        echo 'data';
        exit();
      }
    }
  }
  DownloadCSV::on_load();
}

And here's a screenshot of the activated plugin: Screenshot of Plugin Page showing an activated plugin

And finally here's a screenshot of triggering the download: Screenshot of Downloading a file by URL from an option of the WordPress admin's Tools menu

share|improve this answer
    
Mike, thanks for your help. The only catch with this feature is that I would like the file to be downloaded from the backend. It looks like template_redirect doesn't work on the backend, and if I'm not supposed to use admin_init, I wonder what I should use instead. admin_init seems to work for me now, I might stick with it at least for the short term. It's a minor feature that only a few people are going to use. –  Dave Morris Nov 3 '10 at 2:37
    
@Dave Morris - Can you define what you mean by "back end"? Do you mean on the server? If yes, 'template_redirect' most definitely runs on the server. If not, I'd totally confused; can you clarify the concern? Thanks in advance. –  MikeSchinkel Nov 3 '10 at 12:00
    
@Dave: If you mean the admin area by the "back end", this will still work. The download URL starts with /downloads/data.csv, which is a non-existing file, so the WordPress "front end" will handle this request and eventually reach template-redirect. You just create a link in the admin area that points to this frontside URL. (It must be said that this way, you don't get the admin login protection for free - anyone who knows the URL can download the file, but maybe there is an easy way to fix that?) –  Jan Fabry Nov 3 '10 at 20:41
    
@Jan Fabry - Ah, I understand now. By "back end" he meant from within the admin, right? He can use the function current_user_can() with the above code or take another approach. After this comment I'll add an update to my answer. –  MikeSchinkel Nov 4 '10 at 9:55
    
Yes, I apologize, I am not receiving email alerts from this site, so that explains my delay in answering back. I was indeed referring to the admin area of WordPress when I said "backend". Sorry about that. I will try using template_redirect and see what happens. Thanks! ~Dave –  Dave Morris Nov 5 '10 at 22:47

one more useful plugin for exporting into CSV. may be useful to some one

    <?php

class CSVExport
{
/**
* Constructor
*/
public function __construct()
{
if(isset($_GET['download_report']))
{
$csv = $this->generate_csv();

header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private", false);
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"report.csv\";" );
header("Content-Transfer-Encoding: binary");

echo $csv;
exit;
}

// Add extra menu items for admins
add_action('admin_menu', array($this, 'admin_menu'));

// Create end-points
add_filter('query_vars', array($this, 'query_vars'));
add_action('parse_request', array($this, 'parse_request'));
}

/**
* Add extra menu items for admins
*/
public function admin_menu()
{
add_menu_page('Download Report', 'Download Report', 'manage_options', 'download_report', array($this, 'download_report'));
}

/**
* Allow for custom query variables
*/
public function query_vars($query_vars)
{
$query_vars[] = 'download_report';
return $query_vars;
}

/**
* Parse the request
*/
public function parse_request(&$wp)
{
if(array_key_exists('download_report', $wp->query_vars))
{
$this->download_report();
exit;
}
}

/**
* Download report
*/
public function download_report()
{
echo '<div class="wrap">';
echo '<div id="icon-tools" class="icon32">
</div>';
echo '<h2>Download Report</h2>';
//$url = site_url();

echo '<p>Export the Users';
}

/**
* Converting data to CSV
*/
public function generate_csv()
{
$csv_output = '';
$table = 'users';

$result = mysql_query("SHOW COLUMNS FROM ".$table."");

$i = 0;
if (mysql_num_rows($result) > 0) {
while ($row = mysql_fetch_assoc($result)) {
$csv_output = $csv_output . $row['Field'].",";
$i++;
}
}
$csv_output .= "\n";

$values = mysql_query("SELECT * FROM ".$table."");
while ($rowr = mysql_fetch_row($values)) {
for ($j=0;$j<$i;$j++) {
$csv_output .= $rowr[$j].",";
}
$csv_output .= "\n";
}

return $csv_output;
}
}

// Instantiate a singleton of this plugin
$csvExport = new CSVExport();
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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