gf/util/gvalid/gvalid_validator_check_value.go

603 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gvalid
import (
"context"
"errors"
"reflect"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/utils"
"github.com/gogf/gf/v2/net/gipv4"
"github.com/gogf/gf/v2/net/gipv6"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/gutil"
)
type iTime interface {
Date() (year int, month time.Month, day int)
IsZero() bool
}
type doCheckValueInput struct {
Name string // Name specifies the name of parameter `value`.
Value interface{} // Value specifies the value for the rules to be validated.
Rule string // Rule specifies the validation rules string, like "required", "required|between:1,100", etc.
Messages interface{} // Messages specifies the custom error messages for this rule from parameters input, which is usually type of map/slice.
DataRaw interface{} // DataRaw specifies the `raw data` which is passed to the Validator. It might be type of map/struct or a nil value.
DataMap map[string]interface{} // DataMap specifies the map that is converted from `dataRaw`. It is usually used internally
}
// doCheckSingleValue does the really rules validation for single key-value.
func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Error {
// If there's no validation rules, it does nothing and returns quickly.
if in.Rule == "" {
return nil
}
// It converts value to string and then does the validation.
var (
// Do not trim it as the space is also part of the value.
ruleErrorMap = make(map[string]error)
)
// Custom error messages handling.
var (
msgArray = make([]string, 0)
customMsgMap = make(map[string]string)
)
switch messages := in.Messages.(type) {
case string:
msgArray = strings.Split(messages, "|")
default:
for k, message := range gconv.Map(in.Messages) {
customMsgMap[k] = gconv.String(message)
}
}
// Handle the char '|' in the rule,
// which makes this rule separated into multiple rules.
ruleItems := strings.Split(strings.TrimSpace(in.Rule), "|")
for i := 0; ; {
array := strings.Split(ruleItems[i], ":")
_, ok := allSupportedRules[array[0]]
if !ok && v.getRuleFunc(array[0]) == nil {
if i > 0 && ruleItems[i-1][:5] == "regex" {
ruleItems[i-1] += "|" + ruleItems[i]
ruleItems = append(ruleItems[:i], ruleItems[i+1:]...)
} else {
return newValidationErrorByStr(
internalRulesErrRuleName,
errors.New(internalRulesErrRuleName+": "+in.Rule),
)
}
} else {
i++
}
if i == len(ruleItems) {
break
}
}
var (
hasBailRule = v.bail
hasCaseInsensitive = v.caseInsensitive
)
for index := 0; index < len(ruleItems); {
var (
err error
match = false // whether this rule is matched(has no error)
results = ruleRegex.FindStringSubmatch(ruleItems[index]) // split single rule.
ruleKey = gstr.Trim(results[1]) // rule key like "max" in rule "max: 6"
rulePattern = gstr.Trim(results[2]) // rule pattern is like "6" in rule:"max:6"
customRuleFunc RuleFunc
)
if !hasBailRule && ruleKey == ruleNameBail {
hasBailRule = true
}
if !hasCaseInsensitive && ruleKey == ruleNameCi {
hasCaseInsensitive = true
}
// Ignore logic executing for marked rules.
if markedRuleMap[ruleKey] {
index++
continue
}
if len(msgArray) > index {
customMsgMap[ruleKey] = strings.TrimSpace(msgArray[index])
}
// Custom rule handling.
// 1. It firstly checks and uses the custom registered rules functions in the current Validator.
// 2. It secondly checks and uses the globally registered rules functions.
// 3. It finally checks and uses the build-in rules functions.
customRuleFunc = v.getRuleFunc(ruleKey)
if customRuleFunc != nil {
// It checks custom validation rules with most priority.
message := v.getErrorMessageByRule(ctx, ruleKey, customMsgMap)
if err = customRuleFunc(ctx, RuleFuncInput{
Rule: ruleItems[index],
Message: message,
Value: gvar.New(in.Value),
Data: gvar.New(in.DataRaw),
}); err != nil {
match = false
// The error should have stack info to indicate the error position.
if !gerror.HasStack(err) {
err = gerror.NewCodeSkip(gcode.CodeValidationFailed, 1, err.Error())
}
// The error should have error code that is `gcode.CodeValidationFailed`.
if gerror.Code(err) == gcode.CodeNil {
if e, ok := err.(*gerror.Error); ok {
e.SetCode(gcode.CodeValidationFailed)
}
}
ruleErrorMap[ruleKey] = err
} else {
match = true
}
} else {
// It checks build-in validation rules if there's no custom rule.
match, err = v.doCheckSingleBuildInRules(
ctx,
doCheckBuildInRulesInput{
Index: index,
Value: in.Value,
RuleKey: ruleKey,
RulePattern: rulePattern,
RuleItems: ruleItems,
DataMap: in.DataMap,
CustomMsgMap: customMsgMap,
CaseInsensitive: hasCaseInsensitive,
},
)
if !match && err != nil {
ruleErrorMap[ruleKey] = err
}
}
// Error message handling.
if !match {
// It does nothing if the error message for this rule
// is already set in previous validation.
if _, ok := ruleErrorMap[ruleKey]; !ok {
ruleErrorMap[ruleKey] = errors.New(v.getErrorMessageByRule(ctx, ruleKey, customMsgMap))
}
// Error variable replacement for error message.
if err = ruleErrorMap[ruleKey]; !gerror.HasStack(err) {
var s string
s = gstr.ReplaceByMap(err.Error(), map[string]string{
"{value}": gconv.String(in.Value),
"{pattern}": rulePattern,
"{attribute}": in.Name,
})
s, _ = gregex.ReplaceString(`\s{2,}`, ` `, s)
ruleErrorMap[ruleKey] = errors.New(s)
}
// If it is with error and there's bail rule,
// it then does not continue validating for left rules.
if hasBailRule {
break
}
}
index++
}
if len(ruleErrorMap) > 0 {
return newValidationError(
gcode.CodeValidationFailed,
[]fieldRule{{Name: in.Name, Rule: in.Rule}},
map[string]map[string]error{
in.Name: ruleErrorMap,
},
)
}
return nil
}
type doCheckBuildInRulesInput struct {
Index int // Index of RuleKey in RuleItems.
Value interface{} // Value to be validated.
RuleKey string // RuleKey is like the "max" in rule "max: 6"
RulePattern string // RulePattern is like "6" in rule:"max:6"
RuleItems []string // RuleItems are all the rules that should be validated on single field, like: []string{"required", "min:1"}
DataMap map[string]interface{} // Parameter map.
CustomMsgMap map[string]string // Custom error message map.
CaseInsensitive bool // Case-Insensitive comparison.
}
func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, in doCheckBuildInRulesInput) (match bool, err error) {
valueStr := gconv.String(in.Value)
switch in.RuleKey {
// Required rules.
case
"required",
"required-if",
"required-unless",
"required-with",
"required-with-all",
"required-without",
"required-without-all":
match = v.checkRequired(checkRequiredInput{
Value: in.Value,
RuleKey: in.RuleKey,
RulePattern: in.RulePattern,
DataMap: in.DataMap,
CaseInsensitive: in.CaseInsensitive,
})
// Length rules.
// It also supports length of unicode string.
case
"length",
"min-length",
"max-length",
"size":
if msg := v.checkLength(ctx, valueStr, in.RuleKey, in.RulePattern, in.CustomMsgMap); msg != "" {
return match, errors.New(msg)
} else {
match = true
}
// Range rules.
case
"min",
"max",
"between":
if msg := v.checkRange(ctx, valueStr, in.RuleKey, in.RulePattern, in.CustomMsgMap); msg != "" {
return match, errors.New(msg)
} else {
match = true
}
// Custom regular expression.
case "regex":
// It here should check the rule as there might be special char '|' in it.
for i := in.Index + 1; i < len(in.RuleItems); i++ {
if !gregex.IsMatchString(singleRulePattern, in.RuleItems[i]) {
in.RulePattern += "|" + in.RuleItems[i]
in.Index++
}
}
match = gregex.IsMatchString(in.RulePattern, valueStr)
// Date rules.
case "date":
// support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time.
if value, ok := in.Value.(iTime); ok {
return !value.IsZero(), nil
}
match = gregex.IsMatchString(`\d{4}[\.\-\_/]{0,1}\d{2}[\.\-\_/]{0,1}\d{2}`, valueStr)
// Datetime rule.
case "datetime":
// support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time.
if value, ok := in.Value.(iTime); ok {
return !value.IsZero(), nil
}
if _, err = gtime.StrToTimeFormat(valueStr, `Y-m-d H:i:s`); err == nil {
match = true
}
// Date rule with specified format.
case "date-format":
// support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time.
if value, ok := in.Value.(iTime); ok {
return !value.IsZero(), nil
}
if _, err = gtime.StrToTimeFormat(valueStr, in.RulePattern); err == nil {
match = true
} else {
var (
msg string
)
msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap)
return match, errors.New(msg)
}
// Values of two fields should be equal as string.
case "same":
_, foundValue := gutil.MapPossibleItemByKey(in.DataMap, in.RulePattern)
if foundValue != nil {
if in.CaseInsensitive {
match = strings.EqualFold(valueStr, gconv.String(foundValue))
} else {
match = strings.Compare(valueStr, gconv.String(foundValue)) == 0
}
}
if !match {
var msg string
msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap)
return match, errors.New(msg)
}
// Values of two fields should not be equal as string.
case "different":
match = true
_, foundValue := gutil.MapPossibleItemByKey(in.DataMap, in.RulePattern)
if foundValue != nil {
if in.CaseInsensitive {
match = !strings.EqualFold(valueStr, gconv.String(foundValue))
} else {
match = strings.Compare(valueStr, gconv.String(foundValue)) != 0
}
}
if !match {
var msg string
msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap)
return match, errors.New(msg)
}
// Field value should be in range of.
case "in":
for _, value := range gstr.SplitAndTrim(in.RulePattern, ",") {
if in.CaseInsensitive {
match = strings.EqualFold(valueStr, strings.TrimSpace(value))
} else {
match = strings.Compare(valueStr, strings.TrimSpace(value)) == 0
}
if match {
break
}
}
// Field value should not be in range of.
case "not-in":
match = true
for _, value := range gstr.SplitAndTrim(in.RulePattern, ",") {
if in.CaseInsensitive {
match = !strings.EqualFold(valueStr, strings.TrimSpace(value))
} else {
match = strings.Compare(valueStr, strings.TrimSpace(value)) != 0
}
if !match {
break
}
}
// Phone format validation.
// 1. China Mobile:
// 134, 135, 136, 137, 138, 139, 150, 151, 152, 157, 158, 159, 182, 183, 184, 187, 188,
// 178(4G), 147(Net)
// 172
//
// 2. China Unicom:
// 130, 131, 132, 155, 156, 185, 186 ,176(4G), 145(Net), 175
//
// 3. China Telecom:
// 133, 153, 180, 181, 189, 177(4G)
//
// 4. Satelite:
// 1349
//
// 5. Virtual:
// 170, 173
//
// 6. 2018:
// 16x, 19x
case "phone":
match = gregex.IsMatchString(`^13[\d]{9}$|^14[5,7]{1}\d{8}$|^15[^4]{1}\d{8}$|^16[\d]{9}$|^17[0,2,3,5,6,7,8]{1}\d{8}$|^18[\d]{9}$|^19[\d]{9}$`, valueStr)
// Loose mobile phone number verification(宽松的手机号验证)
// As long as the 11 digit numbers beginning with
// 13, 14, 15, 16, 17, 18, 19 can pass the verification (只要满足 13、14、15、16、17、18、19开头的11位数字都可以通过验证)
case "phone-loose":
match = gregex.IsMatchString(`^1(3|4|5|6|7|8|9)\d{9}$`, valueStr)
// Telephone number:
// "XXXX-XXXXXXX"
// "XXXX-XXXXXXXX"
// "XXX-XXXXXXX"
// "XXX-XXXXXXXX"
// "XXXXXXX"
// "XXXXXXXX"
case "telephone":
match = gregex.IsMatchString(`^((\d{3,4})|\d{3,4}-)?\d{7,8}$`, valueStr)
// QQ number: from 10000.
case "qq":
match = gregex.IsMatchString(`^[1-9][0-9]{4,}$`, valueStr)
// Postcode number.
case "postcode":
match = gregex.IsMatchString(`^\d{6}$`, valueStr)
// China resident id number.
//
// xxxxxx yyyy MM dd 375 0 十八位
// xxxxxx yy MM dd 75 0 十五位
//
// 地区: [1-9]\d{5}
// 年的前两位:(18|19|([23]\d)) 1800-2399
// 年的后两位:\d{2}
// 月份: ((0[1-9])|(10|11|12))
// 天数: (([0-2][1-9])|10|20|30|31) 闰年不能禁止29+
//
// 三位顺序码:\d{3}
// 两位顺序码:\d{2}
// 校验码: [0-9Xx]
//
// 十八位:^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$
// 十五位:^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$
//
// 总:
// (^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$)
case "resident-id":
match = v.checkResidentId(valueStr)
// Bank card number using LUHN algorithm.
case "bank-card":
match = v.checkLuHn(valueStr)
// Universal passport format rule:
// Starting with letter, containing only numbers or underscores, length between 6 and 18.
case "passport":
match = gregex.IsMatchString(`^[a-zA-Z]{1}\w{5,17}$`, valueStr)
// Universal password format rule1:
// Containing any visible chars, length between 6 and 18.
case "password":
match = gregex.IsMatchString(`^[\w\S]{6,18}$`, valueStr)
// Universal password format rule2:
// Must meet password rule1, must contain lower and upper letters and numbers.
case "password2":
if gregex.IsMatchString(`^[\w\S]{6,18}$`, valueStr) &&
gregex.IsMatchString(`[a-z]+`, valueStr) &&
gregex.IsMatchString(`[A-Z]+`, valueStr) &&
gregex.IsMatchString(`\d+`, valueStr) {
match = true
}
// Universal password format rule3:
// Must meet password rule1, must contain lower and upper letters, numbers and special chars.
case "password3":
if gregex.IsMatchString(`^[\w\S]{6,18}$`, valueStr) &&
gregex.IsMatchString(`[a-z]+`, valueStr) &&
gregex.IsMatchString(`[A-Z]+`, valueStr) &&
gregex.IsMatchString(`\d+`, valueStr) &&
gregex.IsMatchString(`[^a-zA-Z0-9]+`, valueStr) {
match = true
}
// Json.
case "json":
if json.Valid([]byte(valueStr)) {
match = true
}
// Integer.
case "integer":
if _, err = strconv.Atoi(valueStr); err == nil {
match = true
}
// Float.
case "float":
if _, err = strconv.ParseFloat(valueStr, 10); err == nil {
match = true
}
// Boolean(1,true,on,yes:true | 0,false,off,no,"":false).
case "boolean":
match = false
if _, ok := boolMap[strings.ToLower(valueStr)]; ok {
match = true
}
// Email.
case "email":
match = gregex.IsMatchString(`^[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9_\-]+(\.[a-zA-Z0-9_\-]+)+$`, valueStr)
// URL
case "url":
match = gregex.IsMatchString(`(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]`, valueStr)
// Domain
case "domain":
match = gregex.IsMatchString(`^([0-9a-zA-Z][0-9a-zA-Z\-]{0,62}\.)+([a-zA-Z]{0,62})$`, valueStr)
// IP(IPv4/IPv6).
case "ip":
match = gipv4.Validate(valueStr) || gipv6.Validate(valueStr)
// IPv4.
case "ipv4":
match = gipv4.Validate(valueStr)
// IPv6.
case "ipv6":
match = gipv6.Validate(valueStr)
// MAC.
case "mac":
match = gregex.IsMatchString(`^([0-9A-Fa-f]{2}[\-:]){5}[0-9A-Fa-f]{2}$`, valueStr)
default:
return match, errors.New("Invalid rule name: " + in.RuleKey)
}
return match, nil
}
type doCheckValueRecursivelyInput struct {
Value interface{}
Type reflect.Type
OriginKind reflect.Kind
ErrorMaps map[string]map[string]error
ResultSequenceRules *[]fieldRule
}
func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValueRecursivelyInput) {
switch in.OriginKind {
case reflect.Struct:
// Ignore data, rules and messages from parent.
validator := v.Clone()
validator.rules = nil
validator.messages = nil
if err := validator.Data(reflect.New(in.Type).Interface()).Assoc(in.Value).Run(ctx); err != nil {
// It merges the errors into single error map.
for k, m := range err.(*validationError).errors {
in.ErrorMaps[k] = m
}
if in.ResultSequenceRules != nil {
*in.ResultSequenceRules = append(*in.ResultSequenceRules, err.(*validationError).rules...)
}
}
case reflect.Map:
var (
dataMap = gconv.Map(in.Value)
)
for _, item := range dataMap {
originTypeAndKind := utils.OriginTypeAndKind(item)
v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
Value: item,
Type: originTypeAndKind.InputType,
OriginKind: originTypeAndKind.OriginKind,
ErrorMaps: in.ErrorMaps,
ResultSequenceRules: in.ResultSequenceRules,
})
// Bail feature.
if v.bail && len(in.ErrorMaps) > 0 {
break
}
}
case reflect.Slice, reflect.Array:
array := gconv.Interfaces(in.Value)
if len(array) == 0 {
return
}
for _, item := range array {
originTypeAndKind := utils.OriginTypeAndKind(item)
v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
Value: item,
Type: originTypeAndKind.InputType,
OriginKind: originTypeAndKind.OriginKind,
ErrorMaps: in.ErrorMaps,
ResultSequenceRules: in.ResultSequenceRules,
})
// Bail feature.
if v.bail && len(in.ErrorMaps) > 0 {
break
}
}
}
}