diff options
Diffstat (limited to 'modules/web/middleware')
-rw-r--r-- | modules/web/middleware/binding.go | 162 | ||||
-rw-r--r-- | modules/web/middleware/cookie.go | 85 | ||||
-rw-r--r-- | modules/web/middleware/data.go | 63 | ||||
-rw-r--r-- | modules/web/middleware/flash.go | 65 | ||||
-rw-r--r-- | modules/web/middleware/locale.go | 59 | ||||
-rw-r--r-- | modules/web/middleware/request.go | 14 |
6 files changed, 448 insertions, 0 deletions
diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go new file mode 100644 index 0000000..8fa71a8 --- /dev/null +++ b/modules/web/middleware/binding.go @@ -0,0 +1,162 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "reflect" + "strings" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "gitea.com/go-chi/binding" +) + +// Form form binding interface +type Form interface { + binding.Validator +} + +func init() { + binding.SetNameMapper(util.ToSnakeCase) +} + +// AssignForm assign form values back to the template data. +func AssignForm(form any, data map[string]any) { + typ := reflect.TypeOf(form) + val := reflect.ValueOf(form) + + for typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + fieldName := field.Tag.Get("form") + // Allow ignored fields in the struct + if fieldName == "-" { + continue + } else if len(fieldName) == 0 { + fieldName = util.ToSnakeCase(field.Name) + } + + data[fieldName] = val.Field(i).Interface() + } +} + +func getRuleBody(field reflect.StructField, prefix string) string { + for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { + if strings.HasPrefix(rule, prefix) { + return rule[len(prefix) : len(rule)-1] + } + } + return "" +} + +// GetSize get size int form tag +func GetSize(field reflect.StructField) string { + return getRuleBody(field, "Size(") +} + +// GetMinSize get minimal size in form tag +func GetMinSize(field reflect.StructField) string { + return getRuleBody(field, "MinSize(") +} + +// GetMaxSize get max size in form tag +func GetMaxSize(field reflect.StructField) string { + return getRuleBody(field, "MaxSize(") +} + +// GetInclude get include in form tag +func GetInclude(field reflect.StructField) string { + return getRuleBody(field, "Include(") +} + +// Validate populates the data with validation error (if any). +func Validate(errs binding.Errors, data map[string]any, f any, l translation.Locale) binding.Errors { + if errs.Len() == 0 { + return errs + } + + data["HasError"] = true + // If the field with name errs[0].FieldNames[0] is not found in form + // somehow, some code later on will panic on Data["ErrorMsg"].(string). + // So initialize it to some default. + data["ErrorMsg"] = l.Tr("form.unknown_error") + AssignForm(f, data) + + typ := reflect.TypeOf(f) + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + + if field, ok := typ.FieldByName(errs[0].FieldNames[0]); ok { + fieldName := field.Tag.Get("form") + if fieldName != "-" { + data["Err_"+field.Name] = true + + trName := field.Tag.Get("locale") + if len(trName) == 0 { + trName = l.TrString("form." + field.Name) + } else { + trName = l.TrString(trName) + } + + switch errs[0].Classification { + case binding.ERR_REQUIRED: + data["ErrorMsg"] = trName + l.TrString("form.require_error") + case binding.ERR_ALPHA_DASH: + data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error") + case binding.ERR_ALPHA_DASH_DOT: + data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error") + case validation.ErrGitRefName: + data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error") + case binding.ERR_SIZE: + data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field)) + case binding.ERR_MIN_SIZE: + data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field)) + case binding.ERR_MAX_SIZE: + data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field)) + case binding.ERR_EMAIL: + data["ErrorMsg"] = trName + l.TrString("form.email_error") + case binding.ERR_URL: + data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message) + case binding.ERR_INCLUDE: + data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field)) + case validation.ErrGlobPattern: + data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message) + case validation.ErrRegexPattern: + data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message) + case validation.ErrUsername: + if setting.Service.AllowDotsInUsernames { + data["ErrorMsg"] = trName + l.TrString("form.username_error") + } else { + data["ErrorMsg"] = trName + l.TrString("form.username_error_no_dots") + } + case validation.ErrInvalidGroupTeamMap: + data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message) + default: + msg := errs[0].Classification + if msg != "" && errs[0].Message != "" { + msg += ": " + } + + msg += errs[0].Message + if msg == "" { + msg = l.TrString("form.unknown_error") + } + data["ErrorMsg"] = trName + ": " + msg + } + return errs + } + } + return errs +} diff --git a/modules/web/middleware/cookie.go b/modules/web/middleware/cookie.go new file mode 100644 index 0000000..f2d25f5 --- /dev/null +++ b/modules/web/middleware/cookie.go @@ -0,0 +1,85 @@ +// Copyright 2020 The Macaron Authors +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "net/http" + "net/url" + "strings" + + "code.gitea.io/gitea/modules/session" + "code.gitea.io/gitea/modules/setting" +) + +// SetRedirectToCookie convenience function to set the RedirectTo cookie consistently +func SetRedirectToCookie(resp http.ResponseWriter, value string) { + SetSiteCookie(resp, "redirect_to", value, 0) +} + +// DeleteRedirectToCookie convenience function to delete most cookies consistently +func DeleteRedirectToCookie(resp http.ResponseWriter) { + SetSiteCookie(resp, "redirect_to", "", -1) +} + +// GetSiteCookie returns given cookie value from request header. +func GetSiteCookie(req *http.Request, name string) string { + cookie, err := req.Cookie(name) + if err != nil { + return "" + } + val, _ := url.QueryUnescape(cookie.Value) + return val +} + +// SetSiteCookie returns given cookie value from request header. +func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) { + // Previous versions would use a cookie path with a trailing /. + // These are more specific than cookies without a trailing /, so + // we need to delete these if they exist. + deleteLegacySiteCookie(resp, name) + cookie := &http.Cookie{ + Name: name, + Value: url.QueryEscape(value), + MaxAge: maxAge, + Path: setting.SessionConfig.CookiePath, + Domain: setting.SessionConfig.Domain, + Secure: setting.SessionConfig.Secure, + HttpOnly: true, + SameSite: setting.SessionConfig.SameSite, + } + resp.Header().Add("Set-Cookie", cookie.String()) +} + +// deleteLegacySiteCookie deletes the cookie with the given name at the cookie +// path with a trailing /, which would unintentionally override the cookie. +func deleteLegacySiteCookie(resp http.ResponseWriter, name string) { + if setting.SessionConfig.CookiePath == "" || strings.HasSuffix(setting.SessionConfig.CookiePath, "/") { + // If the cookie path ends with /, no legacy cookies will take + // precedence, so do nothing. The exception is that cookies with no + // path could override other cookies, but it's complicated and we don't + // currently handle that. + return + } + + cookie := &http.Cookie{ + Name: name, + Value: "", + MaxAge: -1, + Path: setting.SessionConfig.CookiePath + "/", + Domain: setting.SessionConfig.Domain, + Secure: setting.SessionConfig.Secure, + HttpOnly: true, + SameSite: setting.SessionConfig.SameSite, + } + resp.Header().Add("Set-Cookie", cookie.String()) +} + +func init() { + session.BeforeRegenerateSession = append(session.BeforeRegenerateSession, func(resp http.ResponseWriter, _ *http.Request) { + // Ensure that a cookie with a trailing slash does not take precedence over + // the cookie written by the middleware. + deleteLegacySiteCookie(resp, setting.SessionConfig.CookieName) + }) +} diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go new file mode 100644 index 0000000..08d83f9 --- /dev/null +++ b/modules/web/middleware/data.go @@ -0,0 +1,63 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "context" + "time" + + "code.gitea.io/gitea/modules/setting" +) + +// ContextDataStore represents a data store +type ContextDataStore interface { + GetData() ContextData +} + +type ContextData map[string]any + +func (ds ContextData) GetData() ContextData { + return ds +} + +func (ds ContextData) MergeFrom(other ContextData) ContextData { + for k, v := range other { + ds[k] = v + } + return ds +} + +const ContextDataKeySignedUser = "SignedUser" + +type contextDataKeyType struct{} + +var contextDataKey contextDataKeyType + +func WithContextData(c context.Context) context.Context { + return context.WithValue(c, contextDataKey, make(ContextData, 10)) +} + +func GetContextData(c context.Context) ContextData { + if ds, ok := c.Value(contextDataKey).(ContextData); ok { + return ds + } + return nil +} + +func CommonTemplateContextData() ContextData { + return ContextData{ + "IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations, + + "ShowRegistrationButton": setting.Service.ShowRegistrationButton, + "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, + "ShowFooterVersion": setting.Other.ShowFooterVersion, + "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, + + "EnableSwagger": setting.API.EnableSwagger, + "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, + "PageStartTime": time.Now(), + + "RunModeIsProd": setting.IsProd, + } +} diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go new file mode 100644 index 0000000..88da204 --- /dev/null +++ b/modules/web/middleware/flash.go @@ -0,0 +1,65 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "fmt" + "html/template" + "net/url" +) + +// Flash represents a one time data transfer between two requests. +type Flash struct { + DataStore ContextDataStore + url.Values + ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string +} + +func (f *Flash) set(name, msg string, current ...bool) { + if f.Values == nil { + f.Values = make(map[string][]string) + } + showInCurrentPage := len(current) > 0 && current[0] + if showInCurrentPage { + // assign it to the context data, then the template can use ".Flash.XxxMsg" to render the message + f.DataStore.GetData()["Flash"] = f + } else { + // the message map will be saved into the cookie and be shown in next response (a new page response which decodes the cookie) + f.Set(name, msg) + } +} + +func flashMsgStringOrHTML(msg any) string { + switch v := msg.(type) { + case string: + return v + case template.HTML: + return string(v) + } + panic(fmt.Sprintf("unknown type: %T", msg)) +} + +// Error sets error message +func (f *Flash) Error(msg any, current ...bool) { + f.ErrorMsg = flashMsgStringOrHTML(msg) + f.set("error", f.ErrorMsg, current...) +} + +// Warning sets warning message +func (f *Flash) Warning(msg any, current ...bool) { + f.WarningMsg = flashMsgStringOrHTML(msg) + f.set("warning", f.WarningMsg, current...) +} + +// Info sets info message +func (f *Flash) Info(msg any, current ...bool) { + f.InfoMsg = flashMsgStringOrHTML(msg) + f.set("info", f.InfoMsg, current...) +} + +// Success sets success message +func (f *Flash) Success(msg any, current ...bool) { + f.SuccessMsg = flashMsgStringOrHTML(msg) + f.set("success", f.SuccessMsg, current...) +} diff --git a/modules/web/middleware/locale.go b/modules/web/middleware/locale.go new file mode 100644 index 0000000..34a16f0 --- /dev/null +++ b/modules/web/middleware/locale.go @@ -0,0 +1,59 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "net/http" + + "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/translation/i18n" + + "golang.org/x/text/language" +) + +// Locale handle locale +func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale { + // 1. Check URL arguments. + lang := req.URL.Query().Get("lang") + changeLang := lang != "" + + // 2. Get language information from cookies. + if len(lang) == 0 { + ck, _ := req.Cookie("lang") + if ck != nil { + lang = ck.Value + } + } + + // Check again in case someone changes the supported language list. + if lang != "" && !i18n.DefaultLocales.HasLang(lang) { + lang = "" + changeLang = false + } + + // 3. Get language information from 'Accept-Language'. + // The first element in the list is chosen to be the default language automatically. + if len(lang) == 0 { + tags, _, _ := language.ParseAcceptLanguage(req.Header.Get("Accept-Language")) + tag := translation.Match(tags...) + lang = tag.String() + } + + if changeLang { + SetLocaleCookie(resp, lang, 1<<31-1) + } + + return translation.NewLocale(lang) +} + +// SetLocaleCookie convenience function to set the locale cookie consistently +func SetLocaleCookie(resp http.ResponseWriter, lang string, maxAge int) { + SetSiteCookie(resp, "lang", lang, maxAge) +} + +// DeleteLocaleCookie convenience function to delete the locale cookie consistently +// Setting the lang cookie will trigger the middleware to reset the language to previous state. +func DeleteLocaleCookie(resp http.ResponseWriter) { + SetSiteCookie(resp, "lang", "", -1) +} diff --git a/modules/web/middleware/request.go b/modules/web/middleware/request.go new file mode 100644 index 0000000..0bb155d --- /dev/null +++ b/modules/web/middleware/request.go @@ -0,0 +1,14 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "net/http" + "strings" +) + +// IsAPIPath returns true if the specified URL is an API path +func IsAPIPath(req *http.Request) bool { + return strings.HasPrefix(req.URL.Path, "/api/") +} |