Wordpress - How can I force a file download in the WordPress backend?
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):
(source: mikeschinkel.com)
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:
(source: mikeschinkel.com)
And finally here's a screenshot of triggering the download:
(source: mikeschinkel.com)
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();
admin_init Hook or load-(page) Hook seems to work, WordPress has not been set header in this state. I'm using load-(page) Hook because it runs when an administration menu page is loaded. You can load your script for specific page.
You can check load-(page) Hook on WordPress Codex
If you're using admin_init Hook make sure to verify nonce using check_admin_referer or other script maybe pass the condition will get output your download file.