<?php
/**
 * Базовый класс для работы с формами в CMS
 */
class cmsForm {

    /**
     * Форма с табами?
     *
     * @var boolean
     */
    public $is_tabbed = false;

    /**
     * Показывать предупреждение о несохранённых данных формы
     * при уходе со страницы формы
     *
     * @var boolean
     */
    public $show_unsave_notice = true;

    /**
     * Параметры формы, передающиеся в метод init
     *
     * @var array
     */
    private $params = [];

    /**
     * Структура формы
     *
     * @var array
     */
    private $structure = [];

    /**
     * Подготовленная структура формы
     *
     * @var ?array
     */
    private $prepared_structure = null;

    /**
     * Массив имён отключенных полей формы
     * Они не участвуют в валидации,
     * не участвуют в парсинге формы
     *
     * @var array
     */
    protected $disabled_fields = [];

    /**
     * Объект контроллера контекста вызова формы
     *
     * @var object
     */
    protected $controller;

    /**
     * Массив любых данных, необходимых для формы
     *
     * @var array
     */
    protected $data = [];

    /**
     * Имя формы
     * @var string
     */
    protected $name = '';

    public function __construct() {
        $this->name = strtolower(get_called_class());
    }

    /**
     * Возвращает имя формы
     * @return string
     */
    public function getName() {
        return $this->name;
    }

    /**
     * Устанавливает массив параметров формы, которые передются
     * в метод init формы аргументами
     *
     * @param array $params
     */
    public function setParams($params = []) {
        $this->params = $params;
    }

    /**
     * Возвращает массив параметров формы
     *
     * @return array
     */
    public function getParams(){
        return $this->params;
    }

    /**
     * Устанавливает вспомогательные данные
     *
     * @param string $key Ключ
     * @param mixed $value Значение
     * @return \cmsForm
     */
    public function setData($key, $value) {
        $this->data[$key] = $value; return $this;
    }

    /**
     * Возвращает данные по ключу
     *
     * @param string $key Ключ
     * @return mixed
     */
    public function getData($key) {
        return array_key_exists($key, $this->data) ? $this->data[$key] : null;
    }

    /**
     * Сохраняет ссылку на контроллер контекста
     *
     * @param object $controller_obj
     * @return \cmsForm
     */
    public function setContext($controller_obj){

        $this->controller = $controller_obj;

        $this->name .= '_'.$this->controller->name;

        return $this;
    }

    /**
     * Возвращает объект контроллера контекста
     *
     * @return object
     */
    public function getContext(){
        return $this->controller;
    }

//============================================================================//
//========================= Работа со структурой =============================//

    /**
     * Объединяет форму с переданной
     * заменяя наборы полей при совпадении ключей
     *
     * @param cmsForm $form
     * @return void
     */
    public function mergeForm(cmsForm $form) {

        $structure = $form->getStructure();

        foreach($structure as $fid => $fieldset){
            $this->addStructure($fid, $fieldset, true);
        }
    }

    /**
     * Устанавливает массив полей формы
     *
     * @param array $structure
     * @return \cmsForm
     */
    public function setStructure($structure = []) {
        $this->structure = $structure; return $this;
    }

    /**
     * Возвращает массив полей формы "как есть"
     *
     * @return array
     */
    public function getStructure(){
        return $this->structure;
    }

