feat: update gitea vendor
runner nix smoke / nix label and flake smoke (push) Failing after 1m28s

This commit is contained in:
2026-09-26 21:18:24 +00:00
parent c439c1b948
commit d9b2a4e787
3538 changed files with 116131 additions and 44340 deletions
@@ -12,10 +12,10 @@ import (
"time"
"unicode"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/web/middleware"
)
type accessLoggerTmplData struct {
@@ -10,8 +10,8 @@ import (
"testing"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
)
+49 -79
View File
@@ -13,18 +13,18 @@ import (
"strconv"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
web_types "code.gitea.io/gitea/modules/web/types"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/httpcache"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
web_types "gitea.dev/modules/web/types"
)
// APIContext is a specific context for API service
@@ -49,9 +49,10 @@ type APIContext struct {
}
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
// A public-only token cannot reach a private repo; any other token is unrestricted by this check.
// A public-only token cannot reach a private repo or a repo owned by a non-public (limited or
// private) owner; any other token is unrestricted by this check.
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
return !ctx.PublicOnly || !publicOnlyTokenDeniedRepo(ctx, repo)
}
func init() {
@@ -137,16 +138,18 @@ func (ctx *APIContext) apiErrorInternal(skip int, err error) {
})
}
// APIError responds with an error message to client with given obj as the message.
// If status is 500, also it prints error to log.
func (ctx *APIContext) APIError(status int, obj any) {
var message string
if err, ok := obj.(error); ok {
message = err.Error()
} else {
message = fmt.Sprintf("%s", obj)
}
// APIErrorNotFound handles 404s for APIContext
func (ctx *APIContext) APIErrorNotFound(msg ...string) {
ctx.JSON(http.StatusNotFound, APIError{
Message: util.OptionalArg(msg, "not found"),
URL: setting.API.SwaggerURL,
})
}
// APIError responds with an error message to client.
// If status is 500, also it prints error to log.
func (ctx *APIContext) APIError(status int, msg string) {
message := msg
if status == http.StatusInternalServerError {
log.ErrorWithSkip(1, "APIError: %s", message)
@@ -161,6 +164,26 @@ func (ctx *APIContext) APIError(status int, obj any) {
})
}
// APIErrorAuto use error check function to determine the response code
func (ctx *APIContext) APIErrorAuto(err error) {
switch {
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err.Error())
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err.Error())
case errors.Is(err, util.ErrNotExist):
ctx.APIError(http.StatusNotFound, err.Error())
case errors.Is(err, util.ErrAlreadyExist):
ctx.APIError(http.StatusConflict, err.Error())
case errors.Is(err, util.ErrContentTooLarge):
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
case errors.Is(err, util.ErrUnprocessableContent):
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
default:
ctx.apiErrorInternal(1, err)
}
}
type apiContextKeyType struct{}
var apiContextKey = apiContextKeyType{}
@@ -242,36 +265,12 @@ func APIContexter() func(http.Handler) http.Handler {
}
}
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{})
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
}
// APIErrorNotFound handles 404s for APIContext
// String will replace message, errors will be added to a slice
func (ctx *APIContext) APIErrorNotFound(objs ...any) {
var message string
var errs []string
for _, obj := range objs {
// Ignore nil
if obj == nil {
continue
}
if err, ok := obj.(error); ok {
errs = append(errs, err.Error())
} else {
message = obj.(string)
}
}
ctx.JSON(http.StatusNotFound, map[string]any{
"message": util.IfZero(message, "not found"), // do not use locale in API
"url": setting.API.SwaggerURL,
"errors": errs,
})
}
// ReferencesGitRepo injects the GitRepo into the Context
// you can optional skip the IsEmpty check
func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) {
@@ -329,35 +328,6 @@ func RepoRefForAPI(next http.Handler) http.Handler {
})
}
// HasAPIError returns true if error occurs in form validation.
func (ctx *APIContext) HasAPIError() bool {
hasErr, ok := ctx.Data["HasError"]
if !ok {
return false
}
return hasErr.(bool)
}
// GetErrMsg returns error message in form validation.
func (ctx *APIContext) GetErrMsg() string {
msg, _ := ctx.Data["ErrorMsg"].(string)
if msg == "" {
msg = "invalid form data"
}
return msg
}
// NotFoundOrServerError use error check function to determine if the error
// is about not found. It responds with 404 status code for not found error,
// or error context description for logging purpose of 500 server error.
func (ctx *APIContext) NotFoundOrServerError(err error) {
if errors.Is(err, util.ErrNotExist) {
ctx.JSON(http.StatusNotFound, nil)
return
}
ctx.APIErrorInternal(err)
}
// IsUserSiteAdmin returns true if current user is a site admin
func (ctx *APIContext) IsUserSiteAdmin() bool {
return ctx.IsSigned && ctx.Doer.IsAdmin
@@ -365,10 +335,10 @@ func (ctx *APIContext) IsUserSiteAdmin() bool {
// IsUserRepoAdmin returns true if current user is admin in current repo
func (ctx *APIContext) IsUserRepoAdmin() bool {
return ctx.Repo.IsAdmin()
return ctx.Repo.Permission.IsAdmin()
}
// IsUserRepoWriter returns true if current user has "write" privilege in current repo
func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool {
return slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite)
return slices.ContainsFunc(unitTypes, ctx.Repo.Permission.CanWrite)
}
@@ -3,7 +3,7 @@
package context
import "code.gitea.io/gitea/models/organization"
import "gitea.dev/models/organization"
// APIOrganization contains organization and team
type APIOrganization struct {
@@ -8,7 +8,7 @@ import (
"strconv"
"testing"
"code.gitea.io/gitea/modules/setting"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
)
+32 -14
View File
@@ -12,14 +12,14 @@ import (
"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/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"gitea.dev/modules/httplib"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/reqctx"
"gitea.dev/modules/setting"
"gitea.dev/modules/translation"
"gitea.dev/modules/util"
"gitea.dev/modules/web/middleware"
)
type BaseContextKeyType struct{}
@@ -159,12 +159,10 @@ func (b *Base) Redirect(location string, status ...int) {
// So in this case, we should remove the session cookie from the response header
removeSessionCookieHeader(b.Resp)
}
// in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx
if b.Req.Header.Get("HX-Request") == "true" {
b.Resp.Header().Set("HX-Redirect", location)
// we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect
// so as to give htmx redirect logic a chance to run
b.Status(http.StatusNoContent)
// In case the request is made by "fetch-action" module, make JS redirect to the new location
// Otherwise, the JS fetch will follow the redirection and read a "login" page, embed it to the current page, which is not expected.
if b.Req.Header.Get("X-Gitea-Fetch-Action") != "" {
b.JSON(http.StatusOK, map[string]any{"redirect": location})
return
}
http.Redirect(b.Resp, b.Req, location, code)
@@ -190,6 +188,26 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return b.Locale.TrN(cnt, key1, keyN, args...)
}
func CspScriptNonce(ctx reqctx.RequestContext) (ret string) {
// Generate a random nonce for each request and cache it in the context to make it usable during the whole rendering process.
//
// Some "<script>" tags are not in the CSP context, so they don't need nonce,
// these tags are written as "<script nonce>" to help developers to know that "no script nonce attribute is missing"
// (e.g.: when they grep the codebase for "script" tags)
ret, _ = ctx.Value("_cspScriptNonce").(string)
if ret == "" {
ret = util.FastCryptoRandomHex(32) // 16 bytes / 128 bits entropy
ctx.SetContextValue("_cspScriptNonce", ret)
}
return ret
}
func (b *Base) SetHeaderContentSecurityPolicyGeneral() {
if csp := WebContentSecurityPolicy(CspScriptNonce(b)); csp != "" {
b.Resp.Header().Set("Content-Security-Policy", csp)
}
}
func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base {
reqCtx := reqctx.FromContext(req.Context())
b := &Base{
@@ -7,8 +7,9 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/util"
"gitea.dev/modules/base"
"gitea.dev/modules/optional"
"gitea.dev/modules/util"
)
// FormString returns the first value matching the provided key in the form as a string
@@ -35,6 +36,11 @@ func (b *Base) FormStrings(key string) []string {
return nil
}
func (b *Base) FormStringInt64s(key string) []int64 {
vals, _ := base.StringsToInt64s(strings.Split(b.FormString(key), ","))
return vals
}
// 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))
@@ -72,8 +78,3 @@ func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
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)
}
@@ -8,7 +8,7 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/modules/setting"
"gitea.dev/modules/setting"
"github.com/go-chi/chi/v5"
)
@@ -8,7 +8,7 @@ import (
"net/http/httptest"
"testing"
"code.gitea.io/gitea/modules/setting"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
)
@@ -38,9 +38,10 @@ func TestRedirect(t *testing.T) {
req, _ = http.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
req.Header.Add("HX-Request", "true")
req.Header.Add("X-Gitea-Fetch-Action", "1")
b := NewBaseContextForTest(resp, req)
b.Redirect("/other")
assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
assert.Equal(t, http.StatusNoContent, resp.Code)
assert.Contains(t, resp.Header().Get("Content-Type"), "application/json")
assert.JSONEq(t, `{"redirect":"/other"}`, resp.Body.String())
assert.Equal(t, http.StatusOK, resp.Code)
}
@@ -8,14 +8,14 @@ import (
"image/color"
"sync"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/hcaptcha"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/mcaptcha"
"code.gitea.io/gitea/modules/recaptcha"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/turnstile"
"gitea.dev/modules/cache"
"gitea.dev/modules/hcaptcha"
"gitea.dev/modules/log"
"gitea.dev/modules/mcaptcha"
"gitea.dev/modules/recaptcha"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/turnstile"
"gitea.com/go-chi/captcha"
)
@@ -13,19 +13,19 @@ import (
"net/url"
"strings"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
web_types "code.gitea.io/gitea/modules/web/types"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/httpcache"
"gitea.dev/modules/reqctx"
"gitea.dev/modules/session"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/translation"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/modules/web/middleware"
web_types "gitea.dev/modules/web/types"
)
// Render represents a template render
@@ -63,8 +63,6 @@ type Context struct {
Package *Package
}
type TemplateContext map[string]any
func init() {
web.RegisterResponseStatusProvider[*Base](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(BaseContextKey).(*Base)
@@ -106,6 +104,7 @@ func NewTemplateContextForWeb(ctx reqctx.RequestContext, req *http.Request, loca
tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx)
tmplCtx["MiscUtils"] = templates.NewMiscUtils(ctx)
tmplCtx["ActionsUtils"] = templates.NewActionsUtils(ctx)
tmplCtx["RootData"] = ctx.GetData()
tmplCtx["Consts"] = map[string]any{
"RepoUnitTypeCode": unit.TypeCode,
@@ -197,11 +196,7 @@ func Contexter() func(next http.Handler) http.Handler {
}
}
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
if setting.Security.XFrameOptions != "unset" {
ctx.Resp.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
}
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{})
ctx.Data["SystemConfig"] = setting.Config()
@@ -212,7 +207,6 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data["DisableStars"] = setting.Repository.DisableStars
ctx.Data["EnableActions"] = setting.Actions.Enabled && !unit.TypeActions.UnitGlobalDisabled()
ctx.Data["ManifestData"] = setting.ManifestData
ctx.Data["AllLangs"] = translation.AllLangs()
next.ServeHTTP(ctx.Resp, ctx.Req)
@@ -7,8 +7,8 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
"gitea.dev/modules/setting"
"gitea.dev/modules/web/middleware"
)
const CookieNameFlash = "gitea_flash"
@@ -16,14 +16,14 @@ import (
"syscall"
"time"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
user_model "gitea.dev/models/user"
"gitea.dev/modules/httplib"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
"gitea.dev/modules/web/middleware"
)
// RedirectToUser redirect to a differently-named user
@@ -5,30 +5,36 @@ package context
import (
"context"
"html"
"html/template"
"net/http"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/webtheme"
"gitea.dev/modules/htmlutil"
"gitea.dev/modules/httplib"
"gitea.dev/modules/public"
"gitea.dev/modules/reqctx"
"gitea.dev/modules/setting"
"gitea.dev/modules/web/middleware"
"gitea.dev/services/webtheme"
)
type TemplateContext map[string]any
var _ context.Context = TemplateContext(nil)
func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext {
func NewTemplateContext(ctx reqctx.RequestContext, req *http.Request) TemplateContext {
return TemplateContext{"_ctx": ctx, "_req": req}
}
func (c TemplateContext) req() *http.Request {
return c["_req"].(*http.Request)
return c["_req"].(*http.Request) //nolint:forcetypeassert // must exist
}
func (c TemplateContext) parentContext() context.Context {
return c["_ctx"].(context.Context)
func (c TemplateContext) parentContext() reqctx.RequestContext {
return c["_ctx"].(reqctx.RequestContext) //nolint:forcetypeassert // must exist
}
func (c TemplateContext) Deadline() (deadline time.Time, ok bool) {
@@ -73,7 +79,7 @@ func (c TemplateContext) CurrentWebBanner() *setting.WebBannerType {
return nil
}
// AppFullLink returns a full URL link with AppSubURL for the given app link (no AppSubURL)
// AppFullLink returns a full URL link with AppSubURL for the given app link
// If no link is given, it returns the current app full URL with sub-path but without trailing slash (that's why it is not named as AppURL)
func (c TemplateContext) AppFullLink(link ...string) template.URL {
s := httplib.GuessCurrentAppURL(c.parentContext())
@@ -83,3 +89,55 @@ func (c TemplateContext) AppFullLink(link ...string) template.URL {
}
return template.URL(s + "/" + strings.TrimPrefix(link[0], "/"))
}
func (c TemplateContext) ScriptImport(path string, typ ...string) template.HTML {
if len(typ) > 0 {
if typ[0] == "module" {
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" type="module" src="` + html.EscapeString(public.AssetURI(path)) + `"></script>`)
}
panic("unsupported script type: " + typ[0])
}
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" src="` + html.EscapeString(public.AssetURI(path)) + `"></script>`)
}
func (c TemplateContext) CspScriptNonce() (ret string) {
return CspScriptNonce(c.parentContext())
}
func WebContentSecurityPolicy(scriptNonce string) string {
if setting.Security.ContentSecurityPolicyGeneral == "unset" {
return "" // if site admin disables the general CSP, then we don't use it
}
// The CSP problem is more complicated than it looks.
// Gitea was designed to support various "customizations", including:
// * custom themes (custom CSS and JS)
// * custom assets URL (CDN)
// * custom plugins and external renders (e.g.: PlantUML render, and the renders might also load some JS/CSS assets)
// There is no easy way for end users to make the CSP "source" completely right.
//
// There can be 2 approaches in the future:
// A. Let end users to configure their reverse proxy to add CSP header
// * Browsers will merge and use the stricter rules between Gitea and reverse proxy
// B. Introduce some config options in "app.ini"
// * Maybe this approach should be avoided, don't make the config system too complex, just let users use A
// allow all by default (the same as old releases with no CSP)
// * maybe some images or markup (external) renders need "data:", need to investigate
// * avatar upload editor needs "blob:", at least "img-src" and "content-src"
return `default-src * data: blob:;` +
// enforce nonce for all scripts, disallow inline scripts
`script-src * 'nonce-` + scriptNonce + `';` +
// it seems that Vue needs the unsafe-inline, and our custom colors (e.g.: label) also need it
`style-src * 'unsafe-inline';`
}
func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
scriptNonce := c.CspScriptNonce()
csp := WebContentSecurityPolicy(scriptNonce)
if csp == "" {
return ""
}
return htmlutil.HTMLFormat(`<meta http-equiv="Content-Security-Policy" content="%s">`, csp)
}
@@ -9,8 +9,9 @@ import (
"net/url"
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"gitea.dev/modules/reqctx"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
@@ -57,7 +58,7 @@ func TestAppFullLink(t *testing.T) {
defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLNever)()
req := httptest.NewRequest(http.MethodGet, "https://gitea.example.com/sub/", nil)
tmplCtx := NewTemplateContext(req.Context(), req)
tmplCtx := NewTemplateContext(reqctx.NewRequestContextForTest(req.Context()), req)
assert.Equal(t, "https://gitea.example.com/sub", string(tmplCtx.AppFullLink()))
assert.Equal(t, "https://gitea.example.com/sub/user/repo", string(tmplCtx.AppFullLink("user/repo")))
+68 -39
View File
@@ -7,14 +7,14 @@ package context
import (
"strings"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/markup"
"gitea.dev/modules/markup/markdown"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
)
// Organization contains organization context
@@ -174,36 +174,33 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
}
// Team.
shouldSeeAllTeams, err := UserShouldSeeAllOrgTeams(ctx)
if err != nil {
ctx.ServerError("UserShouldSeeAllOrgTeams", err)
return
}
switch {
case shouldSeeAllTeams:
ctx.Org.Teams, err = org.LoadTeams(ctx)
if err != nil {
ctx.ServerError("LoadTeams", err)
return
}
case ctx.IsSigned:
// Signed-in non-members still see teams whose visibility tier
// includes them (public for any signed-in user, plus limited
// for org members), and any team they directly belong to.
ctx.Org.Teams, _, err = organization.SearchTeam(ctx, &organization.SearchTeamOptions{
OrgID: org.ID,
UserID: ctx.Doer.ID,
IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(ctx.Org.IsMember, true),
})
if err != nil {
ctx.ServerError("SearchTeam", err)
return
}
}
if ctx.Org.IsMember {
shouldSeeAllTeams := false
if ctx.Org.IsOwner {
shouldSeeAllTeams = true
} else {
teams, err := org.GetUserTeams(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserTeams", err)
return
}
for _, team := range teams {
if team.IncludesAllRepositories && team.HasAdminAccess() {
shouldSeeAllTeams = true
break
}
}
}
if shouldSeeAllTeams {
ctx.Org.Teams, err = org.LoadTeams(ctx)
if err != nil {
ctx.ServerError("LoadTeams", err)
return
}
} else {
ctx.Org.Teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserTeams", err)
return
}
}
ctx.Data["NumTeams"] = len(ctx.Org.Teams)
}
@@ -214,7 +211,6 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
if strings.EqualFold(team.LowerName, teamName) {
teamExists = true
ctx.Org.Team = team
ctx.Org.IsTeamMember = true
ctx.Data["Team"] = ctx.Org.Team
break
}
@@ -225,13 +221,24 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
return
}
// Membership in a visible team is not implied by its presence in
// ctx.Org.Teams; admins/org owners keep the privileged flag set
// earlier in this function.
if !ctx.Org.IsOwner {
ctx.Org.IsTeamMember, err = organization.IsTeamMember(ctx, org.ID, ctx.Org.Team.ID, ctx.Doer.ID)
if err != nil {
ctx.ServerError("IsTeamMember", err)
return
}
}
ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember
if opts.RequireTeamMember && !ctx.Org.IsTeamMember {
ctx.NotFound(err)
return
}
ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.HasAdminAccess()
isTeamOwnerOrAdmin := ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.HasAdminAccess()
ctx.Org.IsTeamAdmin = ctx.Org.IsOwner || (ctx.Org.IsTeamMember && isTeamOwnerOrAdmin)
ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin
if opts.RequireTeamAdmin && !ctx.Org.IsTeamAdmin {
ctx.NotFound(err)
@@ -255,3 +262,25 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
}
}
}
// UserShouldSeeAllOrgTeams tells if a user has permission to view all teams in the org.
func UserShouldSeeAllOrgTeams(ctx *Context) (bool, error) {
if !ctx.Org.IsMember {
return false, nil
}
if ctx.Org.IsOwner {
return true, nil
}
teams, err := ctx.Org.Organization.GetUserTeams(ctx, ctx.Doer.ID)
if err != nil {
return false, err
}
for _, team := range teams {
if team.IncludesAllRepositories && team.HasAdminAccess() {
return true, nil
}
}
return false, nil
}
@@ -8,14 +8,13 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"gitea.dev/models/organization"
packages_model "gitea.dev/models/packages"
"gitea.dev/models/perm"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
)
// Package contains owner, access mode and optional the package descriptor
@@ -34,11 +33,8 @@ type packageAssignmentCtx struct {
// PackageAssignment returns a middleware to handle Context.Package assignment
func PackageAssignment() func(ctx *Context) {
return func(ctx *Context) {
errorFn := func(status int, obj any) {
err, ok := obj.(error)
if !ok {
err = fmt.Errorf("%s", obj)
}
errorFn := func(status int, msg string) {
err := fmt.Errorf("%s", msg)
if status == http.StatusNotFound {
ctx.NotFound(err)
} else {
@@ -58,11 +54,11 @@ func PackageAssignmentAPI() func(ctx *APIContext) {
}
}
func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package {
func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string)) *Package {
pkgOwner := ctx.ContextUser
accessMode, err := determineAccessMode(ctx.Base, pkgOwner, ctx.Doer)
if err != nil {
errCb(http.StatusInternalServerError, fmt.Errorf("determineAccessMode: %w", err))
errCb(http.StatusInternalServerError, fmt.Sprintf("determineAccessMode: %v", err))
return nil
}
@@ -81,25 +77,25 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) {
errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err))
errCb(http.StatusNotFound, fmt.Sprintf("GetVersionByNameAndVersion: %v", err))
} else {
errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err))
errCb(http.StatusInternalServerError, fmt.Sprintf("GetVersionByNameAndVersion: %v", err))
}
return pkg
}
pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageDescriptor: %w", err))
errCb(http.StatusInternalServerError, fmt.Sprintf("GetPackageDescriptor: %v", err))
return pkg
}
} else {
p, err := packages_model.GetPackageByName(ctx, pkg.Owner.ID, packages_model.Type(packageType), name)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) {
errCb(http.StatusNotFound, fmt.Errorf("GetPackageByName: %w", err))
errCb(http.StatusNotFound, fmt.Sprintf("GetPackageByName: %v", err))
} else {
errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageByName: %w", err))
errCb(http.StatusInternalServerError, fmt.Sprintf("GetPackageByName: %v", err))
}
return pkg
}
@@ -158,10 +154,10 @@ func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.Acces
// 1. Check if user is package owner
if doer.ID == pkgOwner.ID {
accessMode = perm.AccessModeOwner
} else if pkgOwner.Visibility == structs.VisibleTypePublic || pkgOwner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
} else if pkgOwner.Visibility.IsPublic() || (pkgOwner.Visibility.IsLimited() && !doer.IsRestricted) { // 2. Check if package owner is visible to the doer
accessMode = perm.AccessModeRead
}
} else if pkgOwner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
} else if pkgOwner.Visibility.IsPublic() { // 3. Check if package owner is public
accessMode = perm.AccessModeRead
}
}
@@ -0,0 +1,26 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"testing"
"gitea.dev/models/perm"
"gitea.dev/models/user"
"gitea.dev/modules/structs"
"github.com/stretchr/testify/assert"
)
func TestDeterminePackageAccessModeForLimitedOwner(t *testing.T) {
owner := &user.User{ID: 1, Visibility: structs.VisibleTypeLimited}
accessMode, err := determineAccessMode(&Base{}, owner, &user.User{ID: 2, IsActive: true})
assert.NoError(t, err)
assert.Equal(t, perm.AccessModeRead, accessMode)
accessMode, err = determineAccessMode(&Base{}, owner, &user.User{ID: 3, IsActive: true, IsRestricted: true})
assert.NoError(t, err)
assert.Equal(t, perm.AccessModeNone, accessMode)
}
@@ -12,8 +12,8 @@ import (
"slices"
"strings"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/paginator"
"gitea.dev/modules/container"
"gitea.dev/modules/paginator"
)
// Pagination provides a pagination via paginator.Paginator and additional configurations for the link params used in rendering
@@ -33,8 +33,8 @@ func NewPagination(total int64, pagingNum, current, numPages int) *Pagination {
return p
}
func (p *Pagination) WithCurRows(n int) *Pagination {
p.Paginater.SetCurRows(n)
func (p *Pagination) WithUnlimitedPaging(curRows int, hasNext bool) *Pagination {
p.Paginater.SetUnlimitedPaging(curRows, hasNext)
return p
}
@@ -7,7 +7,7 @@ import (
"net/url"
"testing"
"code.gitea.io/gitea/modules/container"
"gitea.dev/modules/container"
"github.com/stretchr/testify/assert"
)
@@ -32,4 +32,24 @@ func TestPagination(t *testing.T) {
params.Del("foo")
v, _ = url.ParseQuery(string(p.GetParams()))
assert.Equal(t, params, v)
p = NewPagination(-1, 1, 1, 1)
p.WithUnlimitedPaging(0, false)
assert.Zero(t, p.Paginater.TotalPages())
assert.False(t, p.Paginater.HasNext())
p = NewPagination(-1, 1, 1, 1)
p.WithUnlimitedPaging(10, false)
assert.Equal(t, 1, p.Paginater.TotalPages()) // first page, no next, so it should know that the total page number is 1
assert.False(t, p.Paginater.HasNext())
p = NewPagination(-1, 1, 2, 1)
p.WithUnlimitedPaging(10, false)
assert.Equal(t, -1, p.Paginater.TotalPages())
assert.False(t, p.Paginater.HasNext())
p = NewPagination(-1, 1, 1, 1)
p.WithUnlimitedPaging(10, true)
assert.Equal(t, -1, p.Paginater.TotalPages())
assert.True(t, p.Paginater.HasNext())
}
@@ -4,14 +4,48 @@
package context
import (
"context"
"net/http"
"slices"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
auth_model "gitea.dev/models/auth"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
)
// isOwnerHidden reports whether repo's owner is not publicly visible (a limited or private owner), so
// the owner's repositories must be hidden from callers that may only reach genuinely public resources.
func isOwnerHidden(ctx context.Context, repo *repo_model.Repository) bool {
if err := repo.LoadOwner(ctx); err != nil || repo.Owner == nil {
return true // fail closed if the owner visibility can't be determined
}
return !repo.Owner.Visibility.IsPublic()
}
// publicOnlyTokenDeniedRepo reports whether a public-only API token must be denied access to
// repo. A public-only token may only reach genuinely public resources, so it is denied for
// private repos and for repos owned by a non-public (limited or private) owner.
func publicOnlyTokenDeniedRepo(ctx context.Context, repo *repo_model.Repository) bool {
if repo == nil {
return false
}
return repo.IsPrivate || isOwnerHidden(ctx, repo)
}
// TokenIsPublicOnly reports whether the request is authenticated by a public-only API token. A
// non-token request, or a token with no recorded scope, is not public-only.
func TokenIsPublicOnly(ctx *Context) bool {
if ctx.Data["IsApiToken"] != true {
return false
}
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if !ok {
return false
}
publicOnly, _ := scope.PublicOnly()
return publicOnly
}
// CheckTokenScopes checks whether the authenticated API token contains any of the given scopes.
func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_model.AccessTokenScope) {
if ctx.Data["IsApiToken"] != true {
@@ -29,7 +63,7 @@ func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_
return
}
if publicOnly && repo != nil && repo.IsPrivate {
if publicOnly && publicOnlyTokenDeniedRepo(ctx, repo) {
ctx.HTTPError(http.StatusForbidden)
return
}
@@ -48,7 +82,7 @@ func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_
// RequireRepoAdmin returns a middleware for requiring repository admin permission
func RequireRepoAdmin() func(ctx *Context) {
return func(ctx *Context) {
if !ctx.IsSigned || !ctx.Repo.IsAdmin() {
if !ctx.IsSigned || !ctx.Repo.Permission.IsAdmin() {
ctx.NotFound(nil)
return
}
@@ -68,7 +102,7 @@ func CanWriteToBranch() func(ctx *Context) {
// RequireUnitWriter returns a middleware for requiring repository write to one of the unit permission
func RequireUnitWriter(unitTypes ...unit.Type) func(ctx *Context) {
return func(ctx *Context) {
if slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite) {
if slices.ContainsFunc(unitTypes, ctx.Repo.Permission.CanWrite) {
return
}
ctx.NotFound(nil)
@@ -79,7 +113,7 @@ func RequireUnitWriter(unitTypes ...unit.Type) func(ctx *Context) {
func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
return func(ctx *Context) {
for _, unitType := range unitTypes {
if ctx.Repo.CanRead(unitType) {
if ctx.Repo.Permission.CanRead(unitType) {
return
}
if unitType == unit.TypeCode && canWriteAsMaintainer(ctx) {
@@ -8,10 +8,11 @@ import (
"net/http"
"time"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/web"
web_types "code.gitea.io/gitea/modules/web/types"
"gitea.dev/modules/graceful"
"gitea.dev/modules/private"
"gitea.dev/modules/process"
"gitea.dev/modules/web"
web_types "gitea.dev/modules/web/types"
)
// PrivateContext represents a context for private routes
@@ -49,6 +50,14 @@ func (ctx *PrivateContext) Err() error {
return ctx.Base.Err()
}
func (ctx *PrivateContext) PrivateError(status int, err error, userMsg string) {
errMsg := ""
if err != nil {
errMsg = err.Error()
}
ctx.JSON(status, private.Response{Err: errMsg, UserMsg: userMsg})
}
type privateContextKeyType struct{}
var privateContextKey privateContextKeyType
+135 -61
View File
@@ -14,25 +14,25 @@ import (
"path"
"strings"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httplib"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/httplib"
code_indexer "gitea.dev/modules/indexer/code"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
asymkey_service "gitea.dev/services/asymkey"
"github.com/editorconfig/editorconfig-core-go/v2"
)
@@ -58,16 +58,30 @@ func (prc *PullRequestContext) CanCreateNewPull() bool {
ctx := prc.ctx
// People who have push access or have forked repository can propose a new pull request.
can := prc.baseRepo.CanContentChange() &&
(ctx.Repo.CanWrite(unit_model.TypeCode) || (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)))
(ctx.Repo.Permission.CanWrite(unit_model.TypeCode) || (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)))
prc.canCreateNewPull = &can
return can
}
// CompareHeadRef formats the head side of a compare link, "owner/repo:branch" is only needed when a fork can share its base repo's owner
func CompareHeadRef(baseRepo, headRepo *repo_model.Repository, headBranch string) string {
if baseRepo.ID == headRepo.ID /* same repo */ {
return headBranch
} else if baseRepo.OwnerID == headRepo.OwnerID /* same owner */ {
return headRepo.FullName() + ":" + headBranch
}
// not the same owner: if there can be multiple forks in one owner, we still need the full name
if setting.Repository.AllowForkIntoSameOwner {
return headRepo.FullName() + ":" + headBranch
}
// if there is only one fork in the different owner, we only need the owner's name for the head ref
return headRepo.OwnerName + ":" + headBranch
}
func (prc *PullRequestContext) MakeDefaultCompareLink(headBranch string) string {
return prc.baseRepo.Link() + "/compare/" +
util.PathEscapeSegments(prc.DefaultTargetBranch()) + "..." +
util.Iif(prc.SameRepo(), "", util.PathEscapeSegments(prc.headRepo.OwnerName)+":") +
util.PathEscapeSegments(headBranch)
util.PathEscapeSegments(CompareHeadRef(prc.baseRepo, prc.headRepo, headBranch))
}
func (prc *PullRequestContext) DefaultTargetBranch() string {
@@ -81,7 +95,7 @@ func (prc *PullRequestContext) DefaultTargetBranch() string {
// Repository contains information to operate a repository
type Repository struct {
access_model.Permission
Permission access_model.Permission
Repository *repo_model.Repository
Owner *user_model.User
@@ -242,7 +256,7 @@ func (r *Repository) CanUseTimetracker(ctx context.Context, issue *issues_model.
// Checking for following:
// 1. Is timetracker enabled
// 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this?
isAssigned, _ := issues_model.IsUserAssignedToIssue(ctx, issue, user)
isAssigned, _ := issues_model.IsUserAssignedToIssue(ctx, issue, user.ID)
return r.Repository.IsTimetrackerEnabled(ctx) && (!r.Repository.AllowOnlyContributorsToTrackTime(ctx) ||
r.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
}
@@ -421,8 +435,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
}
func repoAssignment(ctx *Context, repo *repo_model.Repository) {
func repoAssignmentLegacy(ctx *Context, data *repoAssignmentPrepareDataStruct) {
var err error
repo := data.repo
if err = repo.LoadOwner(ctx); err != nil {
ctx.ServerError("LoadOwner", err)
return
@@ -469,13 +484,25 @@ func InitRepoPullRequestCtx(ctx *Context, base, head *repo_model.Repository) {
ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequestCtx
}
// RepoAssignment returns a middleware to handle repository assignment
func RepoAssignment(ctx *Context) {
type repoAssignmentPrepareDataStruct struct {
ownerName string
repoName string
repo *repo_model.Repository
}
func repoAssignmentPreCheck(ctx *Context) {
if ctx.Data["Repository"] != nil {
setting.PanicInDevOrTesting("RepoAssignment should not be executed twice")
}
if ctx.Repo.GitRepo != nil {
setting.PanicInDevOrTesting("RepoAssignment: GitRepo should be nil")
_ = ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = nil
}
}
var err error
func repoAssignmentPrepareData(ctx *Context) *repoAssignmentPrepareDataStruct {
// HINT: here it doesn't handle ".wiki" extension, it is handled in repoAssignmentAutoRedirectWiki, need to be refactored in the future
userName := ctx.PathParam("username")
repoName := ctx.PathParam("reponame")
repoName = strings.TrimSuffix(repoName, ".git")
@@ -484,7 +511,12 @@ func RepoAssignment(ctx *Context) {
repoName = strings.TrimSuffix(repoName, ".rss")
repoName = strings.TrimSuffix(repoName, ".atom")
}
return &repoAssignmentPrepareDataStruct{ownerName: userName, repoName: repoName}
}
func repoAssignmentPrepareOwner(ctx *Context, data *repoAssignmentPrepareDataStruct) {
var err error
userName := data.ownerName
// Check if the user is the same as the repository owner
if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, userName) {
ctx.Repo.Owner = ctx.Doer
@@ -514,7 +546,10 @@ func RepoAssignment(ctx *Context) {
}
ctx.ContextUser = ctx.Repo.Owner
ctx.Data["ContextUser"] = ctx.ContextUser
}
func repoAssignmentAutoRedirectWiki(ctx *Context, data *repoAssignmentPrepareDataStruct) {
userName, repoName := data.ownerName, data.repoName
// redirect link to wiki
if strings.HasSuffix(repoName, ".wiki") {
// ctx.Req.URL.Path does not have the preceding appSubURL - any redirect must have this added
@@ -534,7 +569,10 @@ func RepoAssignment(ctx *Context) {
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath))
return
}
}
func repoAssignmentPrepareRepo(ctx *Context, data *repoAssignmentPrepareDataStruct) {
repoName := data.repoName
// Get repository.
repo, err := repo_model.GetRepositoryByName(ctx, ctx.Repo.Owner.ID, repoName)
if err != nil {
@@ -557,12 +595,11 @@ func RepoAssignment(ctx *Context) {
return
}
repo.Owner = ctx.Repo.Owner
data.repo = repo
}
repoAssignment(ctx, repo)
if ctx.Written() {
return
}
func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepareDataStruct) {
repo := data.repo
ctx.Repo.RepoLink = repo.Link()
ctx.Data["RepoLink"] = ctx.Repo.RepoLink
ctx.Data["FeedURL"] = ctx.Repo.RepoLink
@@ -584,7 +621,7 @@ func RepoAssignment(ctx *Context) {
}
ctx.Data["NumReleases"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
IncludeDrafts: ctx.Repo.CanWrite(unit_model.TypeReleases),
IncludeDrafts: ctx.Repo.Permission.CanWrite(unit_model.TypeReleases),
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
@@ -596,10 +633,10 @@ func RepoAssignment(ctx *Context) {
ctx.Data["PageTitleCommon"] = repo.Name + " - " + setting.AppName
ctx.Data["Repository"] = repo
ctx.Data["Owner"] = ctx.Repo.Repository.Owner
ctx.Data["CanWriteCode"] = ctx.Repo.CanWrite(unit_model.TypeCode)
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues)
ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests)
ctx.Data["CanWriteActions"] = ctx.Repo.CanWrite(unit_model.TypeActions)
ctx.Data["CanWriteCode"] = ctx.Repo.Permission.CanWrite(unit_model.TypeCode)
ctx.Data["CanWriteIssues"] = ctx.Repo.Permission.CanWrite(unit_model.TypeIssues)
ctx.Data["CanWritePulls"] = ctx.Repo.Permission.CanWrite(unit_model.TypePullRequests)
ctx.Data["CanWriteActions"] = ctx.Repo.Permission.CanWrite(unit_model.TypeActions)
canSignedUserFork, err := repo_module.CanUserForkRepo(ctx, ctx.Doer, ctx.Repo.Repository)
if err != nil {
@@ -655,33 +692,38 @@ func RepoAssignment(ctx *Context) {
return
}
}
}
isHomeOrSettings := ctx.Link == ctx.Repo.RepoLink ||
ctx.Link == ctx.Repo.RepoLink+"/settings" ||
strings.HasPrefix(ctx.Link, ctx.Repo.RepoLink+"/settings/") ||
ctx.Link == ctx.Repo.RepoLink+"/-/migrate/status"
func repoAssignmentIsHomeOrSettings(ctx *Context, data *repoAssignmentPrepareDataStruct) bool {
repoLink := data.repo.Link()
return ctx.Link == repoLink ||
strings.HasPrefix(ctx.Link+"/", repoLink+"/settings/") ||
ctx.Link == repoLink+"/-/migrate/status"
}
func repoAssignmentAutoRedirectNotReady(ctx *Context, data *repoAssignmentPrepareDataStruct) {
// Disable everything when the repo is being created
if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() {
if !isHomeOrSettings {
if !repoAssignmentIsHomeOrSettings(ctx, data) {
ctx.Redirect(ctx.Repo.RepoLink)
}
return
}
}
if ctx.Repo.GitRepo != nil {
setting.PanicInDevOrTesting("RepoAssignment: GitRepo should be nil")
_ = ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = nil
}
func repoAssignmentPrepareGitRepo(ctx *Context, data *repoAssignmentPrepareDataStruct) {
var err error
repo := data.repo
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") {
if ctx.Repo.Repository.IsBeingCreated() {
return
}
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RelativePath(), err)
ctx.Repo.Repository.MarkAsBrokenEmpty()
// Only allow access to base of repo or settings
if !isHomeOrSettings {
if !repoAssignmentIsHomeOrSettings(ctx, data) {
ctx.Redirect(ctx.Repo.RepoLink)
}
return
@@ -689,12 +731,12 @@ func RepoAssignment(ctx *Context) {
ctx.ServerError("RepoAssignment Invalid repo "+repo.FullName(), err)
return
}
}
// Stop at this point when the repo is empty.
if ctx.Repo.Repository.IsEmpty {
func repoAssignmentPrepareBranches(ctx *Context, data *repoAssignmentPrepareDataStruct) {
if data.repo.IsEmpty {
return
}
branchOpts := git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: optional.Some(false),
@@ -716,7 +758,13 @@ func RepoAssignment(ctx *Context) {
}
ctx.Data["BranchesCount"] = branchesTotal
}
func repoAssignmentPreparePullRequests(ctx *Context, data *repoAssignmentPrepareDataStruct) {
repo := data.repo
if repo.IsEmpty {
return
}
// Pull request is allowed if this is a fork repository, and base repository accepts pull requests.
if repo.BaseRepo != nil && repo.BaseRepo.AllowsPulls(ctx) {
// TODO: this (and below) "BaseRepo" var is not clear and should be removed in the future
@@ -727,7 +775,9 @@ func RepoAssignment(ctx *Context) {
ctx.Data["BaseRepo"] = repo
InitRepoPullRequestCtx(ctx, repo, repo)
}
}
func repoAssignmentPrepareRepoTransfer(ctx *Context, data *repoAssignmentPrepareDataStruct) {
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
@@ -736,16 +786,17 @@ func RepoAssignment(ctx *Context) {
}
if err := repoTransfer.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadRecipient", err)
ctx.ServerError("LoadAttributes", err)
return
}
ctx.Data["RepoTransfer"] = repoTransfer
if ctx.Doer != nil {
ctx.Data["CanUserAcceptOrRejectTransfer"] = repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer)
}
ctx.Data["CanUserAcceptOrRejectTransfer"] = ctx.Doer != nil && repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer)
}
}
func repoAssignmentHandleGoGet(ctx *Context, data *repoAssignmentPrepareDataStruct) {
repo := data.repo
if ctx.FormString("go-get") == "1" {
ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, repo.Owner.Name, repo.Name)
fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
@@ -754,6 +805,32 @@ func RepoAssignment(ctx *Context) {
}
}
// RepoAssignment returns a middleware to handle repository assignment
func RepoAssignment(ctx *Context) {
repoAssignmentPreCheck(ctx)
prepareData := repoAssignmentPrepareData(ctx)
funcs := []func(ctx *Context, data *repoAssignmentPrepareDataStruct){
repoAssignmentPrepareOwner,
repoAssignmentAutoRedirectWiki,
repoAssignmentPrepareRepo,
repoAssignmentLegacy,
repoAssignmentPrepareTemplateData,
repoAssignmentAutoRedirectNotReady,
repoAssignmentPrepareGitRepo,
repoAssignmentPrepareRepoTransfer,
repoAssignmentPrepareBranches,
repoAssignmentPreparePullRequests,
repoAssignmentHandleGoGet,
}
for _, f := range funcs {
f(ctx, prepareData)
if ctx.Written() {
return
}
}
}
const headRefName = "HEAD"
func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool) string {
@@ -912,12 +989,9 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName)
if err == nil {
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") {
} else {
// if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users
log.Error("GetBranchCommit: %v", err)
} else {
ctx.ServerError("GetBranchCommit", err)
return
}
} else { // there is a path in request
guessLegacyPath := refType == ""
@@ -0,0 +1,28 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
func TestCompareHeadRef(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner, false)()
baseRepo := &repo_model.Repository{ID: 1, OwnerID: 100, OwnerName: "base-owner", Name: "base-repo"}
sameRepo := baseRepo
sameOwner := &repo_model.Repository{ID: 2, OwnerID: 100, OwnerName: "head-owner", Name: "head-repo"}
diffOwner := &repo_model.Repository{ID: 2, OwnerID: 101, OwnerName: "head-owner", Name: "head-repo"}
assert.Equal(t, "my-branch", CompareHeadRef(baseRepo, sameRepo, "my-branch"))
assert.Equal(t, "head-owner/head-repo:my-branch", CompareHeadRef(baseRepo, sameOwner, "my-branch"))
assert.Equal(t, "head-owner:my-branch", CompareHeadRef(baseRepo, diffOwner, "my-branch"))
setting.Repository.AllowForkIntoSameOwner = true
assert.Equal(t, "head-owner/head-repo:my-branch", CompareHeadRef(baseRepo, diffOwner, "my-branch"))
}
@@ -8,7 +8,7 @@ import (
"net"
"net/http"
web_types "code.gitea.io/gitea/modules/web/types"
web_types "gitea.dev/modules/web/types"
)
// ResponseWriter represents a response writer for HTTP
@@ -11,11 +11,11 @@ import (
"regexp"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/modules/reqctx"
"gitea.dev/modules/setting"
"gitea.dev/services/context"
)
// ErrFileTypeForbidden not allowed file type error
+7 -31
View File
@@ -8,17 +8,14 @@ import (
"net/http"
"strings"
user_model "code.gitea.io/gitea/models/user"
user_model "gitea.dev/models/user"
)
// UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
func UserAssignmentWeb() func(ctx *Context) {
return func(ctx *Context) {
errorFn := func(status int, obj any) {
err, ok := obj.(error)
if !ok {
err = fmt.Errorf("%s", obj)
}
errorFn := func(status int, msg string) {
err := fmt.Errorf("%s", msg)
if status == http.StatusNotFound {
ctx.NotFound(err)
} else {
@@ -30,27 +27,6 @@ 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")
if ctx.IsSigned && ctx.Doer.ID == userID {
ctx.ContextUser = ctx.Doer
} else {
var err error
ctx.ContextUser, err = user_model.GetUserByID(ctx, userID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
}
}
}
}
// UserAssignmentAPI returns a middleware to handle context-user assignment for api routes
func UserAssignmentAPI() func(ctx *APIContext) {
return func(ctx *APIContext) {
@@ -58,7 +34,7 @@ func UserAssignmentAPI() func(ctx *APIContext) {
}
}
func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (contextUser *user_model.User) {
func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string)) (contextUser *user_model.User) {
username := ctx.PathParam("username")
if doer != nil && strings.EqualFold(doer.LowerName, username) {
@@ -71,12 +47,12 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (con
if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil {
RedirectToUser(ctx, doer, username, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
errCb(http.StatusNotFound, err)
errCb(http.StatusNotFound, err.Error())
} else {
errCb(http.StatusInternalServerError, fmt.Errorf("LookupUserRedirect: %w", err))
errCb(http.StatusInternalServerError, fmt.Sprintf("LookupUserRedirect: %v", err))
}
} else {
errCb(http.StatusInternalServerError, fmt.Errorf("GetUserByName: %w", err))
errCb(http.StatusInternalServerError, fmt.Sprintf("GetUserByName: %v", err))
}
}
}