Защищенная от дурака, кросс-браузерная принудительная загрузка в PHP - PullRequest
6 голосов
/ 08 февраля 2010

Я использую принудительную загрузку, чтобы загружать в основном zip и mp3 файлы на сайте, который я сделал (http://pr1pad.kissyour.net) - для отслеживания загрузок в Google Analytics, в базе данных и для скрытия реального пути загрузки:

Это так:

extending CI model

... - bunch of code

function _fullread ($sd, $len) {
 $ret = '';
 $read = 0;
 while ($read < $len && ($buf = fread($sd, $len - $read))) {
  $read += strlen($buf);
  $ret .= $buf;
 }
 return $ret;
}

function download(){    
    /* DOWNLOAD ITSELF */

    ini_set('memory_limit', '160M');
    apache_setenv('no-gzip', '1');
    ob_end_flush();

    header("Pragma: public");
    header("Expires: 0");
    header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
    header("Cache-Control: public",FALSE);
    header("Content-Description: File Transfer");
    header("Content-type: application/octet-stream");
     if (isset($_SERVER['HTTP_USER_AGENT']) && 
      (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false))
      header('Content-Type: application/force-download'); //IE HEADER
    header("Accept-Ranges: bytes");
    header("Content-Disposition: attachment; filename=\"" . basename("dir-with-    files/".$filename) . "\";");
    header("Content-Transfer-Encoding: binary");
    header("Content-Length: " . filesize("dir-with-files/".$filename));

    // Send file for download
    if ($stream = fopen("dir-with-files/$filename", 'rb')){
     while(!feof($stream) && connection_status() == 0){
      //reset time limit for big files
      set_time_limit(0);
      print($this->_fullread($stream,1024*16));
      flush();
     }
     fclose($stream);
    }
}

Это на LAMP с CI 1.7.2 - это мой собственный метод, собранный из различных инструкций по всему интернету, потому что во время разработки возникали следующие проблемы: - ограничение сервера . ini_set не помогло, поэтому я использовал буферизованный _fullread вместо обычного fread, который был использован вместо @readonly - ob_end_flush (), потому что сайт сделан в CI1.7.2 и мне нужно очистить буфер

Теперь ... Это не работает. Это произошло, потом перестало показывать ожидаемый размер / время загрузки - я пытался очистить его, и пока я чистил код, что-то случилось, я не знаю, что и в любой предыдущей версии - это не работал (никаких изменений в настройках) - edit : не работать = выводит все в окно браузера.

Итак, я сказал, прикрути его, я посмотрю здесь.

Итак, я в основном ищу скрипт или функцию, которую я могу поместить в свою выходную модель и сделаю:

  • Вызов принудительной загрузки (в начале Chrome скачать, в IE, FF, Safari открыть модальное открытие / сохранение / отмена)
  • Показать размер файла и приблизительное время (это зависит от браузера, я знаю, но сначала браузер должен знать размер файла
  • РАБОТА (проверено и подтверждено!) В IE6,7,8, FF3, Opera, Chrome и Safari на ПК + Mac (Linux ... мне все равно) - это часть заголовка
  • на сервере, у меня есть что-то вроде лимита памяти 56 МБ, к которому я не могу добавить, поэтому это тоже важно

Заранее спасибо.

Редактировать : Теперь я чувствую себя не в своей тарелке больше, чем когда-либо / раньше, так как я пытался принудительно загрузить с помощью .htaccess - пока он работал, у него было мало мелких / основных (выберите ваши) проблем

  • показывал полный путь (минор для меня)
  • он ожидает завершения полной загрузки (отображается как «соединение»), а затем просто показывает, что он загружается - и загружается за одну секунду (для меня главное)

Теперь, хотя я удалил .htaccess, он все еще ждет, пока загрузка не будет завершена (как если бы он сначала загружал в кэш), и он просто получает connected и показывает диалог открытия / сохранения.

Ответы [ 7 ]

6 голосов
/ 11 февраля 2010

Итак, я использовал этот код (это измененная версия возобновляемой загрузки http, найденная в интернете)

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();
}

и затем в модели:

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);

}

