From 5c2da610a2cfd3fa9666d20739e054c3588b4d05 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sun, 13 Apr 2014 01:57:42 -0400 Subject: [PATCH] Move binding as subrepo --- bee.json | 2 - gogs.go | 2 +- modules/auth/admin.go | 4 +- modules/auth/auth.go | 22 +- modules/auth/issue.go | 4 +- modules/auth/repo.go | 6 +- modules/auth/setting.go | 4 +- modules/auth/user.go | 5 +- modules/base/base.go | 46 ++ modules/middleware/binding.go | 426 ++++++++++++++++++ modules/middleware/binding_test.go | 701 +++++++++++++++++++++++++++++ routers/user/home.go | 196 ++++++++ routers/user/social.go | 206 ++++++++- routers/user/social_github.go | 73 --- routers/user/social_google.go | 71 --- routers/user/social_qq.go | 83 ---- routers/user/user.go | 183 -------- web.go | 5 +- 18 files changed, 1594 insertions(+), 445 deletions(-) create mode 100644 modules/middleware/binding.go create mode 100644 modules/middleware/binding_test.go create mode 100644 routers/user/home.go delete mode 100644 routers/user/social_github.go delete mode 100644 routers/user/social_google.go delete mode 100644 routers/user/social_qq.go diff --git a/bee.json b/bee.json index ff120f0c9..e427c5525 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 a42d7225b..1e614f495 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 fe889c238..877af19af 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 7329cbdcd..350ef4fcb 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 36c876279..f73ddc744 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 aa94058f9..f67fbf671 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 cada7eea6..7cee00dec 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 015059f7d..973894221 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 7c08dcc5c..84cf41c8d 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 000000000..cde9ae9cc --- /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 000000000..654cef29f --- /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 000000000..50f16f094 --- /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 = ` +
%s
%s
` +) + +func Feeds(ctx *middleware.Context, form auth.FeedsForm) { + actions, err := models.GetFeeds(form.UserId, form.Page*20, false) + if err != nil { + ctx.JSON(500, err) + } + + feeds := make([]string, len(actions)) + for i := range actions { + feeds[i] = fmt.Sprintf(TPL_FEED, base.ActionIcon(actions[i].OpType), + base.TimeSince(actions[i].Created), base.ActionDesc(actions[i])) + } + ctx.JSON(200, &feeds) +} + +func Issues(ctx *middleware.Context) { + ctx.Data["Title"] = "Your Issues" + ctx.Data["ViewType"] = "all" + + page, _ := base.StrTo(ctx.Query("page")).Int() + repoId, _ := base.StrTo(ctx.Query("repoid")).Int64() + + ctx.Data["RepoId"] = repoId + + var posterId int64 = 0 + if ctx.Query("type") == "created_by" { + posterId = ctx.User.Id + ctx.Data["ViewType"] = "created_by" + } + + // Get all repositories. + repos, err := models.GetRepositories(ctx.User) + if err != nil { + ctx.Handle(200, "user.Issues(get repositories)", err) + return + } + + showRepos := make([]models.Repository, 0, len(repos)) + + isShowClosed := ctx.Query("state") == "closed" + var closedIssueCount, createdByCount, allIssueCount int + + // Get all issues. + allIssues := make([]models.Issue, 0, 5*len(repos)) + for i, repo := range repos { + issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, isShowClosed, false, "", "") + if err != nil { + ctx.Handle(200, "user.Issues(get issues)", err) + return + } + + allIssueCount += repo.NumIssues + closedIssueCount += repo.NumClosedIssues + + // Set repository information to issues. + for j := range issues { + issues[j].Repo = &repos[i] + } + allIssues = append(allIssues, issues...) + + repos[i].NumOpenIssues = repo.NumIssues - repo.NumClosedIssues + if repos[i].NumOpenIssues > 0 { + showRepos = append(showRepos, repos[i]) + } + } + + showIssues := make([]models.Issue, 0, len(allIssues)) + ctx.Data["IsShowClosed"] = isShowClosed + + // Get posters and filter issues. + for i := range allIssues { + u, err := models.GetUserById(allIssues[i].PosterId) + if err != nil { + ctx.Handle(200, "user.Issues(get poster): %v", err) + return + } + allIssues[i].Poster = u + if u.Id == ctx.User.Id { + createdByCount++ + } + + if repoId > 0 && repoId != allIssues[i].Repo.Id { + continue + } + + if isShowClosed == allIssues[i].IsClosed { + showIssues = append(showIssues, allIssues[i]) + } + } + + ctx.Data["Repos"] = showRepos + ctx.Data["Issues"] = showIssues + ctx.Data["AllIssueCount"] = allIssueCount + ctx.Data["ClosedIssueCount"] = closedIssueCount + ctx.Data["OpenIssueCount"] = allIssueCount - closedIssueCount + ctx.Data["CreatedByCount"] = createdByCount + ctx.HTML(200, "issue/user") +} + +func Pulls(ctx *middleware.Context) { + ctx.HTML(200, "user/pulls") +} + +func Stars(ctx *middleware.Context) { + ctx.HTML(200, "user/stars") +} diff --git a/routers/user/social.go b/routers/user/social.go index ea47d71b1..29c4fa97c 100644 --- a/routers/user/social.go +++ b/routers/user/social.go @@ -7,12 +7,14 @@ package user import ( "encoding/json" "fmt" + "net/http" "net/url" + "strconv" "strings" "code.google.com/p/goauth2/oauth" - "github.com/go-martini/martini" + "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" @@ -115,3 +117,205 @@ func SocialSignIn(params martini.Params, ctx *middleware.Context) { log.Trace("socialId: %v", oa.Id) ctx.Redirect(next) } + +// ________.__ __ ___ ___ ___. +// / _____/|__|/ |_ / | \ __ _\_ |__ +// / \ ___| \ __\/ ~ \ | \ __ \ +// \ \_\ \ || | \ Y / | / \_\ \ +// \______ /__||__| \___|_ /|____/|___ / +// \/ \/ \/ + +type SocialGithub struct { + Token *oauth.Token + *oauth.Transport +} + +func (s *SocialGithub) Type() int { + return models.OT_GITHUB +} + +func init() { + github := &SocialGithub{} + name := "github" + config := &oauth.Config{ + ClientId: "09383403ff2dc16daaa1", //base.OauthService.GitHub.ClientId, // FIXME: panic when set + ClientSecret: "0e4aa0c3630df396cdcea01a9d45cacf79925fea", //base.OauthService.GitHub.ClientSecret, + RedirectURL: strings.TrimSuffix(base.AppUrl, "/") + "/user/login/" + name, //ctx.Req.URL.RequestURI(), + Scope: "https://api.github.com/user", + AuthURL: "https://github.com/login/oauth/authorize", + TokenURL: "https://github.com/login/oauth/access_token", + } + github.Transport = &oauth.Transport{ + Config: config, + Transport: http.DefaultTransport, + } + SocialMap[name] = github +} + +func (s *SocialGithub) SetRedirectUrl(url string) { + s.Transport.Config.RedirectURL = url +} + +func (s *SocialGithub) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { + transport := &oauth.Transport{ + Token: token, + } + var data struct { + Id int `json:"id"` + Name string `json:"login"` + Email string `json:"email"` + } + var err error + r, err := transport.Client().Get(s.Transport.Scope) + if err != nil { + return nil, err + } + defer r.Body.Close() + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + return &BasicUserInfo{ + Identity: strconv.Itoa(data.Id), + Name: data.Name, + Email: data.Email, + }, nil +} + +// ________ .__ +// / _____/ ____ ____ ____ | | ____ +// / \ ___ / _ \ / _ \ / ___\| | _/ __ \ +// \ \_\ ( <_> | <_> ) /_/ > |_\ ___/ +// \______ /\____/ \____/\___ /|____/\___ > +// \/ /_____/ \/ + +type SocialGoogle struct { + Token *oauth.Token + *oauth.Transport +} + +func (s *SocialGoogle) Type() int { + return models.OT_GOOGLE +} + +func init() { + google := &SocialGoogle{} + name := "google" + // get client id and secret from + // https://console.developers.google.com/project + config := &oauth.Config{ + ClientId: "849753812404-mpd7ilvlb8c7213qn6bre6p6djjskti9.apps.googleusercontent.com", //base.OauthService.GitHub.ClientId, // FIXME: panic when set + ClientSecret: "VukKc4MwaJUSmiyv3D7ANVCa", //base.OauthService.GitHub.ClientSecret, + Scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile", + AuthURL: "https://accounts.google.com/o/oauth2/auth", + TokenURL: "https://accounts.google.com/o/oauth2/token", + } + google.Transport = &oauth.Transport{ + Config: config, + Transport: http.DefaultTransport, + } + SocialMap[name] = google +} + +func (s *SocialGoogle) SetRedirectUrl(url string) { + s.Transport.Config.RedirectURL = url +} + +func (s *SocialGoogle) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { + transport := &oauth.Transport{Token: token} + var data struct { + Id string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + } + var err error + + reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo" + r, err := transport.Client().Get(reqUrl) + if err != nil { + return nil, err + } + defer r.Body.Close() + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + return &BasicUserInfo{ + Identity: data.Id, + Name: data.Name, + Email: data.Email, + }, nil +} + +// ________ ________ +// \_____ \ \_____ \ +// / / \ \ / / \ \ +// / \_/. \/ \_/. \ +// \_____\ \_/\_____\ \_/ +// \__> \__> + +type SocialQQ struct { + Token *oauth.Token + *oauth.Transport + reqUrl string +} + +func (s *SocialQQ) Type() int { + return models.OT_QQ +} + +func init() { + qq := &SocialQQ{} + name := "qq" + config := &oauth.Config{ + ClientId: "801497180", //base.OauthService.GitHub.ClientId, // FIXME: panic when set + ClientSecret: "16cd53b8ad2e16a36fc2c8f87d9388f2", //base.OauthService.GitHub.ClientSecret, + Scope: "all", + AuthURL: "https://open.t.qq.com/cgi-bin/oauth2/authorize", + TokenURL: "https://open.t.qq.com/cgi-bin/oauth2/access_token", + } + qq.reqUrl = "https://open.t.qq.com/api/user/info" + qq.Transport = &oauth.Transport{ + Config: config, + Transport: http.DefaultTransport, + } + SocialMap[name] = qq +} + +func (s *SocialQQ) SetRedirectUrl(url string) { + s.Transport.Config.RedirectURL = url +} + +func (s *SocialQQ) UserInfo(token *oauth.Token, URL *url.URL) (*BasicUserInfo, error) { + var data struct { + Data struct { + Id string `json:"openid"` + Name string `json:"name"` + Email string `json:"email"` + } `json:"data"` + } + var err error + // https://open.t.qq.com/api/user/info? + //oauth_consumer_key=APP_KEY& + //access_token=ACCESSTOKEN&openid=openid + //clientip=CLIENTIP&oauth_version=2.a + //scope=all + var urls = url.Values{ + "oauth_consumer_key": {s.Transport.Config.ClientId}, + "access_token": {token.AccessToken}, + "openid": URL.Query()["openid"], + "oauth_version": {"2.a"}, + "scope": {"all"}, + } + r, err := http.Get(s.reqUrl + "?" + urls.Encode()) + if err != nil { + return nil, err + } + defer r.Body.Close() + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + return &BasicUserInfo{ + Identity: data.Data.Id, + Name: data.Data.Name, + Email: data.Data.Email, + }, nil +} diff --git a/routers/user/social_github.go b/routers/user/social_github.go deleted file mode 100644 index e532efd0a..000000000 --- a/routers/user/social_github.go +++ /dev/null @@ -1,73 +0,0 @@ -// 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 ( - "encoding/json" - "net/http" - "net/url" - "strconv" - "strings" - - "code.google.com/p/goauth2/oauth" - "github.com/gogits/gogs/models" - "github.com/gogits/gogs/modules/base" -) - -type SocialGithub struct { - Token *oauth.Token - *oauth.Transport -} - -func (s *SocialGithub) Type() int { - return models.OT_GITHUB -} - -func init() { - github := &SocialGithub{} - name := "github" - config := &oauth.Config{ - ClientId: "09383403ff2dc16daaa1", //base.OauthService.GitHub.ClientId, // FIXME: panic when set - ClientSecret: "0e4aa0c3630df396cdcea01a9d45cacf79925fea", //base.OauthService.GitHub.ClientSecret, - RedirectURL: strings.TrimSuffix(base.AppUrl, "/") + "/user/login/" + name, //ctx.Req.URL.RequestURI(), - Scope: "https://api.github.com/user", - AuthURL: "https://github.com/login/oauth/authorize", - TokenURL: "https://github.com/login/oauth/access_token", - } - github.Transport = &oauth.Transport{ - Config: config, - Transport: http.DefaultTransport, - } - SocialMap[name] = github -} - -func (s *SocialGithub) SetRedirectUrl(url string) { - s.Transport.Config.RedirectURL = url -} - -func (s *SocialGithub) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { - transport := &oauth.Transport{ - Token: token, - } - var data struct { - Id int `json:"id"` - Name string `json:"login"` - Email string `json:"email"` - } - var err error - r, err := transport.Client().Get(s.Transport.Scope) - if err != nil { - return nil, err - } - defer r.Body.Close() - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { - return nil, err - } - return &BasicUserInfo{ - Identity: strconv.Itoa(data.Id), - Name: data.Name, - Email: data.Email, - }, nil -} diff --git a/routers/user/social_google.go b/routers/user/social_google.go deleted file mode 100644 index b585386f2..000000000 --- a/routers/user/social_google.go +++ /dev/null @@ -1,71 +0,0 @@ -// 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 ( - "encoding/json" - "net/http" - "net/url" - "github.com/gogits/gogs/models" - - "code.google.com/p/goauth2/oauth" -) - -type SocialGoogle struct { - Token *oauth.Token - *oauth.Transport -} - -func (s *SocialGoogle) Type() int { - return models.OT_GOOGLE -} - -func init() { - google := &SocialGoogle{} - name := "google" - // get client id and secret from - // https://console.developers.google.com/project - config := &oauth.Config{ - ClientId: "849753812404-mpd7ilvlb8c7213qn6bre6p6djjskti9.apps.googleusercontent.com", //base.OauthService.GitHub.ClientId, // FIXME: panic when set - ClientSecret: "VukKc4MwaJUSmiyv3D7ANVCa", //base.OauthService.GitHub.ClientSecret, - Scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile", - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://accounts.google.com/o/oauth2/token", - } - google.Transport = &oauth.Transport{ - Config: config, - Transport: http.DefaultTransport, - } - SocialMap[name] = google -} - -func (s *SocialGoogle) SetRedirectUrl(url string) { - s.Transport.Config.RedirectURL = url -} - -func (s *SocialGoogle) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { - transport := &oauth.Transport{Token: token} - var data struct { - Id string `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - } - var err error - - reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo" - r, err := transport.Client().Get(reqUrl) - if err != nil { - return nil, err - } - defer r.Body.Close() - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { - return nil, err - } - return &BasicUserInfo{ - Identity: data.Id, - Name: data.Name, - Email: data.Email, - }, nil -} diff --git a/routers/user/social_qq.go b/routers/user/social_qq.go deleted file mode 100644 index d08892ef8..000000000 --- a/routers/user/social_qq.go +++ /dev/null @@ -1,83 +0,0 @@ -// 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. - -// api reference: http://wiki.open.t.qq.com/index.php/OAuth2.0%E9%89%B4%E6%9D%83/Authorization_code%E6%8E%88%E6%9D%83%E6%A1%88%E4%BE%8B -package user - -import ( - "encoding/json" - "net/http" - "net/url" - "github.com/gogits/gogs/models" - - "code.google.com/p/goauth2/oauth" -) - -type SocialQQ struct { - Token *oauth.Token - *oauth.Transport - reqUrl string -} - -func (s *SocialQQ) Type() int { - return models.OT_QQ -} - -func init() { - qq := &SocialQQ{} - name := "qq" - config := &oauth.Config{ - ClientId: "801497180", //base.OauthService.GitHub.ClientId, // FIXME: panic when set - ClientSecret: "16cd53b8ad2e16a36fc2c8f87d9388f2", //base.OauthService.GitHub.ClientSecret, - Scope: "all", - AuthURL: "https://open.t.qq.com/cgi-bin/oauth2/authorize", - TokenURL: "https://open.t.qq.com/cgi-bin/oauth2/access_token", - } - qq.reqUrl = "https://open.t.qq.com/api/user/info" - qq.Transport = &oauth.Transport{ - Config: config, - Transport: http.DefaultTransport, - } - SocialMap[name] = qq -} - -func (s *SocialQQ) SetRedirectUrl(url string) { - s.Transport.Config.RedirectURL = url -} - -func (s *SocialQQ) UserInfo(token *oauth.Token, URL *url.URL) (*BasicUserInfo, error) { - var data struct { - Data struct { - Id string `json:"openid"` - Name string `json:"name"` - Email string `json:"email"` - } `json:"data"` - } - var err error - // https://open.t.qq.com/api/user/info? - //oauth_consumer_key=APP_KEY& - //access_token=ACCESSTOKEN&openid=openid - //clientip=CLIENTIP&oauth_version=2.a - //scope=all - var urls = url.Values{ - "oauth_consumer_key": {s.Transport.Config.ClientId}, - "access_token": {token.AccessToken}, - "openid": URL.Query()["openid"], - "oauth_version": {"2.a"}, - "scope": {"all"}, - } - r, err := http.Get(s.reqUrl + "?" + urls.Encode()) - if err != nil { - return nil, err - } - defer r.Body.Close() - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { - return nil, err - } - return &BasicUserInfo{ - Identity: data.Data.Id, - Name: data.Data.Name, - Email: data.Data.Email, - }, nil -} diff --git a/routers/user/user.go b/routers/user/user.go index e5328173a..bcb2e9783 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -5,12 +5,9 @@ package user import ( - "fmt" "net/url" "strings" - "github.com/go-martini/martini" - "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" @@ -19,75 +16,6 @@ import ( "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) -} - func SignIn(ctx *middleware.Context) { ctx.Data["Title"] = "Log In" @@ -329,117 +257,6 @@ func DeletePost(ctx *middleware.Context) { ctx.Redirect("/user/delete") } -const ( - TPL_FEED = ` -
%s
%s
` -) - -func Feeds(ctx *middleware.Context, form auth.FeedsForm) { - actions, err := models.GetFeeds(form.UserId, form.Page*20, false) - if err != nil { - ctx.JSON(500, err) - } - - feeds := make([]string, len(actions)) - for i := range actions { - feeds[i] = fmt.Sprintf(TPL_FEED, base.ActionIcon(actions[i].OpType), - base.TimeSince(actions[i].Created), base.ActionDesc(actions[i])) - } - ctx.JSON(200, &feeds) -} - -func Issues(ctx *middleware.Context) { - ctx.Data["Title"] = "Your Issues" - ctx.Data["ViewType"] = "all" - - page, _ := base.StrTo(ctx.Query("page")).Int() - repoId, _ := base.StrTo(ctx.Query("repoid")).Int64() - - ctx.Data["RepoId"] = repoId - - var posterId int64 = 0 - if ctx.Query("type") == "created_by" { - posterId = ctx.User.Id - ctx.Data["ViewType"] = "created_by" - } - - // Get all repositories. - repos, err := models.GetRepositories(ctx.User) - if err != nil { - ctx.Handle(200, "user.Issues(get repositories)", err) - return - } - - showRepos := make([]models.Repository, 0, len(repos)) - - isShowClosed := ctx.Query("state") == "closed" - var closedIssueCount, createdByCount, allIssueCount int - - // Get all issues. - allIssues := make([]models.Issue, 0, 5*len(repos)) - for i, repo := range repos { - issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, isShowClosed, false, "", "") - if err != nil { - ctx.Handle(200, "user.Issues(get issues)", err) - return - } - - allIssueCount += repo.NumIssues - closedIssueCount += repo.NumClosedIssues - - // Set repository information to issues. - for j := range issues { - issues[j].Repo = &repos[i] - } - allIssues = append(allIssues, issues...) - - repos[i].NumOpenIssues = repo.NumIssues - repo.NumClosedIssues - if repos[i].NumOpenIssues > 0 { - showRepos = append(showRepos, repos[i]) - } - } - - showIssues := make([]models.Issue, 0, len(allIssues)) - ctx.Data["IsShowClosed"] = isShowClosed - - // Get posters and filter issues. - for i := range allIssues { - u, err := models.GetUserById(allIssues[i].PosterId) - if err != nil { - ctx.Handle(200, "user.Issues(get poster): %v", err) - return - } - allIssues[i].Poster = u - if u.Id == ctx.User.Id { - createdByCount++ - } - - if repoId > 0 && repoId != allIssues[i].Repo.Id { - continue - } - - if isShowClosed == allIssues[i].IsClosed { - showIssues = append(showIssues, allIssues[i]) - } - } - - ctx.Data["Repos"] = showRepos - ctx.Data["Issues"] = showIssues - ctx.Data["AllIssueCount"] = allIssueCount - ctx.Data["ClosedIssueCount"] = closedIssueCount - ctx.Data["OpenIssueCount"] = allIssueCount - closedIssueCount - ctx.Data["CreatedByCount"] = createdByCount - ctx.HTML(200, "issue/user") -} - -func Pulls(ctx *middleware.Context) { - ctx.HTML(200, "user/pulls") -} - -func Stars(ctx *middleware.Context) { - ctx.HTML(200, "user/stars") -} - func Activate(ctx *middleware.Context) { code := ctx.Query("code") if len(code) == 0 { diff --git a/web.go b/web.go index a17be2a34..8ae074ec0 100644 --- a/web.go +++ b/web.go @@ -14,7 +14,6 @@ import ( qlog "github.com/qiniu/log" - "github.com/gogits/binding" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/avatar" "github.com/gogits/gogs/modules/base" @@ -67,7 +66,7 @@ func runWeb(*cli.Context) { reqSignOut := middleware.Toggle(&middleware.ToggleOptions{SignOutRequire: true}) - bindIgnErr := binding.BindIgnErr + bindIgnErr := middleware.BindIgnErr // Routers. m.Get("/", ignSignIn, routers.Home) @@ -102,7 +101,7 @@ func runWeb(*cli.Context) { r.Post("/setting", bindIgnErr(auth.UpdateProfileForm{}), user.SettingPost) }, reqSignIn) m.Group("/user", func(r martini.Router) { - r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds) + r.Get("/feeds", middleware.Bind(auth.FeedsForm{}), user.Feeds) r.Get("/activate", user.Activate) r.Get("/email2user", user.Email2User) r.Get("/forget_password", user.ForgotPasswd)