Я безумен

2010-03-1

Сохранение файлов с другим именем в 1C-Bitrix

Filed under: PHP — thekillerfox @ 8:10 пп
Tags: , ,

Введение

Недавно мне пришлось опять связаться с так не любимой мной CMS’кой 1С-Bitrix. При этом встала своеобразная задача, было множество файлов которые необходимо было отдавать с именами отличными от исходных. Было решено использовать описание файла как имя. Для этого был написан небольшой скриптик который принимает в качестве аргумента идентификатор файла.

Здесь я попробую рассмотреть те особенности с которыми пришлось столкнуться. Для начала рассмотрим что основной кодировкой ресурса является cp1251. И большинство описаний составлено на русском языке.

Реализация

Для начала нам необходимо будет проверять передается ли нам вообще параметр, и если нет нужно мирно завершить работу.

Copy Source | Copy HTML

  1. if (!isset($_REQUEST['file_id']) || !((int)$_REQUEST['file_id']) )
  2.     die();
  3. $file_id=(int)$_REQUEST['file_id'];

Затем определим функцию для определения по юзер-агенту браузера(взял из комментов на сайте php). Эта функция нам понадобится посколько у IE как всегда все не как у людей, и надо будет под него подстраиваться, as usually.

Copy Source | Copy HTML

  1. function browser_info($agent=null) {
  2.   // Declare known browsers to look for
  3.   $known = array('msie', 'firefox', 'safari', 'webkit', 'opera', 'netscape',
  4.     'konqueror', 'gecko');
  5.  
  6.   // Clean up agent and build regex that matches phrases for known browsers
  7.   // (e.g. "Firefox/2.0" or "MSIE 6.0" (This only matches the major and minor
  8.   // version numbers.  E.g. "2.0.0.6" is parsed as simply "2.0"
  9.   $agent = strtolower($agent ? $agent : $_SERVER['HTTP_USER_AGENT']);
  10.   $pattern = '#(?<browser>' . join('|', $known) .
  11.     ')[/ ]+(?<version>[0-9]+(?:\.[0-9]+)?)#';
  12.  
  13.   // Find all phrases (or return empty array if none found)
  14.   if (!preg_match_all($pattern, $agent, $matches)) return array();
  15.  
  16.   // Since some UAs have more than one phrase (e.g Firefox has a Gecko phrase,
  17.   // Opera 7,8 have a MSIE phrase), use the last one found (the right-most one
  18.   // in the UA).  That's usually the most correct.
  19.   $i = count($matches['browser'])-1;
  20.   return array($matches['browser'][$i] => $matches['version'][$i]);
  21. }

Теперь о логике работы, для того чтобы браузер решил что у файла что мы передаем совсем другое имя вы должны ему передать заголовок Content-Disposition: attachment; FILENAME="имя файла";size="размер файла в байтах"
Ну и до кучи некоторые другие стандартные заголовки типа Content-Type и Content-Length. Имя файла мы сформируем из описания, его, а также размер файла и путь к нему необходимо будет получить, так что мы подключаем битриксовые библиотеки:

Copy Source | Copy HTML

  1. require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");

И получаем путь к файлу, и массив с информацией о файле:

Copy Source | Copy HTML

  1. $arr = CFile::MakeFileArray($file_id);
  2. $path = CFile::GetPath($file_id);

Теперь получим расширение файла так как оно нам понадобится при формировании имени.

Copy Source | Copy HTML

  1. $ext = explode('.', $path);
  2. if (count($ext)>1)
  3.    $ext = $ext[count($ext)-1];
  4. else
  5.    $ext='';

Теперь займемся формированием имени файла.

Copy Source | Copy HTML

  1. $description=$arr['description'];
  2. //укорачиваем название если оно слишком длинное()
  3. if (mb_strlen($description)>150)
  4.     $description=mb_substr($description, 0,150).'...';

Описание файла необходимо очистить от символов которые не могут использоваться в именах файлов.
У FAT32,UFS,HFS+ ограничений особых нет, у ext2-3-4 нельзя использовать в названии файла символ ‘/’, но больше всего ограничений накладывается при использовании файловой системы NTFS, где запрещено использовать символов ‘ » / \ * ? | : ‘, что сильно огорчает 😦 Так что будем рассматривать худший случай, и поэтому преобразуем все встречные символы из этого списка в пробелы. Также в имени не может использоваться NUL, во всех из вышеперечисленных файловых систем.

Copy Source | Copy HTML

  1. $pattern = array('"', '/', '\\', '*','?','<','>','|',':');
  2.     for ($i= 0; $i<sizeof($pattern); $i++) {
  3.         $description = mb_ereg_replace($pattern[$i], ' ', $description);
  4.     }