    protected function loadPreparedFormStructure() {

        if ($this->prepared_structure !== null) {
            return $this;
        }

        $default_lang   = cmsConfig::get('language');
        $is_change_lang = cmsConfig::get('is_user_change_lang');

        $langs = cmsCore::getLanguages();

        $structure = $this->structure;

        foreach ($structure as $fid => $fieldset) {

            if (!isset($fieldset['childs'])) {
                continue;
            }

            $childs = [];

            foreach ($fieldset['childs'] as $field) {

                $name = $field->getName();

                if (!empty($field->patterns_hint['patterns'])) {

                    $field->patterns_hint['pattern_fields'] = [];

                    $wrap_symbols = empty($field->patterns_hint['wrap_symbols']) ? ['{', '}'] : $field->patterns_hint['wrap_symbols'];

                    foreach ($field->patterns_hint['patterns'] as $pattern => $p_title) {
                        if (is_numeric($pattern)) {
                            $field->patterns_hint['pattern_fields'][] = '<a href="#">' . $wrap_symbols[0] . $p_title . $wrap_symbols[1] . '</a>';
                        } else {
                            $field->patterns_hint['pattern_fields'][] = '<a title="' . html($p_title, false) . '" href="#">' . $wrap_symbols[0] . $pattern . $wrap_symbols[1] . '</a>';
                        }
                    }
                }

                // проверяем является ли поле элементом массива
                $is_array = strpos($name, ':');

                $field->classes = [
                    'field' . ($field->css_class ? ' ' . $field->css_class : ''),
                    'ft_' . strtolower(substr(get_class($field), 5))
                ];

                if (array_search(['required'], $field->rules) !== false) {
                    $field->classes[] = 'reguired_field';
                }

                if (!empty($field->groups_edit)) {
                    if (!in_array(0, $field->groups_edit)) {
                        $field->classes[] = 'groups-limit';
                        foreach ($field->groups_edit as $group_id) {
                            $field->classes[] = 'group-' . $group_id;
                        }
                    }
                }

                $field->styles = [];

                if (!$field->is_visible) {
                    $field->styles[] = 'display:none';
                }

                if ($field->visible_depend) {
                    $field->classes[] = 'child_field';
                }

                $is_multilanguage = $field->multilanguage && $is_change_lang;

                if ($is_multilanguage) {
                    $field->classes[] = 'multilanguage';
                }

                if (!$is_multilanguage || $field->is_hidden || $field->getOption('is_hidden')) {

                    $childs[] = $field;

                    continue;
                }

                $_childs = [];
                $first   = true;

                foreach ($langs as $lang) {

                    $_field = clone $field;

                    $_field->lang = $lang;

                    if ($default_lang !== $lang) {

                        if (!empty($_field->multilanguage_params['unset_required'])) {

                            $required_key = array_search(['required'], $_field->getRules());
                            if ($required_key !== false) {
                                unset($_field->rules[$required_key]);
                            }
                            $class_key = array_search('reguired_field', $_field->classes);
                            if ($class_key !== false) {
                                unset($_field->classes[$class_key]);
                            }
                        }

                        if ($is_array) {

                            $name_parts = explode(':', $name);

                            $count = count($name_parts);

                            $name_parts[$count - ($count > 2 ? 2 : 1)] .= '_' . $lang;

                            $_field->setName(implode(':', $name_parts));

                        } else {
                            $_field->setName($name . '_' . $lang);
                        }

                    } else {

                        $_field->classes[] = 'multilanguage-base';
                    }

                    if (!$first) {
                        $_field->styles[] = 'display:none';
                    }

                    $_field->element_title = $_field->title . ' [' . strtoupper($lang) . ']';

                    $_field->rel = $lang;

                    $_field->field_tab_title = strtoupper($lang);

                    $_childs[$lang] = $_field;

                    $first = false;
                }

                $childs[] = $_childs;
            }

            $structure[$fid]['childs'] = $childs;
        }

        $this->prepared_structure = $structure;

        return $this;
    }

    /**
     * Возвращает подготовленный массив полей формы
     *
     * @param ?array $item Массив данных формы
     * @return array
     */
    public function getFormStructure($item = null) {

        $this->loadPreparedFormStructure();

        foreach ($this->prepared_structure as $fieldset) {

            if (!isset($fieldset['childs'])) {
                continue;
            }

            foreach ($fieldset['childs'] as $field) {

                if(!is_array($field)){ $_field = [$field]; } else { $_field = $field; }

                foreach ($_field as $field) {
                    if ($item) {
                        $field->setItem($item);
                    }
                }
            }
        }

        return $this->prepared_structure;
    }

    /**
     * Возвращает поле с указанным именем, или null если такого поля нет в форме
     *
     * @param string $name Имя поля
     * @param string $fieldset_id ID набора полей (не указано - поиск по всем наборам формы)
     * @return cmsFormField|null
     */
    public function getField($name, $fieldset_id = null) {
        if ($fieldset_id !== null) {
            if ($this->isFieldsetExists($fieldset_id)) {
                foreach ($this->structure[$fieldset_id]['childs'] as $field) {
                    if ($field->getName() == $name) { return $field; }
                }
            }
        } else {
            foreach ($this->structure as $fieldset_id => $fieldset) {
                foreach ($fieldset['childs'] as $field) {
                    if ($field->getName() == $name) { return $field; }
                }
            }
        }
        return null;
    }

