0

I'm serving an .mp3 file (as a download dialog box, where the end-user can rename and save the file). There are a lot of conflicting reccomendations for this on the forums and even within the comments on the php manual.

I hope to solve my issue and create here a reference on how to best execute this goal: i.e. a clear "best practices summary" usable by begining php coders on the most efficient way to get started delivering downloadable files.

I would really appreciate it if you'd check over the code here and suggest your corrections--this is part of a site that helps artists self-publish music, books, etc.

To test, I am uploading this to my server as index.php; perhaps that is also part of the problem.

The current status of this script is that the browser hangs a bit and then loads the binary as text into the browser display window.

I've thought at many points that my problem was syntax in the important "Content-Length" header, or the order of my headers, so I've tried several versions of all that, but none cut off the download.

Here is the exact code that I am now trying on my own,
where ####### means 7 numbers (the actual file size in bytes), and everything else should be clear:

<?php
header('Server: ');
header('X-Powered-By: ');
header('Cache-Control: no-cache');
header('Expires: -1');
header('Content-Description: File Transfer');
header('Content-Transfer-Encoding: binary');
header('Content-Type: audio/mpeg');
header('Content-Disposition: attachment;filename="suggested-save-name.mp3"');  
header('Content-Length: #######["exact-file-name.mp3"]');
@readfile("http://full-public-url.com/exact-file-name.mp3"); 
ob_clean();
flush();
exit;
?>

Returns these response headers:

>X-Powered-By:PHP/5.4.xy
>Vary:negotiate
>Transfer-Encoding:chunked
>TCN:choice
>Server:Apache
>Keep-Alive:timeout=2, max=200
>Date:Fri, 10 May 2013 16:mm:ss GMT
>Content-Type:text/html
>Content-Location:filename.php
>Connection:Keep-Alive

I hope it is a simple error (or more than one) in the syntax of my script, or the way I created and saved the .php file. I have verified the settings are at default, php is up to date, and there are no .htaccess issues. I have carefully made sure there are no extra spaces at the end of this file, as well as all other files in the web directory, and as well I've tried the script with and without the closing ?>.

Thank you in advance ...


Best script after reading Answers, below:

<?php
$file-variable=('./exact-file-name.mp3')
$size=filesize($file-variable);
header('Server: ');
header('X-Powered-By: ');
header('Cache-Control: no-cache');
header('Expires: -1');
header('Content-Type: application/octet-stream'); //will need to redirect for older IE
header('Content-Disposition: attachment; filename="suggested-save-name.mp3"');  
header('Content-Length: "$size"');
@readfile("$file-variable");
ob_clean();
flush();
exit;

Successful response headers, from the better script (& after removing the UTF-8 BOM):

>Vary:negotiate
>TCN:choice
>Server:Apache //How can I hide this?
>Keep-Alive:timeout=2, max=200
>Expires:-1
>Date:Sat, 11 May 2013 12:mm:ss GMT
>Content-Type:audio/mpeg
>content-transfer-encoding:binary
>Content-Location:filename.php  //I would also like to obfuscate this
>Content-Length:#######
>Content-Disposition:attachment; filename="suggested-save-name.mp3"
>Content-Description:File Transfer
>Connection:Keep-Alive
>Cache-Control:no-cache
olaf atchmi
  • 113
  • 1
  • 9
  • Problem all along, at least with issue at hand (binary dumped as text into browser), was simply the BOM, added by win Notepad. I had previously read about BOM on many other posts; didn't realize what it actually was and how to remove it. – olaf atchmi May 11 '13 at 12:53
  • I also just learned from [this stackoverflow post](http://stackoverflow.com/questions/900207/return-a-php-page-as-an-image/900228#900228), as @martin-geisler pointed out, that for a simple use (e.g. if throwing the download here was the only desired mission) you can save in notepad as ANSI and avoid the BOM. – olaf atchmi May 11 '13 at 14:15
  • windows notepad ( _not a true editor_ ) pops up when you type *editor* into windows ;-} – olaf atchmi May 11 '13 at 17:01

1 Answers1

0

