<?php if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

use Bitrix\Main\Loader;
use Bitrix\Main\Localization\Loc;
use intec\core\base\Security;
use intec\core\collections\Arrays;
use intec\core\helpers\ArrayHelper;
use intec\core\helpers\FileHelper;
use intec\core\helpers\Html;
use intec\core\helpers\RegExp;
use intec\core\helpers\StringHelper;
use intec\core\helpers\Type;
use intec\core\io\Path;
use intec\core\platform\iblock\ElementsQuery;
use intec\core\platform\main\Component;

if (!Loader::includeModule('iblock')) {
    ShowError(Loc::getMessage('REVIEWS_ADD_ERROR_MODULE_IBLOCK'));

    return;
}

if (!Loader::includeModule('intec.core')) {
    ShowError(Loc::getMessage('REVIEWS_ADD_ERROR_MODULE_INTEC_CORE'));

    return;
}

/**
 * Компонент добавления отзыва
 * @author Intec
 * @version 1.0.0
 */
class IntecReviewsAddComponent extends CBitrixComponent
{
    /**
     * Название поля отзыва по умолчанию
     */
    const BASE_FIELD_NAME = 'DETAIL_TEXT';

    /**
     * Тип поля: галочка (checkbox)
     */
    const FIELD_TYPE_CHECKBOX = 'CHECKBOX';

    /**
     * Тип поля: выбор файла (file)
     */
    const FIELD_TYPE_FILE = 'FILE';

    /**
     * Тип поля: скрытое (hidden)
     */
    const FIELD_TYPE_HIDDEN = 'HIDDEN';

    /**
     * Тип поля: радиокнопка (radio)
     */
    const FIELD_TYPE_RADIO = 'RADIO';

    /**
     * Тип поля: выпадающий список (select)
     */
    const FIELD_TYPE_SELECT = 'SELECT';

    /**
     * Тип поля: текс (text)
     */
    const FIELD_TYPE_TEXT = 'TEXT';

    /**
     * Тип поля: многострочный текст (textarea)
     */
    const FIELD_TYPE_TEXTAREA = 'TEXTAREA';

    /**
     * Ошибка: отказано в доступе
     */
    const ERROR_ACCESS = 'ACCESS';

    /**
     * Ошибка: не удалось добавить отзыв
     */
    const ERROR_ADD = 'ADD';

    /**
     * Ошибка: проверочное поле не заполнено
     */
    const ERROR_CAPTCHA_EMPTY = 'CAPTCHA_EMPTY';

    /**
     * Ошбика: неправильно заполнено проверочное поле
     */
    const ERROR_CAPTCHA_WRONG = 'CAPTCHA_WRONG';

    /**
     * Ошбика: значение поля не соответствует списку возможных значений
     */
    const ERROR_FIELD_RANGE = 'FIELD_RANGE';

    /**
     * Ошибка: не заполнено обязательное поле
     */
    const ERROR_FIELD_REQUIRED = 'FIELD_REQUIRED';

    /**
     * Ошибка: не удалось записать файл на диск
     */
    const ERROR_FILE_CANT_WRITE = 'FILE_CANT_WRITE';

    /**
     * Ошибка: ошибка выполнения сценария
     */
    const ERROR_FILE_PHP_EXTENSION = 'FILE_PHP_EXTENSION';

    /**
     * Ошибка: файл превышает максимальный размер, указанный в форме
     */
    const ERROR_FILE_FORM_SIZE = 'FILE_FORM_SIZE';

    /**
     * Ошибка: отсутствует временная папка для загрузки файлов
     */
    const ERROR_FILE_TMP_DIR = 'FILE_TMP_DIR';

    /**
     * Ошибка: размер принятого файла превысил максимально допустимый размер,
     * который задан директивой upload_max_filesize конфигурационного файла
     */
    const ERROR_FILE_INI_SIZE = 'FILE_INI_SIZE';

    /**
     * Ошибка: загружаемый файл был получен только частично
     */
    const ERROR_FILE_PARTIAL = 'FILE_PARTIAL';

    /**
     * Ошибка: тип файла не соотвествует ни одному из разрешенных типов
     */
    const ERROR_FILE_TYPE = 'FILE_TYPE';

    /**
     * Ошибка: неизвестная ошибка
     */
    const ERROR_FILE_UNKNOWN = 'FILE_UNKNOWN';

    /**
     * Статус: отправленные данные корректны
     */
    const STATUS_ACCEPTED = 'ACCEPTED';

    /**
     * Статус: ограничение на количество отзывов,
     * пользователь уже оставил отзыв
     */
    const STATUS_CLOSED = 'CLOSED';

    /**
     * Статус: компонент готов для получения данных
     */
    const STATUS_OPEN = 'OPEN';

    /**
     * Статус: ошибка при валидации введенных данных
     */
    const STATUS_REJECTED = 'REJECTED';

    /**
     * Предупреждение: при добавлении отзыва один или несколько файлов не были добавлены
     */
    const WARNING_FILE_ADD = 'FILE_ADD';

    /**
     * Флаг доступа к работе с компонентом
     * @var bool
     */
    protected $_access;

    /**
     * Флаг активности отзывов после добавления
     * @var bool
     */
    protected $_addActive;

    /**
     * Идентификатор компонента
     * @var string
     */
    protected $_componentId;

    /**
     * Список полученных файлов
     * @var Arrays
     */
    protected $_files;

    /**
     * Флаг доступности добавления отзывов для текущего пользователя
     * при открытом доступе к работе компонента
     * @var bool
     */
    protected $_isAddAllowed;

    /**
     * Флаг авторизованности пользователя в системе
     * @var bool
     */
    protected $_isAuthorized;

    /**
     * Флаг режима добавления элементов с привязкой к другому элементу
     * @var bool
     */
    protected $_modeLink;

    /**
     * Название ключа для хранения добавленных отзывов неавторизованного пользователя,
     * используется для контроля количества отправляемых им отзывов
     * @var string
     */
    protected $_sessionStorage;

