file-downloadI've been working on a WordPress plugin lately, and one thing I've been trying to do is to create a file download out of some plugin settings (i.e. text data, json file).

In general in order to do that via PHP, you have to send the right headers:

1
2
3
4
5
6
7
8
9
10
11
$length = strlen( $content );
header( 'Content-Description: File Transfer' );
header( 'Content-Type: text/plain' );
header( 'Content-Disposition: attachment; filename=export.json' );
header( 'Content-Transfer-Encoding: binary' );
header( 'Content-Length: ' . $length );
header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' );
header( 'Expires: 0' );
header( 'Pragma: public' );
echo $content;
exit;

As you see this will make a user's browser download a file named export.json, with the content $content.

The trouble is, if you're in the settings page, you can't send Headers, so we have to send them earlier.
The easiest way is to hook into admin_init (your plugin probably already has a function that hooks into it)

1
add_action( 'admin_init', array( $this, 'handle_file_download' ) );</p>

Okay, next it would probably be a good idea to make the function only send the headers if it gets the right action parameter, we'll call it export_file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function handle_generate_export_file() {
	if ( isset( $_GET['action'] ) &amp;&amp; $_GET['action'] == 'export_file' ) {
		if ( ! wp_verify_nonce( $_GET['nonce'], 'export_file_nonce' ) ) {
			$content = stripcslashes( get_option( 'plugin_settings' ) );
			$length = strlen( $content );
			header( 'Content-Description: File Transfer' );
			header( 'Content-Type: text/plain' );
			header( 'Content-Disposition: attachment; filename=export.json' );
			header( 'Content-Transfer-Encoding: binary' );
			header( 'Content-Length: ' . $length );
			header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' );
			header( 'Expires: 0' );
			header( 'Pragma: public' );
			echo $content;
			exit;
		}
	}
}

So first I'm checking if the action is there, and then I'm verifying the nonce (WP's Nonce, very important to use this for security).

Notice I'm just using the get_option() function to get my plugin settings (generic name of course), you can pull whatever you want to here, including using file_get_contents.

Now it's time to build the side that actually makes the call.
After doing some research, I found out the safest way to make sure it works on all browsers, is to append an IFrame to the document's body. This would work on all browsers.

1
2
3
4
5
6
7
8
form.find( '.export-button' ).click( function() {
	if ( $( '#export_file_iframe' ).length ) {
		$( '#export_file_iframe' ).attr( 'src', function ( i, val ) { return val; });
	} else {
		var iframeHTML = '<iframe id="export_file_iframe" src="' + document.URL + '&action=export_file&nonce=' + extraStrings.nonce + '"></iframe>';
		var windowIFrame = $( 'body' ).append( iframeHTML );
	}
});

Ok now let's break it down a bit, first of all we need to check if the IFrame already exists, so we don't append multiple IFrames.
If the IFrame exists, we just want to refresh it, easiest way to do that would be to change the 'src' attribute its current value (i.e. src = src), that's what that function does, it can also be done via:

1
2
var src = $( '#export_file_iframe' ).attr( 'src');
$( '#export_file_iframe' ).attr( 'src', src);

But the former solution saves you a line (and looks cooler).
Now, when we build the iframe, you notice document.URL is of course the current URL, and extraStrings.nonce is being sent on the PHP page that's calling the script, using wp_localize_script().