snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:41:59 +05:00
commit 52ae0c5802
6280 changed files with 624193 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package middleware
import (
"net/http"
"reflect"
"strings"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/translation"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"gitea.com/go-chi/binding" //nolint:depguard // this package wraps it
)
type (
ValidateContext = structs.ValidateContext
FormDefaultValidator = structs.FormDefaultValidator
)
type Form interface {
Validate(ctx *ValidateContext, errs validation.BindingErrors) validation.BindingErrors
}
// BindFormAny binds the request to the form of type T and returns the pointer to the form and any binding errors.
// Only the rules defined in the struct field's "binding" tag are applied.
// It can bind to any struct, doesn't call the struct's "Form.Validate" interface.
func BindFormAny[T any](req *http.Request, binder *binding.Binder, _ T) (ret *T, _ validation.BindingErrors) {
typ := reflect.TypeFor[T]()
if typ.Kind() != reflect.Struct {
panic("BindFormAny: template type must be a struct and the function returns its pointer")
}
form := new(T)
errs := binder.Bind(req, form)
return form, errs
}
// BindFormValidate binds the request to the form of type T which must be a pointer implementing Form interface
// After binding, the Form.Validate is also called so we can do more validation checks
func BindFormValidate[T Form](req *http.Request, binder *binding.Binder) (ret T, _ validation.BindingErrors) {
locale := req.Context().Value(translation.ContextKey).(translation.Locale) //nolint:forcetypeassert // must exist
ptrType := reflect.TypeFor[T]()
structType := ptrType.Elem()
ptrVal := reflect.New(structType)
form := ptrVal.Interface().(Form) //nolint:forcetypeassert // must implement Form
errs := binder.Bind(req, form)
errs = form.Validate(&ValidateContext{Locale: locale}, errs)
return form.(T), errs //nolint:forcetypeassert // must be type T
}
// AssignForm assign form values back to the template data, the template variable names are in "snake_case"
func AssignForm(form any, data map[string]any) {
typ := reflect.TypeOf(form)
val := reflect.ValueOf(form)
for typ.Kind() == reflect.Pointer {
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, ruleName string) string {
prefix := ruleName + "("
for rule := range strings.SplitSeq(field.Tag.Get("binding"), ";") {
if strings.HasPrefix(rule, prefix) {
return rule[len(prefix) : len(rule)-1]
}
}
return ""
}
func AddValidationError(errs validation.BindingErrors, fieldName, errorMsg string) validation.BindingErrors {
errs.Add([]string{fieldName}, validation.ErrCustomMessage, errorMsg)
return errs
}
func getFieldDisplayNameForMessage(f any, l translation.Locale, fieldNames []string) (field reflect.StructField, ok bool, displayName string) {
if len(fieldNames) == 0 {
return field, false, ""
}
typ := reflect.TypeOf(f)
if typ.Kind() == reflect.Pointer {
typ = typ.Elem()
}
fieldName := fieldNames[0]
field, fieldExists := typ.FieldByName(fieldName)
if !fieldExists {
for tryField := range typ.Fields() {
if util.ToSnakeCase(tryField.Name) == fieldName || tryField.Tag.Get("form") == fieldName {
field, fieldExists = tryField, true
}
}
if !fieldExists {
return field, false, ""
}
}
if field.Tag.Get("form") == "-" {
return field, false, ""
}
trKeyFallback := "form." + field.Name
trKey := util.IfZero(field.Tag.Get("locale"), trKeyFallback)
if l.HasKey(trKey) {
displayName = l.TrString(trKey)
} else {
displayName = field.Name
}
return field, true, displayName
}
func BuildValidationErrorForUser(f any, l translation.Locale, bindingErrs validation.BindingErrors) (errorMessage, errorFieldName string, fieldNames []string) {
if bindingErrs.Len() == 0 {
return "", "", nil
}
bindingErr := bindingErrs[0]
fieldNames, classification, bindingErrMsg := bindingErr.FieldNames, bindingErr.Classification, bindingErr.Message
field, ok, fieldDisplayName := getFieldDisplayNameForMessage(f, l, fieldNames)
if !ok {
return l.TrString("error.occurred"), "", fieldNames
}
errorFieldName = field.Name
switch classification {
case binding.ERR_REQUIRED:
errorMessage = l.TrString("form.require_error", fieldDisplayName)
case binding.ERR_ALPHA_DASH:
errorMessage = l.TrString("form.alpha_dash_error", fieldDisplayName)
case binding.ERR_ALPHA_DASH_DOT:
errorMessage = l.TrString("form.alpha_dash_dot_error", fieldDisplayName)
case binding.ERR_MIN_SIZE:
errorMessage = l.TrString("form.min_size_error", fieldDisplayName, getRuleBody(field, "MinSize"))
case binding.ERR_MAX_SIZE:
errorMessage = l.TrString("form.max_size_error", fieldDisplayName, getRuleBody(field, "MaxSize"))
case binding.ERR_RANGE:
rangeMin, rangeMax, _ := strings.Cut(getRuleBody(field, "Range"), ",")
errorMessage = l.TrString("form.range_error", fieldDisplayName, rangeMin, rangeMax)
case binding.ERR_EMAIL:
errorMessage = l.TrString("form.email_error", fieldDisplayName)
case binding.ERR_URL:
errorMessage = l.TrString("form.url_error", fieldDisplayName)
case binding.ERR_IN:
ruleBody := getRuleBody(field, "In")
if strings.HasPrefix(ruleBody, ",") {
ruleBody = "(empty)" + ruleBody
}
errorMessage = l.TrString("form.in_error", fieldDisplayName, ruleBody)
case binding.ERR_INCLUDE:
errorMessage = l.TrString("form.include_error", fieldDisplayName, getRuleBody(field, "Include"))
case validation.ErrCustomMessage:
errorMessage = bindingErrMsg
case validation.ErrGitRefName:
errorMessage = l.TrString("form.git_ref_name_error", fieldDisplayName)
case validation.ErrGlobPattern:
errorMessage = l.TrString("form.glob_pattern_error", fieldDisplayName, bindingErrMsg)
case validation.ErrRegexPattern:
errorMessage = l.TrString("form.regex_pattern_error", fieldDisplayName, bindingErrMsg)
case validation.ErrUsername:
errorMessage = l.TrString("form.username_error", fieldDisplayName)
case validation.ErrInvalidGroupTeamMap:
errorMessage = l.TrString("form.invalid_group_team_map_error", fieldDisplayName, bindingErrMsg)
case validation.ErrInvalidBadgeSlug:
errorMessage = l.TrString("form.invalid_slug_error", fieldDisplayName)
default:
setting.PanicInDevOrTesting("unknown binding error classification for field %T.%s: %v, err: %s", f, errorFieldName, classification, bindingErrMsg)
var msg string
if classification != "" && bindingErrMsg != "" {
msg = classification + ": " + bindingErrMsg
} else {
msg = util.IfZero(bindingErrMsg, classification)
if msg == "" {
setting.PanicInDevOrTesting("no error message for binding error: %v", bindingErr)
}
msg = util.IfZero(msg, "unknown error")
}
errorMessage = l.TrString("form.field_invalid_message", fieldDisplayName, msg)
}
return errorMessage, errorFieldName, fieldNames
}

View File

@@ -0,0 +1,28 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package middleware
import (
"testing"
"gitea.dev/modules/translation"
"gitea.dev/modules/validation"
"github.com/stretchr/testify/assert"
)
type testRangeForm struct {
FormDefaultValidator
Hours int `binding:"Range(0,1000)"`
}
func TestBuildValidationErrorForUser(t *testing.T) {
// an out-of-range value must reach its own message instead of the panicking "default" branch
form := &testRangeForm{Hours: 2000}
errs := validation.Binder().Validate(t.Context(), form)
errorMessage, errorFieldName, fieldNames := BuildValidationErrorForUser(form, translation.MockLocale{}, errs)
assert.Equal(t, "form.range_error:form.Hours,0,1000", errorMessage)
assert.Equal(t, "Hours", errorFieldName)
assert.Equal(t, []string{"Hours"}, fieldNames)
}

View File

@@ -0,0 +1,107 @@
// 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"
"gitea.dev/modules/session"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
const (
CookieWebBannerDismissed = "gitea_disbnr"
CookieTheme = "gitea_theme"
cookieRedirectTo = "redirect_to"
)
func GetRedirectToCookie(req *http.Request) string {
return GetSiteCookie(req, cookieRedirectTo)
}
// SetRedirectToCookie convenience function to set the RedirectTo cookie consistently
func SetRedirectToCookie(resp http.ResponseWriter, value string) {
SetSiteCookie(resp, cookieRedirectTo, value, 0)
}
// DeleteRedirectToCookie convenience function to delete most cookies consistently
func DeleteRedirectToCookie(resp http.ResponseWriter) {
SetSiteCookie(resp, cookieRedirectTo, "", -1)
}
func RedirectLinkUserLogin(req *http.Request) string {
if req.Header.Get("X-Gitea-Fetch-Action") != "" {
// when building the redirect link for a fetch request, the current link might be a partial page,
// so we only redirect to the login page without redirect_to parameter
return setting.AppSubURL + "/user/login"
}
return setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(setting.AppSubURL+req.URL.RequestURI())
}
// 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)
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
cookie := &http.Cookie{
Name: name,
Value: url.QueryEscape(value),
MaxAge: maxAge,
Path: util.IfZero(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)
})
}

