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):

Screenshot of a download URL for a CSV file
(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:
Screenshot of Plugin Page showing an activated plugin
(source: mikeschinkel.com)

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
(source: mikeschinkel.com)

Leave a Comment