1. Move BindXXX core logic to context.Context for two reasons:
   1.1 Controller should be stateless -- Due to historical reason, it's hard for us to do this but we should try it
   1.2 If users didn't use Controller to write their functions, they should be allowed to use those methods
2. Move XXXResp to context.Context
This commit is contained in:
Ming Deng
2021-08-06 22:45:35 +08:00
parent 0ace49bd46
commit e7d91a2bed
9 changed files with 365 additions and 233 deletions

View File

@@ -27,24 +27,38 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"time"
"google.golang.org/protobuf/proto"
"gopkg.in/yaml.v3"
"github.com/beego/beego/v2/core/utils"
"github.com/beego/beego/v2/server/web/session"
)
// Commonly used mime-types
const (
ApplicationJSON = "application/json"
ApplicationXML = "application/xml"
ApplicationYAML = "application/x-yaml"
TextXML = "text/xml"
ApplicationJSON = "application/json"
ApplicationXML = "application/xml"
ApplicationForm = "application/x-www-form-urlencoded"
ApplicationProto = "application/x-protobuf"
ApplicationYAML = "application/x-yaml"
TextXML = "text/xml"
formatTime = "15:04:05"
formatDate = "2006-01-02"
formatDateTime = "2006-01-02 15:04:05"
formatDateTimeT = "2006-01-02T15:04:05"
)
// NewContext return the Context with Input and Output
@@ -65,6 +79,108 @@ type Context struct {
_xsrfToken string
}
func (ctx *Context) Bind(obj interface{}) error {
ct, exist := ctx.Request.Header["Content-Type"]
if !exist || len(ct) == 0 {
return ctx.BindJSON(obj)
}
i, l := 0, len(ct[0])
for i < l && ct[0][i] != ';' {
i++
}
switch ct[0][0:i] {
case ApplicationJSON:
return ctx.BindJSON(obj)
case ApplicationXML, TextXML:
return ctx.BindXML(obj)
case ApplicationForm:
return ctx.BindForm(obj)
case ApplicationProto:
return ctx.BindProtobuf(obj.(proto.Message))
case ApplicationYAML:
return ctx.BindYAML(obj)
default:
return errors.New("Unsupported Content-Type:" + ct[0])
}
}
// Resp sends response based on the Accept Header
// By default response will be in JSON
func (ctx *Context) Resp(data interface{}) error {
accept := ctx.Input.Header("Accept")
switch accept {
case ApplicationYAML:
return ctx.YamlResp(data)
case ApplicationXML, TextXML:
return ctx.XMLResp(data)
case ApplicationProto:
return ctx.ProtoResp(data.(proto.Message))
default:
return ctx.JSONResp(data)
}
}
func (ctx *Context) JSONResp(data interface{}) error {
return ctx.Output.JSON(data, false, false)
}
func (ctx *Context) XMLResp(data interface{}) error {
return ctx.Output.XML(data, false)
}
func (ctx *Context) YamlResp(data interface{}) error {
return ctx.Output.YAML(data)
}
func (ctx *Context) ProtoResp(data proto.Message) error {
return ctx.Output.Proto(data)
}
// BindYAML only read data from http request body
func (ctx *Context) BindYAML(obj interface{}) error {
return yaml.Unmarshal(ctx.Input.RequestBody, obj)
}
// BindForm will parse form values to struct via tag.
func (ctx *Context) BindForm(obj interface{}) error {
err := ctx.Request.ParseForm()
if err != nil {
return err
}
return ParseForm(ctx.Request.Form, obj)
}
// BindJSON only read data from http request body
func (ctx *Context) BindJSON(obj interface{}) error {
return json.Unmarshal(ctx.Input.RequestBody, obj)
}
// BindProtobuf only read data from http request body
func (ctx *Context) BindProtobuf(obj proto.Message) error {
return proto.Unmarshal(ctx.Input.RequestBody, obj)
}
// BindXML only read data from http request body
func (ctx *Context) BindXML(obj interface{}) error {
return xml.Unmarshal(ctx.Input.RequestBody, obj)
}
// ParseForm will parse form values to struct via tag.
func ParseForm(form url.Values, obj interface{}) error {
objT := reflect.TypeOf(obj)
objV := reflect.ValueOf(obj)
if !isStructPtr(objT) {
return fmt.Errorf("%v must be a struct pointer", obj)
}
objT = objT.Elem()
objV = objV.Elem()
return parseFormToStruct(form, objT, objV)
}
func isStructPtr(t reflect.Type) bool {
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct
}
// Reset initializes Context, BeegoInput and BeegoOutput
func (ctx *Context) Reset(rw http.ResponseWriter, r *http.Request) {
ctx.Request = r
@@ -91,7 +207,7 @@ func (ctx *Context) Abort(status int, body string) {
// WriteString writes a string to response body.
func (ctx *Context) WriteString(content string) {
ctx.ResponseWriter.Write([]byte(content))
_, _ = ctx.ResponseWriter.Write([]byte(content))
}
// GetCookie gets a cookie from a request for a given key.
@@ -124,7 +240,10 @@ func (ctx *Context) GetSecureCookie(Secret, key string) (string, bool) {
sig := parts[2]
h := hmac.New(sha256.New, []byte(Secret))
fmt.Fprintf(h, "%s%s", vs, timestamp)
_, err := fmt.Fprintf(h, "%s%s", vs, timestamp)
if err != nil {
return "", false
}
if fmt.Sprintf("%02x", h.Sum(nil)) != sig {
return "", false
@@ -138,7 +257,7 @@ func (ctx *Context) SetSecureCookie(Secret, name, value string, others ...interf
vs := base64.URLEncoding.EncodeToString([]byte(value))
timestamp := strconv.FormatInt(time.Now().UnixNano(), 10)
h := hmac.New(sha256.New, []byte(Secret))
fmt.Fprintf(h, "%s%s", vs, timestamp)
_, _ = fmt.Fprintf(h, "%s%s", vs, timestamp)
sig := fmt.Sprintf("%02x", h.Sum(nil))
cookie := strings.Join([]string{vs, timestamp, sig}, "|")
ctx.Output.Cookie(name, cookie, others...)

189
server/web/context/form.go Normal file
View File

@@ -0,0 +1,189 @@
// Copyright 2020 beego
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context
import (
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
var (
sliceOfInts = reflect.TypeOf([]int(nil))
sliceOfStrings = reflect.TypeOf([]string(nil))
)
// ParseForm will parse form values to struct via tag.
// Support for anonymous struct.
func parseFormToStruct(form url.Values, objT reflect.Type, objV reflect.Value) error {
for i := 0; i < objT.NumField(); i++ {
fieldV := objV.Field(i)
if !fieldV.CanSet() {
continue
}
fieldT := objT.Field(i)
if fieldT.Anonymous && fieldT.Type.Kind() == reflect.Struct {
err := parseFormToStruct(form, fieldT.Type, fieldV)
if err != nil {
return err
}
continue
}
tag, ok := formTagName(fieldT)
if !ok {
continue
}
value, ok := formValue(tag, form, fieldT)
if !ok {
continue
}
switch fieldT.Type.Kind() {
case reflect.Bool:
b, err := parseFormBoolValue(value)
if err != nil {
return err
}
fieldV.SetBool(b)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
fieldV.SetInt(x)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
x, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return err
}
fieldV.SetUint(x)
case reflect.Float32, reflect.Float64:
x, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
fieldV.SetFloat(x)
case reflect.Interface:
fieldV.Set(reflect.ValueOf(value))
case reflect.String:
fieldV.SetString(value)
case reflect.Struct:
if fieldT.Type.String() == "time.Time" {
t, err := parseFormTime(value)
if err != nil {
return err
}
fieldV.Set(reflect.ValueOf(t))
}
case reflect.Slice:
if fieldT.Type == sliceOfInts {
formVals := form[tag]
fieldV.Set(reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(int(1))), len(formVals), len(formVals)))
for i := 0; i < len(formVals); i++ {
val, err := strconv.Atoi(formVals[i])
if err != nil {
return err
}
fieldV.Index(i).SetInt(int64(val))
}
} else if fieldT.Type == sliceOfStrings {
formVals := form[tag]
fieldV.Set(reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf("")), len(formVals), len(formVals)))
for i := 0; i < len(formVals); i++ {
fieldV.Index(i).SetString(formVals[i])
}
}
}
}
return nil
}
// nolint
func parseFormTime(value string) (time.Time, error) {
var pattern string
if len(value) >= 25 {
value = value[:25]
pattern = time.RFC3339
} else if strings.HasSuffix(strings.ToUpper(value), "Z") {
pattern = time.RFC3339
} else if len(value) >= 19 {
if strings.Contains(value, "T") {
pattern = formatDateTimeT
} else {
pattern = formatDateTime
}
value = value[:19]
} else if len(value) >= 10 {
if len(value) > 10 {
value = value[:10]
}
pattern = formatDate
} else if len(value) >= 8 {
if len(value) > 8 {
value = value[:8]
}
pattern = formatTime
}
return time.ParseInLocation(pattern, value, time.Local)
}
func parseFormBoolValue(value string) (bool, error) {
if strings.ToLower(value) == "on" || strings.ToLower(value) == "1" || strings.ToLower(value) == "yes" {
return true, nil
}
if strings.ToLower(value) == "off" || strings.ToLower(value) == "0" || strings.ToLower(value) == "no" {
return false, nil
}
return strconv.ParseBool(value)
}
// nolint
func formTagName(fieldT reflect.StructField) (string, bool) {
tags := strings.Split(fieldT.Tag.Get("form"), ",")
var tag string
if len(tags) == 0 || tags[0] == "" {
tag = fieldT.Name
} else if tags[0] == "-" {
return "", false
} else {
tag = tags[0]
}
return tag, true
}
func formValue(tag string, form url.Values, fieldT reflect.StructField) (string, bool) {
formValues := form[tag]
var value string
if len(formValues) == 0 {
defaultValue := fieldT.Tag.Get("default")
if defaultValue != "" {
value = defaultValue
} else {
return "", false
}
}
if len(formValues) == 1 {
value = formValues[0]
if value == "" {
return "", false
}
}
return value, true
}

View File

@@ -31,7 +31,8 @@ import (
"strings"
"time"
yaml "gopkg.in/yaml.v2"
"google.golang.org/protobuf/proto"
"gopkg.in/yaml.v2"
)
// BeegoOutput does work for sending response header.
@@ -154,7 +155,7 @@ func (output *BeegoOutput) Cookie(name string, value string, others ...interface
fmt.Fprintf(&b, "; HttpOnly")
}
}
// default empty
if len(others) > 5 {
if v, ok := others[5].(string); ok && len(v) > 0 {
@@ -224,6 +225,19 @@ func (output *BeegoOutput) YAML(data interface{}) error {
return output.Body(content)
}
// Proto writes protobuf to the response body.
func (output *BeegoOutput) Proto(data proto.Message) error {
output.Header("Content-Type", "application/x-protobuf; charset=utf-8")
var content []byte
var err error
content, err = proto.Marshal(data)
if err != nil {
http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError)
return err
}
return output.Body(content)
}
// JSONP writes jsonp to the response body.
func (output *BeegoOutput) JSONP(data interface{}, hasIndent bool) error {
output.Header("Content-Type", "application/javascript; charset=utf-8")