plugins/form2/classes/yf_form2.class.php

Summary

Maintainability
F
1 mo
Test Coverage
<?php

/**
 * Form2 high-level generator and handler, mostly using bootstrap html/css framework.
 *
 * @author        YFix Team <yfix.dev@gmail.com>
 * @version        1.0
 */
class yf_form2
{
    public $CLASS_FORM_MAIN = 'form-horizontal';// col-md-6'
    public $CLASS_FORM_CONTROL = 'form-control';
    public $CLASS_CKEDITOR = 'ckeditor';
    public $CLASS_TPL_BADGE = 'badge badge-%name';
    public $CLASS_TPL_LABEL = 'label label-%name';
    public $CLASS_BTN_MINI = 'btn btn-default btn-mini btn-xs';
    public $CLASS_BTN_DEFAULT = 'btn btn-default';
    public $CLASS_BTN_SUBMIT = 'btn btn-default btn-primary';
    public $CLASS_ICON_SAVE = 'icon-save fa fa-save';
    public $CLASS_ICON_PSWD = 'icon-key fa fa-key fa-fw';
    public $CLASS_ICON_LOGIN = 'icon-user fa fa-user fa-fw';
    public $CLASS_ICON_EMAIL = 'icon-email fa fa-at fa-fw';
    public $CLASS_ICON_CURRENCY = 'icon-dollar fa fa-dollar fa-fw';
    public $CLASS_ICON_CALENDAR = 'icon icon-calendar fa fa-calendar fa-fw';
    public $CLASS_ICON_PREVIEW = 'icon-eye fa fa-eye';
    public $CLASS_LABEL_INFO = 'label label-info';
    public $CLASS_ERROR = 'alert alert-error alert-danger';
    public $CLASS_REQUIRED = 'control-group-required form-group-required';
    public $CLASS_STACKED_ROW = 'stacked-row';

    public $CONF_BOXES_USE_BTN_GROUP = false;
    public $CONF_CSRF_PROTECTION = true;
    public $CONF_CSRF_NAME = '_token';
    public $CONF_FORM_ID_FIELD = '__form_id__';
    public $CONF_FORM_AUTOID_PREFIX = 'form_autoid_';

    /**
     * Catch missing method call.
     * @param mixed $name
     * @param mixed $args
     */
    public function __call($name, $args)
    {
        return main()->extend_call($this, $name, $args, $this->_chained_mode);
    }

    /**
     * We cleanup object properties when cloning.
     */
    public function __clone()
    {
        $keep_prefix = 'CLASS_';
        $keep_len = strlen($keep_prefix);
        $keep_prefix2 = 'CONF_';
        $keep_len2 = strlen($keep_prefix2);
        foreach ((array) get_object_vars($this) as $k => $v) {
            if (substr($k, 0, $keep_len) === $keep_prefix) {
                continue;
            }
            if (substr($k, 0, $keep_len2) === $keep_prefix2) {
                continue;
            }
            $this->$k = null;
        }
    }

    /**
     * Need to avoid calling render() without params.
     */
    public function __toString()
    {
        try {
            return (string) $this->render();
        } catch (Exception $e) {
            return '';
        }
    }

    /**
     * @param mixed $name
     * @param mixed $func
     */
    public function _extend($name, $func)
    {
        $this->_extend[$name] = $func;
    }

    /**
     * Wrapper for template engine
     * Example:
     *    return form($replace)
     *        ->text('login','Login')
     *        ->text('password','Password')
     *        ->text('first_name','First Name')
     *        ->text('last_name','Last Name')
     *        ->text('go_after_login','Url after login')
     *        ->box_with_link('group_box','Group','groups_link')
     *        ->active('active','Active')
     *        ->info('add_date','Added');.
     * @param mixed $replace
     * @param mixed $params
     */
    public function chained_wrapper($replace = [], $params = [])
    {
        if ($replace && is_string($replace)) {
            $sql = $replace;
            $this->_sql = $sql;
            $db = is_object($params['db']) ? $params['db'] : db();
            $replace = $db->get_2d($sql);
        }
        if (isset($params['filter']) && ! is_array($params['filter']) && is_numeric($params['filter']) || is_bool($params['filter']) && ! empty($params['filter'])) {
            $filter_name = $params['filter_name'] ?: $_GET['object'] . '__' . $_GET['action'];
            $params['selected'] = $_SESSION[$filter_name];
            $replace['form_action'] = $replace['form_action'] ?: url('/@object/filter_save/' . $filter_name);
            $replace['clear_url'] = $replace['clear_url'] ?: url('/@object/filter_save/' . $filter_name . '/clear');
            if (MAIN_TYPE_ADMIN && ! isset($params['class'])) {
                $params['class'] = 'form-vertical';
            }
        }
        if ( ! $params['no_chained_mode']) {
            $this->_chained_mode = true;
        }
        $this->_replace = $replace;
        $this->_params = $params;
        return $this;
    }

    /**
     * @param mixed $a
     * @param mixed $params
     * @param mixed $replace
     */
    public function array_to_form($a = [], $params = [], $replace = [])
    {
        $this->_params = $params + (array) $this->_params;
        $this->_replace = $replace + (array) $this->_replace;
        // Example of row: ['text', 'login', ['class' => 'input-medium']]
        foreach ((array) $a as $v) {
            $func = '';
            if (is_string($v)) {
                $func = $v;
                $v = [];
            } elseif (is_array($v)) {
                $func = $v[0];
            }
            if ( ! $func || ! method_exists($this, $func)) {
                continue;
            }
            $this->$func($v[1], $v[2], $v[3], $v[4], $v[5]);
        }
        return $this;
    }

    /**
     * Wrapper for template engine
     * Example template:
     *    {form_row('form_begin')}
     *    {form_row('text','login')}
     *    {form_row('text','password')}
     *    {form_row('text','first_name')}
     *    {form_row('text','last_name')}
     *    {form_row('text','go_after_login','Url after login')}
     *    {form_row('box_with_link','group_box','Group','groups_link')}
     *    {form_row('active_box')}
     *    {form_row('info','add_date','Added')}
     *    {form_row('save_and_back')}
     *    {form_row('form_end')}.
     *
     *    {catch("field_name")}some_other_field{/catch} {form_row('text','%field_name')}
     *    {catch("t_password")}My password inside replace['t_password']{/catch} {form_row('text','pswd','%t_password')}
     * @param mixed $type
     * @param mixed $replace
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     */
    public function tpl_row($type = 'input', $replace = [], $name = '', $desc = '', $extra = [])
    {
        $name = trim($name);
        if ($name && $name[0] == '%') {
            $_name = substr($name, 1);
            if (isset($replace[$_name])) {
                $name = $replace[$_name];
            }
        }
        $desc = trim($desc);
        if ($desc && $desc[0] == '%') {
            $_desc = substr($desc, 1);
            if (isset($replace[$_desc])) {
                $desc = $replace[$_desc];
            }
        }
        // Allow to pass extra params like this: param1=val1;param2=val2
        if (is_string($extra)) {
            $extra = trim($extra);
            if (false !== strpos($extra, ';') && false !== strpos($extra, '=')) {
                $extra = _attrs_string2array($extra);
            }
        }
        if ( ! is_array($extra)) {
            // Suppose we have 3rd argument as edit link here
            if ( ! empty($extra)) {
                $extra = ['edit_link' => $extra];
            } else {
                $extra = [];
            }
        }
        return $this->$type($name, $desc, $extra, $replace);
    }

    /**
     * Enable automatic fields parsing mode.
     * @param mixed $table
     * @param mixed $id
     * @param mixed $params
     */
    public function auto($table = '', $id = '', $params = [])
    {
        return _class('form2_auto', 'classes/form2/')->auto($table, $id, $params, $this);
    }

    /**
     * @param mixed $form_id
     */
    public function _get_extra_override($form_id = '')
    {
        if ( ! strlen($form_id)) {
            return [];
        }
        $autoid_prefix = $this->CONF_FORM_AUTOID_PREFIX;
        if (is_unit_test() || (strlen($autoid_prefix) && substr($form_id, 0, strlen($autoid_prefix)) === $autoid_prefix)) {
            return [];
        }
        $extra_override = [];
        // Data from database have highest priority, so we init it first
        $all_attrs_override = main()->get_data('form_attributes');
        $extra_override = $all_attrs_override[$form_id];
        // Search for override params inside shared files
        $suffix = $form_id . '.form.php';
        $slen = strlen($suffix);
        $pattern = '{,plugins/*/}{,share/}form/' . $form_id . '*' . $suffix;
        $paths = [
            'frawework' => YF_PATH . $pattern,
            'config' => CONFIG_PATH . $pattern,
            'project' => PROJECT_PATH . $pattern,
        ];
        if (SITE_PATH != PROJECT_PATH) {
            $paths['site'] = SITE_PATH . $pattern;
        }
        $names = [];
        foreach ((array) $paths as $glob) {
            foreach (glob($glob, GLOB_BRACE) as $path) {
                $name = substr(basename($path), 0, -$slen);
                $names[$name] = $path;
            }
        }
        // Allow override framework defaults inside project
        foreach ($names as $name => $path) {
            $data = [];
            include $path;
            foreach ((array) $data as $field => $attrs) {
                $extra_override[$field] = (array) $extra_override[$field] + (array) $attrs;
            }
        }
        return $extra_override;
    }

    /**
     * @param mixed $extra
     * @param mixed $replace
     */
    public function _get_form_id($extra = [], $replace = [])
    {
        $form_id = $this->_form_id;
        $replace = $replace ?: $this->_replace;
        $params = &$this->_params;
        if (isset($replace[$this->CONF_FORM_ID_FIELD]) && $replace[$this->CONF_FORM_ID_FIELD]) {
            $form_id = $replace[$this->CONF_FORM_ID_FIELD];
        } elseif (isset($params[$this->CONF_FORM_ID_FIELD]) && $params[$this->CONF_FORM_ID_FIELD]) {
            $form_id = $params[$this->CONF_FORM_ID_FIELD];
        }
        if ( ! $form_id) {
            $form_id = $this->CONF_FORM_AUTOID_PREFIX . strtolower($_GET['object'] . '_' . $_GET['action']) . '_' . ++main()->_unique_widget_ids['form'];
        }
        $this->_form_id = $form_id;
        return $form_id;
    }

    /**
     * @param mixed $form_id
     */
    public function _set_hidden_form_id($form_id = '')
    {
        if ($this->_isset_hidden_form_id) {
            return true;
        }
        if ( ! $form_id) {
            $form_id = $this->_get_form_id();
        }
        $this->_replace[$this->CONF_FORM_ID_FIELD] = $form_id;
        $this->hidden($this->CONF_FORM_ID_FIELD, ['value' => $form_id]);
        $this->_isset_hidden_form_id = true;
    }

