views:

304

answers:

5

Hi, I am working on a website where the visitor should be able to download a pdf file. (There are three links to choose from but that is irrelevant) I wanted to know how to make it so that the visitor can simply click the link and not have to

right click > Save (target) As...

I am open to PHP and or Javascript solutions. Thanks.

EDIT: Can I use javascript to call the PHP and save the file via AJAX?

EDIT2: I used Nirmal's solution in the end, since it was the simplest to change for all three files. I didn't need to make 3 files for the three PDF's and I didn't need to hand code the switch. BalusC gets the check though since his/her code was up first and does the trick too.

+1  A: 

Try using PHP to serve the file while first sending a header of Content-type: application/octet-stream.

Amber
This isn't a very elegant solution.
Jeffrey Aylesworth
True. I had forgotten about the content-disposition header, and would agree that using it would be preferrable.
Amber
+6  A: 

All you basically need to do is to set the Content-Disposition header to attachment to get a 'Save As' dialogue. Here's a kickoff PHP example:

<?php
    header('Content-Type: application/pdf');
    header('Content-Disposition: attachment;filename="foo.pdf"');
    readfile('/path/to/foo.pdf');
?>

You can't and don't want to do this with Javascript.

Important note: due to a poor feature, in MSIE the default filename in 'Save As' dialogue won't be derived from the content-disposition header, it will instead be the last part of the pathinfo in the request URL. To workaround this, append the PDF filename to the link, e.g. http://example.com/pdf/foo.pdf. You can even make use of it in PHP to read the in the pathinfo specified PDF file. Here's a basic example of pdf.php:

<?php
    $file_name = $_SERVER['PATH_INFO'];
    $file = '/path/to/pdf/files' . $file_name;
    if (file_exists($file)) {
        header('Content-Type: application/pdf');
        header('Content-Disposition: attachment;filename="' . basename($file_name) . '"');
        header('Content-Length: ' . filesize($file));
        readfile($file);
    } else {
        header('HTTP/1.1 404 Not Found');
    }
?>

This however assumes that you've MultiViews on so that /pdf/ will go through this PHP file, or at least a RewriteRule from /pdf/ to /pdf.php/.

The major advantage of this approach is that you don't need to change the code whenever you want to add a new PDF file or change the PDF file name.

You can even make it more generic by automatically determining and setting the correct content type:

<?php
    $file_name = $_SERVER['PATH_INFO'];
    $file = '/path/to/all/files' . $file_name;
    if (file_exists($file)) {
        header('Content-Type: ' . mime_content_type($file_name));
        header('Content-Disposition: attachment;filename="' . basename($file_name) . '"');
        header('Content-Length: ' . filesize($file));
        readfile($file);
    } else {
        header('HTTP/1.1 404 Not Found');
    }
?>

Name it files.php or so and then you have a generic PHP downloader which you can access by for example http://example.com/files/foo.pdf, http://example.com/files/bar.zip, etcetera.

Hope this helps.