    /**
     * Присутствует ли поле с указанным именем в форме
     *
     * @param string $name Имя поля
     * @param string $fieldset_id ID набора полей
     * @return boolean
     */
    public function hasField($name, $fieldset_id = null) {
        return $this->getField($name, $fieldset_id) !== null;
    }

    /**
     * Проверяет, существует ли набор
     *
     * @param string $id ID набора
     * @return boolean
     */
    public function isFieldsetExists($id) {
        return isset($this->structure[$id]);
    }

    /**
     * Возвращает ID последнего набора
     *
     * @return string
     */
    public function getLastFieldsetId(){

        $ids = array_keys($this->structure);

        return $ids[count($ids)-1];
    }

    /**
     * Добавляет набор в конец формы
     *
     * @param string $id ID добавляемого набора
     * @param array $structure
     * @param boolean $is_overwrite
     * @return string $id
     */
    private function addStructure($id, $structure, $is_overwrite = false) {

        if ($id === null) {
            $id = count($this->structure);
        }

        if (!$is_overwrite && $this->isFieldsetExists($id)) {
            return $id;
        }

        $this->structure[$id] = $structure;

        return $id;
    }

    /**
     * Добавляет набор в начало формы
     *
     * @param string $id ID добавляемого набора
     * @param array $structure
     * @return string $id
     */
    private function addStructureToBeginning($id, $structure) {

        if ($id === null) {
            $id = count($this->structure);
        }

        if ($this->isFieldsetExists($id)) {
            return $id;
        }

        $this->structure = [$id => $structure] + $this->structure;

        return $id;
    }

    /**
     * Добавляет набор после заданного, если нет его, добавляет в конец
     *
     * @param string $after_id ID набора, после которого вставить
     * @param string $id ID добавляемого набора
     * @param array $structure
     * @return string|null $id
     */
    private function addStructureAfter($after_id, $id, $structure) {

        if ($id === null) {
            $id = count($this->structure);
        }

        if ($this->isFieldsetExists($id)) {
            return $id;
        }

        $pos = array_search($after_id, array_keys($this->structure), true);

        if($pos !== false){

            $before = array_slice($this->structure, 0, $pos + 1, true);
            $after  = array_slice($this->structure, $pos + 1, null, true);

            $this->structure = $before + [$id => $structure] + $after;

            return $id;
        }

        return $this->addStructure($id, $structure);
    }

    /**
     * Убирает из набора все поля
     *
     * @param string $fieldset_id ID набора полей
     * @return cmsForm
     */
    public function clearFieldset($fieldset_id) {
        $this->structure[$fieldset_id]['childs'] = [];
        return $this;
    }

    /**
     * Удаляет набор полей из формы
     *
     * @param string $fieldset_id ID набора полей
     * @return cmsForm
     */
    public function removeFieldset($fieldset_id) {
        unset($this->structure[$fieldset_id]);
        return $this;
    }

    /**
     * Удаляет поле из формы
     *
     * @param string $fieldset_id ID набора полей, null - поиск по всем
     * @param string $field_name Название поля
     * @return cmsForm
     */
    public function removeField($fieldset_id, $field_name) {
        if ($fieldset_id !== null) {
            if ($this->isFieldsetExists($fieldset_id)) {
                foreach ($this->structure[$fieldset_id]['childs'] as $field_id => $field) {
                    if ($field->getName() == $field_name) {
                        unset($this->structure[$fieldset_id]['childs'][$field_id]);
                        break;
                    }
                }
            }
        } else {
            foreach ($this->structure as $fieldset_id => $fieldset) {
                foreach ($fieldset['childs'] as $field_id => $field) {
                    if ($field->getName() == $field_name) {
                        unset($this->structure[$fieldset_id]['childs'][$field_id]);
                        break;
                    }
                }
            }
        }
        return $this;
    }