    /**
     * @param mixed $csrf_guard
     */
    public function _set_hidden_token($csrf_guard)
    {
        if ($this->_isset_hidden_token) {
            return true;
        }
        $this->_replace[$this->CONF_CSRF_NAME] = $csrf_guard->generate();
        $this->hidden($this->CONF_CSRF_NAME);
        $this->_isset_hidden_token = true;
    }

    /**
     * Render result form html, gathered by row functions
     * Params here not required, but if provided - will be passed to form_begin().
     * @param mixed $extra
     * @param mixed $replace
     */
    public function render($extra = [], $replace = [])
    {
        if (isset($this->_rendered)) {
            return $this->_rendered;
        }
        if (DEBUG_MODE) {
            $ts = microtime(true);
        }
        _class('core_events')->fire('form.before_render', [$extra, $replace, $this]);
        $this->_extra = $extra;
        $on_before_render = isset($extra['on_before_render']) ? $extra['on_before_render'] : $this->_on['on_before_render'];
        if (is_callable($on_before_render)) {
            $on_before_render($extra, $replace, $this);
        }
        if ( ! is_array($this->_body)) {
            $this->_body = [];
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra_override = [];
        $form_id = $this->_get_form_id($extra, $replace);
        if ($form_id) {
            $extra_override = $this->_get_extra_override($form_id);
        }
        $headless_form = ($extra['no_form'] || $this->_params['no_form']);

        $csrf_protect = isset($extra['csrf']) ? (bool) $extra['csrf'] : (isset($this->_params['csrf']) ? $this->_params['csrf'] : $this->CONF_CSRF_PROTECTION);
        if (isset($extra['method']) && strtolower($extra['method']) != 'post') {
            $csrf_protect = false;
        }
        if ($headless_form) {
            $csrf_protect = false;
        }
        if ($csrf_protect && is_callable($csrf_protect)) {
            $csrf_protect = $csrf_protect($this, $extra);
        }
        if ($csrf_protect) {
            $csrf_guard = _class('csrf_guard')->configure([
                'form_id' => $form_id,
                'token_name' => $this->CONF_CSRF_NAME,
            ]);
        }
        if (is_post()) {
            $is_current_form = isset($_POST[$this->CONF_FORM_ID_FIELD]) && ($_POST[$this->CONF_FORM_ID_FIELD] == $form_id);
            if ($csrf_protect && $is_current_form && ! $csrf_guard->validate($_POST[$this->CONF_CSRF_NAME])) {
                // We need this as validation now is skipping empty values
                if ( ! isset($_POST[$this->CONF_CSRF_NAME]) || (trim($_POST[$this->CONF_CSRF_NAME]) == '')) {
                    $_POST[$this->CONF_CSRF_NAME] = '__wrong_token_' . md5(microtime()) . '__';
                }
                $this->_params['show_alerts'] = true;
                $this->_validate_rules[$this->CONF_CSRF_NAME] = function ($in, $p, $a, &$error_msg) use ($form_id, $csrf_guard) {
                    $csrf_guard->log_error(['form_id' => $form_id]);
                    $error_msg = 'Invalid CSRF token. Send the form again. If you did not send this request then close this page.';
                    return false;
                };
                $this->_set_hidden_token($csrf_guard);
            }

            $on_post = isset($extra['on_post']) ? $extra['on_post'] : $this->_on['on_post'];
            if (is_callable($on_post)) {
                $on_post($extra, $replace, $this);
            }
            $v = $this->_validate;
            if (isset($v) && is_callable($v['func'])) {
                $func = $v['func'];
                $func($v['validate_rules'], $v['post'], $v['extra'], $this);
            }
            $up = $this->_db_change_if_ok;
            if (isset($up) && is_callable($up['func'])) {
                $func = $up['func'];
                $func($up['table'], $up['fields'], $up['type'], $up['extra'], $this);
            }
        }
        if ($csrf_protect) {
            $this->_set_hidden_token($csrf_guard);
        }

        $r = (array) $this->_replace + (array) $replace;

        if ( ! $headless_form) {
            // Call these methods, if not done yet, save 2 api calls
            if ( ! isset($this->_body['form_begin'])) {
                $this->form_begin('', '', $extra + (array) $extra_override['form_begin'], $r);
            }
            if ( ! isset($this->_body['form_end'])) {
                $this->form_end($extra + (array) $extra_override['form_end'], $r);
            }
            // Force form_begin as first array element
            $form_begin = $this->_body['form_begin'];
            unset($this->_body['form_begin']);
            array_unshift($this->_body, $form_begin);

            // Force form_end as last array element
            $form_end = $this->_body['form_end'];
            unset($this->_body['form_end']);
            $this->_body['form_end'] = $form_end;
        }

        $tabbed_mode = false;
        $tabbed_buffer = [];
        $tabs = [];
        $tabs_extra = [];
        $tabs_name = '';
        $tabs_container = '';

        // Create tree of row_start and its children
        $item_row = [];
        $row_items = [];
        $row = false;
        $body_ids_to_extra = [];
        foreach ((array) $this->_body as $k => $v) {
            if ($v['name'] == 'row_start') {
                $row = $k;
            } elseif ($v['name'] == 'row_end') {
                $row = false;
            } elseif ($row) {
                $item_row[$k] = $row;
                $row_items[$row][$k] = $v['extra']['id'] ?: $v['extra']['name'];
            }
        }
        $all_errors = common()->_get_error_messages();

        foreach ((array) $this->_body as $k => $v) {
            if ( ! is_array($v)) {
                continue;
            }
            $_extra = (array) $v['extra'] + (array) $extra_override[$v['extra']['name']];
            $_replace = (array) $r + (array) $v['replace'];
            $func = $v['func'];
            if ($v['name'] == 'row_start') {
                // Mark row as containing errors, if children elements has at least one error
                foreach ((array) $row_items[$k] as $_k => $_id) {
                    if ( ! $_id || ! isset($all_errors[$_id])) {
                        continue;
                    }
                    $_extra['errors'][$v['extra']['name']] = $all_errors[$_id];
                }
            }
            if ($this->_stacked_mode_on) {
                $_extra['stacked'] = $_extra['stacked'] ?: true;
            }
            // Callback to decide if we need to show this field or not
            if (isset($_extra['display_func']) && is_callable($_extra['display_func'])) {
                $_display_allowed = $_extra['display_func']($_extra, $_replace, $this);
                if ( ! $_display_allowed) {
                    $this->_body[$k] = '';
                    continue;
                }
            }
            if (DEBUG_MODE) {
                $_debug_fields[$k] = [
                    'name' => $v['name'],
                    'extra' => $_extra,
                ];
            }
            $this->_body[$k]['rendered'] = $func($_extra, $_replace, $this);

            if ($this->_tabbed_mode_on) {
                $tabbed_mode = true;
                $tabbed_buffer[$k] = $this->_body[$k]['rendered'];
                if ($v['name'] == 'tab_start') {
                    $this->_tabs_counter++;
                    $tabs_name = $this->_tabs_name ?: 'tabs_' . $this->_tabs_counter;
                    $tabs_extra['by_id'][$tabs_name] = $this->_tabs_extra;
                }
                if ($v['name'] == 'tab_start' && ! $tabs_container) {
                    $tabs_container = $k;
                    $this->_body[$k]['rendered'] = '__TAB_START__';
                } else {
                    unset($this->_body[$k]);
                }
            } elseif ($tabbed_mode) { // switch off
                if ( ! $this->_tabbed_mode_on) {
                    $tabbed_mode = false;
                }
                $tabs[$tabs_name] = implode(PHP_EOL, $tabbed_buffer);
                $tabbed_buffer = [];
                $tabs_name = '';
            }
        }
        if ($tabbed_buffer) {
            $tabs['tab_last'] = implode(PHP_EOL, $tabbed_buffer);
            $tabbed_buffer = [];
        }
        if ($tabs) {
            $this->_body[$tabs_container]['rendered'] = html()->tabs($tabs, (array) $this->_params['tabs'] + (array) $tabs_extra);
        }
        if ($this->_params['show_alerts']) {
            $errors = common()->_get_error_messages();
            if ($errors) {
                $e = [];
                foreach ((array) $errors as $msg) {
                    $e[] = '<div class="' . $this->CLASS_ERROR . '"><button type="button" class="close" data-dismiss="alert">&times;</button>' . $msg . '</div>';
                }
                $this->_body = array_slice($this->_body, 0, 1, true) + ['error_message' => implode(PHP_EOL, $e)] + array_slice($this->_body, 1, null, true);
            }
        }
        if ($this->_params['stpl'] || $this->_params['return_array']) {
            $data = [];
            foreach ($this->_body as $k => $v) {
                $name = fix_html_attr_id($v['extra']['name'] ?: $v['extra']['id'] ?: $k);
                if ($name === 'form_action') {
                    $name = 'form_begin';
                }
                if (isset($data['form'][$name]) && ! empty($data['form'][$name])) {
                    if (in_array($name, ['form_id', '_token', 'token'])) {
                        // allow only once
                    } else {
                        if ( ! is_array($data['form'][$name])) {
                            $tmp = $data['form'][$name];
                            $data['form'][$name] = [];
                            $data['form'][$name][] = $tmp;
                            unset($tmp);
                        }
                        $data['form'][$name][] = $v['rendered'];
                    }
                } else {
                    $_rendered = '';
                    if (is_array($v)) {
                        $_rendered = array_key_exists('rendered', $v) ? (string) $v['rendered'] : '';
                    } else {
                        $_rendered = $v;
                    }
                    $data['form'][$name] = $_rendered;
                }
            }
            // Fixes for easier usage
            if ($data['form']['form_id']) {
                $data['form']['form_begin'] .= PHP_EOL . $data['form']['form_id'];
                unset($data['form']['form_id']);
            }
            if (isset($data['form']['token']) && ! isset($data['form']['_token'])) {
                $data['form']['_token'] = $data['form']['token'];
                unset($data['form']['token']);
            }
            if (isset($data['form']['_token'])) {
                $data['form']['form_begin'] .= PHP_EOL . $data['form']['_token'];
                unset($data['form']['_token']);
            }
            if ( ! isset($data['form']['begin'])) {
                $data['form']['begin'] = $data['form']['form_begin'];
                unset($data['form']['form_begin']);
            }
            if ( ! isset($data['form']['end'])) {
                $data['form']['end'] = $data['form']['form_end'];
                unset($data['form']['form_end']);
            }
            if ($this->_params['return_array']) {
                return $data['form'];
            }
            if (false === strpos($this->_params['stpl'], ' ') && tpl()->exists($this->_params['stpl'])) {
                $this->_rendered = tpl()->parse($this->_params['stpl'], $data);
            } else {
                $this->_rendered = tpl()->parse_string($this->_params['stpl'], $data);
            }

            unset($data);
        } else {
            $rendered = [];
            foreach ($this->_body as $k => $v) {
                if (is_array($v)) {
                    $rendered[$k] = array_key_exists('rendered', $v) ? (string) $v['rendered'] : '';
                } else {
                    $rendered[$k] = $v;
                }
            }
            $this->_rendered = implode(PHP_EOL, $rendered);
            unset($rendered);
        }
        unset($this->_body); // Save some memory

        $css_framework = $extra['css_framework'] ?: ($this->_params['css_framework'] ?: conf('css_framework'));
        $extra['css_framework'] = $css_framework;
        $this->_rendered = _class('html5fw')->form_render_out($this->_rendered, $extra, $r, $this);

        $on_after_render = isset($extra['on_after_render']) ? $extra['on_after_render'] : $this->_on['on_after_render'];
        if (is_callable($on_after_render)) {
            $on_after_render($extra, $replace, $this);
        }
        _class('core_events')->fire('form.after_render', [$extra, $replace, $this]);
        if (DEBUG_MODE) {
            debug('form2[]', [
                'params' => $this->_params,
                'fields' => $_debug_fields,
                'time' => round(microtime(true) - $ts, 5),
                'trace' => main()->trace_string(),
            ]);
        }
        return $this->_rendered;
    }

    /**
     * @param mixed $name
     * @param mixed $method
     * @param mixed $extra
     * @param mixed $replace
     */
    public function form_begin($name = '', $method = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        // Merge params passed to table2() and params passed here, with params here have more priority:
        $tmp = $this->_params;
        foreach ((array) $extra as $k => $v) {
            $tmp[$k] = $v;
        }
        $extra = $tmp;
        unset($tmp);

        $extra['name'] = $extra['name'] ?: ($name ?: 'form_action');
        $extra['method'] = $extra['method'] ?: ($method ?: 'post');

        $func = function ($extra, $r, $form) {
            $enctype = '';
            if ($extra['enctype']) {
                $enctype = $extra['enctype'];
            } elseif ($extra['for_upload']) {
                $enctype = 'multipart/form-data';
            }
            $extra['enctype'] = $enctype;
            if ( ! isset($extra['action'])) {
                $get_id = isset($_GET['id']) && strlen($_GET['id']) ? urlencode($_GET['id']) : '';
                $get_page = isset($_GET['page']) && strlen($_GET['page']) ? urlencode($_GET['page']) : '';
                $extra['action'] = isset($r[$extra['name']]) ? $r[$extra['name']] : url('/@object/@action/' . $get_id . '/' . $get_page) . $form->_params['links_add'];
            }
            if (MAIN_TYPE_USER) {
                if (strpos($extra['action'], 'http://') === false && strpos($extra['action'], 'https://') !== 0) {
                    $extra['action'] = process_url($extra['action'], true);
                }
            }
            $extra['class'] = $extra['class'] ?: $form->CLASS_FORM_MAIN;// col-md-6';
            if ($extra['class_add']) {
                $extra['class'] .= ' ' . $extra['class_add'];
            }
            $extra['autocomplete'] = isset($extra['autocomplete']) ? $extra['autocomplete'] : true;

            $advanced_js_validation = conf('form_advanced_js_validation');
            if ($advanced_js_validation) {
                $extra['data-fv-framework'] = 'bootstrap';
            }

            $body = '<form' . _attrs($extra, ['method', 'action', 'class', 'style', 'id', 'name', 'autocomplete', 'enctype', 'novalidate', 'target']) . '>' . PHP_EOL;
            $form->_fieldset_mode_on = true;
            $body .= '<fieldset' . _attrs($extra['fieldset'], ['class', 'style', 'id', 'name']) . '>';
            if ($extra['legend']) {
                $body .= PHP_EOL . '<legend>' . _htmlchars(t($extra['legend'])) . '</legend>' . PHP_EOL;
            }
            return $body;
        };
        if ($this->_chained_mode) {
            $this->_body[__FUNCTION__] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $extra
     * @param mixed $replace
     */
    public function form_end($extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $func = function ($extra, $r, $form) {
            $form->_fieldset_mode_on = false;
            $body .= '</fieldset>' . PHP_EOL;
            $body .= '</form>' . PHP_EOL;
            return $body;
        };
        if ($this->_chained_mode) {
            $this->_body[__FUNCTION__] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * Shortcut for adding fieldset.
     * @param mixed $name
     * @param mixed $extra
     */
    public function fieldset_start($name = '', $extra = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        $extra['name'] = $extra['name'] ?: $name;
        $func = function ($extra, $r, $form) {
            if ($form->_fieldset_mode_on) {
                $body = '</fieldset>' . PHP_EOL;
            } else {
                $form->_fieldset_mode_on = true;
            }
            $body .= '<fieldset' . _attrs($extra, ['class', 'style', 'id', 'name']) . '>';
            if ($extra['legend']) {
                $body .= PHP_EOL . '<legend>' . _htmlchars(t($extra['legend'])) . '</legend>' . PHP_EOL;
            }
            return $body;
        };
        if ($this->_chained_mode || $extra['chained_mode']) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * Paired with fieldset_start.
     * @param mixed $extra
     */
    public function fieldset_end($extra = [])
    {
        $func = function ($extra, $r, $form) {
            if ($form->_fieldset_mode_on) {
                $form->_fieldset_mode_on = false;
                return '</fieldset>' . PHP_EOL;
            }
        };
        if ($this->_chained_mode || $extra['chained_mode']) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * Shortcut for starting form row, needed to build row with several inlined inputs.
     * @param mixed $name
     * @param mixed $extra
     */
    public function row_start($name = '', $extra = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        $extra['name'] = $extra['name'] ?: $name;
        $func = function ($extra, $r, $form) {
            // auto-close row_end(), if not called implicitely
            if ($form->_stacked_mode_on) {
                $form->row_end();
            }
            $form->_stacked_mode_on = true;
            if ( ! isset($extra['id']) && $extra['name']) {
                $extra['id'] = $extra['name'];
            }
            $extra['class_add_form_group'] = trim($form->CLASS_STACKED_ROW . ' ' . $extra['class_add_form_group']);
            return $form->_row_html('', ['only_row_start' => 1] + (array) $extra);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * Paired with row_start.
     * @param mixed $extra
     */
    public function row_end($extra = [])
    {
        $func = function ($extra, $r, $form) {
            $form->_stacked_mode_on = false;
            return $form->_row_html('', ['only_row_end' => 1] + (array) $extra);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * Shortcut for making tabbable form.
     * @param mixed $name
     * @param mixed $extra
     */
    public function tab_start($name = '', $extra = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        $extra['name'] = $extra['name'] ?: $name;
        $func = function ($extra, $r, $form) {
            // auto-close tab_end(), if not called implicitely
            if ($form->_tabbed_mode_on) {
                $form->tab_end();
            }
            $form->_tabbed_mode_on = true;
            $form->_tabs_name = $extra['name'];
            $form->_tabs_extra = $extra;
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * Paired with tab_start.
     * @param mixed $extra
     */
    public function tab_end($extra = [])
    {
        $func = function ($extra, $r, $form) {
            $form->_tabbed_mode_on = false;
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $content
     * @param mixed $extra
     * @param mixed $replace
     */
    public function _row_html($content, $extra = [], $replace = [])
    {
        if ( ! strlen($content) && ($extra['hide_empty'] || $this->_params['hide_empty'])) {
            return '';
        }
        if ($this->_params['only_content']) {
            return $content;
        }
        if ($this->_params['dd_mode']) {
            return $this->_dd_row_html($content, $extra, $replace);
        }
        if ($extra['form_input_no_append'] || $this->_params['form_input_no_append'] || conf('form_input_no_append')) {
            $extra['append'] = '';
            $extra['prepend'] = '';
        }
        if ($this->_stacked_mode_on) {
            $extra['stacked'] = $extra['stacked'] ?: true;
        }
        $css_framework = $extra['css_framework'] ?: ($this->_params['css_framework'] ?: conf('css_framework'));
        $extra['css_framework'] = $css_framework;
        return _class('html5fw')->form_row($content, $extra, $replace, $this);
    }

    /**
     * Generate form row using dl>dt,dd html tags. Useful for user profle and other simple table-like content.
     * @param mixed $content
     * @param mixed $extra
     * @param mixed $replace
     */
    public function _dd_row_html($content, $extra = [], $replace = [])
    {
        if ($extra['hide_empty'] && ! strlen($content)) {
            return '';
        }
        if ($this->_stacked_mode_on) {
            $extra['stacked'] = true;
        }
        $css_framework = $extra['css_framework'] ?: ($this->_params['css_framework'] ?: conf('css_framework'));
        $extra['css_framework'] = $css_framework;
        return _class('html5fw')->form_dd_row($content, $extra, $replace, $this);
    }

    /**
     * @param mixed $value
     * @param mixed $extra
     * @param mixed $replace
     */
    public function _show_tip($value = '', $extra = [], $replace = [])
    {
        return tip($value, $replace);
    }

    /**
     * @param mixed $attr
     */
    public function _prepare_custom_attr($attr = [])
    {
        $body = [];
        foreach ((array) $attr as $k => $v) {
            $body[] = _htmlchars($k) . '="' . _htmlchars($v) . '"';
        }
        return implode(' ', $body);
    }

    /**
     * @param mixed $default_class
     * @param mixed $value
     */
    public function _prepare_css_class($default_class = '', $value = '', &$extra)
    {
        $css_class = $default_class;
        if ($extra['badge']) {
            $badge = is_array($extra['badge']) && isset($extra['badge'][$value]) ? $extra['badge'][$value] : $extra['badge'];
            if ($badge) {
                $css_class = str_replace('%name', $badge, $this->CLASS_TPL_BADGE);
            }
        } elseif ($extra['label']) {
            $label = is_array($extra['label']) && isset($extra['label'][$value]) ? $extra['label'][$value] : $extra['label'];
            if ($label) {
                $css_class = str_replace('%name', $label, $this->CLASS_TPL_LABEL);
            }
        } elseif ($extra['class']) {
            $_css_class = is_array($extra['class']) && isset($extra['class'][$value]) ? $extra['class'][$value] : $extra['class'];
            if ($_css_class) {
                $css_class = $_css_class;
            }
        }
        // Needed to not modify original class of element (sometimes complex), but just add css class there
        if (isset($extra['class_add'])) {
            $_css_class_add = is_array($extra['class_add']) && isset($extra['class_add'][$value]) ? $extra['class_add'][$value] : $extra['class_add'];
            if ($_css_class_add) {
                $css_class .= ' ' . $_css_class_add;
            }
        }
        if ($this->_params['big_labels']) {
            $css_class .= ' labels-big';
        }
        return $css_class ? ' ' . $css_class : '';
    }

    /**
     * @param mixed $default
     */
    public function _prepare_id(&$extra, $default = '')
    {
        $out = $extra['id'];
        if ( ! $out) {
            $out = $extra['name'];
            $is_html_array = (false !== strpos($out, '['));
            if ($is_html_array) {
                $out = str_replace(['[', ']'], ['_', ''], trim($out, ']['));
            }
        }
        ! $out && $out = $default;
        return $out;
    }

    /**
     * @param mixed $input
     */
    public function _prepare_desc(&$extra, $input = '')
    {
        $out = $extra['desc'];
        ! $out && $out = $input;
        if ( ! $out) {
            $out = ucfirst(str_replace('_', ' ', $extra['name']));
            $is_html_array = (false !== strpos($out, '['));
            if ($is_html_array) {
                $out = str_replace(['[', ']'], ['.', ''], trim($out, ']['));
            }
        }
        return $out;
    }


    public function _prepare_value(&$extra, &$replace, &$params)
    {
        $name = $extra['name'];
        $is_html_array = (false !== strpos($name, '['));
        if ($is_html_array) {
            $name_dots = str_replace(['[', ']'], ['.', ''], trim($name, ']['));
            $replace_dots = array_dot($replace);
        }
        $value = '';
        if ($extra['value']) {
            $value = $extra['value'];
        } elseif ($replace[$name]) {
            $value = $replace[$name];
        } elseif ($is_html_array && $replace_dots[$name_dots]) {
            $value = $replace_dots[$name_dots];
        } elseif ($extra['selected']) {
            $value = $extra['selected'];
        } elseif ($params['selected'][$name]) {
            $value = $params['selected'][$name];
        } elseif ($is_html_array && $params['selected'][$name_dots]) {
            $value = $params['selected'][$name_dots];
        }
        return $value;
    }

    /**
     * @param mixed $name
     */
    public function _prepare_selected($name, &$extra, &$r)
    {
        $is_array = strpos($name, '[');
        if ($is_array !== false) {
            $value = &$r;
            $keys = explode('[', $name);
            foreach ($keys as $key) {
                $key = trim(rtrim($key, ']'));
                if ( ! isset($value[$key])) {
                    $value = null;
                    break;
                }
                $value = &$value[$key];
            }
            $selected = $value;
        } else {
            $selected = $r[$name];
        }
        // $selected = $r[$name];
        if (isset($extra['selected'])) {
            $selected = $extra['selected'];
        } elseif (isset($this->_params['selected'])) {
            $selected = $this->_params['selected'][$name];
        }
        return $selected;
    }

    /**
     * @param mixed $name
     */
    public function _prepare_inline_error(&$extra, $name = '')
    {
        $name = $name ?: $extra['name'];
        $is_html_array = (false !== strpos($name, '['));
        if ($is_html_array) {
            $name_orig = $name;
            $name = str_replace(['[', ']'], ['.', ''], trim($name, ']['));
        }
        $extra['errors'] = common()->_get_error_messages();
        if (isset($extra['errors'][$name])) {
            $remove_errors = true;
            $var_name = 'do_not_remove_errors';
            if ($extra[$var_name] || $this->_params[$var_name] || $this->_extra[$var_name] || $this->_params['extra'][$var_name]) {
                $remove_errors = false;
            }
            if ($remove_errors) {
                common()->_remove_error_messages($name);
            }
            $extra['inline_help'] = $extra['errors'][$name];
        }
    }

    /**
     * Bootstrap-compatible html wrapper for any custom content inside.
     * Can be used for inline rich editor editing with ckeditor, enable with: $extra = ['ckeditor' => true].
     * @param mixed $text
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function container($text, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $text = (string) $text;
        $extra['text'] = $text;
        $extra['desc'] = $this->_prepare_desc($extra, $desc);

        $func = function ($extra, $r, $form) {
            $extra['edit_link'] = $extra['edit_link'] ? (isset($r[$extra['edit_link']]) ? $r[$extra['edit_link']] : $extra['edit_link']) : '';
            $extra['contenteditable'] = isset($extra['ckeditor']) ? 'true' : 'false';
            $extra['id'] = $form->_prepare_id($extra, 'content_editable');
            $extra['desc'] = ! $form->_params['no_label'] ? $extra['desc'] : '';

            $attrs_names = ['id', 'contenteditable', 'style', 'class', 'title'];
            if ($extra['ckeditor']) {
                $extra['ckeditor_inline'] = true;
            }
            return $form->_row_html(isset($extra['ckeditor']) ? '<div' . _attrs($extra, $attrs_names) . '>' . $extra['text'] . '</div>' : $extra['text'], $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * General input.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function input($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: $name;
        $extra['desc'] = $this->_prepare_desc($extra, $desc);
        $func = function ($extra, $r, $form) {
            $form->_prepare_inline_error($extra);
            $extra['id'] = $form->_prepare_id($extra);
            $extra['placeholder'] = t(isset($extra['placeholder']) ? $extra['placeholder'] : $extra['desc']);
            $extra['value'] = $form->_prepare_value($extra, $r, $form->_params);
            $extra['type'] = $extra['type'] ?: 'text';
            $extra['edit_link'] = $extra['edit_link'] ? (isset($r[$extra['edit_link']]) ? $r[$extra['edit_link']] : $extra['edit_link']) : '';
            $extra['class'] = $form->CLASS_FORM_CONTROL . $form->_prepare_css_class('', $r[$extra['name']], $extra);
            if ($this->_params['filter'] && ! isset($extra['sizing'])) {
                $extra['sizing'] = 'sm';
            }
            // Supported: sm, lg
            if ($extra['sizing']) {
                $extra['class'] .= ' input-' . $extra['sizing'];
            }
            if ($form->_params['no_label']) {
                $extra['desc'] = '';
            }
            $extra = $form->_input_assign_params_from_validate($extra);
            $attrs_names = ['name', 'type', 'id', 'class', 'style', 'placeholder', 'value', 'data', 'size', 'maxlength', 'pattern', 'disabled', 'readonly', 'required', 'autocomplete', 'accept', 'target', 'autofocus', 'title', 'min', 'max', 'step'];
            return $form->_row_html('<input' . _attrs($extra, $attrs_names) . '>', $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function textarea($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: $name;
        $extra['desc'] = $this->_prepare_desc($extra, $desc);
        $func = function ($extra, $r, $form) {
            $form->_prepare_inline_error($extra);
            $extra['id'] = $form->_prepare_id($extra);
            $extra['placeholder'] = t(isset($extra['placeholder']) ? $extra['placeholder'] : $extra['desc']);
            $extra['value'] = $form->_prepare_value($extra, $r, $form->_params);
            $extra['edit_link'] = $extra['edit_link'] ? (isset($r[$extra['edit_link']]) ? $r[$extra['edit_link']] : $extra['edit_link']) : '';
            $extra['contenteditable'] = ( ! isset($extra['contenteditable']) || $extra['contenteditable']) ? 'true' : false;
            $use_ckeditor = isset($extra['ckeditor']) ? $extra['ckeditor'] : false;
            $extra['class'] = ($use_ckeditor ? $form->CLASS_CKEDITOR . ' ' : '') . $form->CLASS_FORM_CONTROL . $form->_prepare_css_class('', $r[$extra['name']], $extra);
            if ($form->_params['no_label']) {
                $extra['desc'] = '';
            }
            $extra = $form->_input_assign_params_from_validate($extra);
            $attrs_names = ['id', 'name', 'placeholder', 'contenteditable', 'class', 'style', 'cols', 'rows', 'title', 'required', 'size', 'disabled', 'readonly', 'autocomplete', 'autofocus'];
            return $form->_row_html('<textarea' . _attrs($extra, $attrs_names) . '>' . ( ! isset($extra['no_escape']) ? _htmlchars($extra['value']) : $extra['value']) . '</textarea>', $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * Embedding ckeditor (http://ckeditor.com/) with kcfinder (http://kcfinder.sunhater.com/).
     * Best way to include it into project:.
     *
     * git submodule add https://github.com/yfix/ckeditor-releases.git www/ckeditor/ && cd www/ckeditor/ && git checkout latest/full
     * git submodule add https://github.com/yfix//kcfinder.git www/kcfinder
     *
     * 'www/' usually means PROJECT_PATH inside project working copy.
     * P.S. You can use free CDN for ckeditor as alternate solution: <script src="//cdnjs.cloudflare.com/ajax/libs/ckeditor/4.0.1/ckeditor.js"></script>
     * @param mixed $extra
     * @param mixed $replace
     */
    public function _ckeditor_html($extra = [], $replace = [])
    {
        return _class('form2_ckeditor', 'classes/form2/')->_ckeditor_html($extra, $replace, $this);
    }

    /**
     * @param mixed $extra
     * @param mixed $replace
     */
    public function _tinymce_html($extra = [], $replace = [])
    {
        return _class('form2_tinymce', 'classes/form2/')->_tinymce_html($extra, $replace, $this);
    }

    /**
     * @param mixed $extra
     * @param mixed $replace
     */
    public function _ace_editor_html($extra = [], $replace = [])
    {
        return _class('form2_ace_editor', 'classes/form2/')->_ace_editor_html($extra, $replace, $this);
    }

    /**
     * Just hidden input.
     * @param mixed $name
     * @param mixed $extra
     * @param mixed $replace
     */
    public function hidden($name, $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: $name;
        $func = function ($extra, $r, $form) {
            $extra['id'] = $form->_prepare_id($extra);
            $extra['value'] = $form->_prepare_value($extra, $r, $form->_params);
            $extra['type'] = 'hidden';

            $attrs_names = ['type', 'id', 'name', 'value', 'data'];
            return '<input' . _attrs($extra, $attrs_names) . '>';
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function text($name, $desc = '', $extra = [], $replace = [])
    {
        $extra['type'] = 'text';
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function password($name = '', $desc = '', $extra = [], $replace = [])
    {
        $extra['type'] = 'password';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! $name) {
            $name = 'password';
        }
        $extra['prepend'] = isset($extra['prepend']) ? $extra['prepend'] : '<i class="' . $this->CLASS_ICON_PSWD . '"></i>';
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function file($name, $desc = '', $extra = [], $replace = [])
    {
        $extra['type'] = 'file';
        $this->_params['for_upload'] = true;
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * Image upload.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function image($name = '', $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra += $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $this->_params['for_upload'] = true;
        $extra['name'] = $extra['name'] ?: ($name ?: 'image');
        $extra['desc'] = $this->_prepare_desc($extra, $desc);
        $extra['type'] = 'file';
        return $this->input($extra['name'], $extra['desc'], $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function button($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! $desc) {
            $desc = ucfirst(str_replace('_', ' ', $name));
        }
        $extra['type'] = 'button';
        if ( ! isset($extra['value'])) {
            $extra['value'] = $desc;
        }
        $extra['value'] = t($extra['value']);
        if ( ! $extra['class']) {
            $extra['class'] = $this->CLASS_BTN_DEFAULT;
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * Custom.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function login($name = '', $desc = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = $extra['type'] ?: 'text';
        $extra['prepend'] = isset($extra['prepend']) ? $extra['prepend'] : '<i class="' . $this->CLASS_ICON_LOGIN . '"></i>';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'login';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function email($name = '', $desc = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'email';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'email';
        }
        $extra['prepend'] = isset($extra['prepend']) ? $extra['prepend'] : '<i class="' . $this->CLASS_ICON_EMAIL . '"></i>';
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function number($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'number';
        $extra['maxlength'] = isset($extra['maxlength']) ? $extra['maxlength'] : '10';
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function integer($name, $desc = '', $extra = [], $replace = [])
    {
        return $this->number($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function float($name, $desc = '', $extra = [], $replace = [])
    {
        return $this->decimal($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function decimal($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['step'] = $extra['step'] ?: '0.01';
        return $this->number($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function money($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['prepend'] = isset($extra['prepend']) ? $extra['prepend'] : ($this->_params['currency'] ?: '<i class="' . $this->CLASS_ICON_CURRENCY . '"></i>');
        $extra['append'] = isset($extra['append']) ? $extra['append'] : ''; // '.00';
        $extra['maxlength'] = isset($extra['maxlength']) ? $extra['maxlength'] : '8';
        return $this->decimal($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function price($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['min'] = $extra['min'] ?: '0';
        return $this->money($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function url($name = '', $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'url';
        $extra['prepend'] = isset($extra['prepend']) ? $extra['prepend'] : 'url';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'url';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function color($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'color';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'color';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function date($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'date';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'date';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function datetime($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'datetime';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'datetime';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function datetime_local($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'datetime-local';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'datetime_local';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function month($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'month';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'month';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function range($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'range';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'range';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function search($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'search';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'search';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function tel($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'tel';
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'tel';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * Alias.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function phone($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'tel';
        if (is_array($name)) {
            $extra += $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'phone';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function time($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'time';
        if (is_array($name)) {
            $extra += $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'time';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * HTML5.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function week($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['type'] = 'week';
        if (is_array($name)) {
            $extra += $name;
            $name = '';
        }
        if ( ! $name) {
            $name = 'week';
        }
        return $this->input($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function active_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra += $name;
            $desc = '';
        }
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: ($name ?: 'active');
        $extra['desc'] = $this->_prepare_desc($extra, $desc);
        $func = function ($extra, $r, $form) {
            $form->_prepare_inline_error($extra);
            $as_btn_group = isset($extra['btn_group']) ? $extra['btn_group'] : $form->CONF_BOXES_USE_BTN_GROUP;
            if ($as_btn_group) {
                $extra['class_add_controls'] = 'btn-group';
                $extra['controls']['data-toggle'] = 'buttons';
            }
            if ( ! $extra['items']) {
                $data_handler = $as_btn_group ? 'pair_active_btn_group' : 'pair_active';
                $extra['items'] = main()->get_data($data_handler);
            }
            $extra['values'] = $extra['items'];
            $extra['desc'] = ! $form->_params['no_label'] ? $extra['desc'] : '';
            $extra['id'] = $form->_prepare_id($extra);
            if ( ! isset($extra['horizontal'])) {
                $extra['horizontal'] = true;
            }
            $extra['selected'] = isset($extra['selected']) ? $extra['selected'] : $r[$extra['name']];
            if (isset($form->_params['selected'])) {
                $extra['selected'] = $form->_params['selected'][$extra['name']];
            }
            $extra = $form->_input_assign_params_from_validate($extra);
            if ($this->_params['filter'] && ! $extra['renderer']) {
                $extra['no_label'] = 1;
                $extra['label_right'] = 1;
                $extra['renderer'] = 'button_yes_no_box';
            }
            $renderer = $extra['renderer'] ?: 'radio_box';
            return $form->_row_html(html()->$renderer($extra), $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function allow_deny_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! isset($this->_pair_allow_deny)) {
            $this->_pair_allow_deny = main()->get_data('pair_allow_deny');
        }
        $extra['items'] = $this->_pair_allow_deny;
        if ($this->_params['filter'] || $extra['v2']) {
            $extra['no_label'] = 1;
            $extra['label_right'] = 1;
            $extra['renderer'] = 'button_allow_deny_box';
        }
        $func = 'active_box';
        return $this->$func($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function yes_no_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! isset($this->_pair_yes_no)) {
            $this->_pair_yes_no = main()->get_data('pair_yes_no');
        }
        $extra['items'] = $this->_pair_yes_no;
        if ($this->_params['filter'] || $extra['v2']) {
            $extra['no_label'] = 1;
            $extra['label_right'] = 1;
            $extra['renderer'] = 'button_yes_no_box';
        }
        $func = 'active_box';
        return $this->$func($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $data
     * @param mixed $extra
     * @param mixed $replace
     */
    public function order_box($name = '', $data = [], $extra = [], $replace = [])
    {
        $data = $data ?: [
            'asc' => 'Ascending',
            'desc' => 'Descending',
        ];
        $extra['horizontal'] = isset($extra['horizontal']) ? $extra['horizontal'] : 1;
        return $this->radio_box($name ?: 'order_direction', t($data), $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $value
     * @param mixed $extra
     * @param mixed $replace
     */
    public function submit($name = '', $value = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if (is_array($value)) {
            $extra = (array) $extra + $value;
            $value = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: $name;
        $extra['value'] = isset($extra['value']) ? $extra['value'] : ($value ?: 'Save');
        $extra['id'] = $extra['id'] ?: ($extra['name'] ?: strtolower($extra['value']));
        $func = function ($extra, $r, $form) {
            $form->_prepare_inline_error($extra);
            $extra['link_url'] = $extra['link_url'] ? (isset($r[$extra['link_url']]) ? $r[$extra['link_url']] : $extra['link_url']) : '';
            if (false === strpos($extra['link_url'], '/')) {
                $extra['link_url'] = '';
            }
            $extra['link_name'] = $extra['link_name'] ?: '';
            $extra['class'] = $extra['class'] ?: $form->CLASS_BTN_SUBMIT . $form->_prepare_css_class('', $r[$extra['name']], $extra);
            $extra['value'] = $extra['value'];
            $extra['type'] = 'submit';
            $button_text = $extra['desc'];
            $extra['desc'] = '';
            $extra['buttons_controls'] = true;
            if ($this->_params['filter'] && ! isset($extra['sizing'])) {
                $extra['sizing'] = 'sm';
            }
            // Supported: xs, sm, md, lg
            if ($extra['sizing']) {
                $extra['class'] .= ' btn-' . $extra['sizing'];
                $extra['link_class'] .= ' btn-' . $extra['sizing'];
            }
            $attrs_names = ['type', 'name', 'id', 'class', 'style', 'value', 'disabled', 'target'];
            if ( ! $extra['as_input']) {
                $icon = ($extra['icon'] ? '<i class="' . $extra['icon'] . '"></i> ' : '');
                $value = ( ! isset($extra['no_escape']) ? _htmlchars($extra['value']) : $extra['value']);
                $button_text = $icon . t($button_text ?: $value);
                return $form->_row_html('<button' . _attrs($extra, $attrs_names) . '>' . $button_text . '</button>', $extra, $r);
            }
            return $form->_row_html('<input' . _attrs($extra, $attrs_names) . '>', $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function save($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! isset($extra['icon'])) {
            $extra['icon'] = $this->CLASS_ICON_SAVE;
        }
        return $this->submit($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function save_and_back($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! $name) {
            $name = 'back_link';
            $r = $replace ? $replace : $this->_replace;
            if ( ! isset($r[$name]) && isset($r['back_url'])) {
                $name = 'back_url';
            }
        }
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['link_url'] = $name;
        $extra['link_name'] = $desc ?: 'Back';
        if ( ! isset($extra['icon'])) {
            $extra['icon'] = $this->CLASS_ICON_SAVE;
        }
        return $this->submit($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function save_and_clear($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! $name) {
            $name = 'clear_link';
            $r = $replace ? $replace : $this->_replace;
            if ( ! isset($r[$name]) && isset($r['clear_url'])) {
                $name = 'clear_url';
            }
        }
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['link_url'] = $name;
        $extra['link_name'] = $desc ?: 'Clear';
        if ( ! isset($extra['icon'])) {
            $extra['icon'] = $this->CLASS_ICON_SAVE;
        }
        return $this->submit($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $value
     * @param mixed $extra
     * @param mixed $replace
     */
    public function preview($name = '', $value = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if (is_array($value)) {
            $extra = (array) $extra + $value;
            $value = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $name = $extra['name'] ?: $name;
        $extra['desc'] = $extra['desc'] ?: 'Preview';
        $extra['class_add'] = $extra['class_add'] ?: 'preview';
        if ( ! $name) {
            $name = 'preview';
        }
        if ( ! isset($extra['icon'])) {
            $extra['icon'] = $this->CLASS_ICON_PREVIEW;
        }
        return $this->submit($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function info($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: $name;
        $extra['desc'] = $this->_prepare_desc($extra, $desc);
        $func = function ($extra, $r, $form) {
            $form->_prepare_inline_error($extra);
            $extra['desc'] = ! $extra['no_label'] && ! $form->_params['no_label'] ? $extra['desc'] : '';

            $value = $r[$extra['name']] ?: $extra['value'];
            if (is_array($extra['data'])) {
                if (isset($extra['data'][$value])) {
                    $value = $extra['data'][$value];
                } elseif (isset($extra['data'][$extra['name']])) {
                    $value = $extra['data'][$extra['name']];
                }
            }
            $value = ! isset($extra['no_escape']) ? _htmlchars($value) : $value;
            if ( ! $extra['no_translate']) {
                $extra['desc'] = t($extra['desc']);
                $value = t($value);
            }
            if ($extra['no_text']) {
                $value = '';
            }
            if ($extra['link']) {
                if (MAIN_TYPE_ADMIN && main()->ADMIN_GROUP != 1 && ! _class('admin_methods')->_admin_link_is_allowed($extra['link'])) {
                    $extra['link'] = '';
                }
            }
            $icon = $extra['icon'] ? '<i class="' . $extra['icon'] . '"></i> ' : '';
            $content = '';
            if ($extra['link']) {
                if ($extra['rewrite']) {
                    $extra['link'] = url($extra['link']);
                }
                $extra['class'] = $extra['class'] ?: $form->CLASS_BTN_MINI;
                $extra['class'] = $form->_prepare_css_class($extra['class'], $r[$extra['name']], $extra);
                $extra['href'] = $extra['link'];
                $extra['title'] = $extra['title'] ?: $extra['desc'] ?: $extra['name'];
                $attrs_names = ['href', 'name', 'class', 'style', 'disabled', 'target', 'alt', 'title'];
                $content = '<a' . _attrs($extra, $attrs_names) . '>' . $icon . $value . '</a>';
            } else {
                $extra['class'] = $extra['class'] ?: $form->CLASS_LABEL_INFO;
                $content = '<span class="' . $form->_prepare_css_class($extra['class'], $r[$extra['name']], $extra) . '">' . $icon . $value . '</span>';
            }
            return $form->_row_html($content, $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function user_info($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_info', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function admin_info($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_info', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $format
     * @param mixed $extra
     * @param mixed $replace
     */
    public function info_date($name = '', $format = '', $extra = [], $replace = [])
    {
        $r = (array) $this->_replace + (array) $replace;
        if (is_array($format)) {
            $extra = (array) $extra + $format;
            $format = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['format'] = $extra['format'] ?: $format;
        $replace[$name] = _format_date($r[$name], $extra['format']);
        $this->_replace[$name] = $replace[$name];
        return $this->info($name, $format, $extra, $replace);
    }

    /**
     * Mostly for {form_row()}, as it can be emulated from php easily.
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function info_link($name = '', $link = '', $extra = [], $replace = [])
    {
        $r = (array) $this->_replace + (array) $replace;
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['link'] = $extra['link'] ?: ($link ?: $r[$name]);
        return $this->info($name, '', $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $extra
     * @param mixed $replace
     */
    public function info_lang($name = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: $name;
        $func = function ($extra, $r, $form) {
            $extra['desc'] = ! $extra['no_label'] && ! $form->_params['no_label'] ? $extra['desc'] : '';
            $value = $r[$extra['name']] ?: $extra['value'];
            $lang = $value;
            asset('bfh-select');
            if ( ! isset($form->lang_def_country)) {
                $form->lang_def_country = main()->get_data('lang_def_country');
            }
            $content = html()->icon('bfh-flag-' . $form->lang_def_country[$lang], strtoupper($lang));
            return $form->_row_html($content, $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function link($name = '', $link = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if (is_array($link)) {
            $extra = (array) $extra + $link;
            $link = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['link'] = isset($extra['link']) ? $extra['link'] : $link;
        $extra['value'] = isset($extra['value']) ? $extra['value'] : $name;
        if ( ! $extra['desc']) {
            $extra['no_label'] = 1;
        }
        return $this->info($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     * @param mixed $func_html_control
     */
    public function _html_control($name, $values, $extra = [], $replace = [], $func_html_control = '')
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if (is_array($name)) {
            $extra = (array) $extra + $name;
        } else {
            $extra['name'] = $name;
        }
        $extra['desc'] = $this->_prepare_desc($extra, $desc);
        $extra['values'] = isset($extra['values']) ? $extra['values'] : (array) $values; // Required
        $extra['func_html_control'] = $extra['func_html_control'] ?: $func_html_control;
        $func = function ($extra, $r, $form) {
            $form->_prepare_inline_error($extra);
            $extra['edit_link'] = $extra['edit_link'] ? (isset($r[$extra['edit_link']]) ? $r[$extra['edit_link']] : $extra['edit_link']) : '';
            $extra['selected'] = $form->_prepare_selected($extra['name'], $extra, $r);
            $extra['id'] = $extra['name'];
            $extra = $form->_input_assign_params_from_validate($extra);
            if ($this->_params['filter']) {
                $extra['class_add'] .= ' input-sm';
            }
            $func = $extra['func_html_control'];
            $content = _class('html')->$func($extra);
            if ($extra['no_label'] || $form->_params['no_label']) {
                $extra['desc'] = '';
            }
            if ($extra['hide_empty'] && ! strlen($content)) {
                return '';
            }
            return $form->_row_html($content, $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function box($name, $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: $name;
        $extra['desc'] = $this->_prepare_desc($extra, $desc);
        $func = function ($extra, $r, $form) {
            $form->_prepare_inline_error($extra);
            $extra['edit_link'] = $extra['edit_link'] ? (isset($r[$extra['edit_link']]) ? $r[$extra['edit_link']] : $extra['edit_link']) : '';
            $extra['values'] = isset($extra['values']) ? $extra['values'] : (array) $values; // Required
            $extra['selected'] = $form->_prepare_selected($extra['name'], $extra, $r);
            $extra['id'] = $form->_prepare_id($extra);
            $extra = $form->_input_assign_params_from_validate($extra);

            return $form->_row_html($r[$extra['name']], $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $link
     * @param mixed $replace
     */
    public function box_with_link($name, $desc = '', $link = '', $replace = [])
    {
        return $this->box($name, $desc, ['edit_link' => $link], $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function select_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'select_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function multi_select($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'multi_select_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function multi_select_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'multi_select_box');
    }

    /**
     * @param mixed $name
     * @param mixed $value
     * @param mixed $extra
     * @param mixed $replace
     */
    public function check_box($name, $value = '', $extra = [], $replace = [])
    {
        if (is_array($value)) {
            $extra = (array) $extra + $value;
            $value = '';
        }
        $as_btn_group = isset($extra['btn_group']) ? $extra['btn_group'] : $this->CONF_BOXES_USE_BTN_GROUP;
        if ($as_btn_group) {
            $extra['class_add_controls'] = 'btn-group';
            $extra['controls']['data-toggle'] = 'buttons';
            $extra['class_add_label_checkbox'] = 'btn btn-xs btn-default';
            $extra['desc'] = '<span><i class="icon icon-check fa fa-check"></i></span> ' . $extra['desc'];
        }
        return $this->_html_control($name, $value, $extra, $replace, 'check_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function multi_check_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'multi_check_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function radio_box($name, $values, $extra = [], $replace = [])
    {
        $as_btn_group = isset($extra['btn_group']) ? $extra['btn_group'] : $this->CONF_BOXES_USE_BTN_GROUP;
        if ($as_btn_group) {
            $extra['class_add_controls'] = 'btn-group';
            $extra['controls']['data-toggle'] = 'buttons';
            $extra['class_add_label_radio'] = 'btn btn-xs btn-default';
        }
        return $this->_html_control($name, $values, $extra, $replace, 'radio_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function div_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'div_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function list_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'list_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function button_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'button_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function button_split_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'button_split_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function button_check_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'button_check_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function button_radio_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'button_radio_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function button_yes_no_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'button_yes_no_box');
    }

    /**
     * @param mixed $name
     * @param null|mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function select2_box($name, $values = null, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'select2_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function chosen_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'chosen_box');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function image_select_box($name, $values, $extra = [], $replace = [])
    {
        return $this->_html_control($name, $values, $extra, $replace, 'image_select_box');
    }

    /**
     * @param mixed $name
     * @param null|mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function user_select_box($name, $values = null, $extra = [], $replace = [])
    {
        _class('form_api')->{__FUNCTION__}($name, $values, $extra, $replace);
        return $this->_html_control($name, $values, $extra, $replace, 'select2_box');
    }

    /**
     * @param mixed $name
     * @param mixed $extra
     * @param mixed $replace
     */
    public function phone_box($name = '', $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
        } else {
            $extra['name'] = $name;
        }
        if ( ! $extra['name']) {
            $name = $extra['name'] = 'phone';
        }
        $func = function ($extra, $r, $form) {
            asset('jquery-formvalidation');
            jquery('
                var yf_phone_callback = function(value, validator, $field) {
                    var isValid = value === "" || $field.intlTelInput("isValidNumber"),
                        err     = $field.intlTelInput("getValidationError"),
                        message = null;
                    switch (err) {
                        case intlTelInputUtils.validationError.INVALID_COUNTRY_CODE:
                            message = "' . t('The country code is not valid') . '";
                            break;
                        case intlTelInputUtils.validationError.TOO_SHORT:
                            message = "' . t('The phone number is too short') . '";
                            break;
                        case intlTelInputUtils.validationError.TOO_LONG:
                            message = "' . t('The phone number is too long') . '";
                            break;
                        case intlTelInputUtils.validationError.NOT_A_NUMBER:
                            message = "' . t('The value is not a number') . '";
                            break;
                        default:
                            message = "' . t('The phone number is not valid') . '";
                            break;
                    }
                    return {
                        valid: isValid,
                        message: message
                    };
                }
                var form = $("#' . addslashes($extra['name']) . '").closest("form")
                form.formValidation({
                    framework: "bootstrap",
                    fields: {
                        "' . addslashes($extra['name']) . '": {
                            validators: {
                                callback: {
                                    callback: yf_phone_callback,
                                }
                            }
                        }
                    }
                })
                // Revalidate the number when changing the country
                .on("click", ".country-list", function() {
                    form.formValidation("revalidateField", "' . addslashes($extra['name']) . '");
                });
            ');
            $form->_prepare_inline_error($extra);
            $extra['edit_link'] = $extra['edit_link'] ? (isset($r[$extra['edit_link']]) ? $r[$extra['edit_link']] : $extra['edit_link']) : '';
            $extra['selected'] = $form->_prepare_selected($extra['name'], $extra, $r);
            $extra['value'] = &$extra['selected'];
            $extra['id'] = $extra['id'] ?: $extra['name'];

            $extra = $form->_input_assign_params_from_validate($extra);
            $content = _class('html')->phone_box($extra);
            if ($extra['no_label'] || $form->_params['no_label']) {
                $extra['desc'] = '';
            }
            if ($extra['hide_empty'] && ! strlen($content)) {
                return '';
            }
            return $form->_row_html($content, $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function date_box($name = '', $values = [], $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! $name) {
            $name = 'date';
        }
        return $this->_html_control($name, $values, $extra, $replace, 'date_box2');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function time_box($name = '', $values = [], $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! $name) {
            $name = 'time';
        }
        return $this->_html_control($name, $values, $extra, $replace, 'time_box2');
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function datetime_box($name = '', $values = [], $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! $name) {
            $name = 'datetime';
        }
        if ( ! isset($extra['show_what'])) {
            $extra['show_what'] = 'ymdhis';
        }
        return $this->date_box($name, $values, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $values
     * @param mixed $extra
     * @param mixed $replace
     */
    public function birth_box($name = '', $values = [], $extra = [], $replace = [])
    {
        if (is_array($name)) {
            $extra = (array) $extra + $name;
            $name = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! $name) {
            $name = 'birth';
        }
        return $this->date_box($name, $values, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function country_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function region_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function city_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function currency_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function locale_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function language_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function timezone_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function icon_select_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function method_select_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function user_method_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['for_type'] = 'user';
        return $this->method_select_box($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function admin_method_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['for_type'] = 'admin';
        return $this->method_select_box($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function template_select_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function user_template_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['for_type'] = 'user';
        return $this->template_select_box($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function admin_template_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['for_type'] = 'admin';
        return $this->template_select_box($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function location_select_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_boxes', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function user_location_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['for_type'] = 'user';
        return $this->location_select_box($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function admin_location_box($name = '', $desc = '', $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['for_type'] = 'admin';
        return $this->location_select_box($name, $desc, $extra, $replace);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function google_maps($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_google_maps', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    public function upload($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_upload', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function captcha($name = '', $desc = '', $extra = [], $replace = [])
    {
        if (is_array($desc)) {
            $extra = (array) $extra + $desc;
            $desc = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: ($name ?: 'captcha');
        $extra['desc'] = $this->_prepare_desc($extra, $desc);
        $func = function ($extra, $r, $form) {
            $form->_prepare_inline_error($extra);
            $extra['id'] = $form->_prepare_id($extra);
            $extra['required'] = true;
            $extra['value'] = $r['captcha'];
            $extra['input_attrs'] = _attrs($extra, ['class', 'style', 'placeholder', 'pattern', 'disabled', 'required', 'autocomplete', 'accept', 'value', 'size']);
            $extra = $form->_input_assign_params_from_validate($extra);
            return $form->_row_html(_class('captcha')->show_block(url('/dynamic/captcha_image'), $extra), $extra, $r);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function ui_range($name, $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_ui_range', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * Custom function, useful to insert custom html and not breaking form chain.
     * @param mixed $name
     * @param mixed $func
     * @param mixed $extra
     * @param mixed $replace
     */
    public function func($name, $func, $extra = [], $replace = [])
    {
        if (is_array($func)) {
            $extra = (array) $extra + $func;
            $func = '';
        }
        if ( ! is_array($extra)) {
            $extra = [];
        }
        if ( ! $func) {
            if (isset($extra['callback'])) {
                $func = $extra['callback'];
            } elseif (isset($extra['function'])) {
                $func = $extra['function'];
            } else {
                $func = $extra['func'];
            }
        }
        $extra['name'] = $extra['name'] ?: $name;
        $extra['desc'] = $this->_prepare_desc($extra);
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $custom_fields
     * @param mixed $extra
     * @param mixed $replace
     */
    public function custom_fields($name, $custom_fields, $extra = [], $replace = [])
    {
        if ( ! is_array($extra)) {
            $extra = [];
        }
        $extra['name'] = $extra['name'] ?: $name;
        $extra['custom_fields'] = $custom_fields;
        $func = function ($extra, $r, $form) {
            $custom_fields = explode(',', $extra['custom_fields']);
            $sub_array_name = $extra['sub_array'] ?: 'custom';
            $custom_info = _attrs_string2array($r[$extra['name']]);

            $body = [];
            $form->_chained_mode = false;
            foreach ((array) $custom_fields as $field_name) {
                if (empty($field_name)) {
                    continue;
                }
                $str = _class('html')->input([
                    'id' => 'custom_' . $field_name . '_' . $r['id'],
                    'name' => $sub_array_name . '[' . $field_name . ']', // Example: custom[color]
                    'desc' => $field_name,
                    'value' => $custom_info[$field_name],
                ]);
                $desc = ucfirst(str_replace('_', ' ', $field_name)) . ' [Custom]';
                $body[] = $form->container($str, $desc);
            }
            $form->_chained_mode = true;
            return implode(PHP_EOL, $body);
        };
        if ($this->_chained_mode) {
            $this->_body[] = ['func' => $func, 'extra' => $extra, 'replace' => $replace, 'name' => __FUNCTION__];
            return $this;
        }
        return $func((array) $extra + (array) $this->_extra, (array) $replace + (array) $this->_replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function stars($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_stars', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * Star selector, got from http://fontawesome.io/examples/#custom.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function stars_select($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_stars', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * Datetimepicker, src: http://tarruda.github.io/bootstrap-datetimepicker/
     * params :  no_date // no date picker
     *            no_time // no time picker.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function datetime_select($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_datetime', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * Daterange picker (Alias).
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function daterange($name = '', $desc = '', $extra = [], $replace = [])
    {
        return $this->daterange_select($name, $desc, $extra, $replace);
    }

    /**
     * Daterange picker.
     * @param mixed $name
     * @param mixed $desc
     * @param mixed $extra
     * @param mixed $replace
     */
    public function daterange_select($name = '', $desc = '', $extra = [], $replace = [])
    {
        return _class('form2_daterange', 'classes/form2/')->{__FUNCTION__}($name, $desc, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     */
    public function _remove_items_by_name($name = '')
    {
        if (is_array($name)) {
            $func = __FUNCTION__;
            foreach ((array) $name as $_name) {
                $this->$func($_name);
            }
            return true;
        }
        if ( ! $name) {
            return false;
        }
        foreach ((array) $this->_body as $k => $v) {
            if ($v['name'] == $name) {
                unset($this->_body[$k]);
            }
        }
        return true;
    }

    /**
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function tbl_link($name, $link, $extra = [], $replace = [])
    {
        return _class('form2_tbl_funcs', 'classes/form2/')->{__FUNCTION__}($name, $link, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function tbl_link_add($name = '', $link = '', $extra = [], $replace = [])
    {
        return _class('form2_tbl_funcs', 'classes/form2/')->{__FUNCTION__}($name, $link, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function tbl_link_edit($name = '', $link = '', $extra = [], $replace = [])
    {
        return _class('form2_tbl_funcs', 'classes/form2/')->{__FUNCTION__}($name, $link, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function tbl_link_delete($name = '', $link = '', $extra = [], $replace = [])
    {
        return _class('form2_tbl_funcs', 'classes/form2/')->{__FUNCTION__}($name, $link, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function tbl_link_clone($name = '', $link = '', $extra = [], $replace = [])
    {
        return _class('form2_tbl_funcs', 'classes/form2/')->{__FUNCTION__}($name, $link, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function tbl_link_view($name = '', $link = '', $extra = [], $replace = [])
    {
        return _class('form2_tbl_funcs', 'classes/form2/')->{__FUNCTION__}($name, $link, $extra, $replace, $this);
    }

    /**
     * @param mixed $name
     * @param mixed $link
     * @param mixed $extra
     * @param mixed $replace
     */
    public function tbl_link_active($name = '', $link = '', $extra = [], $replace = [])
    {
        return _class('form2_tbl_funcs', 'classes/form2/')->{__FUNCTION__}($name, $link, $extra, $replace, $this);
    }

    /**
     * Form validation handler.
     * Here we have special rule, called __form_id__ , it is used to track which form need to be validated from $_POST.
     * @param mixed $validate_rules
     * @param mixed $post
     * @param mixed $extra
     */
    public function validate($validate_rules = [], $post = [], $extra = [])
    {
        $this->_validate_prepare($validate_rules, $extra);

        $func = function ($validate_rules, $post, $extra, $form) {
            $form->_validate_prepare($validate_rules, $extra);
            $form_id = $form->_get_form_id();
            // Do not do validation until data is empty (usually means that form is just displayed and we wait user input)
            $data = (array) ( ! empty($post) ? $post : $_POST);
            foreach ((array) $data as $k => $v) {
                // We fix case when array key is present but have empty values like this: ['cat_id' => []]
                if (is_array($v) && ! empty($v)) {
                    // Convert multi-dimensional arrays into single-dimensional array dot notation: array('k1' => ['k2' => 'v2'])  ==>  ['k1.k2' => 'v2']
                    $dots = array_dot($v);
                    if ( ! empty($dots)) {
                        $data[$k] = $dots;
                    }
                }
            }
            if (empty($data)) {
                return $form;
            }
            // We need this to validate only correct form on page, where there can be several forms with validation at once
            if ($form_id && $data[$this->CONF_FORM_ID_FIELD] != $form_id) {
                return $form;
            }
            $on_before_validate = isset($extra['on_before_validate']) ? $extra['on_before_validate'] : $form->_on['on_before_validate'];
            if (is_callable($on_before_validate)) {
                $on_before_validate($form->_validate_rules, $data, $this);
            }
            $events = _class('core_events');
            $events->fire('form.before_validate', [$form->_validate_rules, $data]);
            // Processing of prepared rules
            $validate_ok = $form->_validate_rules_process($form->_validate_rules, $data, $extra);
            if ($validate_ok) {
                $form->_validate_ok = true;
                $on_validate_ok = isset($extra['on_validate_ok']) ? $extra['on_validate_ok'] : $form->_on['on_validate_ok'];
                if (is_callable($on_validate_ok)) {
                    $on_validate_ok($data, $extra, $form->_validate_rules, $this);
                }
                $events->fire('form.validate_ok', [$form->_validate_rules, $data, $extra]);
            } else {
                $form->_validate_ok = false;
                $on_validate_error = isset($extra['on_validate_error']) ? $extra['on_validate_error'] : $form->_on['on_validate_error'];
                if (is_callable($on_validate_error)) {
                    $on_validate_error($data, $extra, $form->_validate_rules, $this);
                }
                $events->fire('form.validate_error', [$form->_validate_rules, $data, $extra]);
            }
            $on_after_validate = isset($extra['on_after_validate']) ? $extra['on_after_validate'] : $form->_on['on_after_validate'];
            if (is_callable($on_after_validate)) {
                $on_after_validate($form->_validate_ok, $form->_validate_rules, $data, $extra, $this);
            }
            $events->fire('form.after_validate', [$form->_validate_ok, $form->_validate_rules, $data, $extra]);
            $form->_validated_fields = $data;
        };
        if ($this->_chained_mode) {
            $this->_validate = [
                'func' => $func,
                'extra' => $extra,
                'post' => $post,
                'validate_rules' => $validate_rules,
            ];
            return $this;
        }
        return $this;
    }

    /**
     * @param mixed $validate_rules
     * @param mixed $extra
     */
    public function _validate_prepare($validate_rules = [], $extra = [])
    {
        $form_global_validate = isset($this->_params['validate']) ? $this->_params['validate'] : (isset($this->_replace['validate']) ? $this->_replace['validate'] : []);
        foreach ((array) $form_global_validate as $name => $rules) {
            $this->_validate_rules[$name] = $rules;
        }
        foreach ((array) $this->_body as $v) {
            $_extra = $v['extra'];
            if (isset($_extra['validate']) && isset($_extra['name'])) {
                $this->_validate_rules[$_extra['name']] = $_extra['validate'];
            }
        }
        foreach ((array) $validate_rules as $name => $rules) {
            $this->_validate_rules[$name] = $rules;
        }
        $form_id = '';
        if (isset($this->_validate_rules[$this->CONF_FORM_ID_FIELD])) {
            $form_id = $this->_validate_rules[$this->CONF_FORM_ID_FIELD];
            unset($this->_validate_rules[$this->CONF_FORM_ID_FIELD]);
        } elseif (isset($this->_params[$this->CONF_FORM_ID_FIELD])) {
            $form_id = $this->_params[$this->CONF_FORM_ID_FIELD];
            unset($this->_params[$this->CONF_FORM_ID_FIELD]);
        }
        if ($form_id) {
            $this->_form_id = $form_id;
        } else {
            $form_id = $this->_get_form_id();
        }
        $this->_set_hidden_form_id($form_id);

        $this->_validate_rules = $this->_validate_rules_cleanup($this->_validate_rules);
        // Prepare array of rules by form method for quick access
        if ($this->_validate_rules) {
            foreach ((array) $this->_validate_rules as $item => $rules) {
                foreach ((array) $rules as $rule) {
                    if (is_string($rule[0])) {
                        $this->_validate_rules_names[$item][$rule[0]] = $rule[1] ?: true;
                    }
                }
            }
        }
    }

    /**
     * @param mixed $validate_rules
     */
    public function _validate_rules_process($validate_rules = [], &$data)
    {
        $validate_ok = true;
        foreach ((array) $validate_rules as $name => $rules) {
            $is_required = false;
            foreach ((array) $rules as $rule) {
                if (is_string($rule[0]) && substr($rule[0], 0, strlen('required')) === 'required') {
                    $is_required = true;
                    break;
                }
            }
            foreach ((array) $rules as $rule) {
                $is_ok = true;
                $error_msg = '';
                $func = $rule[0];
                $param = $rule[1];
                // PHP pure function, from core or user
                if (is_string($func) && function_exists($func)) {
                    $data[$name] = $this->_apply_existing_func($func, $data[$name]);
                } elseif (is_callable($func)) {
                    $is_ok = $func($data[$name], null, $data, $error_msg);
                } else {
                    if (is_array($data[$name]) && ! empty($data[$name])) {
                        foreach ($data[$name] as $k => $v) {
                            $is_ok = _class('validate')->$func($v, ['param' => $param], $data, $error_msg, ['field' => $name]);
                            if ( ! $is_ok) {
                                break;
                            }
                        }
                    } else {
                        $is_ok = _class('validate')->$func($data[$name], ['param' => $param], $data, $error_msg, ['field' => $name]);
                    }
                    if ( ! $is_ok && empty($error_msg)) {
                        $desc = $this->_find_field_desc($name) ?: $name;
                        $error_param = $this->_find_field_desc($param) ?: $param;
                        // Search for custom error message, also able to divide error by validate func
                        $error_msg = $this->_find_custom_validate_error($name, $func);
                        if ($error_msg) {
                            $error_msg = str_replace(['%field', '%param'], [$desc, $error_param], $error_msg);
                        } else {
                            // Default error message
                            $error_msg = t('form_validate_' . $func, ['%field' => $desc, '%param' => $error_param]);
                        }
                    }
                }
                // In this case we do not track error if field is empty and not required
                if ( ! $is_ok && ! $is_required && ! strlen($data[$name])) {
                    $is_ok = true;
                    $error_msg = '';
                }
                if ( ! $is_ok) {
                    $validate_ok = false;
                    if ( ! $error_msg) {
                        $error_msg = 'Wrong field ' . $name;
                    }
                    _re($error_msg, $name);
                    // In case when we see any validation rule is not OK - we stop checking further for this field
                    continue 2;
                }
            }
        }
        return $validate_ok;
    }

    /**
     * @param mixed $func
     * @param mixed $data
     */
    public function _apply_existing_func($func, $data)
    {
        if (is_array($data)) {
            $self = __FUNCTION__;
            foreach ($data as $k => $v) {
                $data[$k] = $this->$self($func, $v);
            }
            return $data;
        }
        return $func($data);
    }

    /**
     * @param mixed $name
     */
    public function _find_field_desc($name)
    {
        if ( ! strlen($name)) {
            return '';
        }
        $desc = $name;
        foreach ((array) $this->_body as $a) {
            if ( ! isset($a['extra']) || ! strlen($a['extra']['desc'])) {
                continue;
            }
            // Now we also support array elements descriptions searching
            if ($a['extra']['name'] != $name && $a['extra']['name'] != $name . '[]') {
                continue;
            }
            $desc = $a['extra']['desc'];
            break;
        }
        return $desc;
    }

    /**
     * @param mixed $name
     * @param mixed $func
     */
    public function _find_custom_validate_error($name, $func)
    {
        if ( ! strlen($name)) {
            return '';
        }
        $custom_error = '';
        foreach ((array) $this->_body as $a) {
            if ( ! isset($a['extra']) || ! isset($a['extra']['validate_error'])) {
                continue;
            }
            // Now we also support array elements descriptions searching
            if ($a['extra']['name'] != $name && $a['extra']['name'] != $name . '[]') {
                continue;
            }
            $custom_error = $a['extra']['validate_error'];
            break;
        }
        // Support for separate errors by validate functions
        if (is_array($custom_error)) {
            return isset($custom_error[$func]) ? $custom_error[$func] : '';
        }
        return $custom_error;
    }

    /**
     * Examples of validate rules setting:
     *     'name1' => 'trim|required',
     *     'name2' => ['trim', 'required'],
     *     'name3' => ['trim|required', 'other_rule|other_rule2|other_rule3'],
     *     'name4' => ['trim|required', function() { return true; } ],
     *     'name5' => ['trim', 'required', function() { return true; } ],
     *     '__before__' => 'trim',
     *     '__after__' => 'some_method2|some_method3',.
     * @param mixed $validate_rules
     */
    public function _validate_rules_cleanup($validate_rules = [])
    {
        $func = __FUNCTION__;
        return _class('validate')->$func($validate_rules);
    }

    /**
     * @param mixed $raw
     */
    public function _validate_rules_array_from_raw($raw = '')
    {
        $func = __FUNCTION__;
        return _class('validate')->$func($raw);
    }

    /**
     * @param mixed $extra
     */
    public function _input_assign_params_from_validate($extra = [])
    {
        return _class('form2_validate', 'classes/form2/')->_input_assign_params_from_validate($extra, $this);
    }

    /**
     * Alias.
     * @param mixed $table
     * @param mixed $fields
     * @param mixed $add_fields
     * @param mixed $extra
     */
    public function insert_if_ok($table, $fields, $add_fields = [], $extra = [])
    {
        return $this->db_insert_if_ok($table, $fields, $add_fields, $extra);
    }

    /**
     * @param mixed $table
     * @param mixed $fields
     * @param mixed $add_fields
     * @param mixed $extra
     */
    public function db_insert_if_ok($table, $fields, $add_fields = [], $extra = [])
    {
        $extra['add_fields'] = $add_fields;
        return $this->_db_change_if_ok($table, $fields, 'insert', $extra);
    }

    /**
     * Alias.
     * @param mixed $table
     * @param mixed $fields
     * @param null|mixed $where_id
     * @param mixed $extra
     */
    public function update_if_ok($table, $fields, $where_id = null, $extra = [])
    {
        return $this->db_update_if_ok($table, $fields, $where_id, $extra);
    }

    /**
     * @param mixed $table
     * @param mixed $fields
     * @param null|mixed $where_id
     * @param mixed $extra
     */
    public function db_update_if_ok($table, $fields, $where_id = null, $extra = [])
    {
        $extra['where_id'] = $where_id;
        return $this->_db_change_if_ok($table, $fields, 'update', $extra);
    }

    /**
     * Alias.
     * @param mixed $table
     * @param mixed $fields
     * @param mixed $type
     * @param mixed $extra
     */
    public function change_if_ok($table, $fields, $type, $extra = [])
    {
        return $this->_db_change_if_ok($table, $fields, $type, $extra);
    }

    /**
     * @param mixed $table
     * @param mixed $fields
     * @param mixed $type
     * @param mixed $extra
     */
    public function _db_change_if_ok($table, $fields, $type, $extra = [])
    {
        $func = function ($table, $fields, $type, $extra, $form) {
            if ( ! $table || ! $type || empty($_POST)) {
                return $form;
            }
            $validate_ok = ($form->_validate_ok || $extra['force']);
            if ( ! $validate_ok) {
                return $form;
            }
            $data = [];
            foreach ((array) $fields as $k => $name) {
                // Example $fields = ['login','email'];
                if (is_numeric($k)) {
                    $db_field_name = $name;
                // Example $fields = ['pswd' => 'password'];
                } else {
                    $db_field_name = $name;
                    $name = $k;
                }
                if (isset($form->_validated_fields[$name])) {
                    $data[$db_field_name] = $form->_validated_fields[$name];
                }
            }
            // This is non-validated list of fields to add to the insert array
            foreach ((array) $extra['add_fields'] as $db_field_name => $value) {
                $data[$db_field_name] = $value;
            }
            // Callback/hook function implementation
            $on_before_update = isset($extra['on_before_update']) ? $extra['on_before_update'] : $form->_on['on_before_update'];
            if ($data && $table && is_callable($on_before_update)) {
                $on_before_update($data, $table, $fields, $type, $extra, $this);
            }
            _class('core_events')->fire('form.before_update', [$data, $table, $fields, $type, $extra]);
            if ($data && $table) {
                $db = is_object($form->_params['db']) ? $form->_params['db'] : db();
                if ($type == 'update') {
                    $where_id = $extra['where_id'] ?: $form->_replace[$form->_params['id'] ?: 'id'];
                    if ($where_id) {
                        $db->update_safe($table, $data, $where_id);
                    } else {
                        throw new Exception(__CLASS__ . '::' . __FUNCTION__ . ' where_id param is empty, not updating table: ' . $table);
                    }
                } elseif ($type == 'insert') {
                    $db->insert_safe($table, $data);
                }
                // Callback/hook function implementation
                $on_after_update = isset($extra['on_after_update']) ? $extra['on_after_update'] : $form->_on['on_after_update'];
                if (is_callable($on_after_update)) {
                    $on_after_update($data, $table, $fields, $type, $extra, $this);
                }
                _class('core_events')->fire('form.after_update', [$data, $table, $fields, $type, $extra]);
                $on_success_text = isset($extra['on_success_text']) ? $extra['on_success_text'] : $form->_on['on_success_text'];
                if ($on_success_text) {
                    common()->set_notice($on_success_text);
                }
                $redirect_link = $extra['redirect_link'] ?: $form->_replace['redirect_link'];
                if ( ! $redirect_link) {
                    $redirect_link = $form->_replace['back_link'];
                }
                if ( ! $redirect_link || false === strpos($redirect_link, '/')) {
                    $redirect_link = url('/@object/@action/@id');
                }
                if ( ! $extra['no_redirect'] && ! main()->is_console()) {
                    js_redirect($redirect_link);
                }
            }
        };
        if ($this->_chained_mode) {
            $this->_db_change_if_ok = [
                'func' => $func,
                'table' => $table,
                'fields' => $fields,
                'type' => $type,
                'extra' => $extra,
            ];
            return $this;
        }
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_post($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_before_render($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_after_render($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_before_validate($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_after_validate($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_validate_ok($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_validate_error($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_success_text($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_before_update($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }

    /**
     * @param mixed $func
     */
    public function on_after_update($func)
    {
        $this->_on[__FUNCTION__] = $func;
        return $this;
    }
}