    /**
     * Ключ доступа
     * @var string
     */
    protected $_token;

    /**
     * Имя текущего пользователя
     * @var string
     */
    protected $_user;

    /**
     * @inheritdoc
     */
    public function onPrepareComponentParams($arParams)
    {
        if (!Type::isArray($arParams))
            $arParams = [];

        $arParams = array_merge([
            'IBLOCK_TYPE' => null,
            'IBLOCK_ID' => null,
            'MAIN_MODE' => 'DEFAULT',
            'MAIN_ADD_MODE' => 'DISABLED',
            'MAIN_ADD_SECTION' => null,
            'MAIN_ACCESS' => 'REGISTERED',
            'MAIN_LIMIT' => 'N',
            'MAIN_SESSION_STORAGE' => 'REVIEWS_STORAGE',
            'CAPTCHA_USE' => 'N',
            'MAIN_PROPERTIES' => [],
            'PROPERTY_LINK' => null,
            'LINK_IBLOCK_TYPE' => null,
            'LINK_IBLOCK_ID' => null,
            'LINK_ITEM_ID' => null,
            'PREVIEW_USE' => 'N',
            'PREVIEW_TRUNCATE_USE' => 'N',
            'PREVIEW_TRUNCATE_VALUE' => 0,
            'CACHE_TYPE' => 'N',
            'CACHE_TIME' => 0
        ], $arParams);

        if (!Type::isArray($arParams['MAIN_PROPERTIES']))
            $arParams['MAIN_PROPERTIES'] = [];
        else
            $arParams['MAIN_PROPERTIES'] = array_filter($arParams['MAIN_PROPERTIES']);

        if (!empty($arParams['MAIN_SESSION_STORAGE']))
            $arParams['MAIN_SESSION_STORAGE'] = RegExp::matchBy(
                '/\w+/',
                $this->arParams['MAIN_SESSION_STORAGE']
            );

        return $arParams;
    }

