CASE: Механизм скачивания файлов

Уже неоднократно сталкивался с задачей выведения пользователю на скачивание файлов, залитых в качестве свойств инфоблока... И если это самое свойство ставить с типом "Файл" то битрикс автоматически данному файлу присваивал уникальное имя, конфликта файлов в системе не возникало, но вот выдавать этот файл было крайне проблематично. 

Старый мой пост, перенесен из моего блога на сайте 1С-Битрикс.

Уже неоднократно сталкивался с задачей выведения пользователю на скачивание файлов, залитых в качестве свойств инфоблока... И если это самое свойство ставить с типом "Файл" то битрикс автоматически данному файлу присваивал уникальное имя, конфликта файлов в системе не возникало, но вот выдавать этот файл было крайне проблематично. 

До недавнего времени вместо свойства "Файл" ставил другое - "Привязка к файлу на сервере" и тогда по ИД файла вытягивалось его имя, файл выдавался на скачивание и все было просто замечательно. 

Но, в один прекрасный момент, возникла задача и необходимость более профессионального подхода к данному вопросу и вот решил сделать полноценный механизм по закачке/скачке файлов. 

Итак, сама задача: 

Пользователь сайта через компонент добавления нового элемента ИБ добавляет свой элемент. Одним из свойств элемента является поле "Файл" с типом "Файл". Дальше этот элемент через вывод списка новостей выводим другим пользователям. Файл должен быть доступен для скачивания с тем именем, с которым его заливали, а не с той "кракозяброй", с которой он хранится на сервере. 

Решение: 

Заливка файла проходит нормально, обычным образом, поэтому на ней останавливаться не буду. 

Вывод элементов происходит с помощью стандартного компонента 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) все параметры приводить к целому.

Количество показов: 4883
14.07.2011

Возврат к списку

Если вам была полезна статья можете отблагодарить автора:
Ethereum:

0x16Df809287333C49D3A237296C6248A6c08702Bc

Разработка сайта

Подайте заявку на разработку сайта на базе готового решения от компании 1С-Битрикс или одного из партнеров компании. Максимально подробно опишите, чему будет посвящен сайт, если это интернет-магазин - что он будет продавать, нужна ли мультиязычность, будут ли разные типы цен (розница, опт, крупный опт), будет ли интеграция с 1С, будет ли выгрузка товаров на различные торговые площадки...

Сопровождение сайта

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

Работы по сайту

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


Fatal error: Declaration of Pai\Sitemap\PaiSeoSitemap::Create($site_id, $max_execution_time, $NS, $arOptions, $modules, $iblocks, $clearIndexes) must be compatible with CAllSiteMap::Create($site_id, $max_execution_time, $NS, $arOptions = []) in /home/bitrix/www/bitrix/modules/pai.sitemap/lib/paiseositemap.php on line 183
[ErrorException] E_COMPILE_ERROR
Declaration of Pai\Sitemap\PaiSeoSitemap::Create($site_id, $max_execution_time, $NS, $arOptions, $modules, $iblocks, $clearIndexes) must be compatible with CAllSiteMap::Create($site_id, $max_execution_time, $NS, $arOptions = []) (0)
/home/bitrix/www/bitrix/modules/pai.sitemap/lib/paiseositemap.php:183
----------