Как отдать пользователю файл скриптом

Я уже затрагивал эту тему в одной из предыдущих заметок. Сегодня мы рассмотрим этот вопрос более подробно - не только как отдать файл на загрузку, но и как реализовать поддержку докачки.

Файл download.php

<?php
if(isset($_SERVER['HTTP_USER_AGENT']) and strpos($_SERVER['HTTP_USER_AGENT'],'MSIE'))
  header('Content-Type: application/force-download');
else
  header('Content-Type: application/octet-stream');
header('Accept-Ranges: bytes');
header('Content-disposition: attachment; filename="pricelist.zip"');
?>

Тогда пользователь зашедший на download.php предстанет перед выбором, что делать с файлом pricelist.zip: сохранить на диск, открыть или отменить скачивание. Заголовок

Content-Type: application/force-download

нужен для старых версий MS IE.

С поддержкой докачки:

<?php

$filename = 'pricelist.zip';
 
// если файла нет
if (!file_exists($filename)) {
  header ("HTTP/1.0 404 Not Found");
  exit;
}
 
// получим размер файла
$fsize = filesize($filename);
// дата модификации файла для кеширования
$ftime = date("D, d M Y H:i:s T", filemtime($filename));
// смещение от начала файла
$range = 0;
 
// пробуем открыть
$handle = @fopen($filename, "rb");

// если не удалось
if (!$handle){
  header ("HTTP/1.0 403 Forbidden");
  exit;
}
 
// Если запрашивающий агент поддерживает докачку
if ($_SERVER["HTTP_RANGE"]) {
  $range = $_SERVER["HTTP_RANGE"];
  $range = str_replace("bytes=", "", $range);
  $range = str_replace("-", "", $range);
  // смещаемся по файлу на нужное смещение
  if ($range) fseek($handle, $range);
}
 
// если есть смещение
if ($range) {
  header("HTTP/1.1 206 Partial Content");
} else {
  header("HTTP/1.1 200 OK");
}
 
header("Content-Disposition: attachment; filename=\"{$filename}\"");
header("Last-Modified: {$ftime}");
header("Content-Length: ".($fsize-$range));
header("Accept-Ranges: bytes");
header("Content-Range: bytes {$range}-".($fsize - 1)."/".$fsize);
 
// подправляем под IE что б не умничал
if(isset($_SERVER['HTTP_USER_AGENT']) and strpos($_SERVER['HTTP_USER_AGENT'],'MSIE'))
  Header('Content-Type: application/force-download');
else
  Header('Content-Type: application/octet-stream');
 
while(!feof($handle)) {
  $buf = fread($handle,512);
  print($buf);
}
 
fclose($handle);
 
?>

Чтобы получить от сервера не весь документ, а только оставшуюся часть, клиент должен отправить заголовок

Range: bytes=num-

где num - смещение в байтах от начала файла.

Сервер в свою очередь устанавливает переменную окружения HTTP_RANGE и должен отправить заголовок

HTTP/1.1 206 Partial Content

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

Content-Type: application/octet-stream
Content-Disposition: attachment; filename=имя_файла
Last-Modified: время_модификации_файла
Accept-Ranges: bytes
Content-Length: длина_отдаваемой_части
Content-Range: от-до/размер

Поясню последний заголовок на примере: имеем файл размером 4000 байт, отдаем кусок с 5 по 288 байт. Тогда заголовок будет выглядеть так:

Content-Range: 5-288/4000

В общем случае, протокол HTTP 1.1 позволяет клиенту запросить только часть объекта (от и до). Если клиент посылает заголовок

Range-Unit: 2015 | 1024

это означает, что клиент хочет получить кусок документа, начиная с 2015 байта и длиной в 1 килобайт. Если сервер поддерживает докачку и документ не является динамическим, то будет выдана запрашиваемая часть. В противном случае сервер вернёт ошибку о том, что действие не поддерживается или начнёт выдавать документ полностью.

Доступ к элементам формы (продолжение)

Доступ к кнопкам-переключателям

var bth = f.elements["radiobutton"][i];
s += btn.value + ":" + btn.checked + "\n";

В отличие от флажков, кнопки-переключатели в HTML-форме всегда организуются в группы. Это означает, что у нескольких кнопок-переключателей может быть один и тот же атрибут name, но разные его значения. Следовательно, доступ ко всей группе кнопок-переключателей как к массиву осуществляется следующим образом:
document.forms[номер].elements[группа_кнопок]
Каждый элемент данного массива представляет собой отдельную кнопку-переключатель и поддерживает свойство checked. Это свойство действует таким же образом, как и аналогичное свойство флажка, возвращая логическое значение true, если кнопка-переключатель выбрана, а в противном случае — логическое значение false.

