classes/yf_validate.class.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php

/**
 * Validation common methods, part of this was inspired by codeigniter 2.1 form_validate.
 */
class yf_validate
{
    /** @var int Minimal nick length */
    public $MIN_NICK_LENGTH = 2;
    /** @var array Allowed nick symbols (display for user) */
    public $NICK_ALLOWED_SYMBOLS = ['a-z', '0-9', '_', '\-', '@', '#', ' '];
    /** @var array Reserved words for the profile url (default) */
    public $reserved_words = ['login', 'logout', 'admin', 'admin_modules', 'classes', 'modules', 'functions', 'uploads', 'fonts', 'pages_cache', 'core_cache', 'templates'];

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

    /***/
    public function _init()
    {
        $this->MB_ENABLED = _class('utf8')->MULTIBYTE;
        $this->db = db();
    }

    /***/
    public function _prepare_reserved_words()
    {
        if ($this->_reserved_words_prepared) {
            return $this->reserved_words;
        }
        $user_modules = main()->get_data('user_modules');
        // Merge them with default ones
        if (is_array($user_modules)) {
            $this->reserved_words = array_merge($this->reserved_words, $user_modules);
        }
        $this->_reserved_words_prepared = true;
        return $this->reserved_words;
    }

    /**
     * Method by form-less checking of any custom data for validity.
     * @param mixed $input
     * @param mixed $validate_rules
     */
    public function _input_is_valid($input, $validate_rules = [])
    {
        if ( ! is_array($input)) {
            $input = ['input' => $input];
        }
        $rules = [];
        $global_rules = isset($this->_params['validate']) ? $this->_params['validate'] : $this->_replace['validate'];
        foreach ((array) $global_rules as $name => $_rules) {
            $rules[$name] = $_rules;
        }
        if ( ! is_array($validate_rules)) {
            $validate_rules = ['__before__' => $validate_rules];
        }
        foreach ((array) $validate_rules as $name => $_rules) {
            $rules[$name] = $_rules;
        }
        $rules = $this->_validate_rules_cleanup($rules);
        $ok = $this->_do_check_data_is_valid($rules, $input);
        return (bool) $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 $rules
     */
    public function _do_check_data_is_valid($rules = [], &$data)
    {
        $validate_ok = true;
        $_all = '__all__';
        if (isset($rules[$_all])) {
            $rules_for_all = $rules[$_all];
            foreach ((array) $data as $name => $_tmp) {
                if (isset($rules[$name])) {
                    foreach ((array) $rules_for_all as $_rule) {
                        $rules[$name][] = $_rule;
                    }
                } else {
                    $rules[$name] = $rules_for_all;
                }
            }
            unset($rules[$_all]);
        }
        foreach ((array) $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], $error_msg);
                } elseif (is_callable($func)) {
                    $is_ok = $func($data[$name], null, $data, $error_msg);
                } else {
                    $is_ok = _class('validate')->$func($data[$name], ['param' => $param], $data, $error_msg);
                    if ( ! $is_ok && empty($error_msg)) {
                        $error_msg = t('form_validate_' . $func, ['%field' => $name, '%param' => $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;
    }

    /**
     * Examples of validate rules setting:
     *     'name1' => 'trim|required',
     *     'name2' => array('trim', 'required'),
     *     'name3' => array('trim|required', 'other_rule|other_rule2|other_rule3'),
     *     'name4' => array('trim|required', function() { return true; } ),
     *     'name5' => array('trim', 'required', function() { return true; } ),
     *     'name6,name7,name8' => array('trim', 'required', function() { return true; } ),
     *     '__before__' => 'trim',
     *     '__after__' => 'some_method2|some_method3',.
     * @param mixed $validate_rules
     */
    public function _validate_rules_cleanup($validate_rules = [])
    {
        // Trim names with spaces
        foreach ((array) $validate_rules as $name => $raw) {
            $trimmed = trim($name);
            $tlen = strlen($trimmed);
            if ($trimmed === $name && $tlen) {
                continue;
            }
            if ($tlen) {
                $validate_rules[$trimmed] = $raw;
            }
            unset($validate_rules[$name]);
        }
        // Prepare rule keys, containing several keys, splitted by comma. Example: "test1,test2,test3" => 'required'
        foreach ((array) $validate_rules as $name => $raw) {
            if (strpos($name, ',') === false) {
                continue;
            }
            foreach (explode(',', trim($name)) as $_name) {
                $_name = trim($_name);
                if ( ! strlen($_name)) {
                    continue;
                }
                // Merge with existing rules with same key, for example we want to mass add some more rule to existing.
                if (isset($validate_rules[$_name])) {
                    if ( ! is_array($validate_rules[$_name])) {
                        $validate_rules[$_name] = [$validate_rules[$_name]];
                    }
                    $validate_rules[$_name] = array_merge($validate_rules[$_name], is_array($raw) ? $raw : [$raw]);
                } else {
                    $validate_rules[$_name] = $raw;
                }
            }
            unset($validate_rules[$name]);
        }

        // Add these rules to all validation rules, before them
        $_name = '__before__';
        $all_before = [];
        if (isset($validate_rules[$_name])) {
            $all_before = (array) $this->_validate_rules_array_from_raw($validate_rules[$_name]);
            unset($validate_rules[$_name]);
        }

        // Add these rules to all validation rules, after them
        $_name = '__after__';
        $all_after = [];
        if (isset($validate_rules[$_name])) {
            $all_after = (array) $this->_validate_rules_array_from_raw($validate_rules[$_name]);
            unset($validate_rules[$_name]);
        }
        unset($_name);

        // Special case when only __before__ or __after__ passed
        if (( ! empty($all_after) || ! empty($all_before)) && empty($validate_rules)) {
            $validate_rules = ['__all__' => ''];
        }
        $out = [];
        foreach ((array) $validate_rules as $name => $raw) {
            $is_html_array = (false !== strpos($name, '['));
            if ($is_html_array) {
                $name = str_replace(['[', ']'], ['.', ''], trim($name, ']['));
            }
            $rules = (array) $this->_validate_rules_array_from_raw($raw);
            if ($all_before) {
                $tmp = $all_before;
                foreach ((array) $rules as $_item) {
                    $tmp[] = $_item;
                }
                $rules = $tmp;
                unset($tmp);
            }
            if ($all_after) {
                $tmp = $rules;
                foreach ((array) $all_after as $_item) {
                    $tmp[] = $_item;
                }
                $rules = $tmp;
                unset($tmp);
            }
            // Here we do last parse of the rules params like 'matches[user.email]' into rule item array second element
            foreach ((array) $rules as $k => $rule) {
                if ( ! is_string($rule[0])) {
                    continue;
                }
                $val = trim($rule[0]);
                $param = null;
                // Parsing these: min_length:6, matches:form_item, is_unique:table.field
                // skip regex
                if (strpos($val, ':') !== false && substr($val, -1) != ']') {
                    if (strpos($val, 'regex') !== false || strpos($val, 'regex_match') !== false) {
                        list($_val, $param) = explode(':', $val, 2);
                    } else {
                        list($_val, $param) = explode(':', $val);
                    }
                    $param = trim($param);
                    $val = trim($_val);
                // Parsing these: min_length[6], matches[form_item], is_unique[table.field]
                } elseif (strpos($val, '[') !== false) {
                    list($_val, $param) = explode('[', $val);
                    $param = trim(trim(trim($param), ']['));
                    $val = trim($_val);
                }
                if ( ! is_callable($val) && empty($val)) {
                    unset($rules[$k]);
                    continue;
                }
                $rules[$k] = [
                    0 => $val,
                    1 => $param,
                ];
            }
            if ($rules) {
                $out[$name] = array_values($rules); // array_values needed here to make array keys straight, unit tests will pass fine
            }
        }
        return $out;
    }

    /**
     * This method used by validate() function to do standalone validation processing.
     * @param mixed $raw
     */
    public function _validate_rules_array_from_raw($raw = '')
    {
        $rules = [];
        // esxape '|' to '\|'
        $delimeter = '|';
        $delimeter_regexp = '~(?<![^\\\]\\\)' . preg_quote($delimeter, '~') . '~';
        // At first, we merging all rules sets variants into one array
        if (is_string($raw)) {
            // foreach((array)explode('|', $raw) as $_item) {
            foreach ((array) preg_split($delimeter_regexp, $raw) as $_item) {
                $_item = str_replace('\|', '|', $_item);
                $rules[] = [$_item, null];
            }
        } elseif (is_array($raw)) {
            foreach ((array) $raw as $_raw) {
                if (is_string($_raw)) {
                    // foreach((array)explode('|', $_raw) as $_item) {
                    foreach ((array) preg_split($delimeter_regexp, $_raw) as $_item) {
                        $_item = str_replace('\|', '|', $_item);
                        $rules[] = [$_item, null];
                    }
                } elseif (is_callable($_raw)) {
                    $rules[] = [$_raw, null];
                }
            }
        } elseif (is_callable($raw)) {
            $rules[] = [$raw, null];
        }
        return $rules;
    }

    /**
     * Returns md5() from input string, or null. Usually used to update password inside admin panel or not change it if new value not passed.
     * Example: ["password" => 'trim|min_length[6]|max_length[32]|password_update'].
     */
    public function password_update(&$in)
    {
        if ( ! strlen($in)) {
            $in = null; // Somehow unset($in) not working here...
        } else {
            $in = md5($in);
        }
        return true;
    }

    /**
     * Returns md5() from given input string, only if not empty.
     * Example usage: ["password" => 'trim|min_length[6]|max_length[32]|md5_not_empty'].
     */
    public function md5_not_empty(&$in)
    {
        if (strlen($in)) {
            $in = md5($in);
        }
        return true;
    }

    /**
     * Returns hash() from given input string, only if not empty.  Get list of available algorithms by running: php -r 'echo implode(" ", hash_algos());'.
     * Most popular are: md5 sha1 sha224 sha256 sha384 sha512 ripemd128 ripemd160 ripemd256 ripemd320 gost crc32
     * Example usage: ["password" => 'trim|min_length[6]|max_length[32]|hash_not_empty[sha256]'].
     * @param mixed $params
     */
    public function hash_not_empty(&$in, $params = [])
    {
        $hash_name = is_array($params) ? $params['param'] : $params;
        if (strlen($in) && $hash_name) {
            $in = hash($hash_name, $in);
        }
        return true;
    }

    /**
     * Returns FALSE if form field is empty.
     * @param mixed $in
     */
    public function required($in)
    {
        if (is_array($in)) {
            $func = __FUNCTION__;
            foreach ($in as $v) {
                $result = $this->$func($v);
                if ($result) {
                    return true;
                }
            }
            return false;
        }
        return trim($in) !== '';
    }

    /**
     * Returns true when selected other passed field will be non-empty
     * Examples: required_if[other_field].
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function required_if($in, $params = [], $fields = [])
    {
        $param = trim(is_array($params) ? $params['param'] : $params);
        if ( ! strlen($param)) {
            return false;
        }
        if (isset($fields[$param]) && ! empty($fields[$param])) {
            return $this->required($in);
        }
        return true;
    }

    /**
     * Returns true when _ANY_ of passed fields will be non-empty
     * Examples: required_any[duration_*] or required_any[duration_day,duration_week,duration_month].
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function required_any($in, $params = [], $fields = [])
    {
        $param = trim(is_array($params) ? $params['param'] : $params);
        $wildcard = false;
        // Example: duration_day,duration_week,duration_month
        if (false !== strpos($param, ',')) {
            $field_names = explode(',', $param);
        // Example: duration_*
        } else {
            $wildcard = $param;
        }
        foreach ((array) $fields as $k => $v) {
            $skip = true;
            if ($wildcard && wildcard_compare($wildcard, $k)) {
                $skip = false;
            } elseif ($field_names && in_array($k, $field_names)) {
                $skip = false;
            }
            if ($skip) {
                continue;
            }
            if ($this->required($v)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns FALSE if field does not match field(s) in parameter.
     * Example: matches[password_again].
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function matches($in, $params = [], $fields = [])
    {
        $field = is_array($params) ? $params['param'] : $params;
        return isset($fields[$field], $_POST[$field]) ? ($in === $_POST[$field]) : false;
    }

    /**
     * Returns FALSE if form field(s) defined in parameter are not filled in.
     * Example: depends_on[field_name].
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function depends_on($in, $params = [], $fields = [])
    {
        $field = is_array($params) ? $params['param'] : $params;
        return isset($fields[$field], $_POST[$field]);
    }

    /**
     * The field under validation must be a valid URL according to the checkdnsrr PHP function.
     * @param mixed $in
     */
    public function active_url($in)
    {
        return checkdnsrr(str_replace(['http://', 'https://', 'ftp://'], '', strtolower($in)));
    }

    /**
     * The field under validation must be a value after a given date. The dates will be passed into the PHP strtotime function.
     * Examples: after_date[2012-01-01], after_date[day ago].
     * @param mixed $in
     * @param mixed $params
     */
    public function after_date($in, $params = [])
    {
        $param = is_array($params) ? $params['param'] : $params;
        if ( ! $param) {
            return false;
        }
        if (isset($params['format'])) {
            return DateTime::createFromFormat($params['format'], $in) > DateTime::createFromFormat($params['format'], $param);
        }
        $date = strtotime($param);
        if ( ! $date) {
            return strtotime($in) > strtotime($this->getValue($param));
        }
        return strtotime($in) > $date;
    }

    /**
     * The field under validation must be a value preceding the given date. The dates will be passed into the PHP strtotime function.
     * Example: before_date[2020-12-31], after_date[+1 day].
     * @param mixed $in
     * @param mixed $params
     */
    public function before_date($in, $params = [])
    {
        $param = is_array($params) ? $params['param'] : $params;
        if ( ! $param) {
            return false;
        }
        if (isset($params['format'])) {
            return DateTime::createFromFormat($params['format'], $in) < DateTime::createFromFormat($params['format'], $param);
        }
        $date = strtotime($param);
        if ( ! $date) {
            return strtotime($in) < strtotime($this->getValue($param));
        }
        return strtotime($in) < $date;
    }

    /**
     * The field under validation must be a valid date according to the strtotime PHP function.
     * @param mixed $in
     */
    public function valid_date($in)
    {
        if ($in instanceof DateTime) {
            return true;
        }
        if (strtotime($in) === false) {
            return false;
        }
        $date = date_parse($in);
        return checkdate($date['month'], $date['day'], $date['year']);
    }

    /**
     * The field under validation must match the format defined according to the date_parse_from_format PHP function.
     * @param mixed $in
     * @param mixed $params
     */
    public function valid_date_format($in, $params = [])
    {
        $param = is_array($params) ? $params['param'] : $params;
        $parsed = date_parse_from_format($param, $in);
        return $parsed['error_count'] === 0 && $parsed['warning_count'] === 0;
    }

    /**
     * Returns FALSE if form field is not valid text (letters, numbers, whitespace, dashes, periods and underscores are allowed).
     * @param mixed $in
     */
    public function standard_text($in)
    {
        return (bool) preg_match('~^[a-z0-9\s\t,\._-]+$~ims', $in);
    }

    /**
     * The field under validation must have a size between the given min and max. Strings, numerics, and files are evaluated in the same fashion as the size rule.
     * Examples: between[a,z]  between[44,99].
     * @param mixed $in
     * @param mixed $params
     */
    public function between($in, $params = [])
    {
        $param = is_array($params) ? $params['param'] : $params;
        list($min, $max) = explode(',', $param);
        return $in >= $min && $in <= $max;
    }

    /**
     * Returns FALSE if field contains characters not in the parameter.
     * Example: chars[a,b,c,d,1,2,3,4].
     * @param mixed $in
     * @param mixed $params
     */
    public function chars($in, $params = [])
    {
        $param = is_array($params) ? $params['param'] : $params;
        $chars = [];
        foreach (explode(',', trim($param)) as $char) {
            $char = trim($char);
            if (strlen($char)) {
                $chars[$char] = $char;
            }
        }
        if ( ! count((array) $chars)) {
            return false;
        }
        $regex = '~^[' . preg_quote(implode($chars), '~') . ']+$~ims';
        return (bool) preg_match($regex, $in);
    }

    /**
     * Returns TRUE if given field value is unique inside given database table.field
     * Examples: is_unique[user.login]
     * Alias.
     * @param mixed $in
     * @param mixed $params
     */
    public function unique($in, $params = [])
    {
        return $this->is_unique($in, $params);
    }

    /**
     * Returns TRUE if given field value is unique inside given database table.field
     * Examples: is_unique[user.login].
     * @param mixed $in
     * @param mixed $params
     */
    public function is_unique($in, $params = [])
    {
        if ( ! $in) {
            return true;
        }
        $param = is_array($params) ? $params['param'] : $params;
        if ($param) {
            list($check_table, $check_field) = explode('.', $param);
        }
        if ($check_table && $check_field && $in) {
            $exists = $this->db
                ->select($this->db->es($check_field))
                ->from($check_table)
                ->where($this->db->es($check_field), '=', $this->db->es($in))
                ->get_one();
            if ($exists == $in) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns TRUE if given field value is unique inside given database table.field.pk_value
     * Examples: is_unique_without[user.id.1].
     * @param mixed $in
     * @param mixed $params
     */
    public function is_unique_without($in, $params = [])
    {
        if ( ! $in) {
            return true;
        }
        $param = is_array($params) ? $params['param'] : $params;
        $id_field = $params['id_field'] ?: 'id';
        if ($param) {
            list($check_table, $check_field, $check_id) = explode('.', $param);
        }
        if ($check_table && $check_field && $check_id && $in) {
            $exists = $this->db
                ->select($this->db->es($check_field))
                ->from($check_table)
                ->where($this->db->es($check_field), '=', $this->db->es($in))
                ->where($this->db->es($id_field), '!=', $this->db->es($check_id))
                ->get_one();
            if ($exists == $in) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns TRUE if given field value exists inside database
     * Examples: exists[user.email].
     * @param mixed $in
     * @param mixed $params
     */
    public function exists($in, $params = [])
    {
        if ( ! $in) {
            return false;
        }
        $param = is_array($params) ? $params['param'] : $params;
        if ($param) {
            list($check_table, $check_field) = explode('.', $param);
        }
        if ($check_table && $check_field && $in) {
            $exists = $this->db
                ->select($this->db->es($check_field))
                ->from($check_table)
                ->where($this->db->es($check_field), '=', $this->db->es($in))
                ->get_one();
            if ($exists == $in) {
                return true;
            }
        }
        return false;
    }

    /**
     * Custom regex matching.
     * Example: regex_match[/^[a-z0-9]+$/]
     * Alias.
     * @param mixed $in
     * @param mixed $params
     */
    public function regex($in, $params = [])
    {
        return $this->regex_match($in, $params);
    }

    /**
     * Custom regex matching.
     * Example: regex_match[/^[a-z0-9]+$/].
     * @param mixed $in
     * @param mixed $params
     */
    public function regex_match($in, $params = [])
    {
        $regex = is_array($params) ? $params['param'] : $params;
        return (bool) preg_match($regex, $in);
    }

    /**
     * Returns TRUE if given field value differs from compared field value
     * Example: differs[address_2].
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function differs($in, $params = [], $fields = [])
    {
        $field = is_array($params) ? $params['param'] : $params;
        return ! (isset($fields[$field]) && $_POST[$field] === $in);
    }

    /**
     * Alias.
     * @param mixed $in
     */
    public function host($in)
    {
        return $this->valid_hostname($in);
    }

    /**
     * The original specification of hostnames in RFC 952, mandated that labels could not start with a digit or with a hyphen, and must not end with a hyphen.
     * However, a subsequent specification (RFC 1123) permitted hostname labels to start with digits.
     * http://tools.ietf.org/html/rfc952, http://tools.ietf.org/html/rfc1123
     * Each label within a valid hostname may be no more than 63 octets long.
     * the total length of the hostname must not exceed 255 characters. For more information, please consult RFC-952 and RFC-1123.
     * see also:
     *    http://stackoverflow.com/questions/106179/regular-expression-to-match-hostname-or-ip-address
     *    http://data.iana.org/TLD/tlds-alpha-by-domain.txt.
     * @param mixed $in
     */
    public function valid_hostname($in = '')
    {
        $len = strlen($in);
        if ( ! $len && $len > 255) {
            return false;
        }
        foreach ((array) explode('.', $in) as $v) {
            if (strlen($v) > 63) {
                return false;
            }
        }
        return (bool) preg_match('/^([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])(\.([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9]))*$/i', $in);
    }

    /**
     * Returns TRUE if given field contains valid url. Checking is done in combination of regexp and php built-in filter_val() to ensure most correct results
     * Alias.
     * @param mixed $in
     * @param mixed $params
     */
    public function url($in, $params = [])
    {
        return $this->valid_url($in, $params);
    }

    /**
     * Returns TRUE if given field contains valid url. Checking is done in combination of regexp and php built-in filter_val() to ensure most correct results.
     * @param mixed $in
     * @param mixed $params
     */
    public function valid_url($in, $params = [])
    {
        if (empty($in)) {
            return false;
        } elseif (preg_match('/^(?:([^:]*)\:)?\/\/(.+)$/', $in, $matches)) {
            if (empty($matches[2])) {
                return false;
            } elseif ( ! in_array($matches[1], ['http', 'https'], true)) {
                return false;
            }
            $in = $matches[2];
        }
        $in = 'http://' . $in;
        return filter_var($in, FILTER_VALIDATE_URL) !== false;
    }

    /**
     * Returns TRUE if given field contains valid email address. Alias.
     * @param mixed $in
     */
    public function email($in)
    {
        return $this->valid_email($in);
    }

    /**
     * Returns TRUE if given field contains valid email address.
     * @param mixed $in
     */
    public function valid_email($in)
    {
        return (bool) filter_var($in, FILTER_VALIDATE_EMAIL);
    }

    /**
     * Returns TRUE if given field contains several valid email addresses.
     * @param mixed $in
     */
    public function valid_emails($in)
    {
        if ( ! $in) {
            return false;
        }
        if (strpos($in, ',') === false) {
            return $this->valid_email(trim($in));
        }
        foreach (explode(',', $in) as $email) {
            if (trim($email) !== '' && $this->valid_email(trim($email)) === false) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns TRUE if given field contains correct base64-encoded string.
     * @param mixed $in
     */
    public function valid_base64($in)
    {
        return strlen($in) && (base64_encode(base64_decode($in)) === $in);
    }

    /**
     * Returns TRUE if given field contains valid IP address, ipv4 by default, ipv6 supported too.
     * @param mixed $in
     * @param mixed $params
     */
    public function valid_ip($in, $params = [])
    {
        $which = is_array($params) ? $params['param'] : $params;
        return $this->_valid_ip($in, $which);
    }

    /**
     * Returns TRUE if given field length is no more than specified, excluding exact length.
     * Example: min_length[10].
     * @param mixed $in
     * @param mixed $params
     */
    public function min_length($in, $params = [])
    {
        $val = is_array($params) ? $params['param'] : $params;
        if ( ! is_numeric($val)) {
            return false;
        }
        $val = (int) $val;

        return ($this->MB_ENABLED == 1) ? ($val <= mb_strlen($in)) : ($val <= strlen($in));
    }

    /**
     * Returns TRUE if given field length is more than specified, including exact length.
     * Example: max_length[10].
     * @param mixed $in
     * @param mixed $params
     */
    public function max_length($in, $params = [])
    {
        $val = is_array($params) ? $params['param'] : $params;
        if ( ! is_numeric($val)) {
            return false;
        }
        $val = (int) $val;

        return ($this->MB_ENABLED == 1) ? ($val >= mb_strlen($in)) : ($val >= strlen($in));
    }

    /**
     * Returns TRUE if given field length is more than specified, including exact length.
     * Example: exact_length[10].
     * @param mixed $in
     * @param mixed $params
     */
    public function exact_length($in, $params = [])
    {
        $val = is_array($params) ? $params['param'] : $params;
        if ( ! is_numeric($val)) {
            return false;
        }
        $val = (int) $val;

        return ($this->MB_ENABLED == 1) ? (mb_strlen($in) === $val) : (strlen($in) === $val);
    }

    /**
     * Returns FALSE if the field is too long or too short.
     * Examples: length[1,30] - between 1 and 30 characters long. length[30] - exactly 30 characters long.
     * @param mixed $in
     * @param mixed $params
     */
    public function length($in, $params = [])
    {
        $val = is_array($params) ? $params['param'] : $params;
        if (false === strpos($val, ',')) {
            return $this->exact_length($in, $params);
        }
        list($min, $max) = explode(',', $val);
        $min_check = true;
        if ($min) {
            $min_check = $this->min_length($in, $min);
        }
        $max_check = true;
        if ($max) {
            $max_check = $this->max_length($in, $max);
        }
        return $min_check && $max_check;
        return false;
    }

    /**
     * Returns TRUE if given field value is a number and greater than specified, not including exact value
     * Example: greater_than[10].
     * @param mixed $in
     * @param mixed $params
     */
    public function greater_than($in, $params = [])
    {
        $min = is_array($params) ? $params['param'] : $params;
        return is_numeric($in) ? ($in > $min) : false;
    }

    /**
     * Alias.
     * @param mixed $in
     * @param mixed $params
     */
    public function gt($in, $params = [])
    {
        return $this->greater_than($in, $params);
    }

    /**
     * Returns TRUE if given field value is a number and less than specified, not including exact value
     * Example: less_than[10].
     * @param mixed $in
     * @param mixed $params
     */
    public function less_than($in, $params = [])
    {
        $max = is_array($params) ? $params['param'] : $params;
        return is_numeric($in) ? ($in < $max) : false;
    }

    /**
     * Alias.
     * @param mixed $in
     * @param mixed $params
     */
    public function lt($in, $params = [])
    {
        return $this->less_than($in, $params);
    }

    /**
     * Returns TRUE if given field value is a number and greater than specified, including exact value
     * Example: greater_than_equal_to[10].
     * @param mixed $in
     * @param mixed $params
     */
    public function greater_than_equal_to($in, $params = [])
    {
        $min = is_array($params) ? $params['param'] : $params;
        return is_numeric($in) ? ($in >= $min) : false;
    }

    /**
     * Alias.
     * @param mixed $in
     * @param mixed $params
     */
    public function gte($in, $params = [])
    {
        return $this->greater_than_equal_to($in, $params);
    }

    /**
     * Returns TRUE if given field value is a number and less than specified, including exact value
     * Example: less_than_equal_to[10].
     * @param mixed $in
     * @param mixed $params
     */
    public function less_than_equal_to($in, $params = [])
    {
        $max = is_array($params) ? $params['param'] : $params;
        return is_numeric($in) ? ($in <= $max) : false;
    }

    /**
     * Alias.
     * @param mixed $in
     * @param mixed $params
     */
    public function lte($in, $params = [])
    {
        return $this->less_than_equal_to($in, $params);
    }

    /**
     * Returns TRUE if given field value contains only latin1 letters, lower and uppercase allowed.
     * @param mixed $in
     */
    public function alpha($in)
    {
        return ctype_alpha($in);
    }

    /**
     * Returns TRUE if given field value contains only latin1 letters, lower and uppercase allowed, and digits.
     * @param mixed $in
     */
    public function alpha_numeric($in)
    {
        if (is_array($in) || is_object($in) || (is_callable($in) && ! function_exists($in))) {
            return false;
        }
        return ctype_alnum((string) $in);
    }

    /**
     * Returns TRUE if given field value contains only latin1 letters, lower and uppercase allowed, and spaces.
     * @param mixed $in
     */
    public function alpha_spaces($in)
    {
        return (bool) preg_match('/^[A-Z ]+$/i', $in);
    }

    /**
     * Returns TRUE if given field value contains only latin1 letters, lower and uppercase allowed, and spaces and digits.
     * @param mixed $in
     */
    public function alpha_numeric_spaces($in)
    {
        return (bool) preg_match('/^[A-Z0-9 ]+$/i', $in);
    }

    /**
     * Returns TRUE if given field value contains only latin1 letters, lower and uppercase allowed, and dash and underscore symbols.
     * @param mixed $in
     */
    public function alpha_dash($in)
    {
        return (bool) preg_match('/^[a-z0-9_-]+$/i', $in);
    }

    /**
     * Returns TRUE if given field value contains only latin1 letters, lower and uppercase allowed, and dash and underscore and dots symbols.
     * @param mixed $in
     */
    public function alpha_dash_dots($in)
    {
        return (bool) preg_match('/^[a-z0-9_\.-]+$/i', $in);
    }

    /**
     * Same as alpha(), but including unicode characters too.
     * @param mixed $in
     */
    public function unicode_alpha($in)
    {
        return (bool) preg_match('/^[\pL\pM]+$/u', $in);
    }

    /**
     * Same as alpha_numeric(), but including unicode characters too.
     * @param mixed $in
     */
    public function unicode_alpha_numeric($in)
    {
        return (bool) preg_match('/^[\pL\pM\pN]+$/u', $in);
    }

    /**
     * Same as alpha_spaces(), but including unicode characters too.
     * @param mixed $in
     */
    public function unicode_alpha_spaces($in)
    {
        return (bool) preg_match('/^[\pL\pM\s]+$/u', $in);
    }

    /**
     * Same as alpha_numeric_spaces(), but including unicode characters too.
     * @param mixed $in
     */
    public function unicode_alpha_numeric_spaces($in)
    {
        return (bool) preg_match('/^[\pL\pM\pN\s]+$/u', $in);
    }

    /**
     * Same as alpha_dash(), but including unicode characters too.
     * @param mixed $in
     */
    public function unicode_alpha_dash($in)
    {
        return (bool) preg_match('/^[\pL\pM\pN_-]+$/u', $in);
    }

    /**
     * Returns TRUE if given field value contains only numbers, including integers, floats and decimals.
     * @param mixed $in
     */
    public function numeric($in)
    {
        return (bool) preg_match('/^[\-+]?[0-9]*\.?[0-9]+$/', $in);
    }

    /**
     * Returns TRUE if given field value contains only integers.
     * @param mixed $in
     */
    public function integer($in)
    {
        return (bool) preg_match('/^[\-+]?[0-9]+$/', $in);
    }

    /**
     * Returns TRUE if given field value contains only decimals.
     * @param mixed $in
     */
    public function decimal($in)
    {
        return (bool) preg_match('/^[\-+]?[0-9]+\.[0-9]+$/', $in);
    }

    /**
     * Returns TRUE if given field value contains only numbers that are natural.
     * @param mixed $in
     */
    public function is_natural($in)
    {
        return ctype_digit((string) $in);
    }

    /**
     * Returns TRUE if given field value contains only numbers that are natural except 0.
     * @param mixed $in
     */
    public function is_natural_no_zero($in)
    {
        return $in != 0 && ctype_digit((string) $in);
    }

    /**
     * Do url preparation, not validates anything.
     * @param mixed $in
     */
    public function prep_url($in)
    {
        if ($in === 'http://' or $in === '') {
            return '';
        }
        if (strpos($in, 'http://') !== 0 && strpos($in, 'https://') !== 0) {
            return 'http://' . $in;
        }
        return $in;
    }

    /**
     * Returns TRUE is captcha user input value is valid.
     * @param mixed $in
     */
    public function captcha($in)
    {
        return _class('captcha')->check('captcha', $in);
    }

    /**
     * Clean string from possible XSS, using security class.
     * @param mixed $in
     */
    public function xss_clean($in)
    {
        return _class('security')->xss_clean($in);
    }

    /**
     * Internal IP validity checking method.
     * @param mixed $ip
     * @param mixed $ip_version
     */
    public function _valid_ip($ip, $ip_version = 'ipv4')
    {
        $ip_version = strtolower($ip_version);
        if ( ! $ip_version) {
            $ip_version = 'ipv4';
        }
        if ($ip_version == 'ipv6') {
            $filter_flag = FILTER_FLAG_IPV6;
        } else {
            $filter_flag = FILTER_FLAG_IPV4;
        }
        return (bool) filter_var($ip, FILTER_VALIDATE_IP, $filter_flag);
    }

    /**
     * Cleanup input phone to match international notation. Examples good: +380631234567, 063 123 45 67. See more valid examples inside unit tests.
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function phone_cleanup($in, $params = [], $fields = [], &$error = '')
    {
        $error = false;
        $country_prefix = $params['param'] ?: '38';
        $p_len = strlen($country_prefix);
        $phone = preg_replace('/[^0-9]+/ims', '', strip_tags($in));
        if (strlen($phone) == 10) { // 063 123 45 67 (spaces here for example readability)
            $phone = '+' . $country_prefix . $phone;
        } elseif (strlen($phone) == 9) { // 63 123 45 67 (spaces here for example readability)
            $phone = '+' . $country_prefix . '0' . $phone;
        } elseif (strlen($phone) == (10 + $p_len)) {
            if (substr($phone, 0, $p_len) != $country_prefix) {
                $error = t('phone error: incorrect country') . ': ' . $phone;
            } else {
                $phone = '+' . $phone;
            }
        } else {
            $error = t('phone error: number is incorrect') . ': ' . $phone;
            $phone = '';
        }
        $phone = $phone ? '+' . ltrim($phone, '+') : '';
        return $phone;
    }

    /**
     * Ensures input phone has valid international format.
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function valid_phone($in, $params = [], $fields = [], &$error = '')
    {
        $phone = $this->phone_cleanup($in, $params, $fields, $error);
        return empty($error) ? true : false;
    }

    /**
     * Check user nick.
     * @param mixed $CUR_VALUE
     * @param null|mixed $force_value_to_check
     * @param mixed $name_in_form
     */
    public function _check_user_nick($CUR_VALUE = '', $force_value_to_check = null, $name_in_form = 'nick')
    {
        // TODO: rewrite me
        $TEXT_TO_CHECK = $_POST[$name_in_form];
        if ($force_value_to_check !== null) {
            $TEXT_TO_CHECK = $force_value_to_check;
            $OVERRIDE_MODE = true;
        }
        $_nick_pattern = implode('', $this->NICK_ALLOWED_SYMBOLS);
        if (empty($TEXT_TO_CHECK) || (strlen($TEXT_TO_CHECK) < $this->MIN_NICK_LENGTH)) {
            _re(t('Nick must have at least @num symbols', ['@num' => $this->MIN_NICK_LENGTH]));
        } elseif ( ! preg_match('/^[' . $_nick_pattern . ']+$/iu', $TEXT_TO_CHECK)) {
            _re(t('Nick can contain only these characters: @text1', ['@text1' => _prepare_html(stripslashes(implode('" , "', $this->NICK_ALLOWED_SYMBOLS)))]));
            if ( ! $OVERRIDE_MODE) {
                $_POST[$name_in_form] = preg_replace('/[^' . $_nick_pattern . ']+/iu', '', $_POST[$name_in_form]);
            }
        } elseif ($TEXT_TO_CHECK != $CUR_VALUE) {
            // TODO: convert into query buidler
            $NICK_ALREADY_EXISTS = ($this->db->query_num_rows('SELECT id FROM ' . $this->db->_real_name('user') . ' WHERE nick="' . $this->db->es($TEXT_TO_CHECK) . '"') >= 1);
            if ($NICK_ALREADY_EXISTS) {
                _re(t('Nick "@name" is already reserved. Please try another one.', ['@name' => $TEXT_TO_CHECK]));
            }
        }
    }

    /**
     * Check user profile url.
     * @param mixed $CUR_VALUE
     * @param null|mixed $force_value_to_check
     * @param mixed $name_in_form
     */
    public function _check_profile_url($CUR_VALUE = '', $force_value_to_check = null, $name_in_form = 'profile_url')
    {
        // TODO: rewrite me
        $TEXT_TO_CHECK = $_POST[$name_in_form];
        // Override value to check
        if ($force_value_to_check !== null) {
            $TEXT_TO_CHECK = $force_value_to_check;
            $OVERRIDE_MODE = true;
        }
        // Ignore empty values
        if (empty($TEXT_TO_CHECK)) {
            return false;
        }
        $this->_prepare_reserved_words();
        if ( ! empty($CUR_VALUE)) {
            _re('You have already chosen your profile url. You are not allowed to change it!');
        } elseif ( ! preg_match('/^[a-z0-9]{0,64}$/ims', $TEXT_TO_CHECK)) {
            _re('Wrong Profile url format! Letters or numbers only with no spaces');
        } elseif (in_array($TEXT_TO_CHECK, $this->reserved_words)) {
            _re('This profile url ("' . $TEXT_TO_CHECK . '") is our site reserved name. Please try another one.');
        // TODO: convert into query buidler
        } elseif ($this->db->query_num_rows('SELECT id FROM ' . $this->db->_real_name('user') . ' WHERE profile_url="' . $this->db->es($TEXT_TO_CHECK) . '"') >= 1) {
            _re('This profile url ("' . $TEXT_TO_CHECK . '") has already been registered with us! Please try another one.');
        }
    }

    /**
     * Check user login.
     */
    public function _check_login()
    {
        // TODO: rewrite me
        if ($_POST['login'] == '') {
            _re('Login required');
        // TODO: convert into query buidler
        } elseif ($this->db->query_num_rows('SELECT id FROM ' . $this->db->_real_name('user') . ' WHERE login="' . $this->db->es($_POST['login']) . '"') >= 1) {
            _re('This login ' . $_POST['login'] . ' has already been registered with us!');
        }
    }

    /**
     * Check selected location (country, region, city).
     * @param mixed $cur_country
     * @param mixed $cur_region
     * @param mixed $cur_city
     */
    public function _check_location($cur_country = '', $cur_region = '', $cur_city = '')
    {
        // TODO: rewrite me
        if (FEATURED_COUNTRY_SELECT && ! empty($_POST['country']) && substr($_POST['country'], 0, 2) == 'f_') {
            $_POST['country'] = substr($_POST['country'], 2);
        }
        if ( ! empty($_POST['country'])) {
            if ( ! isset($GLOBALS['countries'])) {
                $GLOBALS['countries'] = main()->get_data('countries');
            }
            if ( ! isset($GLOBALS['countries'][$_POST['country']])) {
                $_POST['country'] = '';
                $_POST['region'] = '';
                $_POST['state'] = '';
                $_POST['city'] = '';
            } else {
                $GLOBALS['_country_name'] = $GLOBALS['countries'][$_POST['country']];
            }
        }
        if ( ! empty($_POST['region'])) {
            // TODO: convert into query buidler
            $region_info = $this->db->query_fetch('SELECT * FROM ' . $this->db->_real_name('geo_regions') . ' WHERE country = "' . $this->db->es($_POST['country']) . '" AND code="' . $this->db->es($_POST['region']) . '"');
            if (empty($region_info)) {
                $_POST['region'] = '';
                $_POST['state'] = '';
                $_POST['city'] = '';
            } else {
                $GLOBALS['_region_name'] = $region_info['name'];
            }
        }
        if ( ! empty($_POST['city'])) {
            // TODO: convert into query buidler
            $city_info = $this->db->query_fetch('SELECT * FROM ' . $this->db->_real_name('geo_city_location') . ' WHERE region = "' . $this->db->es($_POST['region']) . '" AND country = "' . $this->db->es($_POST['country']) . '" AND city="' . $this->db->es($_POST['city']) . '"');
            if (empty($city_info)) {
                $_POST['city'] = '';
            }
        }
    }

    /**
     * Check user birth date.
     * @param mixed $CUR_VALUE
     */
    public function _check_birth_date($CUR_VALUE = '')
    {
        // TODO: rewrite me
        $_POST['birth_date'] = $CUR_VALUE;

        $_POST['year_birth'] = (int) ($_POST['year_birth']);
        $_POST['month_birth'] = (int) ($_POST['month_birth']);
        $_POST['day_birth'] = (int) ($_POST['day_birth']);
        if ($_POST['year_birth'] >= 1915 && $_POST['year_birth'] <= (date('Y') - 17)
            && $_POST['month_birth'] >= 1 && $_POST['month_birth'] <= 12
            && $_POST['day_birth'] >= 1 && $_POST['day_birth'] <= 31
        ) {
            if ($_POST['month_birth'] < 10) {
                $_POST['month_birth'] = '0' . $_POST['month_birth'];
            }
            if ($_POST['day_birth'] < 10) {
                $_POST['day_birth'] = '0' . $_POST['day_birth'];
            }
            $_POST['birth_date'] = $_POST['year_birth'] . '-' . $_POST['month_birth'] . '-' . $_POST['day_birth'];
        }
        if ( ! empty($_POST['birth_date'])) {
            $_POST['age'] = _get_age_from_birth($_POST['birth_date']);
        }
    }

    /**
     * Internal method.
     * @param mixed $email
     * @param mixed $check_mx
     * @param mixed $check_by_smtp
     * @param mixed $check_blacklists
     */
    public function _email_verify($email = '', $check_mx = false, $check_by_smtp = false, $check_blacklists = false)
    {
        return _class('remote_files', 'classes/common/')->_email_verify($email, $check_mx, $check_by_smtp, $check_blacklists);
    }

    /**
     * Internal method.
     * @param mixed $url
     */
    public function _validate_url_by_http($url)
    {
        return _class('remote_files', 'classes/common/')->_validate_url_by_http($url);
    }

    /**
     * Alias.
     * @param mixed $in
     */
    public function _url_verify($in = '')
    {
        return $this->valid_url($in);
    }

    /**
     * Alias.
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function valid_image($in, $params = [], $fields = [])
    {
        return $this->image($in, $params, $fields);
    }

    /**
     * The file under validation must be an image (jpeg, png, bmp, or gif).
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function image($in, $params = [], $fields = [])
    {
        // TODO
    }

    /**
     * The file under validation must have a MIME type corresponding to one of the listed extensions.  mime:jpeg,bmp,png.
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function mime($in, $params = [], $fields = [])
    {
        // TODO
    }

    /**
     * Returns FALSE if credit card is not valid.
     * Examples: credit_card[mastercard].
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function credit_card($in, $params = [], $fields = [])
    {
        // TODO
    }

    /**
     * Same as is_unique(), but tells form validator to include ajax form checking.
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function ajax_is_unique($in, $params = [], $fields = [])
    {
        return $this->is_unique($in, $params, $fields);
    }

    /**
     * Same as is_unique_without(), but tells form validator to include ajax form checking.
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function ajax_is_unique_without($in, $params = [], $fields = [])
    {
        return $this->is_unique_without($in, $params, $fields);
    }

    /**
     * Same as exists(), but tells form validator to include ajax form checking.
     * @param mixed $in
     * @param mixed $params
     * @param mixed $fields
     */
    public function ajax_exists($in, $params = [], $fields = [])
    {
        return $this->exists($in, $params, $fields);
    }
}