May 7th, 2013

Tech Notes: Forcing ‘Save As…’ for downloads in WordPress

I quite commonly build sites that have resource or report downloads.

This snippet doesn’t enforce any login or permissions checks (I’ll post about this another time), but it does force the browser to do a “Save as…”

The trick is to send the request for the file to a new URL which causes some special HTTP headers to be sent that tell the browser what to do with the file.

The first snippet is the code that goes in the file that serves the new file. Create a new file in your theme or plugin directory (or a subdirectory) and use this code – changing values where necessary.

/*
* This code delivers a file, specified in the URL parameter, as a forced "Save As" download.
*/
 
// First, get WordPress loaded
require_once('../../../wp-load.php');  // Modify this if necessary
 
// Grab the requested URL
$request_file = $_REQUEST['url'];
 
// Get WordPress's upload directory details - this returns an array of things - see the Codex for details
$upload_path_info = wp_upload_dir();
 
// Translate the requested URL into a local file path
$request_file_path = str_replace( $upload_path_info['baseurl'], $upload_path_info['basedir'], $request_file );
 
// Because you could ask for, say, /wp-content/uploads/2013/01/../../../../wp-config.php we need a security check
// to make sure we're actually getting a file form /uploads.  The canonical directory of the request MUST be the 
// basedir of the uploads directory.
$realdir = realpath(dirname($request_file_path));
// Make sure this is a === test as a successful result will be 0 !!
if (strpos($realdir, $upload_path_info['basedir']) === false) {
    echo "Haha! Nice try. I don't think so.";
    exit;
}
 
// Check if the file exists
if (file_exists($request_file_path)) {
 
  // Send appropriate headers (this from the examples here: http://php.net/manual/en/function.readfile.php);
  header('Content-Description: File Transfer');
  header('Content-Type: application/octet-stream');
  header('Content-Disposition: attachment; filename='.basename($request_file_path));
  header('Content-Transfer-Encoding: binary');
  header('Expires: 0');
  header('Cache-Control: must-revalidate');
  header('Pragma: public');
  header('Content-Length: ' . filesize($request_file_path));
  ob_clean();
  flush();
  readfile($request_file_path);
  exit;
}

This second snippet is the code that I use in my theme to print the download link. If you were creating this functionality in a plugin you could create function that takes a download URL and translates it. You could also, if you wanted to and knew how, use .htaccess to automatically redirect all requests for, say, .pdf files.

<?php
  if ($resource_url) {
     $download_url = get_template_directory_uri() . '/save-resource.php?url=' . urlencode($resource_url);
?>
    <a href="<?php echo $download_url; ?>">
      Download
    </a>
<?php
  }
?>