С помощью свойства value доступно также значение каждой кнопки-переключателя. В приведенном ниже фрагменте кода осуществляется циклическое обращение к каждой кнопке-переключателю и вывод ее состояния.

<script type="text/javascript">
function showStatus(f) {
  var s= "";
  for (var i=0; f.elements["radiobutton"].length; i++) {
    var bth = f.elements["radiobutton"][i];
    s += btn.value + ":" + btn.checked + "\n";
  }
  window.alert(s);
}
</script>
<form>
  <input type="radio" name="radiobutton" value="R" />red
  <input type="radio" name="radiobutton" value="G" />green
  <input type="radio" name="radiobutton" value="B" />blue
  <input type="button" value="Show status" onclick="showStatus(this.form);" />
</form>

Доступ к спискам выбора

Список выбора в HTML-форме состоит из двух элементов. Так, элемент <select> закладывает основание для такого списка и предоставляет его имя, хранящееся в атрибуте name. А отдельные элементы списка представлены элементами <option>, предоставляющими следующие данные: надпись элемента списка, отображаемую в окне браузера, а также значение, посылаемое серверу при предъявлении формы.

В JavaScript имеются два средства для доступа к данным из списка выбора.

  • selectedIndex. Предоставляет начинающийся с нуля индекс элемента. выбранного в настоящий момент из списка. Значение -1 этого свойства означает, что ни один из элементов не был выбран, что справедливо только для списков, состоящих из более чем одного элемента.
  • options. Массив со всеми элементами выбора из списка. Каждый такой элемент поддерживает свойство selected. Если это свойство принимает логическое значение true, значит, данный элемент выбран из списка.

Как правило, свойства selectedIndex оказывается достаточно для проверки. А свойство options очень удобно для доступа к выбранному из списка элементу. В этом случае атрибут value выбранного элемента списка предоставляет данные, отправляемые серверу, а свойство text возвращает надпись данного элемента, отображаемую в окне браузера.

В приведенном ниже листинге осуществляется доступ ко всем важным данным, связанным с выбранным элементом списка.

<script type="text/javascript">
function showStatus(f) {
  var index = f.elements["selectionlist"].selectedIndex;
  if (index == -1) {
    window.alert("No element selected");
  } else {
    var element = f.elements["selectionlist"].options[index];
    window.alert("Element #" + index +
      " (caption: " + element.text +
      ", value: " + element.value +
      ") selected");
  }
}
</script>
<form>
  <select name="selectionlist" size="3">
    <option value="R">red</option>
    <option value="G">green</option>
    <option value="B">blue</option>
  </select>
  <input type="button" value="Show status" onclick="showStatus(this.form);" />
</form>

Кэширование в HTTP

Механизм кэширования применяется с целью опртимизации пересылки данных между клиентом и сервером. Запрошенный пользователем по HTTP-протоколу документ может быть сохранен в кэше промежуточного сервера или браузера, и при повторном его запросе будет выдаваться без обращения к источнику.

Кэши принято подразделять на два вида: локальные и глобальные. Локальный кэш создается браузером клиента, тогда как глобальный располагается на прокси-сервере провыйдера (или организации, в которой имеется свой внутренний прокси-сервер).

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

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

Для подавления кэширования можно использовать HTTP-заголовки:

<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Дата в прошлом
header("Last-Modified: ".gmdate("D, d M Y H:i:s")."GMT");
header("Cache-Control: no-cache, must-revalidate"); // HTTP 1.1
header("Pragma: no-cache"); // HTTP 1.0
?>

HTTP-заголовок Expires задает дату, по достижению которой документ считается устаревшим, поэтому задание для этого заголовка уже прошедшей даты предотвращает кэширование данной страницы.

HTTP-заголовок Last-Modified определяет дату последнего изменения страницы. Если с момента последнего обращения к ней прокси-сервера значение этого заголовка изменилось, происходит происходит повторная загрузка страницы, поэтому присвоение этой директиве текущего времени прикаждом обращении предотвращает кэширование.

HTTP-заголовок Cache-Control предназначен для управления кэшированием, и указание его значения равным no-cache также приводит к запрету кэширования. В устаревшем стандарте HTTP 1.0 для запрета кэширования нужно присвоить значение HTTP-заголовку Pragma.