diff --git a/bee.json b/bee.json index ff120f0c90..e427c55254 100644 --- a/bee.json +++ b/bee.json @@ -12,8 +12,6 @@ "models": "", "others": [ "modules", - "$GOPATH/src/github.com/gogits/binding", - "$GOPATH/src/github.com/gogits/webdav", "$GOPATH/src/github.com/gogits/logs", "$GOPATH/src/github.com/gogits/git", "$GOPATH/src/github.com/gogits/gfm" diff --git a/gogs.go b/gogs.go index a42d7225ba..1e614f4958 100644 --- a/gogs.go +++ b/gogs.go @@ -19,7 +19,7 @@ import ( // Test that go1.2 tag above is included in builds. main.go refers to this definition. const go12tag = true -const APP_VER = "0.2.8.0412 Alpha" +const APP_VER = "0.2.8.0413 Alpha" func init() { base.AppVer = APP_VER diff --git a/modules/auth/admin.go b/modules/auth/admin.go index fe889c2385..877af19af3 100644 --- a/modules/auth/admin.go +++ b/modules/auth/admin.go @@ -10,8 +10,6 @@ import ( "github.com/go-martini/martini" - "github.com/gogits/binding" - "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" ) @@ -35,7 +33,7 @@ func (f *AdminEditUserForm) Name(field string) string { return names[field] } -func (f *AdminEditUserForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *AdminEditUserForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 7329cbdcdf..350ef4fcbf 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -11,8 +11,6 @@ import ( "github.com/go-martini/martini" - "github.com/gogits/binding" - "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" ) @@ -39,7 +37,7 @@ func (f *RegisterForm) Name(field string) string { return names[field] } -func (f *RegisterForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *RegisterForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } @@ -72,7 +70,7 @@ func (f *LogInForm) Name(field string) string { return names[field] } -func (f *LogInForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *LogInForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } @@ -100,7 +98,7 @@ func getMinMaxSize(field reflect.StructField) string { return "" } -func validate(errors *binding.Errors, data base.TmplData, form Form) { +func validate(errors *base.BindingErrors, data base.TmplData, form Form) { typ := reflect.TypeOf(form) val := reflect.ValueOf(form) @@ -121,17 +119,17 @@ func validate(errors *binding.Errors, data base.TmplData, form Form) { if err, ok := errors.Fields[field.Name]; ok { data["Err_"+field.Name] = true switch err { - case binding.RequireError: + case base.BindingRequireError: data["ErrorMsg"] = form.Name(field.Name) + " cannot be empty" - case binding.AlphaDashError: + case base.BindingAlphaDashError: data["ErrorMsg"] = form.Name(field.Name) + " must be valid alpha or numeric or dash(-_) characters" - case binding.MinSizeError: + case base.BindingMinSizeError: data["ErrorMsg"] = form.Name(field.Name) + " must contain at least " + getMinMaxSize(field) + " characters" - case binding.MaxSizeError: + case base.BindingMaxSizeError: data["ErrorMsg"] = form.Name(field.Name) + " must contain at most " + getMinMaxSize(field) + " characters" - case binding.EmailError: + case base.BindingEmailError: data["ErrorMsg"] = form.Name(field.Name) + " is not a valid e-mail address" - case binding.UrlError: + case base.BindingUrlError: data["ErrorMsg"] = form.Name(field.Name) + " is not a valid URL" default: data["ErrorMsg"] = "Unknown error: " + err @@ -196,7 +194,7 @@ func (f *InstallForm) Name(field string) string { return names[field] } -func (f *InstallForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *InstallForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } diff --git a/modules/auth/issue.go b/modules/auth/issue.go index 36c8762791..f73ddc7447 100644 --- a/modules/auth/issue.go +++ b/modules/auth/issue.go @@ -10,8 +10,6 @@ import ( "github.com/go-martini/martini" - "github.com/gogits/binding" - "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" ) @@ -31,7 +29,7 @@ func (f *CreateIssueForm) Name(field string) string { return names[field] } -func (f *CreateIssueForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *CreateIssueForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } diff --git a/modules/auth/repo.go b/modules/auth/repo.go index aa94058f9d..f67fbf671a 100644 --- a/modules/auth/repo.go +++ b/modules/auth/repo.go @@ -10,8 +10,6 @@ import ( "github.com/go-martini/martini" - "github.com/gogits/binding" - "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" ) @@ -33,7 +31,7 @@ func (f *CreateRepoForm) Name(field string) string { return names[field] } -func (f *CreateRepoForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *CreateRepoForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } @@ -71,7 +69,7 @@ func (f *MigrateRepoForm) Name(field string) string { return names[field] } -func (f *MigrateRepoForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *MigrateRepoForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } diff --git a/modules/auth/setting.go b/modules/auth/setting.go index cada7eea66..7cee00dec6 100644 --- a/modules/auth/setting.go +++ b/modules/auth/setting.go @@ -11,8 +11,6 @@ import ( "github.com/go-martini/martini" - "github.com/gogits/binding" - "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" ) @@ -30,7 +28,7 @@ func (f *AddSSHKeyForm) Name(field string) string { return names[field] } -func (f *AddSSHKeyForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *AddSSHKeyForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) AssignForm(f, data) diff --git a/modules/auth/user.go b/modules/auth/user.go index 015059f7dc..973894221d 100644 --- a/modules/auth/user.go +++ b/modules/auth/user.go @@ -10,7 +10,6 @@ import ( "github.com/go-martini/martini" - "github.com/gogits/binding" "github.com/gogits/session" "github.com/gogits/gogs/models" @@ -93,7 +92,7 @@ func (f *UpdateProfileForm) Name(field string) string { return names[field] } -func (f *UpdateProfileForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *UpdateProfileForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } @@ -126,7 +125,7 @@ func (f *UpdatePasswdForm) Name(field string) string { return names[field] } -func (f *UpdatePasswdForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { +func (f *UpdatePasswdForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { if req.Method == "GET" || errors.Count() == 0 { return } diff --git a/modules/base/base.go b/modules/base/base.go index 7c08dcc5ce..84cf41c8d6 100644 --- a/modules/base/base.go +++ b/modules/base/base.go @@ -8,3 +8,49 @@ type ( // Type TmplData represents data in the templates. TmplData map[string]interface{} ) + +// __________.__ .___.__ +// \______ \__| ____ __| _/|__| ____ ____ +// | | _/ |/ \ / __ | | |/ \ / ___\ +// | | \ | | \/ /_/ | | | | \/ /_/ > +// |______ /__|___| /\____ | |__|___| /\___ / +// \/ \/ \/ \//_____/ + +// Errors represents the contract of the response body when the +// binding step fails before getting to the application. +type BindingErrors struct { + Overall map[string]string `json:"overall"` + Fields map[string]string `json:"fields"` +} + +// Total errors is the sum of errors with the request overall +// and errors on individual fields. +func (err BindingErrors) Count() int { + return len(err.Overall) + len(err.Fields) +} + +func (this *BindingErrors) Combine(other BindingErrors) { + for key, val := range other.Fields { + if _, exists := this.Fields[key]; !exists { + this.Fields[key] = val + } + } + for key, val := range other.Overall { + if _, exists := this.Overall[key]; !exists { + this.Overall[key] = val + } + } +} + +const ( + BindingRequireError string = "Required" + BindingAlphaDashError string = "AlphaDash" + BindingMinSizeError string = "MinSize" + BindingMaxSizeError string = "MaxSize" + BindingEmailError string = "Email" + BindingUrlError string = "Url" + BindingDeserializationError string = "DeserializationError" + BindingIntegerTypeError string = "IntegerTypeError" + BindingBooleanTypeError string = "BooleanTypeError" + BindingFloatTypeError string = "FloatTypeError" +) diff --git a/modules/middleware/binding.go b/modules/middleware/binding.go new file mode 100644 index 0000000000..cde9ae9cc0 --- /dev/null +++ b/modules/middleware/binding.go @@ -0,0 +1,426 @@ +// Copyright 2013 The Martini Contrib Authors. All rights reserved. +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package middleware + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "regexp" + "strconv" + "strings" + "unicode/utf8" + + "github.com/go-martini/martini" + + "github.com/gogits/gogs/modules/base" +) + +/* + To the land of Middle-ware Earth: + + One func to rule them all, + One func to find them, + One func to bring them all, + And in this package BIND them. +*/ + +// Bind accepts a copy of an empty struct and populates it with +// values from the request (if deserialization is successful). It +// wraps up the functionality of the Form and Json middleware +// according to the Content-Type of the request, and it guesses +// if no Content-Type is specified. Bind invokes the ErrorHandler +// middleware to bail out if errors occurred. If you want to perform +// your own error handling, use Form or Json middleware directly. +// An interface pointer can be added as a second argument in order +// to map the struct to a specific interface. +func Bind(obj interface{}, ifacePtr ...interface{}) martini.Handler { + return func(context martini.Context, req *http.Request) { + contentType := req.Header.Get("Content-Type") + + if strings.Contains(contentType, "form-urlencoded") { + context.Invoke(Form(obj, ifacePtr...)) + } else if strings.Contains(contentType, "multipart/form-data") { + context.Invoke(MultipartForm(obj, ifacePtr...)) + } else if strings.Contains(contentType, "json") { + context.Invoke(Json(obj, ifacePtr...)) + } else { + context.Invoke(Json(obj, ifacePtr...)) + if getErrors(context).Count() > 0 { + context.Invoke(Form(obj, ifacePtr...)) + } + } + + context.Invoke(ErrorHandler) + } +} + +// BindIgnErr will do the exactly same thing as Bind but without any +// error handling, which user has freedom to deal with them. +// This allows user take advantages of validation. +func BindIgnErr(obj interface{}, ifacePtr ...interface{}) martini.Handler { + return func(context martini.Context, req *http.Request) { + contentType := req.Header.Get("Content-Type") + + if strings.Contains(contentType, "form-urlencoded") { + context.Invoke(Form(obj, ifacePtr...)) + } else if strings.Contains(contentType, "multipart/form-data") { + context.Invoke(MultipartForm(obj, ifacePtr...)) + } else if strings.Contains(contentType, "json") { + context.Invoke(Json(obj, ifacePtr...)) + } else { + context.Invoke(Json(obj, ifacePtr...)) + if getErrors(context).Count() > 0 { + context.Invoke(Form(obj, ifacePtr...)) + } + } + } +} + +// Form is middleware to deserialize form-urlencoded data from the request. +// It gets data from the form-urlencoded body, if present, or from the +// query string. It uses the http.Request.ParseForm() method +// to perform deserialization, then reflection is used to map each field +// into the struct with the proper type. Structs with primitive slice types +// (bool, float, int, string) can support deserialization of repeated form +// keys, for example: key=val1&key=val2&key=val3 +// An interface pointer can be added as a second argument in order +// to map the struct to a specific interface. +func Form(formStruct interface{}, ifacePtr ...interface{}) martini.Handler { + return func(context martini.Context, req *http.Request) { + ensureNotPointer(formStruct) + formStruct := reflect.New(reflect.TypeOf(formStruct)) + errors := newErrors() + parseErr := req.ParseForm() + + // Format validation of the request body or the URL would add considerable overhead, + // and ParseForm does not complain when URL encoding is off. + // Because an empty request body or url can also mean absence of all needed values, + // it is not in all cases a bad request, so let's return 422. + if parseErr != nil { + errors.Overall[base.BindingDeserializationError] = parseErr.Error() + } + + mapForm(formStruct, req.Form, errors) + + validateAndMap(formStruct, context, errors, ifacePtr...) + } +} + +func MultipartForm(formStruct interface{}, ifacePtr ...interface{}) martini.Handler { + return func(context martini.Context, req *http.Request) { + ensureNotPointer(formStruct) + formStruct := reflect.New(reflect.TypeOf(formStruct)) + errors := newErrors() + + // Workaround for multipart forms returning nil instead of an error + // when content is not multipart + // https://code.google.com/p/go/issues/detail?id=6334 + multipartReader, err := req.MultipartReader() + if err != nil { + errors.Overall[base.BindingDeserializationError] = err.Error() + } else { + form, parseErr := multipartReader.ReadForm(MaxMemory) + + if parseErr != nil { + errors.Overall[base.BindingDeserializationError] = parseErr.Error() + } + + req.MultipartForm = form + } + + mapForm(formStruct, req.MultipartForm.Value, errors) + + validateAndMap(formStruct, context, errors, ifacePtr...) + } +} + +// Json is middleware to deserialize a JSON payload from the request +// into the struct that is passed in. The resulting struct is then +// validated, but no error handling is actually performed here. +// An interface pointer can be added as a second argument in order +// to map the struct to a specific interface. +func Json(jsonStruct interface{}, ifacePtr ...interface{}) martini.Handler { + return func(context martini.Context, req *http.Request) { + ensureNotPointer(jsonStruct) + jsonStruct := reflect.New(reflect.TypeOf(jsonStruct)) + errors := newErrors() + + if req.Body != nil { + defer req.Body.Close() + } + + if err := json.NewDecoder(req.Body).Decode(jsonStruct.Interface()); err != nil && err != io.EOF { + errors.Overall[base.BindingDeserializationError] = err.Error() + } + + validateAndMap(jsonStruct, context, errors, ifacePtr...) + } +} + +// Validate is middleware to enforce required fields. If the struct +// passed in is a Validator, then the user-defined Validate method +// is executed, and its errors are mapped to the context. This middleware +// performs no error handling: it merely detects them and maps them. +func Validate(obj interface{}) martini.Handler { + return func(context martini.Context, req *http.Request) { + errors := newErrors() + validateStruct(errors, obj) + + if validator, ok := obj.(Validator); ok { + validator.Validate(errors, req, context) + } + context.Map(*errors) + } +} + +var ( + alphaDashPattern = regexp.MustCompile("[^\\d\\w-_]") + emailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?") + urlPattern = regexp.MustCompile(`(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?`) +) + +func validateStruct(errors *base.BindingErrors, obj interface{}) { + typ := reflect.TypeOf(obj) + val := reflect.ValueOf(obj) + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + // Allow ignored fields in the struct + if field.Tag.Get("form") == "-" { + continue + } + + fieldValue := val.Field(i).Interface() + if field.Type.Kind() == reflect.Struct { + validateStruct(errors, fieldValue) + continue + } + + zero := reflect.Zero(field.Type).Interface() + + // Match rules. + for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { + if len(rule) == 0 { + continue + } + + switch { + case rule == "Required": + if reflect.DeepEqual(zero, fieldValue) { + errors.Fields[field.Name] = base.BindingRequireError + break + } + case rule == "AlphaDash": + if alphaDashPattern.MatchString(fmt.Sprintf("%v", fieldValue)) { + errors.Fields[field.Name] = base.BindingAlphaDashError + break + } + case strings.HasPrefix(rule, "MinSize("): + min, err := strconv.Atoi(rule[8 : len(rule)-1]) + if err != nil { + errors.Overall["MinSize"] = err.Error() + break + } + if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) < min { + errors.Fields[field.Name] = base.BindingMinSizeError + break + } + v := reflect.ValueOf(fieldValue) + if v.Kind() == reflect.Slice && v.Len() < min { + errors.Fields[field.Name] = base.BindingMinSizeError + break + } + case strings.HasPrefix(rule, "MaxSize("): + max, err := strconv.Atoi(rule[8 : len(rule)-1]) + if err != nil { + errors.Overall["MaxSize"] = err.Error() + break + } + if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) > max { + errors.Fields[field.Name] = base.BindingMaxSizeError + break + } + v := reflect.ValueOf(fieldValue) + if v.Kind() == reflect.Slice && v.Len() > max { + errors.Fields[field.Name] = base.BindingMinSizeError + break + } + case rule == "Email": + if !emailPattern.MatchString(fmt.Sprintf("%v", fieldValue)) { + errors.Fields[field.Name] = base.BindingEmailError + break + } + case rule == "Url": + if !urlPattern.MatchString(fmt.Sprintf("%v", fieldValue)) { + errors.Fields[field.Name] = base.BindingUrlError + break + } + } + } + } +} + +func mapForm(formStruct reflect.Value, form map[string][]string, errors *base.BindingErrors) { + typ := formStruct.Elem().Type() + + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" { + structField := formStruct.Elem().Field(i) + if !structField.CanSet() { + continue + } + + inputValue, exists := form[inputFieldName] + + if !exists { + continue + } + + numElems := len(inputValue) + if structField.Kind() == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for i := 0; i < numElems; i++ { + setWithProperType(sliceOf, inputValue[i], slice.Index(i), inputFieldName, errors) + } + formStruct.Elem().Field(i).Set(slice) + } else { + setWithProperType(typeField.Type.Kind(), inputValue[0], structField, inputFieldName, errors) + } + } + } +} + +// ErrorHandler simply counts the number of errors in the +// context and, if more than 0, writes a 400 Bad Request +// response and a JSON payload describing the errors with +// the "Content-Type" set to "application/json". +// Middleware remaining on the stack will not even see the request +// if, by this point, there are any errors. +// This is a "default" handler, of sorts, and you are +// welcome to use your own instead. The Bind middleware +// invokes this automatically for convenience. +func ErrorHandler(errs base.BindingErrors, resp http.ResponseWriter) { + if errs.Count() > 0 { + resp.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, ok := errs.Overall[base.BindingDeserializationError]; ok { + resp.WriteHeader(http.StatusBadRequest) + } else { + resp.WriteHeader(422) + } + errOutput, _ := json.Marshal(errs) + resp.Write(errOutput) + return + } +} + +// This sets the value in a struct of an indeterminate type to the +// matching value from the request (via Form middleware) in the +// same type, so that not all deserialized values have to be strings. +// Supported types are string, int, float, and bool. +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value, nameInTag string, errors *base.BindingErrors) { + switch valueKind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if val == "" { + val = "0" + } + intVal, err := strconv.ParseInt(val, 10, 64) + if err != nil { + errors.Fields[nameInTag] = base.BindingIntegerTypeError + } else { + structField.SetInt(intVal) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if val == "" { + val = "0" + } + uintVal, err := strconv.ParseUint(val, 10, 64) + if err != nil { + errors.Fields[nameInTag] = base.BindingIntegerTypeError + } else { + structField.SetUint(uintVal) + } + case reflect.Bool: + structField.SetBool(val == "on") + case reflect.Float32: + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, 32) + if err != nil { + errors.Fields[nameInTag] = base.BindingFloatTypeError + } else { + structField.SetFloat(floatVal) + } + case reflect.Float64: + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, 64) + if err != nil { + errors.Fields[nameInTag] = base.BindingFloatTypeError + } else { + structField.SetFloat(floatVal) + } + case reflect.String: + structField.SetString(val) + } +} + +// Don't pass in pointers to bind to. Can lead to bugs. See: +// https://github.com/codegangsta/martini-contrib/issues/40 +// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 +func ensureNotPointer(obj interface{}) { + if reflect.TypeOf(obj).Kind() == reflect.Ptr { + panic("Pointers are not accepted as binding models") + } +} + +// Performs validation and combines errors from validation +// with errors from deserialization, then maps both the +// resulting struct and the errors to the context. +func validateAndMap(obj reflect.Value, context martini.Context, errors *base.BindingErrors, ifacePtr ...interface{}) { + context.Invoke(Validate(obj.Interface())) + errors.Combine(getErrors(context)) + context.Map(*errors) + context.Map(obj.Elem().Interface()) + if len(ifacePtr) > 0 { + context.MapTo(obj.Elem().Interface(), ifacePtr[0]) + } +} + +func newErrors() *base.BindingErrors { + return &base.BindingErrors{make(map[string]string), make(map[string]string)} +} + +func getErrors(context martini.Context) base.BindingErrors { + return context.Get(reflect.TypeOf(base.BindingErrors{})).Interface().(base.BindingErrors) +} + +type ( + // Implement the Validator interface to define your own input + // validation before the request even gets to your application. + // The Validate method will be executed during the validation phase. + Validator interface { + Validate(*base.BindingErrors, *http.Request, martini.Context) + } +) + +var ( + // Maximum amount of memory to use when parsing a multipart form. + // Set this to whatever value you prefer; default is 10 MB. + MaxMemory = int64(1024 * 1024 * 10) +) diff --git a/modules/middleware/binding_test.go b/modules/middleware/binding_test.go new file mode 100644 index 0000000000..654cef29f1 --- /dev/null +++ b/modules/middleware/binding_test.go @@ -0,0 +1,701 @@ +// Copyright 2013 The Martini Contrib Authors. All rights reserved. +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package middleware + +import ( + "bytes" + "mime/multipart" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + + "github.com/codegangsta/martini" +) + +func TestBind(t *testing.T) { + testBind(t, false) +} + +func TestBindWithInterface(t *testing.T) { + testBind(t, true) +} + +func TestMultipartBind(t *testing.T) { + index := 0 + for test, expectStatus := range bindMultipartTests { + handler := func(post BlogPost, errors Errors) { + handle(test, t, index, post, errors) + } + recorder := testMultipart(t, test, Bind(BlogPost{}), handler, index) + + if recorder.Code != expectStatus { + t.Errorf("On test case %v, got status code %d but expected %d", test, recorder.Code, expectStatus) + } + + index++ + } +} + +func TestForm(t *testing.T) { + testForm(t, false) +} + +func TestFormWithInterface(t *testing.T) { + testForm(t, true) +} + +func TestEmptyForm(t *testing.T) { + testEmptyForm(t) +} + +func TestMultipartForm(t *testing.T) { + for index, test := range multipartformTests { + handler := func(post BlogPost, errors Errors) { + handle(test, t, index, post, errors) + } + testMultipart(t, test, MultipartForm(BlogPost{}), handler, index) + } +} + +func TestMultipartFormWithInterface(t *testing.T) { + for index, test := range multipartformTests { + handler := func(post Modeler, errors Errors) { + post.Create(test, t, index) + } + testMultipart(t, test, MultipartForm(BlogPost{}, (*Modeler)(nil)), handler, index) + } +} + +func TestJson(t *testing.T) { + testJson(t, false) +} + +func TestJsonWithInterface(t *testing.T) { + testJson(t, true) +} + +func TestEmptyJson(t *testing.T) { + testEmptyJson(t) +} + +func TestValidate(t *testing.T) { + handlerMustErr := func(errors Errors) { + if errors.Count() == 0 { + t.Error("Expected at least one error, got 0") + } + } + handlerNoErr := func(errors Errors) { + if errors.Count() > 0 { + t.Error("Expected no errors, got", errors.Count()) + } + } + + performValidationTest(&BlogPost{"", "...", 0, 0, []int{}}, handlerMustErr, t) + performValidationTest(&BlogPost{"Good Title", "Good content", 0, 0, []int{}}, handlerNoErr, t) + + performValidationTest(&User{Name: "Jim", Home: Address{"", ""}}, handlerMustErr, t) + performValidationTest(&User{Name: "Jim", Home: Address{"required", ""}}, handlerNoErr, t) +} + +func handle(test testCase, t *testing.T, index int, post BlogPost, errors Errors) { + assertEqualField(t, "Title", index, test.ref.Title, post.Title) + assertEqualField(t, "Content", index, test.ref.Content, post.Content) + assertEqualField(t, "Views", index, test.ref.Views, post.Views) + + for i := range test.ref.Multiple { + if i >= len(post.Multiple) { + t.Errorf("Expected: %v (size %d) to have same size as: %v (size %d)", post.Multiple, len(post.Multiple), test.ref.Multiple, len(test.ref.Multiple)) + break + } + if test.ref.Multiple[i] != post.Multiple[i] { + t.Errorf("Expected: %v to deep equal: %v", post.Multiple, test.ref.Multiple) + break + } + } + + if test.ok && errors.Count() > 0 { + t.Errorf("%+v should be OK (0 errors), but had errors: %+v", test, errors) + } else if !test.ok && errors.Count() == 0 { + t.Errorf("%+v should have errors, but was OK (0 errors): %+v", test) + } +} + +func handleEmpty(test emptyPayloadTestCase, t *testing.T, index int, section BlogSection, errors Errors) { + assertEqualField(t, "Title", index, test.ref.Title, section.Title) + assertEqualField(t, "Content", index, test.ref.Content, section.Content) + + if test.ok && errors.Count() > 0 { + t.Errorf("%+v should be OK (0 errors), but had errors: %+v", test, errors) + } else if !test.ok && errors.Count() == 0 { + t.Errorf("%+v should have errors, but was OK (0 errors): %+v", test) + } +} + +func testBind(t *testing.T, withInterface bool) { + index := 0 + for test, expectStatus := range bindTests { + m := martini.Classic() + recorder := httptest.NewRecorder() + handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) } + binding := Bind(BlogPost{}) + + if withInterface { + handler = func(post BlogPost, errors Errors) { + post.Create(test, t, index) + } + binding = Bind(BlogPost{}, (*Modeler)(nil)) + } + + switch test.method { + case "GET": + m.Get(route, binding, handler) + case "POST": + m.Post(route, binding, handler) + } + + req, err := http.NewRequest(test.method, test.path, strings.NewReader(test.payload)) + req.Header.Add("Content-Type", test.contentType) + + if err != nil { + t.Error(err) + } + m.ServeHTTP(recorder, req) + + if recorder.Code != expectStatus { + t.Errorf("On test case %v, got status code %d but expected %d", test, recorder.Code, expectStatus) + } + + index++ + } +} + +func testJson(t *testing.T, withInterface bool) { + for index, test := range jsonTests { + recorder := httptest.NewRecorder() + handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) } + binding := Json(BlogPost{}) + + if withInterface { + handler = func(post BlogPost, errors Errors) { + post.Create(test, t, index) + } + binding = Bind(BlogPost{}, (*Modeler)(nil)) + } + + m := martini.Classic() + switch test.method { + case "GET": + m.Get(route, binding, handler) + case "POST": + m.Post(route, binding, handler) + case "PUT": + m.Put(route, binding, handler) + case "DELETE": + m.Delete(route, binding, handler) + } + + req, err := http.NewRequest(test.method, route, strings.NewReader(test.payload)) + if err != nil { + t.Error(err) + } + m.ServeHTTP(recorder, req) + } +} + +func testEmptyJson(t *testing.T) { + for index, test := range emptyPayloadTests { + recorder := httptest.NewRecorder() + handler := func(section BlogSection, errors Errors) { handleEmpty(test, t, index, section, errors) } + binding := Json(BlogSection{}) + + m := martini.Classic() + switch test.method { + case "GET": + m.Get(route, binding, handler) + case "POST": + m.Post(route, binding, handler) + case "PUT": + m.Put(route, binding, handler) + case "DELETE": + m.Delete(route, binding, handler) + } + + req, err := http.NewRequest(test.method, route, strings.NewReader(test.payload)) + if err != nil { + t.Error(err) + } + m.ServeHTTP(recorder, req) + } +} + +func testForm(t *testing.T, withInterface bool) { + for index, test := range formTests { + recorder := httptest.NewRecorder() + handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) } + binding := Form(BlogPost{}) + + if withInterface { + handler = func(post BlogPost, errors Errors) { + post.Create(test, t, index) + } + binding = Form(BlogPost{}, (*Modeler)(nil)) + } + + m := martini.Classic() + switch test.method { + case "GET": + m.Get(route, binding, handler) + case "POST": + m.Post(route, binding, handler) + } + + req, err := http.NewRequest(test.method, test.path, nil) + if err != nil { + t.Error(err) + } + m.ServeHTTP(recorder, req) + } +} + +func testEmptyForm(t *testing.T) { + for index, test := range emptyPayloadTests { + recorder := httptest.NewRecorder() + handler := func(section BlogSection, errors Errors) { handleEmpty(test, t, index, section, errors) } + binding := Form(BlogSection{}) + + m := martini.Classic() + switch test.method { + case "GET": + m.Get(route, binding, handler) + case "POST": + m.Post(route, binding, handler) + } + + req, err := http.NewRequest(test.method, test.path, nil) + if err != nil { + t.Error(err) + } + m.ServeHTTP(recorder, req) + } +} + +func testMultipart(t *testing.T, test testCase, middleware martini.Handler, handler martini.Handler, index int) *httptest.ResponseRecorder { + recorder := httptest.NewRecorder() + + m := martini.Classic() + m.Post(route, middleware, handler) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + writer.WriteField("title", test.ref.Title) + writer.WriteField("content", test.ref.Content) + writer.WriteField("views", strconv.Itoa(test.ref.Views)) + if len(test.ref.Multiple) != 0 { + for _, value := range test.ref.Multiple { + writer.WriteField("multiple", strconv.Itoa(value)) + } + } + + req, err := http.NewRequest(test.method, test.path, body) + req.Header.Add("Content-Type", writer.FormDataContentType()) + + if err != nil { + t.Error(err) + } + + err = writer.Close() + if err != nil { + t.Error(err) + } + + m.ServeHTTP(recorder, req) + + return recorder +} + +func assertEqualField(t *testing.T, fieldname string, testcasenumber int, expected interface{}, got interface{}) { + if expected != got { + t.Errorf("%s: expected=%s, got=%s in test case %d\n", fieldname, expected, got, testcasenumber) + } +} + +func performValidationTest(data interface{}, handler func(Errors), t *testing.T) { + recorder := httptest.NewRecorder() + m := martini.Classic() + m.Get(route, Validate(data), handler) + + req, err := http.NewRequest("GET", route, nil) + if err != nil { + t.Error("HTTP error:", err) + } + + m.ServeHTTP(recorder, req) +} + +func (self BlogPost) Validate(errors *Errors, req *http.Request) { + if len(self.Title) < 4 { + errors.Fields["Title"] = "Too short; minimum 4 characters" + } + if len(self.Content) > 1024 { + errors.Fields["Content"] = "Too long; maximum 1024 characters" + } + if len(self.Content) < 5 { + errors.Fields["Content"] = "Too short; minimum 5 characters" + } +} + +func (self BlogPost) Create(test testCase, t *testing.T, index int) { + assertEqualField(t, "Title", index, test.ref.Title, self.Title) + assertEqualField(t, "Content", index, test.ref.Content, self.Content) + assertEqualField(t, "Views", index, test.ref.Views, self.Views) + + for i := range test.ref.Multiple { + if i >= len(self.Multiple) { + t.Errorf("Expected: %v (size %d) to have same size as: %v (size %d)", self.Multiple, len(self.Multiple), test.ref.Multiple, len(test.ref.Multiple)) + break + } + if test.ref.Multiple[i] != self.Multiple[i] { + t.Errorf("Expected: %v to deep equal: %v", self.Multiple, test.ref.Multiple) + break + } + } +} + +func (self BlogSection) Create(test emptyPayloadTestCase, t *testing.T, index int) { + // intentionally left empty +} + +type ( + testCase struct { + method string + path string + payload string + contentType string + ok bool + ref *BlogPost + } + + emptyPayloadTestCase struct { + method string + path string + payload string + contentType string + ok bool + ref *BlogSection + } + + Modeler interface { + Create(test testCase, t *testing.T, index int) + } + + BlogPost struct { + Title string `form:"title" json:"title" binding:"required"` + Content string `form:"content" json:"content"` + Views int `form:"views" json:"views"` + internal int `form:"-"` + Multiple []int `form:"multiple"` + } + + BlogSection struct { + Title string `form:"title" json:"title"` + Content string `form:"content" json:"content"` + } + + User struct { + Name string `json:"name" binding:"required"` + Home Address `json:"address" binding:"required"` + } + + Address struct { + Street1 string `json:"street1" binding:"required"` + Street2 string `json:"street2"` + } +) + +var ( + bindTests = map[testCase]int{ + // These should bail at the deserialization/binding phase + testCase{ + "POST", + path, + `{ bad JSON `, + "application/json", + false, + new(BlogPost), + }: http.StatusBadRequest, + testCase{ + "POST", + path, + `not multipart but has content-type`, + "multipart/form-data", + false, + new(BlogPost), + }: http.StatusBadRequest, + testCase{ + "POST", + path, + `no content-type and not URL-encoded or JSON"`, + "", + false, + new(BlogPost), + }: http.StatusBadRequest, + + // These should deserialize, then bail at the validation phase + testCase{ + "POST", + path + "?title= This is wrong ", + `not URL-encoded but has content-type`, + "x-www-form-urlencoded", + false, + new(BlogPost), + }: 422, // according to comments in Form() -> although the request is not url encoded, ParseForm does not complain + testCase{ + "GET", + path + "?content=This+is+the+content", + ``, + "x-www-form-urlencoded", + false, + &BlogPost{Title: "", Content: "This is the content"}, + }: 422, + testCase{ + "GET", + path + "", + `{"content":"", "title":"Blog Post Title"}`, + "application/json", + false, + &BlogPost{Title: "Blog Post Title", Content: ""}, + }: 422, + + // These should succeed + testCase{ + "GET", + path + "", + `{"content":"This is the content", "title":"Blog Post Title"}`, + "application/json", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content"}, + }: http.StatusOK, + testCase{ + "GET", + path + "?content=This+is+the+content&title=Blog+Post+Title", + ``, + "", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content"}, + }: http.StatusOK, + testCase{ + "GET", + path + "?content=This is the content&title=Blog+Post+Title", + `{"content":"This is the content", "title":"Blog Post Title"}`, + "", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content"}, + }: http.StatusOK, + testCase{ + "GET", + path + "", + `{"content":"This is the content", "title":"Blog Post Title"}`, + "", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content"}, + }: http.StatusOK, + } + + bindMultipartTests = map[testCase]int{ + // This should deserialize, then bail at the validation phase + testCase{ + "POST", + path, + "", + "multipart/form-data", + false, + &BlogPost{Title: "", Content: "This is the content"}, + }: 422, + // This should succeed + testCase{ + "POST", + path, + "", + "multipart/form-data", + true, + &BlogPost{Title: "This is the Title", Content: "This is the content"}, + }: http.StatusOK, + } + + formTests = []testCase{ + { + "GET", + path + "?content=This is the content", + "", + "", + false, + &BlogPost{Title: "", Content: "This is the content"}, + }, + { + "POST", + path + "?content=This+is+the+content&title=Blog+Post+Title&views=3", + "", + "", + false, // false because POST requests should have a body, not just a query string + &BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3}, + }, + { + "GET", + path + "?content=This+is+the+content&title=Blog+Post+Title&views=3&multiple=5&multiple=10&multiple=15&multiple=20", + "", + "", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3, Multiple: []int{5, 10, 15, 20}}, + }, + } + + multipartformTests = []testCase{ + { + "POST", + path, + "", + "multipart/form-data", + false, + &BlogPost{Title: "", Content: "This is the content"}, + }, + { + "POST", + path, + "", + "multipart/form-data", + false, + &BlogPost{Title: "Blog Post Title", Views: 3}, + }, + { + "POST", + path, + "", + "multipart/form-data", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3, Multiple: []int{5, 10, 15, 20}}, + }, + } + + emptyPayloadTests = []emptyPayloadTestCase{ + { + "GET", + "", + "", + "", + true, + &BlogSection{}, + }, + { + "POST", + "", + "", + "", + true, + &BlogSection{}, + }, + { + "PUT", + "", + "", + "", + true, + &BlogSection{}, + }, + { + "DELETE", + "", + "", + "", + true, + &BlogSection{}, + }, + } + + jsonTests = []testCase{ + // bad requests + { + "GET", + "", + `{blah blah blah}`, + "", + false, + &BlogPost{}, + }, + { + "POST", + "", + `{asdf}`, + "", + false, + &BlogPost{}, + }, + { + "PUT", + "", + `{blah blah blah}`, + "", + false, + &BlogPost{}, + }, + { + "DELETE", + "", + `{;sdf _SDf- }`, + "", + false, + &BlogPost{}, + }, + + // Valid-JSON requests + { + "GET", + "", + `{"content":"This is the content"}`, + "", + false, + &BlogPost{Title: "", Content: "This is the content"}, + }, + { + "POST", + "", + `{}`, + "application/json", + false, + &BlogPost{Title: "", Content: ""}, + }, + { + "POST", + "", + `{"content":"This is the content", "title":"Blog Post Title"}`, + "", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content"}, + }, + { + "PUT", + "", + `{"content":"This is the content", "title":"Blog Post Title"}`, + "", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content"}, + }, + { + "DELETE", + "", + `{"content":"This is the content", "title":"Blog Post Title"}`, + "", + true, + &BlogPost{Title: "Blog Post Title", Content: "This is the content"}, + }, + } +) + +const ( + route = "/blogposts/create" + path = "http://localhost:3000" + route +) diff --git a/routers/user/home.go b/routers/user/home.go new file mode 100644 index 0000000000..50f16f094b --- /dev/null +++ b/routers/user/home.go @@ -0,0 +1,196 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "fmt" + + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" +) + +func Dashboard(ctx *middleware.Context) { + ctx.Data["Title"] = "Dashboard" + ctx.Data["PageIsUserDashboard"] = true + repos, err := models.GetRepositories(&models.User{Id: ctx.User.Id}) + if err != nil { + ctx.Handle(500, "user.Dashboard", err) + return + } + ctx.Data["MyRepos"] = repos + + feeds, err := models.GetFeeds(ctx.User.Id, 0, false) + if err != nil { + ctx.Handle(500, "user.Dashboard", err) + return + } + ctx.Data["Feeds"] = feeds + ctx.HTML(200, "user/dashboard") +} + +func Profile(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Profile" + + // TODO: Need to check view self or others. + user, err := models.GetUserByName(params["username"]) + if err != nil { + ctx.Handle(500, "user.Profile", err) + return + } + + ctx.Data["Owner"] = user + + tab := ctx.Query("tab") + ctx.Data["TabName"] = tab + + switch tab { + case "activity": + feeds, err := models.GetFeeds(user.Id, 0, true) + if err != nil { + ctx.Handle(500, "user.Profile", err) + return + } + ctx.Data["Feeds"] = feeds + default: + repos, err := models.GetRepositories(user) + if err != nil { + ctx.Handle(500, "user.Profile", err) + return + } + ctx.Data["Repos"] = repos + } + + ctx.Data["PageIsUserProfile"] = true + ctx.HTML(200, "user/profile") +} + +func Email2User(ctx *middleware.Context) { + u, err := models.GetUserByEmail(ctx.Query("email")) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "user.Email2User", err) + } else { + ctx.Handle(500, "user.Email2User(GetUserByEmail)", err) + } + return + } + + ctx.Redirect("/user/" + u.Name) +} + +const ( + TPL_FEED = ` +