Idiot-proof, cross-browser force download in PHP

后端 未结 7 1278
借酒劲吻你
借酒劲吻你 2020-12-16 05:40

I\'m using forced download to download mostly zips and mp3s on site i did (http://pr1pad.kissyour.net) - to track downloads in google analytics, in database and to hide real

相关标签:
7条回答
  • 2020-12-16 06:18

    Okay, this is an old question and Adam already accepted his own answer, so presumably he got this working for himself, but he didn't explain why it worked. One thing the I noticed was in the question he used the headers:

    header("Pragma: public");
    header("Cache-Control: public",FALSE);
    

    Whereas in the solution he used:

    header("Cache-control: private");
    header('Pragma: private');
    

    He didn't explain why he changed these but I suspect it relates to the use of SSL. I recently solved a similar problem in software that needs to enable download over both HTTP and HTTPS, using the following to add the correct header:

    if(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) {
        header("Cache-control: private");
        header('Pragma: private');
    } else {
        header('Pragma: public');
    }
    

    Hopefully someone will find the information in this answer a useful addition to the above.

    0 讨论(0)
  • 2020-12-16 06:25

    If you are going to do this sort of "Echo it out with php" method, then you will not be able to show a remaining time, or an expected size to your users. Why? Because if the browser tries to resume your download in the middle, you have no way of handling that case in PHP.

    If you have a normal file download, Apache is capable of supporting resumed downloads over HTTP, but in the case a download is paused, Apache has no way of figuring out where in your script things were executing when a client asks for the next chunk.

    Essentially, when a browser pauses a download, it will terminate the connection to the webserver entirely. When you resume the download, the connection is reopened, and the request contains a flag saying "Start from byte number X". But to the webserver looking at your PHP above, where does byte X come from?

    While in theory it might be possible for the server to identify where to resume your script in the event of an interrupted download, Apache does not attempt to figure out where to resume. As a result, the header sent to the browser states that the server does not support resume, which turns off the expected filesize and time limit items in most major browsers.

    EDIT: It seems you might be able to handle this case, but it's going to take a LOT of code on your part. See http://www.php.net/manual/en/function.fread.php#84115 .

    0 讨论(0)
  • 2020-12-16 06:26

    print($this->_fullread($stream,1024*16));

    I assume _fullread is within a class? If the code looks like the above then $this-> wouldn't work.

    Does it output the file contents to the screen if you commented out all of the header stuff?

    0 讨论(0)
  • 2020-12-16 06:29

    There's one thing I find weird: You are calling ob_end_flush() at the start of the function. This actually cleans the output buffer, but it also outputs everything to the client first (I assume including Content-Headers set by CodeIgniter). Change the call to ob_end_clean(), it clears the buffer and discards it. This will give you a clean start for generating your own headers.

    Another tip:

    Instead of reading the file as a stream and passing it on block-wise, you could give this function a try:

    // ...
    if (file_exists("dir-with-files/$filename")) {
       readfile($file);
    }
    

    This takes care of nearly everything.

    0 讨论(0)
  • 2020-12-16 06:30

    Instead of trying to hide Your downloadpath from the world make it inaccessible from outside and only access the files with the above script. to do so you put a htaccess file ( a textfile named '.htaccess' don't forget leading dot) in the directory. Contents of the htaccess would be this:

    order deny,allow
    deny from all
    allow from localhost
    

    Now trying to access the path from *world will make the webserver create a 401 forbidden.

    Security through obscurity is not what you want.

    0 讨论(0)
  • 2020-12-16 06:31

    So, I used this code (It's modified version of resumable http download found on internet)

    function _output_file($file, $path)
    {
        $size = filesize($path.$file);
    
        @ob_end_clean(); //turn off output buffering to decrease cpu usage
    
        // required for IE, otherwise Content-Disposition may be ignored
        if(ini_get('zlib.output_compression'))
        ini_set('zlib.output_compression', 'Off');
    
        header('Content-Type: application/force-download');
        header('Content-Disposition: attachment; filename="'.basename($file).'"');
        header("Content-Transfer-Encoding: binary");
        header('Accept-Ranges: bytes');
    
        /* The three lines below basically make the 
        download non-cacheable */
        header("Cache-control: no-cache, pre-check=0, post-check=0");
        header("Cache-control: private");
        header('Pragma: private');
        header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
    
        // multipart-download and download resuming support
        if(isset($_SERVER['HTTP_RANGE']))
        {
            list($a, $range) = explode("=",$_SERVER['HTTP_RANGE'],2);
            list($range) = explode(",",$range,2);
            list($range, $range_end) = explode("-", $range);
            $range=intval($range);
            if(!$range_end) {
                $range_end=$size-1;
            } else {
                $range_end=intval($range_end);
            }
    
            $new_length = $range_end-$range+1;
            header("HTTP/1.1 206 Partial Content");
            header("Content-Length: $new_length");
            header("Content-Range: bytes $range-$range_end/$size");
        } else {
            $new_length=$size;
            header("Content-Length: ".$size);
        }
    
        /* output the file itself */
        $chunksize = 1*(1024*1024); //you may want to change this
        $bytes_send = 0;
        if ($file = fopen($path.$file, 'rb'))
        {
            if(isset($_SERVER['HTTP_RANGE']))
            fseek($file, $range);
    
            while
                (!feof($file) && 
                 (!connection_aborted()) && 
                 ($bytes_send<$new_length) )
            {
                $buffer = fread($file, $chunksize);
                print($buffer); //echo($buffer); // is also possible
                flush();
                $bytes_send += strlen($buffer);
            }
        fclose($file);
        } else die('Error - can not open file.');
    
    die();
    }
    

    and then in model:

    function download_file($filename){
        /*
            DOWNLOAD
        */
        $path = "datadirwithmyfiles/"; //directory
    
        //track analytics
    
        include('includes/Galvanize.php'); //great plugin
        $GA = new Galvanize('UA-XXXXXXX-7');
        $GA->trackPageView();
    
        $this->_output_file($filename, $path);
    
    }
    

    It works as expected in all mentiond browser on Win / MAC - so far, no problems with it.

    0 讨论(0)
提交回复
热议问题