Отдельная директория для файлов инфоблока
По умолчанию для всех файлов инфоблоков используется общая папка /upload/iblock/. Файлы сохраняются в подпапки, сгенерированные случайным образом, например, /upload/iblock/ff7/.
Как следствие, все файлы доступны по прямой ссылке. В целом, в этом нет ничего страшного, так как итоговый путь (при условии, что файл отдается через php) сложно подобрать. Но в некоторых случаях требуются дополнительные меры безопасности.
Основная идея – как-то вынести файлы инфоблока в отдельную подпапку и закрыть её с помощью стандартных механизмов nginx / apache.
Небольшим дополнительным плюсом будет то, что можно достаточно просто посмотреть, сколько места занимают файлы отдельного инфоблока.
Суть в следующем: в методе CFile::SaveFile второй параметр ($strSavePath) отвечает за директорию внутри папки upload. Инфоблоки его передают как «iblock», нам надо его заменить.
- Код, приведенный ниже не рекомендуется к использованию, но в целом он позволяет решить задачу (и, в том числе, работает, как в публичной, так и в административной части).
- Случай, когда файлы хранятся в облаке, не рассматривается.
- Скачать исходники. В случае включения в код "как-есть" необходимо прописать пути в методе loadPaths
Общая схема
Регистрируем обработчик на событие OnFileSave. Обработчик будет вызван при сохранении файла. В обработчике каким-то способом получаем ID инфоблока, файл которого сохраняется. Сохраняем файл физически и возвращаем true.
Подробнее
- Регистрируем обработчик на событие OnFileSave модуля main.
use Bitrix\Main\Loader; use Bitrix\Main\EventManager; Loader::registerAutoLoadClasses(null, [ "TeamCabinet\IBlockFilePath" => "/local/php_interface/include/pixelplus/lib/TeamCabinet/IBlockFilePath.php", ]); EventManager::getInstance()->addEventHandlerCompatible("main", "OnFileSave", [ 'TeamCabinet\IBlockFilePath', "onFileSave" ]);
Тут есть несколько важных моментов:
- Во-первых, как в методе CFile::SaveFile, так и в обработчике у нас нет идентификатора инфоблока.
- Во-вторых, $strSavePath в обработчик передается по значению, а не по ссылке, поэтому изменить его просто так не получится.
- Во-третьих, обработчик позволяет только определить собственный способ физического сохранения файла, в таблицу b_file пишет сам метод CFile::SaveFile.
- Путь 1: Значение из $_REQUEST.
При редактировании элемента в административной части url имеет вид /bitrix/admin/iblock_element_edit.php?IBLOCK_ID=170&type=catalog&ID=388118&lang=ru&find_section_section=-1&WF=Y
и, соответственно, в $_REQUEST у нас есть IBLOCK_ID
Этот метод не универсален, т.к. не поддерживает добавление с помощью API (например, из публичной части).
Кроме того, при сохранении элемента может возникнуть ситуация, когда надо добавить элемент с файлом в другой инфоблок.
- Путь 2: Сохранение инфоблока во время вызова события в статический член класса
На событиях обновления OnBeforeIBlockElementUpdate, OnAfterIBlockElementUpdate и OnIBlockElementSetPropertyValues сохранять в статический член класса ID инфоблока.
В данном случае нет нужного события (onBefore) для метода CIBlockElement::SetPropertyValuesEx
Кроме того, проблема с добавлением элемента с файлом в другой инфоблок также остается - Путь 3: Через функцию debug_backtrace().
Это ресурсозатратно, но узнать ID инфоблока можно более точно, чем в предыдущих методах.
-
В классе, который будет использоваться, содержится как сам метод обработчика onFileSave, так и вспомогательные методы. Сокращенный файл класса:
namespace TeamCabinet; use Bitrix\Main\Loader; use Bitrix\Main\FileTable; use Bitrix\Main\Application; use Bitrix\Iblock; /** * Class IBlockFilePath * @package TeamCabinet */ class IBlockFilePath { /** * @var null|self */ protected static $instance = null; /** * @var array $paths */ protected $paths = []; /** * Constructor. */ protected function __construct() { $this->loadPaths(); } /** * @return self */ public static function getInstance() { if (!self::$instance) { self::$instance = new self; } return self::$instance; } public function loadPaths() { //... } public function getPaths() { //... } public function saveFile( &$arFile, $strFileName, $strSavePath, $bForceMD5 = false, $bSkipExt = false, $dirAdd = "" ) { //... } public static function onFileSave( &$arFile, $strFileName, $strSavePath, $bForceMD5 = false, $bSkipExt = false, $dirAdd = "" ) { if ($strSavePath != "iblock") { return false; } //... } }
Созданы отдельные методы, которые позволяют получить/установить пути сохранения файлов для каждого инфоблока. Метод установки вызывается в конструкторе/** * Загружает из внешнего источника (в данном случае просто массив) список путей для каждого инфоблока * Ключи - ID инфоблока, значения - subdir для сохранения файлов */ public function loadPaths() { $this->paths = [ 8 => "iblock/documents/questionnaire", 5 => "iblock/documents/nda", 7 => "iblock/documents/passport", 4 => "iblock/documents/contract", 3 => "iblock/documents/employmentHistory" ]; } /** * Получить список путей для инфоблока * @return array */ public function getPaths() { if (!is_array($this->paths)) { $this->paths = []; } return $this->paths; }
- В обработчике события (метод onFileSave) выполняем все действия только если $strSavePath == "iblock"
if ($strSavePath != "iblock") { return false; }
- В обработчике вызываем debug_backtrace(), анализируем полученную информацию, чтобы узнать ID инфоблока
$trace = debug_backtrace(); $iblockId = 0; foreach ($trace as $traceItem) { if ($traceItem["function"] == "SetPropertyValues" && $traceItem["class"] == "CIBlockElement") { if ($traceItem["args"][1] > 0) { $iblockId = intval($traceItem["args"][1]); break; } else { return false; } } if ($traceItem["function"] == "SetPropertyValuesEx" && $traceItem["class"] == "CAllIBlockElement") { $elementId = intval($traceItem["args"][0]); if ($elementId <= 0) { return false; } $iblockId = intval($traceItem["args"][1]); if ($iblockId <= 0 && Loader::includeModule("iblock")) { $row = Iblock\ElementTable::getRow([ "filter" => [ "=ID" => $elementId ], "select" => [ "IBLOCK_ID" ] ]); if ($row && $row["IBLOCK_ID"]) { $iblockId = intval($row["IBLOCK_ID"]); } } if ($iblockId > 0) { break; } else { return false; } } if ($traceItem["function"] == "SaveForDB" && $traceItem["class"] == "CAllFile") { if ($traceItem["args"][0]["IBLOCK_ID"] > 0) { $iblockId = intval($traceItem["args"][0]["IBLOCK_ID"]); break; } } }
- После того, как мы узнали инфоблок (если его вообще удалось определить), смотрим, нужен ли ему отдельный путь, и есть да, то производим сохранение файла, возвращая из метода true.
if ($iblockId > 0) { $instance = self::getInstance(); $paths = $instance->getPaths(); if (!array_key_exists($iblockId, $paths)) { return false; } $strNewSavePath = $paths[$iblockId]; if ($strNewSavePath == "iblock") { return false; } //Если бы $strSavePath передавался в обработчик по ссылке, то достаточно было бы его просто изменить return $instance->saveFile($arFile, $strFileName, $strNewSavePath, $bForceMD5, $bSkipExt, $dirAdd); }
- Сохраняем файл. Согласно назначению события OnFileSave, файл может быть как-то сохранен. Код для метода сохранения практически полностью скопирован из CFile::SaveFile.
public function saveFile( &$arFile, $strFileName, $strSavePath, $bForceMD5 = false, $bSkipExt = false, $dirAdd = "" ) { $uploadDir = \COption::GetOptionString("main", "upload_dir", "upload"); $io = \CBXVirtualIo::GetInstance(); if ($bForceMD5 != true && \COption::GetOptionString("main", "save_original_file_name", "N") == "Y") { $dirAddEx = $dirAdd; if ($dirAddEx == "") { $i = 0; while (true) { $dirAddEx = substr(md5(uniqid("", true)), 0, 3); if (!$io->FileExists($_SERVER["DOCUMENT_ROOT"]. "/". $uploadDir."/".$strSavePath."/".$dirAddEx."/".$strFileName)) { break; } if ($i >= 25) { $j = 0; while (true) { $dirAddEx = substr(md5(mt_rand()), 0, 3)."/".substr(md5(mt_rand()), 0, 3); if (!$io->FileExists($_SERVER["DOCUMENT_ROOT"]."/" .$uploadDir."/".$strSavePath."/".$dirAddEx."/".$strFileName)) { break; } if ($j >= 25) { $dirAddEx = substr(md5(mt_rand()), 0, 3)."/".md5(mt_rand()); break; } $j++; } break; } $i++; } } if (substr($strSavePath, -1, 1) <> "/") { $strSavePath .= "/".$dirAddEx; } else { $strSavePath .= $dirAddEx."/"; } } else { $strFileExt = ($bSkipExt == true || ($ext = GetFileExtension($strFileName)) == "" ? "" : ".".$ext); while (true) { if (substr($strSavePath, -1, 1) <> "/") { $strSavePath .= "/".substr($strFileName, 0, 3); } else { $strSavePath .= substr($strFileName, 0, 3)."/"; } if (!$io->FileExists($_SERVER["DOCUMENT_ROOT"]."/".$uploadDir."/".$strSavePath."/".$strFileName)) { break; } //try the new name $strFileName = md5(uniqid("", true)).$strFileExt; } } $arFile["SUBDIR"] = $strSavePath; $arFile["FILE_NAME"] = $strFileName; $strDirName = $_SERVER["DOCUMENT_ROOT"]."/".$uploadDir."/".$strSavePath."/"; $strDbFileNameX = $strDirName.$strFileName; $strPhysicalFileNameX = $io->GetPhysicalName($strDbFileNameX); CheckDirPath($strDirName); if (is_set($arFile, "content")) { $f = fopen($strPhysicalFileNameX, "w"); if (!$f) { return false; } if (fwrite($f, $arFile["content"]) === false) { return false; } fclose($f); } elseif (!copy($arFile["tmp_name"], $strPhysicalFileNameX) && !move_uploaded_file($arFile["tmp_name"], $strPhysicalFileNameX)) { \CFile::DoDelete($arFile["old_file"]); return false; } if (isset($arFile["old_file"])) { \CFile::DoDelete($arFile["old_file"]); } @chmod($strPhysicalFileNameX, BX_FILE_PERMISSIONS); //flash is not an image $flashEnabled = !\CFile::IsImage($arFile["ORIGINAL_NAME"], $arFile["type"]); $imgArray = \CFile::GetImageSize($strDbFileNameX, false, $flashEnabled); if (is_array($imgArray)) { $arFile["WIDTH"] = $imgArray[0]; $arFile["HEIGHT"] = $imgArray[1]; if ($imgArray[2] == IMAGETYPE_JPEG) { $exifData = \CFile::ExtractImageExif($strPhysicalFileNameX); if ($exifData && isset($exifData["Orientation"])) { //swap width and height if ($exifData["Orientation"] >= 5 && $exifData["Orientation"] <= 8) { $arFile["WIDTH"] = $imgArray[1]; $arFile["HEIGHT"] = $imgArray[0]; } $properlyOriented = \CFile::ImageHandleOrientation($exifData["Orientation"], $io->GetPhysicalName($strDbFileNameX)); if ($properlyOriented) { $jpgQuality = intval(\COption::GetOptionString("main", "image_resize_quality", "95")); if ($jpgQuality <= 0 || $jpgQuality > 100) { $jpgQuality = 95; } imagejpeg($properlyOriented, $strPhysicalFileNameX, $jpgQuality); clearstatcache(true, $strPhysicalFileNameX); } $arFile["size"] = filesize($strPhysicalFileNameX); } } } else { $arFile["WIDTH"] = 0; $arFile["HEIGHT"] = 0; } return true; }
Т.е. сохраняем физически файл именно так, как это делает сам Битрикс (учитывая все настройки модуля main)
В целом можно и повторно вызывать CFile::SaveFile с новым путем, а затем удалить вручную запись из базы (но делать так по вполне понятным причинам не стоит)
try { $fileId = \CFile::SaveFile($arFile, $strSavePath, $bForceMD5, $bSkipExt, $dirAdd); if ($fileId > 0) { $fileFields = \CFile::GetByID($fileId)->Fetch(); $arFile = array_merge($arFile, [ "SUBDIR" => $fileFields["SUBDIR"], "FILE_NAME" => $fileFields["FILE_NAME"], "WIDTH" => $fileFields["WIDTH"], "HEIGHT" => $fileFields["HEIGHT"] ]); $deleteSize = $fileFields["FILE_SIZE"]; $fileFields["ID"] = intval($fileFields["ID"]); if ($fileFields["ID"] > 0) { $fileTableName = FileTable::getTableName(); $connection = Application::getConnection(FileTable::getConnectionName()); $connection->query("DELETE FROM ".$fileTableName." WHERE ID=".$fileFields["ID"]); \CFile::CleanCache($fileFields["ID"]); if ($deleteSize > 0 && \COption::GetOptionInt("main", "disk_space") > 0) { \CDiskQuota::updateDiskQuota("file", $deleteSize, "delete"); } } return true; } } catch (\Exception $e) { } return false;
Обновляем поля $arFile - После того, как файл сохранен, возвращаем true
Все новые файлы нужных нам инфоблоков с этого момента будут в отдельной папке.
← Назад в раздел