    /**
     * @inheritdoc
     */
    public function executeComponent()
    {
        // Инициализация свойств компонента
        $this->initialize();
        $this->arResult = [
            'ACCESS' => $this->_access,
            'STATUS' => $this->_isAddAllowed ? self::STATUS_OPEN : self::STATUS_CLOSED,
            'ERRORS' => [],
            'WARNINGS' => [],
            'FIELDS' => [
                // Скрытое поле доступа
                $this->_componentId => [
                    'TYPE' => self::FIELD_TYPE_HIDDEN,
                    'NAME' => $this->_componentId,
                    'VALUE' => $this->_token
                ]
            ],
            'CAPTCHA' => [
                'USE' => $this->arParams['CAPTCHA_USE'] === 'Y',
                // Получение полей капчи
                'FIELDS' => $this->getCaptchaFields()
            ]
        ];

        // Создание структуры полей по заданным параметрам
        if ($this->startResultCache(false, $this->generateCacheId())) {
            if (empty($this->arParams['MAIN_PROPERTIES'])) {
                // Ручное добавление поля textarea для свойства по умолчанию,
                // если список используемых пользовательских свойств пуст
                $this->addFieldTextArea(
                    self::BASE_FIELD_NAME,
                    Loc::getMessage('REVIEWS_ADD_FIELD_DETAIL_TEXT_TITLE'),
                    null,
                    'Y'
                );
            } else {
                // Выборка пользовательских свойств инфоблока
                $properties = Arrays::fromDBResult(CIBlockProperty::GetList([], [
                    'IBLOCK_ID' => $this->arParams['IBLOCK_ID'],
                    'ACTIVE' => 'Y',
                    'CHECK_PERMISSIONS' => 'Y',
                    'MIN_PERMISSION' => 'R'
                ]))->indexBy('CODE');

                // Обход указанных свойств инфблока для создания полей
                foreach ($this->arParams['MAIN_PROPERTIES'] as $property) {
                    // Ручное добавление поля textarea для свойства по умолчанию
                    // т.к. оно не является пользовательским свойством
                    if ($property === self::BASE_FIELD_NAME) {
                        $this->addFieldTextArea(
                            self::BASE_FIELD_NAME,
                            Loc::getMessage('REVIEWS_ADD_FIELD_DETAIL_TEXT_TITLE'),
                            null,
                            'Y'
                        );

                        continue;
                    }

                    // Пропуск, если указанное свойство не найдено
                    // в выборке пользовательских свойств
                    if (!$properties->exists($property))
                        continue;

                    // Получение данных пользовательского свойства
                    $property = $properties->get($property);

                    // Добавление полей на основе данных
                    // пользовательских свойств
                    if (
                        $property['PROPERTY_TYPE'] === 'S' &&
                        $property['LIST_TYPE'] === 'L' &&
                        $property['MULTIPLE'] === 'N'
                    ) {
                        // Добавление текстового поля
                        if ($property['USER_TYPE'] === 'HTML') {
                            // Добавление поля textarea
                            $this->addFieldTextArea(
                                $property['CODE'],
                                $property['NAME'],
                                $property['DEFAULT_VALUE']['TEXT'],
                                $property['IS_REQUIRED']
                            );
                        } else {
                            // Добавление поля text
                            $this->addFieldText(
                                $property['CODE'],
                                $property['NAME'],
                                $property['DEFAULT_VALUE'],
                                $property['IS_REQUIRED']
                            );
                        }
                    } else if (
                        $property['PROPERTY_TYPE'] === 'L' &&
                        $property['LIST_TYPE'] === 'L' &&
                        $property['MULTIPLE'] === 'N'
                    ) {
                        // Добавление поля select
                        $this->addFieldSelect(
                            $property['ID'],
                            $property['CODE'],
                            $property['NAME'],
                            $property['DEFAULT_VALUE'],
                            $property['IS_REQUIRED']
                        );
                    } else if (
                        $property['PROPERTY_TYPE'] === 'L' &&
                        $property['LIST_TYPE'] === 'C' &&
                        $property['MULTIPLE'] === 'N'
                    ) {
                        // Добавление поля checkbox или radio
                        // в зависимости от настроек свойства
                        $this->addFieldCheckbox(
                            $property['ID'],
                            $property['CODE'],
                            $property['NAME'],
                            $property['DEFAULT_VALUE'],
                            $property['IS_REQUIRED']
                        );
                    } else if (
                        $property['PROPERTY_TYPE'] === 'F' &&
                        $property['LIST_TYPE'] === 'L'
                    ) {
                        // Добавление поля file
                        $this->addFieldFile(
                            $property['CODE'],
                            $property['NAME'],
                            $property['MULTIPLE'],
                            $property['FILE_TYPE'],
                            $property['IS_REQUIRED']
                        );
                    } else {
                        continue;
                    }
                }

                unset($properties, $property);
            }
            
            // Завершение кеширования структуры полей
            $this->endResultCache();
        }

        // Проверка наличия и принадлежности отправленных данных
        // текущему экземпляру компонента
        if (
            $this->_access &&
            $this->_isAddAllowed &&
            !empty($_POST) &&
            isset($_POST[$this->_componentId])
        ) {
            //Запись отправленных данных в данные компонента
            $this->setPostValues();

            if ($_POST[$this->_componentId] !== $this->_token) {
                // Ошибка несоотвествия значения ключа доступа
                $this->setError(null, self::ERROR_ACCESS);
            } else {
                if ($this->checkCaptcha()) {
                    // Валидация полученных данных на соотвествие
                    // полям формы и их возможных значениям
                    // (поля, не входящие в список игнорируются и не участвуют в работе компонента)
                    foreach ($this->arResult['FIELDS'] as $field) {
                        // Пропуск системного поля доступа
                        if ($field['NAME'] === $this->_componentId)
                            continue;

                        if (
                            $field['TYPE'] === self::FIELD_TYPE_TEXT ||
                            $field['TYPE'] === self::FIELD_TYPE_TEXTAREA
                        ) {
                            // Валидация полей типа text и textarea
                            if ($field['REQUIRED'] && empty($_POST[$field['NAME']]))
                                $this->setError($field['NAME'], self::ERROR_FIELD_REQUIRED);
                        } else if (
                            $field['TYPE'] === self::FIELD_TYPE_CHECKBOX ||
                            $field['TYPE'] === self::FIELD_TYPE_RADIO ||
                            $field['TYPE'] === self::FIELD_TYPE_SELECT
                        ) {
                            // Валидация полей типа checkbox, radio и select
                            if ($field['REQUIRED'] && empty($_POST[$field['NAME']])) {
                                // Ошибка незаполенности обязательного поля
                                $this->setError($field['NAME'], self::ERROR_FIELD_REQUIRED);
                            } else if (!empty($field['OPTIONS'])) {
                                // Проверка на соотвествие отправленного
                                // значения списку допустимых значений
                                if (
                                    $field['TYPE'] === self::FIELD_TYPE_RADIO ||
                                    $field['TYPE'] === self::FIELD_TYPE_SELECT
                                ) {
                                    if (!ArrayHelper::keyExists($_POST[$field['NAME']], $field['OPTIONS'])) {
                                        $this->setError($field['NAME'], self::ERROR_FIELD_RANGE);
                                    }
                                }
                            }
                        } else if ($field['TYPE'] === self::FIELD_TYPE_FILE) {
                            // Проверка наличия отправленных файлов
                            // для проверяемого свойства
                            if (empty($_FILES) || !isset($_FILES[$field['NAME']])) {
                                // Ошбика незаполенности обязательного поля
                                // или при его отсутсвии в списке отправленных полей
                                if ($field['REQUIRED'])
                                    $this->setError($field['NAME'], self::ERROR_FIELD_REQUIRED);

                                continue;
                            }

                            // Получение файлов свойства
                            $files = $this->extractFiles($field['NAME']);

                            //Валидация полученных файлов
                            foreach ($files as $key => $file) {
                                // Проверка правильной загрузки файлов,
                                // заполненности обязательных полей с файлами
                                if (!$this->validateFileUpload($field['NAME'], $file)) {
                                    unset($files[$key]);
                                } else {
                                    // Проверка мета данных и расширений отправленных файлов,
                                    // если валидация загрузки была успешной
                                    if (!empty($field['EXTENSIONS']) || !empty($field['ACCEPT'])) {
                                        if (!$this->validateFileMeta($field['NAME'], $file)) {
                                            unset($files[$key]);
                                        }
                                    }
                                }
                            }

                            // Перенос данных о файлах свойства
                            // в свойство-хранилище объекта
                            if (!empty($files))
                                $this->_files->set($field['NAME'], $files);

                            unset($files);
                        }
                    }

                    unset($field);
                }
            }

            // Установка статуса отправки формы
            if (empty($this->arResult['ERRORS']))
                $this->setStatus(self::STATUS_ACCEPTED);
            else
                $this->setStatus(self::STATUS_REJECTED);

            // Добавление нового элемента в инфоблок при отсуствии ошибок
            if ($this->arResult['STATUS'] === self::STATUS_ACCEPTED) {
                if (!$this->addItem()) {
                    // Ошибка добавления элемента в инфоблок
                    $this->setError(null, self::ERROR_ADD);
                    $this->setStatus(self::STATUS_REJECTED);
                }
            }
        }

        $this->includeComponentTemplate();

        return null;
    }

