mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-03 21:08:25 +00:00 
			
		
		
		
	Clarify path param naming (#32969)
In history (from some legacy frameworks), both `:name` and `name` are supported as path path name, `:name` is an alias to `name`. To make code consistent, now we should only use `name` but not `:name`. Also added panic check in related functions to make sure the name won't be abused in case some downstreams still use them.
This commit is contained in:
		@@ -9,20 +9,15 @@ import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/httplib"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/reqctx"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-chi/chi/v5"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BaseContextKeyType struct{}
 | 
			
		||||
@@ -107,93 +102,6 @@ func (b *Base) RemoteAddr() string {
 | 
			
		||||
	return b.Req.RemoteAddr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PathParam returns the param in request path, eg: "/{var}" => "/a%2fb", then `var == "a/b"`
 | 
			
		||||
func (b *Base) PathParam(name string) string {
 | 
			
		||||
	s, err := url.PathUnescape(b.PathParamRaw(name))
 | 
			
		||||
	if err != nil && !setting.IsProd {
 | 
			
		||||
		panic("Failed to unescape path param: " + err.Error() + ", there seems to be a double-unescaping bug")
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PathParamRaw returns the raw param in request path, eg: "/{var}" => "/a%2fb", then `var == "a%2fb"`
 | 
			
		||||
func (b *Base) PathParamRaw(name string) string {
 | 
			
		||||
	return chi.URLParam(b.Req, strings.TrimPrefix(name, ":"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PathParamInt64 returns the param in request path as int64
 | 
			
		||||
func (b *Base) PathParamInt64(p string) int64 {
 | 
			
		||||
	v, _ := strconv.ParseInt(b.PathParam(p), 10, 64)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetPathParam set request path params into routes
 | 
			
		||||
func (b *Base) SetPathParam(k, v string) {
 | 
			
		||||
	chiCtx := chi.RouteContext(b)
 | 
			
		||||
	chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormString returns the first value matching the provided key in the form as a string
 | 
			
		||||
func (b *Base) FormString(key string) string {
 | 
			
		||||
	return b.Req.FormValue(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormStrings returns a string slice for the provided key from the form
 | 
			
		||||
func (b *Base) FormStrings(key string) []string {
 | 
			
		||||
	if b.Req.Form == nil {
 | 
			
		||||
		if err := b.Req.ParseMultipartForm(32 << 20); err != nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if v, ok := b.Req.Form[key]; ok {
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormTrim returns the first value for the provided key in the form as a space trimmed string
 | 
			
		||||
func (b *Base) FormTrim(key string) string {
 | 
			
		||||
	return strings.TrimSpace(b.Req.FormValue(key))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormInt returns the first value for the provided key in the form as an int
 | 
			
		||||
func (b *Base) FormInt(key string) int {
 | 
			
		||||
	v, _ := strconv.Atoi(b.Req.FormValue(key))
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormInt64 returns the first value for the provided key in the form as an int64
 | 
			
		||||
func (b *Base) FormInt64(key string) int64 {
 | 
			
		||||
	v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormBool returns true if the value for the provided key in the form is "1", "true" or "on"
 | 
			
		||||
func (b *Base) FormBool(key string) bool {
 | 
			
		||||
	s := b.Req.FormValue(key)
 | 
			
		||||
	v, _ := strconv.ParseBool(s)
 | 
			
		||||
	v = v || strings.EqualFold(s, "on")
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value
 | 
			
		||||
// for the provided key exists in the form else it returns optional.None[bool]()
 | 
			
		||||
func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
 | 
			
		||||
	value := b.Req.FormValue(key)
 | 
			
		||||
	if len(value) == 0 {
 | 
			
		||||
		return optional.None[bool]()
 | 
			
		||||
	}
 | 
			
		||||
	s := b.Req.FormValue(key)
 | 
			
		||||
	v, _ := strconv.ParseBool(s)
 | 
			
		||||
	v = v || strings.EqualFold(s, "on")
 | 
			
		||||
	return optional.Some(v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Base) SetFormString(key, value string) {
 | 
			
		||||
	_ = b.Req.FormValue(key) // force parse form
 | 
			
		||||
	b.Req.Form.Set(key, value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PlainTextBytes renders bytes as plain text
 | 
			
		||||
func (b *Base) plainTextInternal(skip, status int, bs []byte) {
 | 
			
		||||
	statusPrefix := status / 100
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								services/context/base_form.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								services/context/base_form.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// FormString returns the first value matching the provided key in the form as a string
 | 
			
		||||
func (b *Base) FormString(key string) string {
 | 
			
		||||
	return b.Req.FormValue(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormStrings returns a string slice for the provided key from the form
 | 
			
		||||
func (b *Base) FormStrings(key string) []string {
 | 
			
		||||
	if b.Req.Form == nil {
 | 
			
		||||
		if err := b.Req.ParseMultipartForm(32 << 20); err != nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if v, ok := b.Req.Form[key]; ok {
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormTrim returns the first value for the provided key in the form as a space trimmed string
 | 
			
		||||
func (b *Base) FormTrim(key string) string {
 | 
			
		||||
	return strings.TrimSpace(b.Req.FormValue(key))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormInt returns the first value for the provided key in the form as an int
 | 
			
		||||
func (b *Base) FormInt(key string) int {
 | 
			
		||||
	v, _ := strconv.Atoi(b.Req.FormValue(key))
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormInt64 returns the first value for the provided key in the form as an int64
 | 
			
		||||
func (b *Base) FormInt64(key string) int64 {
 | 
			
		||||
	v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormBool returns true if the value for the provided key in the form is "1", "true" or "on"
 | 
			
		||||
func (b *Base) FormBool(key string) bool {
 | 
			
		||||
	s := b.Req.FormValue(key)
 | 
			
		||||
	v, _ := strconv.ParseBool(s)
 | 
			
		||||
	v = v || strings.EqualFold(s, "on")
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value
 | 
			
		||||
// for the provided key exists in the form else it returns optional.None[bool]()
 | 
			
		||||
func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
 | 
			
		||||
	value := b.Req.FormValue(key)
 | 
			
		||||
	if len(value) == 0 {
 | 
			
		||||
		return optional.None[bool]()
 | 
			
		||||
	}
 | 
			
		||||
	s := b.Req.FormValue(key)
 | 
			
		||||
	v, _ := strconv.ParseBool(s)
 | 
			
		||||
	v = v || strings.EqualFold(s, "on")
 | 
			
		||||
	return optional.Some(v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Base) SetFormString(key, value string) {
 | 
			
		||||
	_ = b.Req.FormValue(key) // force parse form
 | 
			
		||||
	b.Req.Form.Set(key, value)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								services/context/base_path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								services/context/base_path.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-chi/chi/v5"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PathParam returns the param in request path, eg: "/{var}" => "/a%2fb", then `var == "a/b"`
 | 
			
		||||
func (b *Base) PathParam(name string) string {
 | 
			
		||||
	s, err := url.PathUnescape(b.PathParamRaw(name))
 | 
			
		||||
	if err != nil && !setting.IsProd {
 | 
			
		||||
		panic("Failed to unescape path param: " + err.Error() + ", there seems to be a double-unescaping bug")
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PathParamRaw returns the raw param in request path, eg: "/{var}" => "/a%2fb", then `var == "a%2fb"`
 | 
			
		||||
func (b *Base) PathParamRaw(name string) string {
 | 
			
		||||
	if strings.HasPrefix(name, ":") {
 | 
			
		||||
		setting.PanicInDevOrTesting("path param should not start with ':'")
 | 
			
		||||
		name = name[1:]
 | 
			
		||||
	}
 | 
			
		||||
	return chi.URLParam(b.Req, name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PathParamInt64 returns the param in request path as int64
 | 
			
		||||
func (b *Base) PathParamInt64(p string) int64 {
 | 
			
		||||
	v, _ := strconv.ParseInt(b.PathParam(p), 10, 64)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetPathParam set request path params into routes
 | 
			
		||||
func (b *Base) SetPathParam(name, value string) {
 | 
			
		||||
	if strings.HasPrefix(name, ":") {
 | 
			
		||||
		setting.PanicInDevOrTesting("path param should not start with ':'")
 | 
			
		||||
		name = name[1:]
 | 
			
		||||
	}
 | 
			
		||||
	chi.RouteContext(b).URLParams.Add(name, url.PathEscape(value))
 | 
			
		||||
}
 | 
			
		||||
@@ -40,7 +40,7 @@ func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetOrganizationByParams(ctx *Context) {
 | 
			
		||||
	orgName := ctx.PathParam(":org")
 | 
			
		||||
	orgName := ctx.PathParam("org")
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
@@ -220,7 +220,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
 | 
			
		||||
		ctx.Data["NumTeams"] = len(ctx.Org.Teams)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	teamName := ctx.PathParam(":team")
 | 
			
		||||
	teamName := ctx.PathParam("team")
 | 
			
		||||
	if len(teamName) > 0 {
 | 
			
		||||
		teamExists := false
 | 
			
		||||
		for _, team := range ctx.Org.Teams {
 | 
			
		||||
 
 | 
			
		||||
@@ -316,8 +316,8 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string {
 | 
			
		||||
// This is particular a workaround for "go get" command which does not respect
 | 
			
		||||
// .netrc file.
 | 
			
		||||
func EarlyResponseForGoGetMeta(ctx *Context) {
 | 
			
		||||
	username := ctx.PathParam(":username")
 | 
			
		||||
	reponame := strings.TrimSuffix(ctx.PathParam(":reponame"), ".git")
 | 
			
		||||
	username := ctx.PathParam("username")
 | 
			
		||||
	reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
 | 
			
		||||
	if username == "" || reponame == "" {
 | 
			
		||||
		ctx.PlainText(http.StatusBadRequest, "invalid repository path")
 | 
			
		||||
		return
 | 
			
		||||
@@ -336,8 +336,8 @@ func EarlyResponseForGoGetMeta(ctx *Context) {
 | 
			
		||||
 | 
			
		||||
// RedirectToRepo redirect to a differently-named repository
 | 
			
		||||
func RedirectToRepo(ctx *Base, redirectRepoID int64) {
 | 
			
		||||
	ownerName := ctx.PathParam(":username")
 | 
			
		||||
	previousRepoName := ctx.PathParam(":reponame")
 | 
			
		||||
	ownerName := ctx.PathParam("username")
 | 
			
		||||
	previousRepoName := ctx.PathParam("reponame")
 | 
			
		||||
 | 
			
		||||
	repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -412,8 +412,8 @@ func RepoAssignment(ctx *Context) {
 | 
			
		||||
		err   error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	userName := ctx.PathParam(":username")
 | 
			
		||||
	repoName := ctx.PathParam(":reponame")
 | 
			
		||||
	userName := ctx.PathParam("username")
 | 
			
		||||
	repoName := ctx.PathParam("reponame")
 | 
			
		||||
	repoName = strings.TrimSuffix(repoName, ".git")
 | 
			
		||||
	if setting.Other.EnableFeed {
 | 
			
		||||
		repoName = strings.TrimSuffix(repoName, ".rss")
 | 
			
		||||
@@ -456,7 +456,7 @@ func RepoAssignment(ctx *Context) {
 | 
			
		||||
	if strings.HasSuffix(repoName, ".wiki") {
 | 
			
		||||
		// ctx.Req.URL.Path does not have the preceding appSubURL - any redirect must have this added
 | 
			
		||||
		// Now we happen to know that all of our paths are: /:username/:reponame/whatever_else
 | 
			
		||||
		originalRepoName := ctx.PathParam(":reponame")
 | 
			
		||||
		originalRepoName := ctx.PathParam("reponame")
 | 
			
		||||
		redirectRepoName := strings.TrimSuffix(repoName, ".wiki")
 | 
			
		||||
		redirectRepoName += originalRepoName[len(redirectRepoName)+5:]
 | 
			
		||||
		redirectPath := strings.Replace(
 | 
			
		||||
 
 | 
			
		||||
@@ -97,8 +97,8 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
 | 
			
		||||
	} else if uploadType == "comment" {
 | 
			
		||||
		ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments"
 | 
			
		||||
		ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove"
 | 
			
		||||
		if len(ctx.PathParam(":index")) > 0 {
 | 
			
		||||
			ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.PathParam(":index")) + "/attachments"
 | 
			
		||||
		if len(ctx.PathParam("index")) > 0 {
 | 
			
		||||
			ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.PathParam("index")) + "/attachments"
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/attachments"
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ func UserAssignmentWeb() func(ctx *Context) {
 | 
			
		||||
// UserIDAssignmentAPI returns a middleware to handle context-user assignment for api routes
 | 
			
		||||
func UserIDAssignmentAPI() func(ctx *APIContext) {
 | 
			
		||||
	return func(ctx *APIContext) {
 | 
			
		||||
		userID := ctx.PathParamInt64(":user-id")
 | 
			
		||||
		userID := ctx.PathParamInt64("user-id")
 | 
			
		||||
 | 
			
		||||
		if ctx.IsSigned && ctx.Doer.ID == userID {
 | 
			
		||||
			ctx.ContextUser = ctx.Doer
 | 
			
		||||
@@ -59,7 +59,7 @@ func UserAssignmentAPI() func(ctx *APIContext) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) {
 | 
			
		||||
	username := ctx.PathParam(":username")
 | 
			
		||||
	username := ctx.PathParam("username")
 | 
			
		||||
 | 
			
		||||
	if doer != nil && doer.LowerName == strings.ToLower(username) {
 | 
			
		||||
		contextUser = doer
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user