    /**
     * Отключает поле в форме
     * Поле не удаляется, но перестает участвовать в парсинге и валидации
     *
     * @param string $field_name Название поля
     * @return cmsForm
     */
    public function disableField($field_name) {
        $this->disabled_fields[] = $field_name;
        return $this;
    }

//============================================================================//
//========================= HTML наборы ======================================//

    /**
     * Добавляет набор в виде сформированного HTML
     *
     * @param string $id ID набора
     * @param string $content HTML код
     * @return string ID набора
     */
    public function addHtmlBlock($id, $content = null) {

        return $this->addStructure($id, [
            'type'    => 'html',
            'content' => $content
        ]);
    }

    /**
     * Добавляет набор в виде сформированного HTML в начало формы
     *
     * @param string $id ID набора
     * @param string $content HTML код
     * @return string ID набора
     */
    public function addHtmlBlockToBeginning($id, $content = null) {

        return $this->addStructureToBeginning($id, [
            'type'    => 'html',
            'content' => $content
        ]);
    }

    /**
     * Добавляет набор в виде сформированного HTML после заданного
     *
     * @param string $after_id ID набора, после которого вставить
     * @param string $id ID набора
     * @param string $content HTML код
     * @return string ID набора
     */
    public function addHtmlBlockAfter($after_id, $id, $content = null) {

        return $this->addStructureAfter($after_id, $id, [
            'type'    => 'html',
            'content' => $content
        ]);
    }

//============================================================================//
//========================= Наборы полей =====================================//

    /**
     * Добавляет набор полей в форму
     *
     * @param string $title Заголовок набора полей
     * @param string $id ID набора полей
     * @param array $options Массив данных и опций набора
     * @return string id набора
     */
    public function addFieldset($title = '', $id = null, $options = []) {

        return $this->addStructure($id, array_merge([
            'type'   => 'fieldset',
            'title'  => $title,
            'childs' => []
        ], $options));
    }

    /**
     * Добавляет набор полей в начало формы
     *
     * @param string $title Заголовок набора полей
     * @param string $id ID набора полей
     * @param array $options Массив данных и опций набора
     * @return string id набора
     */
    public function addFieldsetToBeginning($title = '', $id = null, $options = []) {

        return $this->addStructureToBeginning($id, array_merge([
            'type'   => 'fieldset',
            'title'  => $title,
            'childs' => []
        ], $options));
    }

    /**
     *
     * Добавляет набор полей после заданного
     *
     * @param string $after_id ID набора, после которого вставить
     * @param string $title Заголовок набора полей
     * @param string $id ID набора
     * @param array $options Массив данных и опций набора
     * @return string ID набора
     */
    public function addFieldsetAfter($after_id, $title = '', $id = null, $options = []) {

        return $this->addStructureAfter($after_id, $id, array_merge([
            'type'   => 'fieldset',
            'title'  => $title,
            'childs' => []
        ], $options));
    }

//============================================================================//
//========================= Поля наборов =====================================//

    /**
     * Добавляет поле в конец набора полей
     *
     * @param string $fieldset_id ID набора полей
     * @param cmsFormField $field Объект поля
     * @return cmsForm
     */
    public function addField($fieldset_id, $field) {

        if (!$this->isFieldsetExists($fieldset_id)) {
            $this->addFieldset('', $fieldset_id);
        }

        $this->structure[$fieldset_id]['childs'][$field->name] = $field;

        return $this;
    }

    /**
     * Добавляет поле в начало набора полей
     *
     * @param string $fieldset_id ID набора полей
     * @param cmsFormField $field Объект поля
     * @return cmsForm
     */
    public function addFieldToBeginning($fieldset_id, $field) {

        if (!$this->isFieldsetExists($fieldset_id)) {
            $this->addFieldset('', $fieldset_id);
        }

        $this->structure[$fieldset_id]['childs'] = [$field->name => $field] + $this->structure[$fieldset_id]['childs'];

        return $this;
    }

