github.com/gogf/gf@v1.16.9/util/gvalid/gvalid_validator_check_value.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  
     7  package gvalid
     8  
     9  import (
    10  	"github.com/gogf/gf/errors/gcode"
    11  	"github.com/gogf/gf/errors/gerror"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/gogf/gf/internal/json"
    17  	"github.com/gogf/gf/net/gipv4"
    18  	"github.com/gogf/gf/net/gipv6"
    19  	"github.com/gogf/gf/os/gtime"
    20  	"github.com/gogf/gf/text/gregex"
    21  	"github.com/gogf/gf/util/gconv"
    22  	"github.com/gogf/gf/util/gutil"
    23  )
    24  
    25  type apiTime interface {
    26  	Date() (year int, month time.Month, day int)
    27  	IsZero() bool
    28  }
    29  
    30  // CheckValue checks single value with specified rules.
    31  // It returns nil if successful validation.
    32  func (v *Validator) CheckValue(value interface{}) Error {
    33  	return v.doCheckValue(doCheckValueInput{
    34  		Name:     "",
    35  		Value:    value,
    36  		Rule:     gconv.String(v.rules),
    37  		Messages: v.messages,
    38  		DataRaw:  v.data,
    39  		DataMap:  gconv.Map(v.data),
    40  	})
    41  }
    42  
    43  type doCheckValueInput struct {
    44  	Name     string                 // Name specifies the name of parameter `value`.
    45  	Value    interface{}            // Value specifies the value for this rules to be validated.
    46  	Rule     string                 // Rule specifies the validation rules string, like "required", "required|between:1,100", etc.
    47  	Messages interface{}            // Messages specifies the custom error messages for this rule, which is usually type of map/slice.
    48  	DataRaw  interface{}            // DataRaw specifies the `raw data` which is passed to the Validator. It might be type of map/struct or a nil value.
    49  	DataMap  map[string]interface{} // DataMap specifies the map that is converted from `dataRaw`. It is usually used internally
    50  }
    51  
    52  // doCheckSingleValue does the really rules validation for single key-value.
    53  func (v *Validator) doCheckValue(input doCheckValueInput) Error {
    54  	// If there's no validation rules, it does nothing and returns quickly.
    55  	if input.Rule == "" {
    56  		return nil
    57  	}
    58  	// It converts value to string and then does the validation.
    59  	var (
    60  		// Do not trim it as the space is also part of the value.
    61  		errorMsgArray = make(map[string]string)
    62  	)
    63  	// Custom error messages handling.
    64  	var (
    65  		msgArray     = make([]string, 0)
    66  		customMsgMap = make(map[string]string)
    67  	)
    68  	switch v := input.Messages.(type) {
    69  	case string:
    70  		msgArray = strings.Split(v, "|")
    71  	default:
    72  		for k, v := range gconv.Map(input.Messages) {
    73  			customMsgMap[k] = gconv.String(v)
    74  		}
    75  	}
    76  	// Handle the char '|' in the rule,
    77  	// which makes this rule separated into multiple rules.
    78  	ruleItems := strings.Split(strings.TrimSpace(input.Rule), "|")
    79  	for i := 0; ; {
    80  		array := strings.Split(ruleItems[i], ":")
    81  		_, ok := allSupportedRules[array[0]]
    82  		if !ok && v.getRuleFunc(array[0]) == nil {
    83  			if i > 0 && ruleItems[i-1][:5] == "regex" {
    84  				ruleItems[i-1] += "|" + ruleItems[i]
    85  				ruleItems = append(ruleItems[:i], ruleItems[i+1:]...)
    86  			} else {
    87  				return newErrorStr(
    88  					internalRulesErrRuleName,
    89  					internalRulesErrRuleName+": "+input.Rule,
    90  				)
    91  			}
    92  		} else {
    93  			i++
    94  		}
    95  		if i == len(ruleItems) {
    96  			break
    97  		}
    98  	}
    99  	var (
   100  		hasBailRule = false
   101  	)
   102  	for index := 0; index < len(ruleItems); {
   103  		var (
   104  			err            error
   105  			match          = false                                          // whether this rule is matched(has no error)
   106  			results        = ruleRegex.FindStringSubmatch(ruleItems[index]) // split single rule.
   107  			ruleKey        = strings.TrimSpace(results[1])                  // rule name like "max" in rule "max: 6"
   108  			rulePattern    = strings.TrimSpace(results[2])                  // rule value if any like "6" in rule:"max:6"
   109  			customRuleFunc RuleFunc
   110  		)
   111  
   112  		if !hasBailRule && ruleKey == bailRuleName {
   113  			hasBailRule = true
   114  		}
   115  
   116  		// Ignore logic executing for marked rules.
   117  		if markedRuleMap[ruleKey] {
   118  			index++
   119  			continue
   120  		}
   121  
   122  		if len(msgArray) > index {
   123  			customMsgMap[ruleKey] = strings.TrimSpace(msgArray[index])
   124  		}
   125  
   126  		// Custom rule handling.
   127  		// 1. It firstly checks and uses the custom registered rules functions in the current Validator.
   128  		// 2. It secondly checks and uses the globally registered rules functions.
   129  		// 3. It finally checks and uses the build-in rules functions.
   130  		customRuleFunc = v.getRuleFunc(ruleKey)
   131  		if customRuleFunc != nil {
   132  			// It checks custom validation rules with most priority.
   133  			message := v.getErrorMessageByRule(ruleKey, customMsgMap)
   134  			if err := customRuleFunc(v.ctx, ruleItems[index], input.Value, message, input.DataRaw); err != nil {
   135  				match = false
   136  				errorMsgArray[ruleKey] = err.Error()
   137  			} else {
   138  				match = true
   139  			}
   140  		} else {
   141  			// It checks build-in validation rules if there's no custom rule.
   142  			match, err = v.doCheckBuildInRules(doCheckBuildInRulesInput{
   143  				Index:        index,
   144  				Value:        input.Value,
   145  				RuleKey:      ruleKey,
   146  				RulePattern:  rulePattern,
   147  				RuleItems:    ruleItems,
   148  				DataMap:      input.DataMap,
   149  				CustomMsgMap: customMsgMap,
   150  			})
   151  			if !match && err != nil {
   152  				errorMsgArray[ruleKey] = err.Error()
   153  			}
   154  		}
   155  
   156  		// Error message handling.
   157  		if !match {
   158  			// It does nothing if the error message for this rule
   159  			// is already set in previous validation.
   160  			if _, ok := errorMsgArray[ruleKey]; !ok {
   161  				errorMsgArray[ruleKey] = v.getErrorMessageByRule(ruleKey, customMsgMap)
   162  			}
   163  			// If it is with error and there's bail rule,
   164  			// it then does not continue validating for left rules.
   165  			if hasBailRule {
   166  				break
   167  			}
   168  		}
   169  		index++
   170  	}
   171  	if len(errorMsgArray) > 0 {
   172  		return newError(gcode.CodeValidationFailed, []fieldRule{{Name: input.Name, Rule: input.Rule}}, map[string]map[string]string{
   173  			input.Name: errorMsgArray,
   174  		})
   175  	}
   176  	return nil
   177  }
   178  
   179  type doCheckBuildInRulesInput struct {
   180  	Index        int
   181  	Value        interface{}
   182  	RuleKey      string
   183  	RulePattern  string
   184  	RuleItems    []string
   185  	DataMap      map[string]interface{}
   186  	CustomMsgMap map[string]string
   187  }
   188  
   189  func (v *Validator) doCheckBuildInRules(input doCheckBuildInRulesInput) (match bool, err error) {
   190  	valueStr := gconv.String(input.Value)
   191  	switch input.RuleKey {
   192  	// Required rules.
   193  	case
   194  		"required",
   195  		"required-if",
   196  		"required-unless",
   197  		"required-with",
   198  		"required-with-all",
   199  		"required-without",
   200  		"required-without-all":
   201  		match = v.checkRequired(input.Value, input.RuleKey, input.RulePattern, input.DataMap)
   202  
   203  	// Length rules.
   204  	// It also supports length of unicode string.
   205  	case
   206  		"length",
   207  		"min-length",
   208  		"max-length",
   209  		"size":
   210  		if msg := v.checkLength(valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" {
   211  			return match, gerror.NewOption(gerror.Option{
   212  				Text: msg,
   213  				Code: gcode.CodeValidationFailed,
   214  			})
   215  		} else {
   216  			match = true
   217  		}
   218  
   219  	// Range rules.
   220  	case
   221  		"min",
   222  		"max",
   223  		"between":
   224  		if msg := v.checkRange(valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" {
   225  			return match, gerror.NewOption(gerror.Option{
   226  				Text: msg,
   227  				Code: gcode.CodeValidationFailed,
   228  			})
   229  		} else {
   230  			match = true
   231  		}
   232  
   233  	// Custom regular expression.
   234  	case "regex":
   235  		// It here should check the rule as there might be special char '|' in it.
   236  		for i := input.Index + 1; i < len(input.RuleItems); i++ {
   237  			if !gregex.IsMatchString(singleRulePattern, input.RuleItems[i]) {
   238  				input.RulePattern += "|" + input.RuleItems[i]
   239  				input.Index++
   240  			}
   241  		}
   242  		match = gregex.IsMatchString(input.RulePattern, valueStr)
   243  
   244  	// Date rules.
   245  	case "date":
   246  		// support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time.
   247  		if v, ok := input.Value.(apiTime); ok {
   248  			return !v.IsZero(), nil
   249  		}
   250  		match = gregex.IsMatchString(`\d{4}[\.\-\_/]{0,1}\d{2}[\.\-\_/]{0,1}\d{2}`, valueStr)
   251  
   252  	// Date rule with specified format.
   253  	case "date-format":
   254  		// support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time.
   255  		if v, ok := input.Value.(apiTime); ok {
   256  			return !v.IsZero(), nil
   257  		}
   258  		if _, err := gtime.StrToTimeFormat(valueStr, input.RulePattern); err == nil {
   259  			match = true
   260  		} else {
   261  			var msg string
   262  			msg = v.getErrorMessageByRule(input.RuleKey, input.CustomMsgMap)
   263  			msg = strings.Replace(msg, ":format", input.RulePattern, -1)
   264  			return match, gerror.NewOption(gerror.Option{
   265  				Text: msg,
   266  				Code: gcode.CodeValidationFailed,
   267  			})
   268  		}
   269  
   270  	// Values of two fields should be equal as string.
   271  	case "same":
   272  		_, foundValue := gutil.MapPossibleItemByKey(input.DataMap, input.RulePattern)
   273  		if foundValue != nil {
   274  			if strings.Compare(valueStr, gconv.String(foundValue)) == 0 {
   275  				match = true
   276  			}
   277  		}
   278  		if !match {
   279  			var msg string
   280  			msg = v.getErrorMessageByRule(input.RuleKey, input.CustomMsgMap)
   281  			msg = strings.Replace(msg, ":field", input.RulePattern, -1)
   282  			return match, gerror.NewOption(gerror.Option{
   283  				Text: msg,
   284  				Code: gcode.CodeValidationFailed,
   285  			})
   286  		}
   287  
   288  	// Values of two fields should not be equal as string.
   289  	case "different":
   290  		match = true
   291  		_, foundValue := gutil.MapPossibleItemByKey(input.DataMap, input.RulePattern)
   292  		if foundValue != nil {
   293  			if strings.Compare(valueStr, gconv.String(foundValue)) == 0 {
   294  				match = false
   295  			}
   296  		}
   297  		if !match {
   298  			var msg string
   299  			msg = v.getErrorMessageByRule(input.RuleKey, input.CustomMsgMap)
   300  			msg = strings.Replace(msg, ":field", input.RulePattern, -1)
   301  			return match, gerror.NewOption(gerror.Option{
   302  				Text: msg,
   303  				Code: gcode.CodeValidationFailed,
   304  			})
   305  		}
   306  
   307  	// Field value should be in range of.
   308  	case "in":
   309  		array := strings.Split(input.RulePattern, ",")
   310  		for _, v := range array {
   311  			if strings.Compare(valueStr, strings.TrimSpace(v)) == 0 {
   312  				match = true
   313  				break
   314  			}
   315  		}
   316  
   317  	// Field value should not be in range of.
   318  	case "not-in":
   319  		match = true
   320  		array := strings.Split(input.RulePattern, ",")
   321  		for _, v := range array {
   322  			if strings.Compare(valueStr, strings.TrimSpace(v)) == 0 {
   323  				match = false
   324  				break
   325  			}
   326  		}
   327  
   328  	// Phone format validation.
   329  	// 1. China Mobile:
   330  	//    134, 135, 136, 137, 138, 139, 150, 151, 152, 157, 158, 159, 182, 183, 184, 187, 188,
   331  	//    178(4G), 147(Net);
   332  	//    172
   333  	//
   334  	// 2. China Unicom:
   335  	//    130, 131, 132, 155, 156, 185, 186 ,176(4G), 145(Net), 175
   336  	//
   337  	// 3. China Telecom:
   338  	//    133, 153, 180, 181, 189, 177(4G)
   339  	//
   340  	// 4. Satelite:
   341  	//    1349
   342  	//
   343  	// 5. Virtual:
   344  	//    170, 173
   345  	//
   346  	// 6. 2018:
   347  	//    16x, 19x
   348  	case "phone":
   349  		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)
   350  
   351  	// Loose mobile phone number verification(宽松的手机号验证)
   352  	// As long as the 11 digit numbers beginning with
   353  	// 13, 14, 15, 16, 17, 18, 19 can pass the verification (只要满足 13、14、15、16、17、18、19开头的11位数字都可以通过验证)
   354  	case "phone-loose":
   355  		match = gregex.IsMatchString(`^1(3|4|5|6|7|8|9)\d{9}$`, valueStr)
   356  
   357  	// Telephone number:
   358  	// "XXXX-XXXXXXX"
   359  	// "XXXX-XXXXXXXX"
   360  	// "XXX-XXXXXXX"
   361  	// "XXX-XXXXXXXX"
   362  	// "XXXXXXX"
   363  	// "XXXXXXXX"
   364  	case "telephone":
   365  		match = gregex.IsMatchString(`^((\d{3,4})|\d{3,4}-)?\d{7,8}$`, valueStr)
   366  
   367  	// QQ number: from 10000.
   368  	case "qq":
   369  		match = gregex.IsMatchString(`^[1-9][0-9]{4,}$`, valueStr)
   370  
   371  	// Postcode number.
   372  	case "postcode":
   373  		match = gregex.IsMatchString(`^\d{6}$`, valueStr)
   374  
   375  	// China resident id number.
   376  	//
   377  	// xxxxxx yyyy MM dd 375 0  十八位
   378  	// xxxxxx   yy MM dd  75 0  十五位
   379  	//
   380  	// 地区:     [1-9]\d{5}
   381  	// 年的前两位:(18|19|([23]\d))  1800-2399
   382  	// 年的后两位:\d{2}
   383  	// 月份:     ((0[1-9])|(10|11|12))
   384  	// 天数:     (([0-2][1-9])|10|20|30|31) 闰年不能禁止29+
   385  	//
   386  	// 三位顺序码:\d{3}
   387  	// 两位顺序码:\d{2}
   388  	// 校验码:   [0-9Xx]
   389  	//
   390  	// 十八位:^[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]$
   391  	// 十五位:^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$
   392  	//
   393  	// 总:
   394  	// (^[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}$)
   395  	case "resident-id":
   396  		match = v.checkResidentId(valueStr)
   397  
   398  	// Bank card number using LUHN algorithm.
   399  	case "bank-card":
   400  		match = v.checkLuHn(valueStr)
   401  
   402  	// Universal passport format rule:
   403  	// Starting with letter, containing only numbers or underscores, length between 6 and 18.
   404  	case "passport":
   405  		match = gregex.IsMatchString(`^[a-zA-Z]{1}\w{5,17}$`, valueStr)
   406  
   407  	// Universal password format rule1:
   408  	// Containing any visible chars, length between 6 and 18.
   409  	case "password":
   410  		match = gregex.IsMatchString(`^[\w\S]{6,18}$`, valueStr)
   411  
   412  	// Universal password format rule2:
   413  	// Must meet password rule1, must contain lower and upper letters and numbers.
   414  	case "password2":
   415  		if gregex.IsMatchString(`^[\w\S]{6,18}$`, valueStr) &&
   416  			gregex.IsMatchString(`[a-z]+`, valueStr) &&
   417  			gregex.IsMatchString(`[A-Z]+`, valueStr) &&
   418  			gregex.IsMatchString(`\d+`, valueStr) {
   419  			match = true
   420  		}
   421  
   422  	// Universal password format rule3:
   423  	// Must meet password rule1, must contain lower and upper letters, numbers and special chars.
   424  	case "password3":
   425  		if gregex.IsMatchString(`^[\w\S]{6,18}$`, valueStr) &&
   426  			gregex.IsMatchString(`[a-z]+`, valueStr) &&
   427  			gregex.IsMatchString(`[A-Z]+`, valueStr) &&
   428  			gregex.IsMatchString(`\d+`, valueStr) &&
   429  			gregex.IsMatchString(`[^a-zA-Z0-9]+`, valueStr) {
   430  			match = true
   431  		}
   432  
   433  	// Json.
   434  	case "json":
   435  		if json.Valid([]byte(valueStr)) {
   436  			match = true
   437  		}
   438  
   439  	// Integer.
   440  	case "integer":
   441  		if _, err := strconv.Atoi(valueStr); err == nil {
   442  			match = true
   443  		}
   444  
   445  	// Float.
   446  	case "float":
   447  		if _, err := strconv.ParseFloat(valueStr, 10); err == nil {
   448  			match = true
   449  		}
   450  
   451  	// Boolean(1,true,on,yes:true | 0,false,off,no,"":false).
   452  	case "boolean":
   453  		match = false
   454  		if _, ok := boolMap[strings.ToLower(valueStr)]; ok {
   455  			match = true
   456  		}
   457  
   458  	// Email.
   459  	case "email":
   460  		match = gregex.IsMatchString(`^[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9_\-]+(\.[a-zA-Z0-9_\-]+)+$`, valueStr)
   461  
   462  	// URL
   463  	case "url":
   464  		match = gregex.IsMatchString(`(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]`, valueStr)
   465  
   466  	// Domain
   467  	case "domain":
   468  		match = gregex.IsMatchString(`^([0-9a-zA-Z][0-9a-zA-Z\-]{0,62}\.)+([a-zA-Z]{0,62})$`, valueStr)
   469  
   470  	// IP(IPv4/IPv6).
   471  	case "ip":
   472  		match = gipv4.Validate(valueStr) || gipv6.Validate(valueStr)
   473  
   474  	// IPv4.
   475  	case "ipv4":
   476  		match = gipv4.Validate(valueStr)
   477  
   478  	// IPv6.
   479  	case "ipv6":
   480  		match = gipv6.Validate(valueStr)
   481  
   482  	// MAC.
   483  	case "mac":
   484  		match = gregex.IsMatchString(`^([0-9A-Fa-f]{2}[\-:]){5}[0-9A-Fa-f]{2}$`, valueStr)
   485  
   486  	default:
   487  		return match, gerror.NewOption(gerror.Option{
   488  			Text: "Invalid rule name: " + input.RuleKey,
   489  			Code: gcode.CodeInvalidParameter,
   490  		})
   491  	}
   492  	return match, nil
   493  }