    /**
     * Инициализация свойств компонента
     */
    protected function initialize()
    {
        /**
         * @global CAllUser $USER
         */
        global $USER;

        $this->_isAuthorized = $USER->IsAuthorized();
        $this->_access = ArrayHelper::fromRange([
            'REGISTERED',
            'EVERYONE'
        ], $this->arParams['MAIN_ACCESS']) === 'REGISTERED' ? $this->_isAuthorized : $this->_access = true;
        $this->_addActive = ArrayHelper::fromRange([
                'DISABLED',
                'ACTIVE'
        ], $this->arParams['MAIN_ADD_MODE']) === 'ACTIVE';
        $this->_modeLink = ArrayHelper::fromRange([
                'DEFAULT',
                'LINK'
        ], $this->arParams['MAIN_MODE']) === 'LINK' && !empty($this->arParams['PROPERTY_LINK']);
        $this->_sessionStorage = $this->arParams['MAIN_SESSION_STORAGE'];
        $this->_componentId = md5(session_id());//md5(Html::getUniqueId(Component::getUniqueId($this), null));
        $this->_token = md5(self::class . session_id());
        $this->_user = $this->_isAuthorized ? $USER->GetID() : (new Security())->generateRandomString(6);
        $this->_isAddAllowed = $this->isAddAllowed();
        $this->_files = Arrays::from([]);
    }

    /**
     * Добавляет поле типа флажок (checkbox)
     * @param int|string $id Идентификатор свойства
     * @param string $name Имя поля
     * @param string $title Название поля
     * @param int|string $default Значение по умолчанию
     * @param string $required Обязательность заполнения
     */
    protected function addFieldCheckbox($id, $name, $title, $default, $required)
    {
        // Получение списка значений свойства
        $options = Arrays::fromDBResult(CIBlockProperty::GetPropertyEnum($id))
            ->indexBy('ID')
            ->asArray(function ($key, $value) {
                return [
                    'key' => $key,
                    'value' => $value['VALUE']
                ];
            });

        // Установка значения по умолчанию, если оно не задано в свойстве
        if (empty($default))
            $default = ArrayHelper::getFirstKey($options);

        if (count($options) > 1) {
            // Добавление свойства как список радиокнопок,
            // если у свойства больше одного значений
            $this->arResult['FIELDS'][$name] = [
                'TYPE' => self::FIELD_TYPE_RADIO,
                'REQUIRED' => $required === 'Y',
                'NAME' => $name,
                'OPTIONS' => $options,
                'VALUE' => $default,
                'TITLE' => $title,
                'ERROR' => null
            ];
        } else {
            // Добавление свойства
            $this->arResult['FIELDS'][$name] = [
                'TYPE' => self::FIELD_TYPE_CHECKBOX,
                'NAME' => $name,
                'REQUIRED' => $required === 'Y',
                'OPTIONS' => $options,
                'VALUE' => $default,
                'CHECKED' => !empty($_POST[$name]),
                'TITLE' => $title,
                'ERROR' => null
            ];
        }
    }

    /**
     * Добавляет поле типа файл (file)
     * @param string $name Имя поля
     * @param string $title Название поля
     * @param string $multiple Обычное/множественное
     * @param string|array $accept Список разрешенных расширений файлов
     * @param string $required Обязательность заполнения
     */
    protected function addFieldFile($name, $title, $multiple, $accept, $required)
    {
        // Преобразование разрешенных для свойства расширений файлов
        // из строки в массив для дальнейшей работы
        $extensions = $this->getExtensions($accept);

        // Добавление свойства
        $this->arResult['FIELDS'][$name] = [
            'TYPE' => self::FIELD_TYPE_FILE,
            'REQUIRED' => $required === 'Y',
            'NAME' => $name,
            'EXTENSIONS' => $extensions,
            'MULTIPLE' => $multiple === 'Y',
            // Получение MIME-типов из массива
            // разрешенных для свойства расширений файлов
            'ACCEPT' => $this->getMimeByExtensions($extensions),
            'TITLE' => $title,
            'ERROR' => null
        ];
    }

    /**
     * Добавляет поле типа список (select)
     * @param int|string $id Идентификатор свойства
     * @param string $name Имя поля
     * @param string $title Название поля
     * @param int|string $default Значение по умолчанию
     * @param string $required Обязательность заполнения
     */
    protected function addFieldSelect($id, $name, $title, $default, $required)
    {
        // Получение списка значений свойства
        $options = Arrays::fromDBResult(CIBlockProperty::GetPropertyEnum($id))
            ->indexBy('ID')
            ->asArray(function ($key, $value) {
                return [
                    'key' => $key,
                    'value' => $value['VALUE']
                ];
            });

        // Установка значения по умолчанию, если оно не задано в свойстве
        if (empty($default))
            $default = ArrayHelper::getFirstKey($options);

        // Добавление свойства
        $this->arResult['FIELDS'][$name] = [
            'TYPE' => self::FIELD_TYPE_SELECT,
            'REQUIRED' => $required === 'Y',
            'NAME' => $name,
            'OPTIONS' => $options,
            'VALUE' => $default,
            'TITLE' => $title,
            'ERROR' => null
        ];
    }

    /**
     * Добавляет поле типа текст (text)
     * @param string $name Имя поля
     * @param string $title Название поля
     * @param int|float|string $default Значение по умолчанию
     * @param string $required Обязательность заполнения
     */
    protected function addFieldText($name, $title, $default, $required)
    {
        // Добавление свойства
        $this->arResult['FIELDS'][$name] = [
            'TYPE' => self::FIELD_TYPE_TEXT,
            'REQUIRED' => $required === 'Y',
            'NAME' => $name,
            'VALUE' => $default,
            'TITLE' => $title,
            'ERROR' => null
        ];
    }

    /**
     * Добавляет поле типа многострочны текст (textarea)
     * @param string $name Имя поля
     * @param string $title Название поля
     * @param int|float|string $default Значение по умолчанию
     * @param string $required Обязательность заполнения
     */
    protected function addFieldTextArea($name, $title, $default, $required)
    {
        // Добавление свойства
        $this->arResult['FIELDS'][$name] = [
            'TYPE' => self::FIELD_TYPE_TEXTAREA,
            'REQUIRED' => $required === 'Y',
            'NAME' => $name,
            'VALUE' => $default,
            'TITLE' => $title,
            'ERROR' => null
        ];
    }

