Уже неоднократно сталкивался с задачей выведения пользователю на скачивание файлов, залитых в качестве свойств инфоблока... И если это самое свойство ставить с типом "Файл" то битрикс автоматически данному файлу присваивал уникальное имя, конфликта файлов в системе не возникало, но вот выдавать этот файл было крайне проблематично.
До недавнего времени вместо свойства "Файл" ставил другое - "Привязка к файлу на сервере" и тогда по ИД файла вытягивалось его имя, файл выдавался на скачивание и все было просто замечательно.
Но, в один прекрасный момент, возникла задача и необходимость более профессионального подхода к данному вопросу и вот решил сделать полноценный механизм по закачке/скачке файлов.
Итак, сама задача:
Пользователь сайта через компонент добавления нового элемента ИБ добавляет свой элемент. Одним из свойств элемента является поле "Файл" с типом "Файл". Дальше этот элемент через вывод списка новостей выводим другим пользователям. Файл должен быть доступен для скачивания с тем именем, с которым его заливали, а не с той "кракозяброй", с которой он хранится на сервере.
Решение:
Заливка файла проходит нормально, обычным образом, поэтому на ней останавливаться не буду.
Вывод элементов происходит с помощью стандартного компонента news.list. При выводе элементов, когда добираемся до свойства с файлом, получаем всего-лишь ИД файла и ничего больше.
Т.к. у меня таких файлов могло быть несколько, то данный блок получился такой:
<foreach($elem["PROPERTIES"]["U_FILE"]["VALUE"]as $ar_File):
$rsFile = CFile::GetByID($ar_File);
$arFile = $rsFile->Fetch();
$descr=$arFile["ORIGINAL_NAME"];
echo "<a href=filedownload.php?file=".$ar_File." >".$descr."</a>";
endforeach;?>
Тут мы делаем перебор по всем файлам, добавленным в данное свойство элемента. Получаем файл по его ИД. Для отображения пользователю нормального, читабельного имени, получаем имя, с которым данный файл был загружен на сервер:
$arFile["ORIGINAL_NAME"]
Дальше выводим ссылку пользователю на скачивание файла:
echo "<a href=filedownload.php?file=".$ar_File." >".$descr."</a>";
Скачивание файла не напрямую, а через другой вспомогательный файл со скриптом, было сделано сначала просто потому, что пользователю файл при сохранении выдавал все-таки не то имя, которое мы отображаем, а то, с которым он хранится на сервере и с этим надо было как-то бороться. Вот и пришел к решению, что нужно создать доп. файл, в котором будет определяться нужное имя файла. Этому скрипту передаем ИД файла, который нужно скачать...
Дальше дело переходит уже к скрипту.
<?//возможность скачать файла залитого в свойство ИБ с его первоначальным именем
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
function rus2translit($string)
{
$converter = array(
'а' => 'a', 'б' => 'b', 'в' => 'v',
'г' => 'g', 'д' => 'd', 'е' => 'e',
'ё' => 'e', 'ж' => 'zh', 'з' => 'z',
'и' => 'i', 'й' => 'y', 'к' => 'k',
'л' => 'l', 'м' => 'm', 'н' => 'n',
'о' => 'o', 'п' => 'p', 'р' => 'r',
'с' => 's', 'т' => 't', 'у' => 'u',
'ф' => 'f', 'х' => 'h', 'ц' => 'c',
'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sch',
'ь' => "_", 'ы' => 'y', 'ъ' => "_",
'э' => 'e', 'ю' => 'yu', 'я' => 'ya',
'А' => 'A', 'Б' => 'B', 'В' => 'V',
'Г' => 'G', 'Д' => 'D', 'Е' => 'E',
'Ё' => 'E', 'Ж' => 'Zh', 'З' => 'Z',
'И' => 'I', 'Й' => 'Y', 'К' => 'K',
'Л' => 'L', 'М' => 'M', 'Н' => 'N',
'О' => 'O', 'П' => 'P', 'Р' => 'R',
'С' => 'S', 'Т' => 'T', 'У' => 'U',
'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C',
'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sch',
'Ь' => "_", 'Ы' => 'Y', 'Ъ' => "_",
'Э' => 'E', 'Ю' => 'Yu', 'Я' => 'Ya',
);
return strtr($string, $converter);
}
if(isset($_GET['file'])){
$F_ID=$_GET['file'];
$rsFile = CFile::GetByID($F_ID);
$arFile = $rsFile->Fetch();
$fName = $arFile["ORIGINAL_NAME"];
// далее от греха подальше нужно провести транслитерацию русских символов на английские
$fName=rus2translit($fName);
//определяем тип файла
$c_Type=$arFile["CONTENT_TYPE"];
// опредеяем путь к файлу
$file = CFile::GetPath($F_ID);
$file=$_SERVER['DOCUMENT_ROOT'].$file;
//проверяем, а есть ли вообще этот файл
if(!file_exists($file))
{
die("Error: $file not found.");
}
else
{
// Set headers
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-Disposition: attachment; filename=".$fName);
header("Content-Type: $c_Type");
header("Content-Transfer-Encoding: binary");
// Read the file from disk
readfile($file);
};
} else {die("ОШИБКА: Не указан ID файла");};
?>
Скрипт начинается со строки, в которой мы проверяем указан ли вообще ИД файла:
<? if(isset($_GET['file'])){
Дальше мы снова получаем оригинальное имя файла и транслитерируем его. В Данной транслитерации учтены символы русского и украинского языка. Кому нужны еще какие-то символы - добавляйте.
Для того, чтобы построить нужную нам схему html-файла, которая будет нам выдавать файл на скачивание, нужно указать соответствующие head-функции. Все данные для этих функций можно выбрать из свойств файла. Что в принципе и было сделано...
Обращаю особое внимание на строки:
$file = CFile::GetPath($F_ID); $file=$_SERVER['DOCUMENT_ROOT'].$file;
Битриксовая функция получения пути файла выдает путь не физический (где файл лежит на сервере), а путь относительно корня сайта. поэтому к пути нужно добавить где сайт лежит физически на сервере.
Теперь построчно самый важный кусок всего скрипта:
Имя файла, которое будет выдано пользователю при сохранении:
header("Content-Disposition: attachment; filename=".$fName);
Тип файла - нужен в основном для того, чтобы браузер понимал, что делать с этим файлом:
header("Content-Type: $c_Type");
Непосредственное чтение файла:
readfile($file);
Уже после реализации, осознал несколько преимуществ такого скачивания.
Во-первых, это конечно же транслитерация названия файла. Она является достаточно важным моментом данной реализации, т.к. не все браузеры нормально реагируют на кириллицу.
Во-вторых, пользователь никак не может получить прямую ссылку на файл. И если внутри скрипта поставить доп. проверку на то, а может ли текущий пользователь скачивать этот файл, то воспользоваться прямой ссылкой без авторизации на сайте уже будет очень проблематично...
UPDATE 2017-12-13:
Была обнаружена критическая уязвимость в приведенном скрипте, рекомендую ознакомиться с выводами по уязвимости и принять свое собственное решение, что с этим делать. У меня, к сожалению или счастью, нет проектов, на которых есть необходимость в данном скрипте, но, возможно, кому-то будет полезно. содержит критическую уязвимость, поэтому в приведенном виде не может быть использован в конечных проектах.
Описание уязвимости:
1) перебором ID можно выкачать все загруженные файлы - а вы не можете знать, какие из них защищены по доступу
2) можно указать путь к файлу - т.к. к целому значению параметр не приводится, то скачается любой произвольный файл, включая dbconn.php с паролем к БД
К сожалению, этот уязвимый скрипт уже разошелся по ряду проектов. Если Вы лично имеете отношение к таким проектам,
рекомендую срочно внести правки, либо удалить этот скрипт на них.
Могу порекомендовать:
1) Передавать не только ID картинки, но и ID элемента инфоблока и проверять, что в этом элементе есть эта картинка
2) все параметры приводить к целому.