Он работает, как и ожидалось, во всех упомянутых браузерах на Win / MAC - пока проблем с ним нет.

2 голосов
/ 23 июня 2011

Хорошо, это старый вопрос, и Адам уже принял свой собственный ответ, так что, вероятно, он получил это на себя, но не объяснил, почему это сработало. Одна вещь, которую я заметил, была в вопросе, он использовал заголовки:

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

В то время как в растворе он использовал:

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

Он не объяснил, почему он изменил их, но я подозреваю, что это связано с использованием SSL. Недавно я решил похожую проблему в программном обеспечении, которое должно разрешить загрузку через HTTP и HTTPS, используя следующее для добавления правильного заголовка:

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

Надеюсь, кто-то найдет информацию в этом ответе полезным дополнением к вышесказанному.

1 голос
/ 08 февраля 2010

Есть одна вещь, которую я нахожу странной: вы вызываете ob_end_flush() в начале функции. Это на самом деле очищает буфер вывода, но также сначала выводит все на клиент (я полагаю, включая Content-Headers, установленный CodeIgniter) Измените вызов на ob_end_clean(), он очищает буфер и сбрасывает его. Это даст вам возможность начать создавать собственные заголовки.

Еще один совет:

Вместо того, чтобы читать файл как поток и передавать его по блокам, вы можете попробовать эту функцию:

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

Это заботится почти обо всем.

0 голосов
/ 01 марта 2014

Вместо того, чтобы пытаться скрыть Ваш путь загрузки от мира, сделайте его недоступным извне и получите доступ только к файлам с помощью вышеуказанного сценария. для этого вы помещаете в каталог файл htaccess (текстовый файл с именем .htaccess, не забудьте начальную точку). Содержимое htaccess будет таким:

order deny,allow
deny from all
allow from localhost

Теперь попытка доступа к пути из * world заставит веб-сервер создать запрещенный 401.

Безопасность через неизвестность - это не то, что вам нужно.

0 голосов
/ 08 февраля 2010

Если вы собираетесь использовать метод «Echo it out with php», вы не сможете показать оставшееся время или ожидаемый размер для ваших пользователей. Зачем? Потому что, если браузер пытается возобновить загрузку в середине, у вас нет способа обработать этот случай в PHP.

Если у вас нормальная загрузка файлов, Apache способен поддерживать возобновленную загрузку по HTTP, но в случае, если загрузка приостановлена, Apache не может выяснить, где в вашем скрипте выполняются вещи, когда клиент запрашивает следующий кусок.

По сути, когда браузер приостанавливает загрузку, он полностью разрывает соединение с веб-сервером. Когда вы возобновите загрузку, соединение будет вновь открыто, и в запросе будет указан флаг «Начать с байта номер X». Но веб-серверу, который рассматривает ваш PHP выше, откуда байт X?

Хотя теоретически сервер может определить, где возобновить выполнение сценария в случае прерванной загрузки, Apache не пытается выяснить, где продолжить. В результате в заголовке, отправляемом в браузер, указывается, что сервер не поддерживает возобновление, что отключает элементы ожидаемого размера файла и ограничения времени в большинстве основных браузеров.

РЕДАКТИРОВАТЬ: Кажется, вы могли бы справиться с этим делом, но это займет много кода с вашей стороны. См http://www.php.net/manual/en/function.fread.php#84115.

0 голосов
/ 08 февраля 2010

Просто выстрел в темноте ... каждый заголовок, который я посылаю в своем коде принудительной загрузки (который не так хорошо проверен, как ваш), совпадает с вашим, за исключением того, что я звоню header («Cache-Control: private», false);

вместо: заголовок («Cache-Control: public», FALSE);

Я не знаю, поможет это или нет.

0 голосов
/ 08 февраля 2010

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

Я полагаю, _fullread находится в классе? Если код выглядит как выше, то $this-> не будет работать.

Выводит ли содержимое файла на экран, если вы закомментировали все содержимое заголовка?

...