    /**
     * Добавляет загруженные файлы в систему
     * @return array Идентификаторы добавленных файлов
     */
    protected function addFiles()
    {
        $result = [];

        // Добавление файлов в соответствующие свойства
        $this->_files->each(function ($key, $files) use (&$result) {
            if (!empty($files)) {
                $result[$key] = [];

                foreach ($files as $file) {
                    // Добавление необходимых ключей для добавления файла в систему
                    $file['MODULE_ID'] = 'iblock';
                    $file['del'] = 'Y';

                    // Добавление файла в систему
                    $fileId = CFile::SaveFile($file, 'iblock');

                    // Если файл успешно добавлен,
                    // его идентификатор сохраняется в результирующий массив
                    // для дальнейшего привязки файла к элементу,
                    // иначе генерируется предупреждение,
                    // что один из файлов не удалось добавить
                    if ($fileId)
                        $result[$key][] = $fileId;
                    else
                        $this->setWarning(self::WARNING_FILE_ADD);
                }
            }
        });

        return $result;
    }

    /**
     * Добавляет новый элемент в инфоблок
     * на основе отправленных данных
     * @return bool Успешность добавления элемента
     */
    protected function addItem()
    {
        $object = new CIBlockElement();

        // Добавление нового элемента в инфоблок
        // на основе полученных данных
        $item = $object->Add($this->extractData());

        // Доп. обработка, если элемент был успешно добавлен
        if ($item) {
            // Обработка полученных файлов, если таковые имеются
            if (!$this->_files->isEmpty()) {
                // Добавление полученных файлов в систему
                $files = $this->addFiles();

                // Привязка добавленных файлов
                // к соотвествующим свойсвам нового элемента
                foreach ($files as $code => $file) {
                    $object->SetPropertyValues(
                        $item,
                        $this->arParams['IBLOCK_ID'],
                        $file,
                        $code
                    );
                }
            }

            // Если пользователь не авторизован,
            // идентификатор нового элемента регистрируется в сессии
            // для возможности контроля за количеством оставляемых отзывов
            if (!$this->_isAuthorized)
                $this->setAddedItem($item);
        }

        return Type::toBoolean($item);
    }

    /**
     * Генерирует массив для доп. идентификатора кеширования
     * @return array Массив для доп. идентификатора кеширования
     */
    protected function generateCacheId()
    {
        /**
         * @var CAllUser $USER
         */
        global $USER;

        return [
            $USER->GetGroups()
        ];
    }

    /**
     * Генерирует имя пользователя
     * на основе авторизации его в системе
     * @return string Имя пользователя
     */
    protected function generateItemName()
    {
        if ($this->_isAuthorized) {
            return Loc::getMessage('REVIEWS_ADD_NAME_BY_USER', [
                '#ID#' => $this->_user
            ]);
        } else {
            return Loc::getMessage('REVIEWS_ADD_NAME_BY_GUEST', [
                '#ID#' => $this->_user
            ]);
        }
    }

    /**
     * Генерирует поля CAPTCHA
     * @return array Поля CAPTCHA
     */
    protected function getCaptchaFields()
    {
        global $APPLICATION;

        return [
            'CAPTCHA_SID' => [
                'TYPE' => self::FIELD_TYPE_HIDDEN,
                'NAME' => 'CAPTCHA_SID',
                // Получение кода капчи
                'VALUE' => $APPLICATION->CaptchaGetCode()
            ],
            'CAPTCHA_WORD' => [
                'TYPE' => self::FIELD_TYPE_TEXT,
                'REQUIRED' => true,
                'NAME' => 'CAPTCHA_WORD',
                'VALUE' => null,
                'TITLE' => Loc::getMessage('REVIEWS_ADD_CAPTCHA_FIELD_WORD'),
                'ERROR' => null
            ]
        ];
    }

    /**
     * Обрабатывает список расширений файлов, приводя его к необходимому виду,
     * если передается строка, то расширения файлов должны быть разделены запятой
     * @param string|array $value Список расширений файлов
     * @return array Отформатированный список расширений файлов
     */
    protected function getExtensions($value)
    {
        if (empty($value))
            return [];

        $result = [];

        // Если переданное значение не является массивом,
        // то оно преобразуется в него
        if (!Type::isArray($value))
            $value = explode(',', $value);

        foreach ($value as $extension) {
            $extension = trim($extension);

            // Обрезка точек в начале расширения, если они есть
            if (StringHelper::startsWith($extension, '.'))
                $extension = StringHelper::cut($extension, 0, 1);

            // Добавление в результирующий массив только уникальных значений
            if (!ArrayHelper::isIn($extension, $result))
                $result[] = $extension;
        }

        return $result;
    }

    /**
     * Обрабатывает список разрешений файлов, преобразуя его в соответствующие MIME-типы,
     * если передается строка, то расширения файлов должны быть разделены запятой
     * @param string|array $extensions Список расширений файлов
     * @return array Список MIME-типов
     */
    protected function getMimeByExtensions($extensions)
    {
        if (empty($extensions))
            return [];

        $result = [];

        // Если переданное значение не является массивом,
        // то оно преобразуется в него
        if (!Type::isArray($extensions))
            $extensions = $this->getExtensions($extensions);

        foreach ($extensions as $extension) {
            $extension = trim($extension);

            // Добавление точки к расширению, если его нет,
            // для корректного получений MIME-типа
            if (!StringHelper::startsWith($extension, '.'))
                $extension = '.' . $extension;

            // Получение MIME-типа из таблицы по расширению
            $extension = FileHelper::getMimeTypeByExtension($extension);

            // Добавление в результирующий массив только уникальных значений
            if (!ArrayHelper::isIn($extension, $result))
                $result[] = $extension;
        }

        return $result;
    }