Step 1 would be to remove all nonsense headers that are not defined in the HTTP standard.

This will eliminate Content-Description and Content-Transfer-Encoding. Both are useless at best, and might interfere with normal browser operations in the worst case.

Step 2 is to optimize the file delivery. Do not download the MP3 with a HTTP request, access the FILE on the server. Do not use a URL, do use a file path. If the MP3 is right next to your script, this will work:

readfile('./exact-file-name.mp3');

At this point you should usually end up with a working download. If not, try changing the Content-Type to something more generic. audio/mpeg might trigger the audio player in some browser, application/octet-stream should work in most browsers but older Internet Explorer, which do inappropriate content sniffing on certain mime types including this one. application/x-ms-download is supposed to work then.

Make sure the header is sent. PHP does not send HTTP headers if the HTTP body was already startet, and any whitespace including the UTF-8 BOM will trigger body output.

Some comments on your "final" headers in general:

Content-Length: should only have an integer stating the length in bytes, nothing more. Especially no mention of any filename in square brackets.

Content-Transfer-Encoding and Content-Description are still useless.

Content-Location is not needed. If you don't know what it does, omit it. Obfuscation will not work here, the browser needs to know the URL he is accessing. Duplicating this URL in this header does not change anything, obfuscating it will likely break things somewhere.

The two headers you really only need for a download are: Content-Type and (if you want to pre-define a filename for the user) Content-Disposition.

Sven
  • 69,403
  • 10
  • 107
  • 109
  • Danke Sven. As of now this is not working. Using your advice I further reduced my test script (I've added it above as 'reference 3'), but I get the same problem. I would also appreciate a few side comments: why might I still get the same error even after I test different _Content-Length_'s (even putting an incorrect value)? Also, why do I need the _'.'_ before the file path. I thought period was only used as a concatenation operator in php? – olaf atchmi May 09 '13 at 20:11
  • Does anyone know why this might not be working? Do I have to use a_$variable_ to open a file with php? Thanks again in advance – olaf atchmi May 10 '13 at 14:04
  • What does "does not work" mean for you? Could you install the Liveheaders plugin into Firefox, start it and grab the headers sent from your server after clicking the download. I have the feeling something is simply wrong on your local environment. – Sven May 10 '13 at 14:44
  • And by the way: `Content-Length: #######["song.mp3"]` is wrong, of course, and I guess you just replaced this with an integer stating the number of bytes in the MP3 file. Let the webserver create this for you, i.e. do not add this header yourself. – Sven May 10 '13 at 14:47
  • By 'does not work' I just mean it does the same problem-thing I mentioned in the post (dumps file as text into browser). I'm testing each time with several different local enviornments; so I guess the problem is in the code or my server/host – olaf atchmi May 10 '13 at 16:01
  • I will experiment now with your new tip to script the filesize into Content-Length. Yes, where # = an integer, was how I was doing it before – olaf atchmi May 10 '13 at 16:01
  • added response headers to above – olaf atchmi May 10 '13 at 16:54
  • I see your edit, and now I have to decide whether the headers are broken by your server, or are incorrectly pasted by you. What's this `GMTContent-Type` header? Please double check if it really is present like this. Interestingly enough, the content type is text/html, which is wrong. A `header()` call usually is only non-working if PHP already sent the headers. So also double check there is no whitespace in front of the first ` – Sven May 11 '13 at 02:32
  • I corrected the linebreak in my paste of the `Content-Type` header, above; good call ;) – olaf atchmi May 11 '13 at 11:07
  • Will double check for the BOM; thanks that is really helpful, to know when `header()` call might fail – olaf atchmi May 11 '13 at 11:10
  • PROBLEM SOLVED: re-saved same script with emacs, which **removed the utf-8 bom, now ALL of the above scripts work.** Now I'll just optimize for standards and hiding the source directory. **3xDanke for Sven** added successful headers above – olaf atchmi May 11 '13 at 12:28
  • thanks for the updated Answer, and the summary "the two headers you really only need..." I marked it as best solution and will clean up my original question to reflect more like: original code --> best code. – olaf atchmi May 11 '13 at 16:57