View File

@@ -0,0 +1,48 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package middleware
import (
"context"
"time"
"gitea.dev/modules/public"
"gitea.dev/modules/reqctx"
"gitea.dev/modules/setting"
)
const (
ContextDataKeySignedUser = "SignedUser"
// ContextDataKeyImpersonator holds the admin acting as the signed-in user, if any.
ContextDataKeyImpersonator = "Impersonator"
// ContextDataKeyAuthCredential names the credential the request authenticated with, e.g. "access-token:42".
ContextDataKeyAuthCredential = "AuthCredential"
)
func GetContextData(c context.Context) reqctx.ContextData {
if rc := reqctx.GetRequestDataStore(c); rc != nil {
return rc.GetData()
}
return nil
}
func CommonTemplateContextData() reqctx.ContextData {
return reqctx.ContextData{
"PageTitleCommon": setting.AppName,
"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,
"ViteModeIsDev": public.IsViteDevMode(),
}
}

View File

@@ -0,0 +1,92 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package middleware
import (
"fmt"
"html/template"
"net/http"
"net/url"
"gitea.dev/modules/reqctx"
)
// Flash represents a one time data transfer between two requests.
type Flash struct {
DataStore reqctx.RequestDataStore
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...)
}
func ParseCookieFlashMessage(val string) *Flash {
if vals, _ := url.ParseQuery(val); len(vals) > 0 {
return &Flash{
Values: vals,
ErrorMsg: vals.Get("error"),
SuccessMsg: vals.Get("success"),
InfoMsg: vals.Get("info"),
WarningMsg: vals.Get("warning"),
}
}
return nil
}
func GetSiteCookieFlashMessage(dataStore reqctx.RequestDataStore, req *http.Request, cookieName string) (string, *Flash) {
// Get the last flash message from cookie
lastFlashCookie := GetSiteCookie(req, cookieName)
lastFlashMsg := ParseCookieFlashMessage(lastFlashCookie)
if lastFlashMsg != nil {
lastFlashMsg.DataStore = dataStore
return lastFlashCookie, lastFlashMsg
}
return lastFlashCookie, nil
}

