Скрипт скачивания различных документов с сервера
Опубликовано: 27 Май 2013
Недавно, для одного интернет-магазина на базе joomla-virtuemart , сделал скрипт, позволяющий на основе информации о товарах в базе данных генерировать каталоги товаров в виде XML документа с соблюдением формата Yandex Market Language (YML-формат каталога). Скрипт генерирует такие каталоги и сохраняет в файловой системе сайта. Но, клиент очень скоро пожаловался, что неудобно скачивать каталоги с сайта. И правда, при прямом обращении к каталогу, он просто открывается в браузере и чтобы его скачать нужно дополнительно нажать правой кнопкой мыши и кликнуть «сохранить как…».
Действительно, для рядового пользователя интернета, это может быть довольно не удобно, и к тому же нужно еще додуматься, что можно кликнуть мышкой и скачать. Не для всех это кажется очевидным. Поэтому я решил написать коротенький скриптик, который по запросу позволял бы скачать текстовый файл с сервера, без открытия его в браузере. В этой статье я предлагаю листинг его кода с комментариями.
if (isset($_GET['file']) and preg_match("/^Catalog[a-zA-Z0-9_]{0,50}\.xml$/",$_GET['file']) and file_exists($_GET['file'])) { $content = file_get_contents($_GET['file']); header('Content-Type: '.$ctype.'; charset=utf-8'); header("Content-Disposition: attachment; filename=".$_GET['file']); ob_end_clean(); ob_start(); echo $content; ob_end_flush(); exit(); } else { echo "Файл не найден."; exit(); }
Принцип работы этого скрипта довольно прост. Допустим, скрипт записан в php файл с именем «download.php» и помещен в папку «files». Формируется ссылка на этот скрипт, в которой передается GET параметр file и в этом параметре передается имя требуемого текстового документа. Например, мы хотим скачать текстовый файл с именем test.xml или test.txt:
http://www.domen-saita.ru/files/download.php?file=test.xml или http://www.domen-saita.ru/files/download.php?file=test.txt
При этом, если файл найден в файловой системе, в браузере всплывает диалоговое окно сохранения документа на компьютер.
Безопасность
Однако, в такой организации скачивания документов с сервера таится одна серьезная опасность. Если я введу в адресной строке браузера такой запрос:
http://www.domen-saita.ru/files/download.php?file=../configuration.php
в таком случае начнется то, чего ни в коем случае допускать нельзя! Начнется скачивание файла конфигурации Вашего сайта (в данном случае имеется ввиду файл конфигурации joomla, однако это в равной степени касается всех CMS). А это автоматически означает полный крах, потому что в таком случае в руки пользователю попадает вся жизненно важная информация связанная с работоспособностью сайта. И вопрос взлома – это уже вопрос ближайшего времени. Поэтому, чтобы предотвратить такую возможность, мы должны позаботиться о безопасности. Именно для этого в первой строке скрипта полученный запрос проходит проверку регулярным выражением.
preg_match("/^Catalog[a-zA-Z0-9_]{0,50}\.xml$/",$_GET['file'])
В данном случае, регулярное выражение допускает возможность скачивания только документа, имя которого может быть следующим:
Catalog.xml Catalog_krovati.xml Catalog_shkafy_i_tumby.xml
и тому подобное… То есть имя файла, в данном случае, должно начинаться с «Catalog» и заканчиваться на «.xml», с учетом регистра - любые другие вариации будут отклонены и на экран будет выведено сообщение «файл не найден». Хотя, физически он может находиться на сервере, но его скачивать не разрешено.
Причем, так же в этом регулярном выражении нет возможностьи пропустить символ «/» и «.» (кроме точки в окончании имени файла). Это предотвращает возможность в качестве имени файла указать путь до файла в другой папке, аналогично примеру с «configuration.php», или например:
http://www.domen-saita.ru/files/download.php?file=../drugaya-papka/test.xml
То есть, данный скрипт позволит скачивать xml файлы только из той папки, в которой находится сам скрипт.
Однако, Вы можете изменить структуру регулярного выражения так, чтобы допустить скачивание любых «txt», «xml», «pdf», а так же любых графических файлов типа «png», «gif» и прочее… В общем, тут Вы можете полностью сами решать какие файлы можно скачивать с помощью этого скрипта. Главное ни в коем случае не допускайте через этот скрипт скачивание системных файлов вашего сайта, которые могут нести какие-то важные настройки, логины и пароли. Если с помощью этого скрипта захотите организовать скачивание из разных директорий файловой системы, то подумайте о том чтобы не было возможности скачивать из папки типа «administrator» или «admin», или из папок, которые содержат файлы расширений CMS, в том числе их файлы конфигурации.
На этом, собственно, вопрос безопасности завершается и дальше идет просто техническая работа по подготовке к скачиванию, если проверка пройдена успешно и файл с таким именем существует.
Во второй строке скрипта запрашивается содержимое целевого файла и помещается в переменную.
Далее передаются заголовки, в первом заголовке определяется тип данных и кодировка, а во втором заголовке передается требование вывести диалог сохранения документа на компьютер до вывода его в окне браузера.
Далее предварительно опустошается (на всякий случай) и разрушается буфер. Затем запускается буферизация и все что выводится из переменной $content помещается в буфер, после чего выводится оттуда для скачивания.
Алтернативный код
Здесь представлен код того же скрипта, но чтение и дальнейшее скачивание файла с сервера происходит через функцию readfile(), что согласно документации php точно не должно вызвать проблем с переполнением памяти. На всякий случай, перед вызовом этой функции происходит очищение буфера с помощью ob_clean().
if (isset($_GET['file']) and preg_match("/^Catalog[a-zA-Z0-9_]{0,50}\.xml$/",$_GET['file']) and file_exists($_GET['file'])) { header('Content-Type: '.$ctype.'; charset=utf-8'); header("Content-Disposition: attachment; filename=".$_GET['file']); ob_clean(); readfile($_GET['file']); exit(); } else { echo "Файл не найден."; exit(); }
Однако, я не хочу сказать что скрипт указанный в первой части статьи хуже, просто для него я не могу сказать как он поведет себя при скачивании файлов большого размера. В случае же второго варианта, со слов самих разработчиков php – таких проблем точно не возникнет.
Дополнительное улучшение
Если Вы надумаете использовать скрипт для скачивания файлов разных форматов, то будет полезно при каждом скачивании в заголовках правильно указывать тип передаваемых данных, т.н. MIME-типы (подробнее можно прочитать здесь). Для этого нужно добавить анализ имени файла на предмет его расширения, тогда скрипт будет примерно таков:
if (isset($_GET['file']) and preg_match("/^[a-zA-Z0-9_\.\/\-]{0,50}\.(xml|txt|pdf|png|gif|jpg|jpeg|exe|doc|xls|ppt|zip|)$/",$_GET['file']) and file_exists($_GET['file'])) { $extension = strtolower(substr(strrchr($_GET['file'],"."),1)); switch ($extension) { case "txt": $ctype="text/plain"; break; case "pdf": $ctype="application/pdf"; break; case "exe": $ctype="application/octet-stream"; break; case "zip": $ctype="application/zip"; break; case "doc": $ctype="application/msword"; break; case "xls": $ctype="application/vnd.ms-excel"; break; case "ppt": $ctype="application/vnd.ms-powerpoint"; break; case "gif": $ctype="image/gif"; break; case "png": $ctype="image/png"; break; case "jpeg": $ctype="image/jpg"; break; case "jpg": $ctype="image/jpg"; break; default: $ctype="application/force-download"; } header('Content-Type: '.$ctype.'; charset=utf-8'); header("Content-Disposition: attachment; filename=".$_GET['file']); ob_clean(); readfile($_GET['file']); exit(); } else { echo "Файл не найден."; exit(); }
Кроме того, при желании, можно скрипт доработать таким образом чтобы ограничить количество скачиваний конкретного файла для одного пользователя. При этом прямой доступ к файлам перекрыть настройками сервера (например, через .htaccess на apache).
Если информация этой статьи будет интересна и полезна Вашему кругу друзей и знакомых, то Вы можете опубликовать ссылку - тогда им проще будет ее найти. Они Вам будут благодарны:).