Введение
Недавно мне пришлось опять связаться с так не любимой мной CMS’кой 1С-Bitrix. При этом встала своеобразная задача, было множество файлов которые необходимо было отдавать с именами отличными от исходных. Было решено использовать описание файла как имя. Для этого был написан небольшой скриптик который принимает в качестве аргумента идентификатор файла.
Здесь я попробую рассмотреть те особенности с которыми пришлось столкнуться. Для начала рассмотрим что основной кодировкой ресурса является cp1251. И большинство описаний составлено на русском языке.
Реализация
Для начала нам необходимо будет проверять передается ли нам вообще параметр, и если нет нужно мирно завершить работу.
- if (!isset($_REQUEST['file_id']) || !((int)$_REQUEST['file_id']) )
- die();
- $file_id=(int)$_REQUEST['file_id'];
Затем определим функцию для определения по юзер-агенту браузера(взял из комментов на сайте php). Эта функция нам понадобится посколько у IE как всегда все не как у людей, и надо будет под него подстраиваться, as usually.
- function browser_info($agent=null) {
- // Declare known browsers to look for
- $known = array('msie', 'firefox', 'safari', 'webkit', 'opera', 'netscape',
- 'konqueror', 'gecko');
- // Clean up agent and build regex that matches phrases for known browsers
- // (e.g. "Firefox/2.0" or "MSIE 6.0" (This only matches the major and minor
- // version numbers. E.g. "2.0.0.6" is parsed as simply "2.0"
- $agent = strtolower($agent ? $agent : $_SERVER['HTTP_USER_AGENT']);
- $pattern = '#(?<browser>' . join('|', $known) .
- ')[/ ]+(?<version>[0-9]+(?:\.[0-9]+)?)#';
- // Find all phrases (or return empty array if none found)
- if (!preg_match_all($pattern, $agent, $matches)) return array();
- // Since some UAs have more than one phrase (e.g Firefox has a Gecko phrase,
- // Opera 7,8 have a MSIE phrase), use the last one found (the right-most one
- // in the UA). That's usually the most correct.
- $i = count($matches['browser'])-1;
- return array($matches['browser'][$i] => $matches['version'][$i]);
- }
Теперь о логике работы, для того чтобы браузер решил что у файла что мы передаем совсем другое имя вы должны ему передать заголовок Content-Disposition: attachment; FILENAME="имя файла";size="размер файла в байтах"
Ну и до кучи некоторые другие стандартные заголовки типа Content-Type и Content-Length. Имя файла мы сформируем из описания, его, а также размер файла и путь к нему необходимо будет получить, так что мы подключаем битриксовые библиотеки:
- require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
И получаем путь к файлу, и массив с информацией о файле:
- $arr = CFile::MakeFileArray($file_id);
- $path = CFile::GetPath($file_id);
Теперь получим расширение файла так как оно нам понадобится при формировании имени.
Теперь займемся формированием имени файла.
- $description=$arr['description'];
- //укорачиваем название если оно слишком длинное()
- if (mb_strlen($description)>150)
- $description=mb_substr($description, 0,150).'...';
Описание файла необходимо очистить от символов которые не могут использоваться в именах файлов.
У FAT32,UFS,HFS+ ограничений особых нет, у ext2-3-4 нельзя использовать в названии файла символ ‘/’, но больше всего ограничений накладывается при использовании файловой системы NTFS, где запрещено использовать символов ‘ » / \ * ? | : ‘, что сильно огорчает 😦 Так что будем рассматривать худший случай, и поэтому преобразуем все встречные символы из этого списка в пробелы. Также в имени не может использоваться NUL, во всех из вышеперечисленных файловых систем.
- $pattern = array('"', '/', '\\', '*','?','<','>','|',':');
- for ($i= 0; $i<sizeof($pattern); $i++) {
- $description = mb_ereg_replace($pattern[$i], ' ', $description);
- }
Как выяснилось с кодировками в которых надо отдавать названия тоже проблема. Так Firefox,Opera,Chrome,Safari понимают если им передать название в кодировке UTF-8, в то время как IE просто жаждет cp-1251.
При этом необходимо не забывать что в случае если описание отсутствует стоит воспользоваться исходным именем файла.
- $ua=browser_info();
- if ($description)
- {
- $fname=mb_convert_encoding($description,"UTF-8", "Windows-1251" ).'.'.$ext;
- if (isset($ua['msie']))
- $fname=$description.'.'.$ext;
- }
- else $fname=basename($arr['tmp_name']);
Теперь отдаем необходимые заголовки и содержимое файла
Еще в ходе экспериментов всплыло то что добавление require($_SERVER[«DOCUMENT_ROOT»].»/bitrix/modules/main/include/prolog_after.php»); является чреватым, и инклудить этот файл в конце скрипта не стоит.
Листинг
Полный листинг приведен снизу.
- <?
- if (!isset($_REQUEST['file_id']) || !((int)$_REQUEST['file_id']) )
- die();
- function browser_info($agent=null) {
- // Declare known browsers to look for
- $known = array('msie', 'firefox', 'safari', 'webkit', 'opera', 'netscape',
- 'konqueror', 'gecko');
- // Clean up agent and build regex that matches phrases for known browsers
- // (e.g. "Firefox/2.0" or "MSIE 6.0" (This only matches the major and minor
- // version numbers. E.g. "2.0.0.6" is parsed as simply "2.0"
- $agent = strtolower($agent ? $agent : $_SERVER['HTTP_USER_AGENT']);
- $pattern = '#(?<browser>' . join('|', $known) .
- ')[/ ]+(?<version>[0-9]+(?:\.[0-9]+)?)#';
- // Find all phrases (or return empty array if none found)
- if (!preg_match_all($pattern, $agent, $matches)) return array();
- // Since some UAs have more than one phrase (e.g Firefox has a Gecko phrase,
- // Opera 7,8 have a MSIE phrase), use the last one found (the right-most one
- // in the UA). That's usually the most correct.
- $i = count($matches['browser'])-1;
- return array($matches['browser'][$i] => $matches['version'][$i]);
- }
- $file_id=(int)$_REQUEST['file_id'];
- require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
- $arr = CFile::MakeFileArray($file_id);
- $path = CFile::GetPath($file_id);
- $ext = explode('.', $path);
- $ext = $ext[count($ext)-1];
- $description=$arr['description'];
- if (mb_strlen($description)>200)
- $description=mb_substr($description, 0,200).'...';
- $pattern = array('"', '/', '\\', '*','?','<','>','|',':');
- for ($i= 0; $i<sizeof($pattern); $i++) {
- $description = mb_ereg_replace($pattern[$i], ' ', $description);
- }
- $ua=browser_info();
- if ($description)
- {
- $fname=mb_convert_encoding($description,"UTF-8", "Windows-1251" ).'.'.$ext;
- if (isset($ua['msie']))
- $fname=$description.'.'.$ext;
- }
- else $fname=basename($arr['tmp_name']);
- header('Content-Type: '.$arr['type']);
- header('Content-Length: '.$arr['size']);
- header('Content-Disposition: attachment; FILENAME="'.$fname.'"; size="'.$arr['size'].'"');
- echo file_get_contents($arr['tmp_name']);
- ?>
Ссылки по теме
Тесты на поддержку Content-Disposition в разных браузерах
Проблемы которые всплывают в различных версиях IE
Сравнение_файловых_систем — смотрим на ограничения 🙂
Еще одно сравнение касательно IE
Ну и конечно же RFC 1806 🙂
Хорошая статья. Действительно было интересно почитать. Не часто такое и встречается та.Наверное стоит подписаться на ваше RSS
комментарий от Драйв — 2010-03-19 @ 5:40 дп |