    /**
     * Проверяет корректность проверочного кода CAPTCHA
     * @return bool Успешность проверки
     */
    protected function checkCaptcha()
    {
        global $APPLICATION;

        $match = true;

        if ($this->arResult['CAPTCHA']['USE']) {
            // Проверка капчи, если было введено значение,
            // иначе генерируется ошибка о незаполненности проверочного поля
            if (!empty($_POST['CAPTCHA_WORD'])) {
                // Проверка введенного значения
                $match = $APPLICATION->CaptchaCheckCode(
                    $_POST['CAPTCHA_WORD'],
                    $_POST['CAPTCHA_SID']
                );

                // Ошибка ввода капчи при неудачной проверке
                if (!$match) {
                    $this->arResult['ERRORS'][self::ERROR_CAPTCHA_WRONG] = [
                        'CODE' => self::ERROR_CAPTCHA_WRONG,
                        'TEXT' => Loc::getMessage('REVIEWS_ADD_ERROR_GLOBAL_CAPTCHA_WRONG')
                    ];
                    $this->arResult['CAPTCHA']['FIELDS']['CAPTCHA_WORD']['ERROR'][self::ERROR_CAPTCHA_WRONG] = [
                        'CODE' => self::ERROR_CAPTCHA_WRONG,
                        'TEXT' => Loc::getMessage('REVIEWS_ADD_ERROR_CAPTCHA_WRONG')
                    ];
                }
            } else {
                $match = false;
                $this->arResult['ERRORS'][self::ERROR_CAPTCHA_EMPTY] = [
                    'CODE' => self::ERROR_CAPTCHA_EMPTY,
                    'TEXT' => Loc::getMessage('REVIEWS_ADD_ERROR_GLOBAL_CAPTCHA_EMPTY')
                ];
                $this->arResult['CAPTCHA']['FIELDS']['CAPTCHA_WORD']['ERROR'] = [
                    'CODE' => self::ERROR_CAPTCHA_EMPTY,
                    'TEXT' => Loc::getMessage('REVIEWS_ADD_ERROR_CAPTCHA_EMPTY')
                ];
            }
        }

        return $match;
    }

    /**
     * Определяет, может ли текущий пользователь отправить отзыв
     * @return bool Возможность отправки отзыва
     */
    protected function isAddAllowed()
    {
        // Если лимит отзывов не включен, обработка игнорируется
        if ($this->arParams['MAIN_LIMIT'] !== 'Y')
            return true;

        $allowed = true;

        if ($this->_isAuthorized) {
            // Настройка объекта выборки для поиска отзывов
            // текущего зарегистрированного пользователя
            $query = (new ElementsQuery())
                ->setUseTilda(false)
                ->setWithProperties(false)
                ->setIBlockType($this->arParams['IBLOCK_TYPE'])
                ->setIBlockId($this->arParams['IBLOCK_ID'])
                ->setSelect(['ID'])
                ->setFilter([
                    'NAME' => $this->generateItemName(),
                    'CHECK_PERMISSIONS' => 'Y',
                    'MIN_PERMISSION' => 'R'
                ]);

            // Если указан раздел добавления отзыва,
            // то проверяется наличие элемента в данном разделе
            if (!empty($this->arParams['MAIN_ADD_SECTION']))
                $query->setSectionsId($this->arParams['MAIN_ADD_SECTION']);

            // Если включен режим работы с привязкой к элементу
            // добавляется дополнительное поле для фильтрации с значением элемента,
            // к которому осуществляется привязка
            if ($this->_modeLink)
                $query->extendFilter([
                    'PROPERTY_' . $this->arParams['PROPERTY_LINK'] => $this->arParams['LINK_ITEM_ID']
                ]);

            // Если выборка пуста, то добавление отзывов разрешается
            $allowed = $query->execute()->isEmpty();

            unset($query);
        } else {
            // Если ключ для хранения оставленных отзывов не определен,
            // то обработка игнорируется
            if (isset($_SESSION[$this->_sessionStorage])) {
                $sent = $_SESSION[$this->_sessionStorage];

                // Если в ключе хранения оставленных отзывов нет ключа
                // с идентификатором инфоблока для которого настроен компонент,
                // то обработка игнорируется
                if (!empty($sent[$this->arParams['IBLOCK_ID']])) {
                    // Настройка объекта выборки для поиска отзывов
                    // текущего незарегистрированного пользователя
                    // по идентификаторам, указанным в сессии
                    $query = (new ElementsQuery())
                        ->setUseTilda(false)
                        ->setWithProperties(false)
                        ->setIBlockType($this->arParams['IBLOCK_TYPE'])
                        ->setIBlockId($this->arParams['IBLOCK_ID'])
                        ->setFilter([
                            'ID' => $sent[$this->arParams['IBLOCK_ID']],
                            'CHECK_PERMISSIONS' => 'Y',
                            'MIN_PERMISSION' => 'R'
                        ]);

                    // Если указан раздел добавления отзыва,
                    // то проверяется наличие элемента в данном разделе
                    if (!empty($this->arParams['MAIN_ADD_SECTION']))
                        $query->setSectionsId($this->arParams['MAIN_ADD_SECTION']);

                    // Если включен режим работы с привязкой к элементу
                    // добавляется дополнительное поле для фильтрации с значением элемента,
                    // к которому осуществляется привязка
                    if ($this->_modeLink)
                        $query->extendFilter([
                            'PROPERTY_' . $this->arParams['PROPERTY_LINK'] => $this->arParams['LINK_ITEM_ID']
                        ]);

                    // Если выборка пуста, то добавление отзывов разрешается
                    $allowed = $query->execute()->isEmpty();

                    unset($query);
                }

                unset($sent);
            }
        }

        return $allowed;
    }