BalusC
Can you please provide a complete example with a sample link... ? I'm not following completely.
Moshe
You probably want an `exit;` statement after the `readfile()` call.
Amber
@Dav: not if this is the complete code. I really can't imagine any need to append some other clutter to this code. Just have it as an standalone PHP file.
BalusC
Actually, will this work with AJAX? (Replace foo.pdf with $_GET['filename']) and run javascript on click?
Moshe
@BalusC: if it's complete code, you shouldn't include the `?>` - otherwise you might unintentionally wind up adding whitespace to the end of the output.
Amber
@Moshe: you don't need ajax to download a file. @Dav: just don't add unnecessary template characters.
BalusC
@BalusC- So the link would point to a hard coded file with the file name? Wouldn't ajax be better though since it also calls the page yet I only need one page for three links?
Moshe
As said, make use of pathinfo. E.g. `http://example.com/pdf/file1.pdf`, `http://example.com/pdf/file2.pdf` and `http://example.com/pdf/file3.pdf`. In pdf.php (assuming that you've MultiViews on) get the filename as `$_SERVER['PATH_INFO']`.
BalusC
@BalusC - Ok, gotcha. I'm gonna try AJAX though and see how it works. Please stay tuned for results...
Moshe
@Moshe: ajax doesn't add any value. It would be unnecessarily overcomplicated. Just use a plain vanilla link. The current page won't flash or go blank or so as you seem to think.
BalusC
@BalusC - Maybe you are right. BTW, I don't expect any flashing or blinking. Really. Thank you for your help. It works.
Moshe
Oh, i'm not sure what multiviews is... sorry.
Moshe
@Moshe: it's an Apache setting. Refer its documentation. With this you can just use `/pdf/file.pdf` instead of `/pdf.php?file=file.pdf` which is much more SEO (and MSIE) friendly.
BalusC
@BalusC - it's more a just "simple way to ensure that there isn't a problem". If you leave off the `?>` then you *know* that there's no hidden whitespace; if you don't, then an accidental tap of the spacebar, enter key, et cetera could throw in a hard-to-notice bug.
Amber
Reasonable. You have programmers and programmers. I have no problems if you do it that way, but that's not how I'd do it. Just write good and clean code (with help of a smart editor/IDE which trims trailing spaces on save).
BalusC
+1  A: 

You can add an HTTP header to do that, there is an example in the PHP Docs (See example one)

Jeffrey Aylesworth
A: 

The following code may help you:

<?php
if(isset($_GET['docid'])){
    switch($_GET['docid']){
        case '1':
            $file = 'complete/path/to/pdf/file1';
            break;
        case '2':
            $file = 'complete/path/to/pdf/file2';
            break;
        case '3':
            $file = 'complete/path/to/pdf/file3';
            break;
        default:
            exit;
    }

    if(file_exists($file)){
        header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename='.basename($file));
        header('Content-Transfer-Encoding: binary');
        header('Expires: 0');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header('Pragma: public');
        header('Content-Length: ' . filesize($file));
        ob_clean();
        flush();
        readfile($file);
        exit;
    }
}

Save this code as a php file (say, download.php) and call it from the link. What the script does is that it reads the pdf file and outputs it to the buffer. The headers will force the disposition as download.

To call the first pdf, href to '/path/to/download.php?docid=1'
To call the second pdf, href to '/path/to/download.php?docid=2'
To call the third pdf, href to '/path/to/download.php?docid=3'

So, you don't need AJAX to do the work.

Nirmal
I am using on a website and it must be a relative path - unless you meant the destination path.
Moshe
You can use relative paths too.
Nirmal
@Nirmal - tx +1
Moshe
See my updated code. You don't have to use AJAX.
Nirmal
@Nirmal - thanks. My brain is off right now. I should've figured. A switch statement. Ugh I am *so* stupid.
Moshe
@And please edit your answer so I can give +1. It didn't let me vote up "Content is too old" or sthing like that
Moshe
@Moshe "Ugh I am so stupid.": It happens to everyone. No problem about points. As long as the solution helps you, that's fine.
Nirmal
This is honestly said a *poor* approach. I would't hardcode the invididual filenames and for sure not use a switch statement or so. If you're using request parameters at any way, why don't you just specify the filename as parameter value?
BalusC
@BalusC: I wouldn't have written this solution if you had explained your answer clearly in the first place. Now you have edited your answer, mine became a stupid one. Anyway, learnt from you something new.
Nirmal
@BalusC - Nirmal's code was ready for use in this case (being that there are only 3 files) and that is *specifically* why I wanted to do ajax. The switch statement. But like I said before - "Ugh I am so stupid." :-) Thanks all.
Moshe
+1  A: 

Rather than having to write PHP wrapper scripts, if you are using Apache, you can do this with a .htaccess file in the folder containing the PDFs:

<Files *.pdf>
  Header set Content-Disposition attachment
</Files>

Apparently some versions of IE / Adobe Reader don't respect the Content-Disposition header. You can work around these with ForceType application/octet-stream

<Files *.pdf>
  ForceType application/octet-stream
  Header set Content-Disposition attachment
</Files>
rjmunro