View File

@@ -0,0 +1,71 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package middleware
import (
"net/http"
"gitea.dev/modules/translation"
"gitea.dev/modules/translation/i18n"
"golang.org/x/text/language"
)
// maxAcceptLanguageLen bounds the Accept-Language header before it reaches
// language.ParseAcceptLanguage. That parser has quadratic-time behavior on long
// malformed inputs, and its built-in guard only counts "-" separators while the
// scanner treats "_" as an alias for "-", so a "_"-heavy header slips past the
// guard and burns CPU. Only the leading (highest-priority) languages are used, so
// truncating a longer header is safe.
const maxAcceptLanguageLen = 200
// parseAcceptLanguage parses the Accept-Language header after bounding its length
// to avoid a quadratic-time DoS on attacker-controlled input.
func parseAcceptLanguage(header string) []language.Tag {
if len(header) > maxAcceptLanguageLen {
header = header[:maxAcceptLanguageLen]
}
tags, _, _ := language.ParseAcceptLanguage(header)
return tags
}
// 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 := 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)
}

View File

@@ -0,0 +1,27 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package middleware
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseAcceptLanguage(t *testing.T) {
// a normal header is parsed and its leading language preserved
tags := parseAcceptLanguage("de-DE,de;q=0.9,en;q=0.8")
assert.NotEmpty(t, tags)
assert.Equal(t, "de-DE", tags[0].String())
// an oversized "_"-separated header would drive ParseAcceptLanguage into its
// quadratic-time path (the built-in guard only counts "-"); the length bound
// keeps the input passed to the parser small so it cannot be used for a DoS.
malicious := strings.Repeat("_aaaaaaaaa", 1<<16) // ~640 KiB, zero "-" characters
assert.Greater(t, len(malicious), maxAcceptLanguageLen)
tags = parseAcceptLanguage(malicious)
// no panic / hang, and nothing meaningful is parsed out of the garbage
assert.Empty(t, tags)
}