summaryrefslogtreecommitdiffstats
path: root/modules/web/middleware
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /modules/web/middleware
parentInitial commit. (diff)
downloadforgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.tar.xz
forgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.zip
Adding upstream version 9.0.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--modules/web/middleware/binding.go162
-rw-r--r--modules/web/middleware/cookie.go85
-rw-r--r--modules/web/middleware/data.go63
-rw-r--r--modules/web/middleware/flash.go65
-rw-r--r--modules/web/middleware/locale.go59
-rw-r--r--modules/web/middleware/request.go14
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/")
+}