    /**
     * Извлекает отправленные данные и формирует из них
     * массив для создания нового элемента в инфоблоке
     * @return array Данные для нового элемента в инфоблоке
     */
    protected function extractData()
    {
        $data = [
            'IBLOCK_ID' => $this->arParams['IBLOCK_ID'],
            'ACTIVE' => $this->_addActive ? 'Y' : 'N',
            // Получение имени текущего пользователя
            'NAME' => $this->generateItemName(),
            // Генерация символьного кода на основе имени пользователя транслитом
            'CODE' => CUtil::translit($this->generateItemName(), 'ru') . '_' . substr((string)time(), -3) . substr(md5(mt_rand()), 0, 2),
            'IBLOCK_SECTION' => $this->arParams['MAIN_ADD_SECTION'],
            'DETAIL_TEXT' => null,
            'PROPERTY_VALUES' => []
        ];

        // Если включен режим привязки к элементу инфоблока,
        // добавляется своство с идентификатором этого элемента
        if ($this->_modeLink)
            $data['PROPERTY_VALUES'][$this->arParams['PROPERTY_LINK']] = $this->arParams['LINK_ITEM_ID'];

        $sanitizer = new \CBXSanitizer();
        $sanitizer->SetLevel(\CBXSanitizer::SECURE_LEVEL_HIGH);

        // Получение и преобразование отправленных данных
        foreach ($this->arResult['FIELDS'] as $field) {
            // Если свойство является системным или файловым, то оно пропускается
            if (
                $field['NAME'] === $this->_componentId ||
                $field['TYPE'] === self::FIELD_TYPE_FILE
            )
                continue;

            // Если свойство является базовым свойством отзыва,
            // то оно обрабатывается отдельно
            if ($field['NAME'] === self::BASE_FIELD_NAME) {
                $data['DETAIL_TEXT'] = $sanitizer->SanitizeHtml((string)$field['VALUE']);

                // Генерация текста для анонса на основе текста отзыва, если функция включена
                if ($this->arParams['PREVIEW_USE'] === 'Y') {
                    // Если включена обрезка текста по словам и значение количества слов больше нуля,
                    // иначе текст отзыва попадает в текст для анонса без изменений
                    if (
                        $this->arParams['PREVIEW_TRUNCATE_USE'] === 'Y' &&
                        $this->arParams['PREVIEW_TRUNCATE_VALUE'] > 0
                    ) {
                        $data['PREVIEW_TEXT'] = StringHelper::truncateWords(
                            $data['DETAIL_TEXT'],
                            $this->arParams['PREVIEW_TRUNCATE_VALUE']
                        );
                    } else {
                        $data['PREVIEW_TEXT'] = $data['DETAIL_TEXT'];
                    }
                }

                continue;
            }

            if ($field['TYPE'] === self::FIELD_TYPE_TEXTAREA) {
                // Заполнение данных для свойства типа HTML/Текст
                $data['PROPERTY_VALUES'][$field['NAME']] = [
                    'VALUE' => [
                        // Вставляет HTML-код разрыва строки перед каждым переводом строки
                        'TEXT' => nl2br($sanitizer->SanitizeHtml((string)$field['VALUE'])),
                        'TYPE' => 'html'
                    ]
                ];
            } else if ($field['TYPE'] === self::FIELD_TYPE_CHECKBOX) {
                // Заполнение данных для свойства типа  Флажок (checkbox)
                $data['PROPERTY_VALUES'][$field['NAME']] = [
                    'VALUE' => $field['CHECKED'] ? $field['VALUE'] : null
                ];
            } else {
                // Заполение остальных типов полей,
                // которые заполняются без каких либо особенностей
                $data['PROPERTY_VALUES'][$field['NAME']] = [
                    'VALUE' => $field['VALUE']
                ];
            }
        }

        return $data;
    }

    /**
     * Обрабатывает массив загруженных файлов по указанному имени поля
     * @param string $fieldName Имя поля
     * @return array Массив загруженных файлов
     */
    protected function extractFiles($fieldName)
    {
        $result = [];
        $files = $_FILES[$fieldName];

        // Если свойство множественное, то производится доп. обработка
        // из-за особенности структуры массива файлов при загрузке,
        // чтобы сформировать отдельные массивы для каждого файла
        if ($this->arResult['FIELDS'][$fieldName]['MULTIPLE']) {
            foreach ($files as $keyFile => $file) {
                foreach ($file as $keyField => $field) {
                    if (!isset($result[$keyField]))
                        $result[$keyField] = [];

                    $result[$keyField][$keyFile] = $field;
                }
            }
        } else {
            $result[] = $files;
        }

        return $result;
    }

    /**
     * Устанавливает в сессии идентификатор
     * добавленного отзыва неавторизованным пользователем
     * @param int|string $value Идентификатор отзыва
     */
    protected function setAddedItem($value)
    {
        // Проверка наличия ключа в сессии,
        // в котором хранятся оставленные отзывы
        // текущего незарегистрированного пользователя,
        // при остсутствии происходит его добавление
        if (!isset($_SESSION[$this->_sessionStorage]))
            $_SESSION[$this->_sessionStorage] = [];

        $storage = &$_SESSION[$this->_sessionStorage];

        // Проверка наличия ключа с идентификатором инфоблока,
        // к которому привязан компонент в ключе хранилища отзывов,
        // при отсутствии происходит его добавление
        if (!isset($storage[$this->arParams['IBLOCK_ID']]))
            $storage[$this->arParams['IBLOCK_ID']] = [];

        $storage[$this->arParams['IBLOCK_ID']][] = $value;

        unset($storage);
    }

    /**
     * Регистрирует ошибку
     * @param string $fieldName Имя поля
     * @param string $code Код ошибки
     */
    protected function setError($fieldName, $code)
    {
        // Если указано имя поля, код ошибки добавляется в него
        if (!empty($fieldName))
            $this->arResult['FIELDS'][$fieldName]['ERROR'] = [
                'CODE' => $code,
                'TEXT' => Loc::getMessage('REVIEWS_ADD_ERROR_' . $code)
            ];

        // Код ошибки добавляется только в случае его отсутствия в массиве
        if (!ArrayHelper::keyExists($code, $this->arResult['ERRORS']))
            $this->arResult['ERRORS'][$code] = [
                'CODE' => $code,
                'TEXT' => Loc::getMessage('REVIEWS_ADD_ERROR_GLOBAL_' . $code)
            ];
    }