    /**
     * Добавляет поле после заданного в $after_id
     *
     * @param string $after_id ID поля, после которого нужно добавить
     * @param string $fieldset_id ID набора полей
     * @param cmsFormField $field Объект поля
     * @return cmsForm
     */
    public function addFieldAfter($after_id, $fieldset_id, $field) {

        $pos = array_search($after_id, array_keys($this->structure[$fieldset_id]['childs']), true);

        if ($pos === false) {
            return $this;
        }

        $before = array_slice($this->structure[$fieldset_id]['childs'], 0, $pos + 1, true);
        $after  = array_slice($this->structure[$fieldset_id]['childs'], $pos + 1, null, true);

        $this->structure[$fieldset_id]['childs'] = $before + [$field->name => $field] + $after;

        return $this;
    }

//============================================================================//
//===================== Изменение опций и свойств ============================//

    /**
     * Изменяет атрибут набора полей в форме
     *
     * @param string $fieldset_id ID набора полей
     * @param string $attr_name Название атрибута
     * @param mixed $value Новое значение
     * @return cmsForm
     */
    public function setFieldsetAttribute($fieldset_id, $attr_name, $value) {

        if(isset($this->structure[$fieldset_id])){
            $this->structure[$fieldset_id][$attr_name] = $value;
        }

        return $this;
    }

    /**
     * Изменяет по ID набора и по имени опцию поля в форме
     *
     * @param string $fieldset_id ID набора полей
     * @param string $field_name Название поля
     * @param string $attr_name Имя опции
     * @param mixed $value Новое значение
     * @return cmsForm
     */
    public function setFieldAttribute($fieldset_id, $field_name, $attr_name, $value){
        $field = $this->getField($field_name, $fieldset_id);
        if ($field) { $field->setOption($attr_name, $value); }
        return $this;
    }

    /**
     * Изменяет по имени опцию поля в форме
     *
     * @param string $field_name Название поля
     * @param string $attr_name Имя опции
     * @param mixed $value Новое значение
     * @return cmsForm
     */
    public function setFieldAttributeByName($field_name, $attr_name, $value){
        return $this->setFieldAttribute(null, $field_name, $attr_name, $value);
    }

    /**
     * Изменяет по ID набора и по имени свойство поля в форме
     *
     * @param string $fieldset_id ID набора полей
     * @param string $field_name Название поля
     * @param string $attr_name Имя опции
     * @param mixed $value Новое значение
     * @return cmsForm
     */
    public function setFieldProperty($fieldset_id, $field_name, $attr_name, $value){
        $field = $this->getField($field_name, $fieldset_id);
        if ($field) { $field->{$attr_name} = $value; }
        return $this;
    }

    /**
     * Скрывает набор полей в форме
     *
     * @param string $fieldset_id ID набора полей
     * @return cmsForm
     */
    public function hideFieldset($fieldset_id){
        return $this->setFieldsetAttribute($fieldset_id, 'is_hidden', true);
    }

    /**
     * Скрывает поле в форме
     *
     * @param string $fieldset_id ID набора полей
     * @param string $field_name Название поля
     * @return cmsForm
     */
    public function hideField($fieldset_id, $field_name = '') {

        // Если передан только первый параметр, считаем что это $field_name
        if ($fieldset_id && empty($field_name)) {
            return $this->setFieldAttributeByName($fieldset_id, 'is_hidden', true);
        }

        return $this->setFieldAttribute($fieldset_id, $field_name, 'is_hidden', true);
    }

//============================================================================//
//============================================================================//

