Files

159 lines
5.1 KiB
Go
Raw Permalink 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 2022 E99p1ant. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package form
import (
"mime"
"mime/multipart"
"net/http"
"reflect"
"github.com/flamego/flamego"
"github.com/flamego/template"
"github.com/unknwon/com"
"github.com/wuhan005/govalid"
"github.com/tamakyi/TamaBox/internal/context"
)
type ErrorCategory string
const (
ErrorCategoryDeserialization ErrorCategory = "deserialization"
ErrorCategoryValidation ErrorCategory = "validation"
)
type Error struct {
Category ErrorCategory
Error error
}
const (
multipartMemoryLimit = 10 << 20 // 10 MiB:multipart 内存驻留上限,超出由 mime/multipart 落盘
// MaxRequestBodySize 请求体总大小上限(含落盘部分),略高于内存上限以留出开销。
//
// ⚠️ 必须显式限制:ParseMultipartForm 的参数只约束内存缓冲,超出部分会无上限写入
// 临时文件;匿名可达的提问接口即接受 multipart,无总量限制则可持续撑满磁盘。
// 导出供 internal/route/bodylimit.go 的全局中间件复用——仅靠本 Bind 内限制不够,
// 组级 CSRF 校验先于 Bind 执行且会整包惰性解析 body,须把上限提前到一切解析之前。
MaxRequestBodySize = 12 << 20 // 12 MiB
)
func Bind(model interface{}) flamego.Handler {
if reflect.TypeOf(model).Kind() == reflect.Ptr {
panic("form: pointer can not be accepted as binding model")
}
return func(c context.Context, data template.Data) {
obj := reflect.New(reflect.TypeOf(model))
defer func() { c.Map(obj.Elem().Interface()) }()
r := c.Request()
// 在任何解析之前封顶请求体总大小。c.Request() 是 flamego 包装类型(Body 是方法
// 而非字段),须取内嵌 *http.Request 才能替换。超限后 Read 报错,使 ParseForm /
// ParseMultipartForm 失败并走反序列化错误分支,避免超大 body 落盘或读入内存。
httpReq := r.Request
httpReq.Body = http.MaxBytesReader(c.ResponseWriter(), httpReq.Body, MaxRequestBodySize)
contentType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
switch contentType {
case "application/x-www-form-urlencoded", "":
if err := r.ParseForm(); err != nil {
// ⚠️ 必须 SetError:flamego 的 handler 链只要响应未写入就会自动执行下一 handler,
// 仅 Map(Error) 不中断链,零值结构体会照常进入业务动作(静默清空屏蔽词、解除
// 页面密码等)。SetError 置位 HasError,action 开头的 abortOnFormError 守卫会早退。
c.SetError(err)
c.Map(Error{Category: ErrorCategoryDeserialization, Error: err})
return
}
case "multipart/form-data":
if err := r.ParseMultipartForm(multipartMemoryLimit); err != nil {
c.SetError(err)
c.Map(Error{Category: ErrorCategoryDeserialization, Error: err})
return
}
}
typ := reflect.TypeOf(obj.Interface())
val := reflect.ValueOf(obj.Interface())
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
}
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fieldName := typ.Field(i).Tag.Get("form")
if fieldName == "" {
fieldName = com.ToSnakeCase(field.Name)
}
if r.MultipartForm != nil {
fhType := reflect.TypeOf((*multipart.FileHeader)(nil))
value, ok := r.MultipartForm.Value[fieldName]
if ok {
if val.Field(i).Kind() == reflect.Slice && val.Field(i).Type().Elem() == fhType {
continue
}
// FIXME: We don't implement the slice type yet.
val.Field(i).Set(reflect.ValueOf(value[0]))
} else {
value, ok := r.MultipartForm.File[fieldName]
if ok {
numElems := len(value)
if val.Field(i).Kind() == reflect.Slice && numElems > 0 && val.Field(i).Type().Elem() == fhType {
slice := reflect.MakeSlice(val.Field(i).Type(), numElems, numElems)
for i := 0; i < numElems; i++ {
slice.Index(i).Set(reflect.ValueOf(value[i]))
}
val.Field(i).Set(slice)
} else if val.Field(i).Type() == fhType {
val.Field(i).Set(reflect.ValueOf(value[0]))
}
}
}
} else {
// 非 multipart 请求仅绑定 string 字段,其余类型(如 []*multipart.FileHeader
// 图片字段)保持零值,否则 reflect.Set 会因类型不匹配 panic。
// 带 enctype 的提问表单走上面分支从未触发,纯文本提交(如私聊)会命中此路径。
if val.Field(i).Kind() == reflect.String {
val.Field(i).Set(reflect.ValueOf(r.Form.Get(fieldName)))
}
}
}
errors, ok := govalid.Check(obj.Interface())
if !ok {
Assign(obj.Interface(), data)
c.SetError(errors[0])
c.Map(Error{Category: ErrorCategoryValidation, Error: errors[0]})
return
}
}
}
// Assign assigns form values back to the template data.
func Assign(form interface{}, data map[string]interface{}) {
typ := reflect.TypeOf(form)
val := reflect.ValueOf(form)
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
}
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fieldName := com.ToSnakeCase(field.Name)
data[fieldName] = val.Field(i).Interface()
}
}