Отдельная директория для файлов инфоблока

По умолчанию для всех файлов инфоблоков используется общая папка /upload/iblock/. Файлы сохраняются в подпапки, сгенерированные случайным образом, например, /upload/iblock/ff7/.
Как следствие, все файлы доступны по прямой ссылке. В целом, в этом нет ничего страшного, так как итоговый путь (при условии, что файл отдается через php) сложно подобрать. Но в некоторых случаях требуются дополнительные меры безопасности.

Основная идея – как-то вынести файлы инфоблока в отдельную подпапку и закрыть её с помощью стандартных механизмов nginx / apache.
Небольшим дополнительным плюсом будет то, что можно достаточно просто посмотреть, сколько места занимают файлы отдельного инфоблока.

Суть в следующем: в методе CFile::SaveFile второй параметр ($strSavePath) отвечает за директорию внутри папки upload. Инфоблоки его передают как «iblock», нам надо его заменить.

  1. Код, приведенный ниже не рекомендуется к использованию, но в целом он позволяет решить задачу (и, в том числе, работает, как в публичной, так и в административной части).
  2. Случай, когда файлы хранятся в облаке, не рассматривается.
  3. Скачать исходники. В случае включения в код "как-есть" необходимо прописать пути в методе loadPaths

Общая схема

Регистрируем обработчик на событие OnFileSave. Обработчик будет вызван при сохранении файла. В обработчике каким-то способом получаем ID инфоблока, файл которого сохраняется. Сохраняем файл физически и возвращаем true.

Подробнее

  1. Регистрируем обработчик на событие 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.
    Узнать ID инфоблока в обработчике можно несколькими путями:
    • Путь 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 инфоблока можно более точно, чем в предыдущих методах.
  2. В классе, который будет использоваться, содержится как сам метод обработчика 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;
    }
  3. В обработчике события (метод onFileSave) выполняем все действия только если $strSavePath == "iblock"
    if ($strSavePath != "iblock") {
         return false;
     }
    
  4. В обработчике вызываем 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;
            }
        }
    }
  5. После того, как мы узнали инфоблок (если его вообще удалось определить), смотрим, нужен ли ему отдельный путь, и есть да, то производим сохранение файла, возвращая из метода 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);
    }
  6. Сохраняем файл. Согласно назначению события 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
  7. После того, как файл сохранен, возвращаем true

Все новые файлы нужных нам инфоблоков с этого момента будут в отдельной папке.



Назад в раздел


С чего начинается продвижение сайта, старт работ по поисковому продвижению и раскрутке сайта в компании Пиксель Плюс
Начало работ по поисковому продвижению сайта в компании «Пиксель Плюс». Базовые понятия.
Необходимость ежемесячной оплаты работ по поисковому продвижению сайта. Основные работы по сайту для его эффективной раскрутки и себестоимость работ
Я бы хотел заплатить за продвижение своего сайта 1 раз и быть высоко в выдаче по конкурентным запросам всегда, возможно ли такое?
Продвижение по трафику: вопросы клиентов и ответы на них
Ряд вопросов по продвижению сайта по трафику. Нюансы тарификации, расчёта стоимости работ, абонентской оплаты.
Часто задаваемые вопросы по веб-аналитике (FAQ)
Вопросы, которые часто задаются заказчиками услуги по веб-аналитике и оказанию самой услуги. Что такое веб-аналитика? Зачем проекту нужна веб-аналитика? Зачем нужно определять KPI и какие они бывают? И так далее.
Какие работы НЕ входят в SEO в случае продвижения в «Пиксель Плюс»?
Поисковое продвижение включает в себя большой перечень работ, необходимый для получения максимальных результатов... Но какие же работы не входят в платеж на SEO?
Время продвижения и внесения изменений в результаты продвижения сайта, скорость реагирования Яндекса (Yandex) на внесение изменений на сайте
Я оплатил услуги продвижения сайта на месяц. Прошло уже 10 дней и позиции в Яндексе не улучшились, вы там работаете или нет?