This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user