Как выяснилось с кодировками в которых надо отдавать названия тоже проблема. Так Firefox,Opera,Chrome,Safari понимают если им передать название в кодировке UTF-8, в то время как IE просто жаждет cp-1251.
При этом необходимо не забывать что в случае если описание отсутствует стоит воспользоваться исходным именем файла.

Copy Source | Copy HTML

  1. $ua=browser_info();
  2. if ($description)
  3. {
  4.     $fname=mb_convert_encoding($description,"UTF-8", "Windows-1251" ).'.'.$ext;
  5.         if (isset($ua['msie']))
  6.         $fname=$description.'.'.$ext;
  7. }
  8. else $fname=basename($arr['tmp_name']);

Теперь отдаем необходимые заголовки и содержимое файла

Copy Source | Copy HTML

  1. header('Content-Type: '.$arr['type']);
  2. header('Content-Length: '.$arr['size']);
  3. header('Content-Disposition: attachment; FILENAME="'.$fname.'"; size="'.$arr['size'].'"');
  4. echo file_get_contents($arr['tmp_name']);

Еще в ходе экспериментов всплыло то что добавление require($_SERVER[«DOCUMENT_ROOT»].»/bitrix/modules/main/include/prolog_after.php»); является чреватым, и инклудить этот файл в конце скрипта не стоит.

Листинг

Полный листинг приведен снизу.

Copy Source | Copy HTML

  1. <?
  2. if (!isset($_REQUEST['file_id']) || !((int)$_REQUEST['file_id']) )
  3.     die();
  4. function browser_info($agent=null) {
  5.   // Declare known browsers to look for
  6.   $known = array('msie', 'firefox', 'safari', 'webkit', 'opera', 'netscape',
  7.     'konqueror', 'gecko');
  8.  
  9.   // Clean up agent and build regex that matches phrases for known browsers
  10.   // (e.g. "Firefox/2.0" or "MSIE 6.0" (This only matches the major and minor
  11.   // version numbers.  E.g. "2.0.0.6" is parsed as simply "2.0"
  12.   $agent = strtolower($agent ? $agent : $_SERVER['HTTP_USER_AGENT']);
  13.   $pattern = '#(?<browser>' . join('|', $known) .
  14.     ')[/ ]+(?<version>[0-9]+(?:\.[0-9]+)?)#';
  15.  
  16.   // Find all phrases (or return empty array if none found)
  17.   if (!preg_match_all($pattern, $agent, $matches)) return array();
  18.  
  19.   // Since some UAs have more than one phrase (e.g Firefox has a Gecko phrase,
  20.   // Opera 7,8 have a MSIE phrase), use the last one found (the right-most one
  21.   // in the UA).  That's usually the most correct.
  22.   $i = count($matches['browser'])-1;
  23.   return array($matches['browser'][$i] => $matches['version'][$i]);
  24. }
  25. $file_id=(int)$_REQUEST['file_id'];
  26. require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
  27. $arr = CFile::MakeFileArray($file_id);
  28.  
  29. $path = CFile::GetPath($file_id);
  30. $ext = explode('.', $path);
  31. $ext = $ext[count($ext)-1];
  32.  
  33. $description=$arr['description'];
  34.  
  35. if (mb_strlen($description)>200)
  36.     $description=mb_substr($description, 0,200).'...';
  37. $pattern = array('"', '/', '\\', '*','?','<','>','|',':');
  38. for ($i= 0; $i<sizeof($pattern); $i++) {
  39.      $description = mb_ereg_replace($pattern[$i], ' ', $description);
  40. }
  41. $ua=browser_info();
  42. if ($description)
  43. {
  44.     $fname=mb_convert_encoding($description,"UTF-8", "Windows-1251" ).'.'.$ext;
  45.         if (isset($ua['msie']))
  46.         $fname=$description.'.'.$ext;
  47. }
  48. else $fname=basename($arr['tmp_name']);
  49.  
  50.     header('Content-Type: '.$arr['type']);
  51.     header('Content-Length: '.$arr['size']);
  52.  
  53.     header('Content-Disposition: attachment; FILENAME="'.$fname.'"; size="'.$arr['size'].'"');
  54.     echo file_get_contents($arr['tmp_name']);
  55. ?>

Ссылки по теме

Тесты на поддержку Content-Disposition в разных браузерах
Проблемы которые всплывают в различных версиях IE
Сравнение_файловых_систем — смотрим на ограничения 🙂
Еще одно сравнение касательно IE
Ну и конечно же RFC 1806 🙂

Блог на WordPress.com.