    /**
     * Возвращает массив полей формы, заполнив их значениями переданными в запросе $request
     *
     * @param object $request Объект cmsRequest
     * @param boolean $is_submitted Форма отправлена?
     * @param array $item Массив предыдущих значений формы
     * @return array
     */
    public function parse($request, $is_submitted = false, $item = null) {

        $result = [];

        foreach($this->getFormStructure($item) as $fieldset){

            if (!isset($fieldset['childs'])) { continue; }

            foreach($fieldset['childs'] as $field){

                if(!is_array($field)){ $_field = [$field]; } else { $_field = $field; }

                foreach ($_field as $field) {

                    $name = $field->getName();

                    // если поле отключено, пропускаем поле
                    if (in_array($name, $this->disabled_fields)){ continue; }

                    $is_array = strpos($name, ':');

                    $value = $request->get($name, null, $field->getDefaultVarType());

                    if (is_null($value) && $field->hasDefaultValue() && !$is_submitted) { $value = $field->getDefaultValue(); }

                    $old_value = null;

                    if($item){
                        if ($is_array === false){
                            $old_value = array_key_exists($name, $item) ? $item[$name] : null;
                        }
                        if ($is_array !== false){
                            $old_value = array_value_recursive($name, $item);
                        }
                    }

                    $field->request = $request;

                    $value = $field->store($value, $is_submitted, $old_value);
                    if ($value === false) { continue; }

                    if ($is_array === false){
                        $result[$name] = $value;
                    }

                    if ($is_array !== false){
                        $result = set_array_value_recursive($name, $result, $value);
                    }

                    // если нужна денормализация
                    if($is_submitted && $field->is_denormalization){

                        $d_name = $field->getDenormalName();

                        if ($is_array === false){
                            $result[$d_name] = $field->storeCachedValue($value);
                        } else {
                            $result = set_array_value_recursive($d_name, $result, $field->storeCachedValue($value));
                        }

                    }

                }

            }

        }

        return $result;
    }

    /**
     * Проверяет соответствие массива $data правилам
     * валидации указанным для полей формы
     *
     * @param object $controller Объект cmsController
     * @param array $data Данные, полученные из формы
     * @param boolean $is_check_csrf Проверять валидность csrf токена
     * @return boolean|array Если ошибки не найдены, возвращает false
     *                       Если ошибки найдены, возвращает их массив
     *                       в формате [имя_поля => текст_ошибки]
     */
    public function validate($controller, $data, $is_check_csrf = true) {

        $errors = [];

        //
        // Проверяем CSRF-token
        //
        if ($is_check_csrf) {
            $csrf_token = $controller->request->get('csrf_token', '');
            if (!self::validateCSRFToken($csrf_token)) {
                return ['csrf_token' => ERR_VALIDATE_INVALID];
            }
        }

        //
        // Перебираем поля формы
        //
        foreach ($this->getFormStructure($data) as $fieldset) {

            if (!isset($fieldset['childs'])) {
                continue;
            }

            foreach ($fieldset['childs'] as $field) {

                if (!is_array($field)) {
                    $_field = [$field];
                } else {
                    $_field = $field;
                }

                foreach ($_field as $field) {

                    $name = $field->getName();

                    // если поле отключено, пропускаем поле
                    if (in_array($name, $this->disabled_fields)) {
                        continue;
                    }

                    // правила
                    $rules = $field->getRules();

                    // если нет правил, пропускаем поле
                    if (!$rules) {
                        continue;
                    }

                    // проверяем является ли поле элементом массива
                    $is_array = strpos($name, ':');

                    //
                    // получаем значение поля из массива данных
                    //
                    if ($is_array === false) {
                        $value = array_key_exists($name, $data) ? $data[$name] : '';
                    }

                    if ($is_array !== false) {
                        $value = array_value_recursive($name, $data);
                    }

                    //
                    // перебираем правила для поля
                    // и проверяем каждое из них
                    //
                    foreach ($rules as $rule) {

                        if (!$rule) {
                            continue;
                        }

                        // если правило - это колбэк
                        if (is_callable($rule[0]) && ($rule[0] instanceof Closure)) {

                            $result = $rule[0]($controller, $data, $value);

                            if ($result !== true) {
                                $errors[$name] = $field->setError($result);
                                break;
                            }

                            continue;
                        }

                        // каждое правило это массив
                        // первый элемент - название функции-валидатора
                        $validate_function = "validate_{$rule[0]}";

                        // к остальным элементам добавляем $value, т.к.
                        // в валидаторах $value всегда последний аргумент
                        $rule[] = $value;

                        // убираем название валидатора из массива,
                        // оставляем только параметры (аргументы)
                        unset($rule[0]);

                        // вызываем валидатор и объединяем результат с предыдущими
                        // методы валидации могут быть определены (в порядке приоритета):
                        // в классе формы, в классе поля, в контроллерах
                        if (method_exists($this, $validate_function)) {
                            $result = call_user_func_array([$this, $validate_function], $rule);
                        } elseif (method_exists($field, $validate_function)) {
                            $result = call_user_func_array([$field, $validate_function], $rule);
                        } else {
                            $result = call_user_func_array([$controller, $validate_function], $rule);
                        }

                        // если получилось false, то дальше не проверяем, т.к.
                        // ошибка уже найдена
                        if ($result !== true) {
                            $errors[$name] = $field->setError($result);
                            break;
                        }
                    }
                }
            }
        }

        return $errors;
    }

//============================================================================//
//============================= CSRF =========================================//