    /**
     * Записывает в поля полученные от пользователя значения
     */
    protected function setPostValues()
    {
        foreach ($this->arResult['FIELDS'] as &$field) {
            // Пропуск системного поля
            if ($field['NAME'] === $this->_componentId)
                continue;

            // Если в отправленных данных есть ключ с названием поля,
            // его значение переносится в данные копонента
            if (isset($_POST[$field['NAME']]))
                $field['VALUE'] = $_POST[$field['NAME']];
        }
    }

    /**
     * Устанавливает статус работы компонента
     * @param string $value Код статуса
     */
    protected function setStatus($value)
    {
        $this->arResult['STATUS'] = $value;
    }

    /**
     * Регистрирует предупреждение
     * @param string $code Код предупреждения
     */
    protected function setWarning($code)
    {
        // Код предупреждения добавляется только в случае его отсутствия в массиве
        if (!ArrayHelper::keyExists($code, $this->arResult['WARNINGS']))
            $this->arResult['WARNINGS'][$code] = [
                'CODE' => $code,
                'TEXT' => Loc::getMessage('REVIEWS_ADD_WARNING_GLOBAL_' . $code)
            ];
    }

    /**
     * Валидирует загрузку файла
     * @param string $fieldName Имя поля, по которому будет произведена валидация
     * @param array $file Файл
     * @return bool Успешность проверки
     */
    protected function validateFileUpload($fieldName, $file)
    {
        // Если статус загрузки "Успешно" (0),
        // то обработка не выполняется
        if ($file['error'] == UPLOAD_ERR_OK)
            return true;

        $field = $this->arResult['FIELDS'][$fieldName];

        switch ($file['error']) {
            case UPLOAD_ERR_NO_FILE:
                // Данный код ошибки "Файл не был выбран" (4) учитывается только в случае обязательности поля
                if ($field['REQUIRED'])
                    $this->setError($field['NAME'], self::ERROR_FIELD_REQUIRED);

                break;
            case UPLOAD_ERR_INI_SIZE:
                $this->setError($field['NAME'], self::ERROR_FILE_PHP_EXTENSION);
                break;
            case UPLOAD_ERR_FORM_SIZE:
                $this->setError($field['NAME'], self::ERROR_FILE_FORM_SIZE);
                break;
            case UPLOAD_ERR_PARTIAL:
                $this->setError($field['NAME'], self::ERROR_FILE_PARTIAL);
                break;
            case UPLOAD_ERR_NO_TMP_DIR:
                $this->setError($field['NAME'], self::ERROR_FILE_TMP_DIR);
                break;
            case UPLOAD_ERR_CANT_WRITE:
                $this->setError($field['NAME'], self::ERROR_FILE_CANT_WRITE);
                break;
            case UPLOAD_ERR_EXTENSION:
                $this->setError($field['NAME'], self::ERROR_FILE_CANT_WRITE);
                break;
            default:
                $this->setError($field['NAME'], self::ERROR_FILE_UNKNOWN);
        }

        return false;
    }

    /**
     * Ваалидирует метаданные и раширение файла
     * @param string $fieldName Имя поля, по которому будет произведена валидация
     * @param array $file Файл
     * @return bool Успешность проверки
     */
    protected function validateFileMeta($fieldName, $file)
    {
        $field = $this->arResult['FIELDS'][$fieldName];

        // Получение расширения загруженного файла
        $extension = Path::getExtensionFrom($file['name']);

        // Если MIME-тип или расширение загруженного файла
        // не соответствуют указанным в свойстве, генерируется ошибка
        if (
            !empty($field['ACCEPT']) && !ArrayHelper::isIn($file['type'], $field['ACCEPT']) ||
            !empty($field['EXTENSIONS']) && !ArrayHelper::isIn($extension, $field['EXTENSIONS'])
        ) {
            $this->setError($field['NAME'], self::ERROR_FILE_TYPE);

            return false;
        }

        return true;
    }

    /**
     * Возвращает карту возможных кодов полей
     * @return array Карта кодов полей
     */
    public function getFieldsMap()
    {
        return [
            self::FIELD_TYPE_HIDDEN,
            self::FIELD_TYPE_TEXT,
            self::FIELD_TYPE_TEXTAREA,
            self::FIELD_TYPE_CHECKBOX,
            self::FIELD_TYPE_RADIO,
            self::FIELD_TYPE_SELECT,
            self::FIELD_TYPE_FILE
        ];
    }

    /**
     * Возвращает карту возможных кодов ошибок
     * @return array Карта кодов ошибок
     */
    public function getErrorMap()
    {
        return [
            self::ERROR_ACCESS,
            self::ERROR_ADD,
            self::ERROR_CAPTCHA_EMPTY,
            self::ERROR_CAPTCHA_WRONG,
            self::ERROR_FIELD_REQUIRED,
            self::ERROR_FIELD_RANGE,
            self::ERROR_FILE_INI_SIZE,
            self::ERROR_FILE_FORM_SIZE,
            self::ERROR_FILE_PARTIAL,
            self::ERROR_FILE_TMP_DIR,
            self::ERROR_FILE_CANT_WRITE,
            self::ERROR_FILE_PHP_EXTENSION,
            self::ERROR_FILE_TYPE,
            self::ERROR_FILE_UNKNOWN
        ];
    }

    /**
     * Возвращает карту возможных кодов статусов работы компонента
     * @return array Карта кодов статусов работы компонента
     */
    public function getStatusMap()
    {
        return [
            self::STATUS_OPEN,
            self::STATUS_CLOSED,
            self::STATUS_ACCEPTED,
            self::STATUS_REJECTED
        ];
    }

    /**
     * Возвращает карту возможных кодов предупреждений
     * @return array Карта кодов предупреждений
     */
    public function getWarningMap()
    {
        return [
            self::WARNING_FILE_ADD
        ];
    }

    /**
     * Распечатывает переданные данные на странице в удобном формате
     * @param mixed $data Данные для печати
     * @param bool $dump Печатать с указанием типов данных
     */
    public function print($data, $dump = false)
    {
        echo '<pre>';
        $dump ? var_dump($data) : print_r($data);
        echo '</pre>';
    }
}