    /**
     * Возвращает CSRF токен
     * Это основной метод для его получения
     *
     * @return string
     */
    public static function getCSRFToken(){

        if (cmsUser::isSessionSet('csrf_token')){
            return cmsUser::sessionGet('csrf_token');
        }

        return self::generateCSRFToken();
    }

    /**
     * Создает, сохраняет в сессии и возвращает CSRF токен
     *
     * @return string
     */
    public static function generateCSRFToken() {

        $token = bin2hex(random_bytes(32));

        cmsUser::sessionSet('csrf_token', $token);

        return $token;
    }

    /**
     * Проверяет валидность CSRF токена
     *
     * @param string $csrf_token
     * @return boolean
     */
    public static function validateCSRFToken($csrf_token){
        return (cmsUser::sessionGet('csrf_token') === $csrf_token);
    }

//============================================================================//
//============================================================================//

    /**
     * Разбивает массив полей по группам
     *
     * @param array $fields Массив полей из БД
     * @param ?callable $callback
     * @param ?array $values Массив значений полей
     * @return array
     */
    public static function mapFieldsToFieldsets($fields, $callback = null, $values = null) {

        $fieldsets = [];

        if (!$fields) { return $fieldsets; }

        $current = false;
        $index   = -1;

        $user = cmsUser::getInstance();

        foreach ($fields as $field) {

            if (is_callable($callback)) {
                if (!call_user_func_array($callback, [$field, $user])) {
                    continue;
                }
            }

            if (is_array($values)) {
                if (empty($values[$field['name']])) {
                    continue;
                }
            }

            if ($current !== $field['fieldset']) {

                $current = $field['fieldset'];
                $index   += 1;

                $fieldsets[$index] = [
                    'title'  => $current,
                    'fields' => []
                ];
            }

            $fieldsets[$index]['fields'][] = $field;
        }

        return $fieldsets;
    }

    /**
     * Возвращает список всех имеющихся типов полей
     *
     * @param boolean $only_public Возвращать только публичные поля
     * @param string $controller Название контроллера для контекста вызова
     * @return array
     */
    public static function getAvailableFormFields($only_public = true, $controller = false) {

        $fields_types = [];
        $fields_files = cmsCore::getFilesList('system/fields', '*.php', true, true);

        foreach ($fields_files as $name) {

            $class = 'field' . string_to_camel('_', $name);

            $field = new $class(null, null);

            if ($only_public && !$field->is_public) {
                continue;
            }
            if ($controller && in_array($controller, $field->excluded_controllers)) {
                continue;
            }

            $fields_types[$name] = $field->getTitle();
        }

        asort($fields_types, SORT_STRING);

        return $fields_types;
    }

    /**
     * Инициализирует объект формы
     *
     * @param string $form_file Полный путь к файлу формы
     * @param string $form_name Название формы
     * @param array $params Параметры, передаваемые в метод init формы
     * @param object $controller Объект контроллера контекста
     * @return boolean|string|form_class instanceof cmsForm
     */
    public static function getForm($form_file, $form_name, $params = false, $controller = null) {

        if (!file_exists($form_file)) { return false; }

        include_once $form_file;

        $form_class = 'form' . string_to_camel('_', $form_name);

        if (!class_exists($form_class, false)) {
            return sprintf(ERR_CLASS_NOT_DEFINED, str_replace(PATH, '', $form_file), $form_class);
        }

        $form = new $form_class();

        if ($controller instanceof cmsController) {
            $form->setContext($controller);
        }

        if ($params) {
            $form->setParams($params);
            $form->setStructure(call_user_func_array([$form, 'init'], $params));
        } else {
            $form->setStructure($form->init());
        }

        return $form;
    }

}
