diff options
Diffstat (limited to 'services/context')
-rw-r--r-- | services/context/access_log.go | 106 | ||||
-rw-r--r-- | services/context/access_log_test.go | 71 | ||||
-rw-r--r-- | services/context/api.go | 140 | ||||
-rw-r--r-- | services/context/api_test.go | 2 | ||||
-rw-r--r-- | services/context/base.go | 11 | ||||
-rw-r--r-- | services/context/base_form.go | 4 | ||||
-rw-r--r-- | services/context/base_test.go | 4 | ||||
-rw-r--r-- | services/context/context.go | 60 | ||||
-rw-r--r-- | services/context/context_response.go | 8 | ||||
-rw-r--r-- | services/context/org.go | 332 | ||||
-rw-r--r-- | services/context/package.go | 24 | ||||
-rw-r--r-- | services/context/pagination.go | 6 | ||||
-rw-r--r-- | services/context/permission.go | 19 | ||||
-rw-r--r-- | services/context/private.go | 12 | ||||
-rw-r--r-- | services/context/repo.go | 232 | ||||
-rw-r--r-- | services/context/response.go | 36 | ||||
-rw-r--r-- | services/context/upload/upload.go | 28 | ||||
-rw-r--r-- | services/context/user.go | 20 |
18 files changed, 627 insertions, 488 deletions
diff --git a/services/context/access_log.go b/services/context/access_log.go index 0926748ac5..caade113a7 100644 --- a/services/context/access_log.go +++ b/services/context/access_log.go @@ -5,7 +5,6 @@ package context import ( "bytes" - "fmt" "net" "net/http" "strings" @@ -18,13 +17,14 @@ import ( "code.gitea.io/gitea/modules/web/middleware" ) -type routerLoggerOptions struct { - req *http.Request +type accessLoggerTmplData struct { Identity *string Start *time.Time - ResponseWriter http.ResponseWriter - Ctx map[string]any - RequestID *string + ResponseWriter struct { + Status, Size int + } + Ctx map[string]any + RequestID *string } const keyOfRequestIDInTemplate = ".RequestID" @@ -46,56 +46,70 @@ func parseRequestIDFromRequestHeader(req *http.Request) string { } } if len(requestID) > maxRequestIDByteLength { - requestID = fmt.Sprintf("%s...", requestID[:maxRequestIDByteLength]) + requestID = requestID[:maxRequestIDByteLength] + "..." } return requestID } +type accessLogRecorder struct { + logger log.BaseLogger + logTemplate *template.Template + needRequestID bool +} + +func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) { + var requestID string + if lr.needRequestID { + requestID = parseRequestIDFromRequestHeader(req) + } + + reqHost, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + reqHost = req.RemoteAddr + } + + identity := "-" + data := middleware.GetContextData(req.Context()) + if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + identity = signedUser.Name + } + buf := bytes.NewBuffer([]byte{}) + tmplData := accessLoggerTmplData{ + Identity: &identity, + Start: &start, + Ctx: map[string]any{ + "RemoteAddr": req.RemoteAddr, + "RemoteHost": reqHost, + "Req": req, + }, + RequestID: &requestID, + } + tmplData.ResponseWriter.Status = respWriter.WrittenStatus() + tmplData.ResponseWriter.Size = respWriter.WrittenSize() + err = lr.logTemplate.Execute(buf, tmplData) + if err != nil { + log.Error("Could not execute access logger template: %v", err.Error()) + } + + lr.logger.Log(1, &log.Event{Level: log.INFO}, "%s", buf.String()) +} + +func newAccessLogRecorder() *accessLogRecorder { + return &accessLogRecorder{ + logger: log.GetLogger("access"), + logTemplate: template.Must(template.New("log").Parse(setting.Log.AccessLogTemplate)), + needRequestID: len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate), + } +} + // AccessLogger returns a middleware to log access logger func AccessLogger() func(http.Handler) http.Handler { - logger := log.GetLogger("access") - needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate) - logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate) + recorder := newAccessLogRecorder() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { start := time.Now() - - var requestID string - if needRequestID { - requestID = parseRequestIDFromRequestHeader(req) - } - - reqHost, _, err := net.SplitHostPort(req.RemoteAddr) - if err != nil { - reqHost = req.RemoteAddr - } - next.ServeHTTP(w, req) - rw := w.(ResponseWriter) - - identity := "-" - data := middleware.GetContextData(req.Context()) - if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { - identity = signedUser.Name - } - buf := bytes.NewBuffer([]byte{}) - err = logTemplate.Execute(buf, routerLoggerOptions{ - req: req, - Identity: &identity, - Start: &start, - ResponseWriter: rw, - Ctx: map[string]any{ - "RemoteAddr": req.RemoteAddr, - "RemoteHost": reqHost, - "Req": req, - }, - RequestID: &requestID, - }) - if err != nil { - log.Error("Could not execute access logger template: %v", err.Error()) - } - - logger.Info("%s", buf.String()) + recorder.record(start, w.(ResponseWriter), req) }) } } diff --git a/services/context/access_log_test.go b/services/context/access_log_test.go new file mode 100644 index 0000000000..139a6eb217 --- /dev/null +++ b/services/context/access_log_test.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +type testAccessLoggerMock struct { + logs []string +} + +func (t *testAccessLoggerMock) Log(skip int, event *log.Event, format string, v ...any) { + t.logs = append(t.logs, fmt.Sprintf(format, v...)) +} + +func (t *testAccessLoggerMock) GetLevel() log.Level { + return log.INFO +} + +type testAccessLoggerResponseWriterMock struct{} + +func (t testAccessLoggerResponseWriterMock) Header() http.Header { + return nil +} + +func (t testAccessLoggerResponseWriterMock) Before(f func(ResponseWriter)) {} + +func (t testAccessLoggerResponseWriterMock) WriteHeader(statusCode int) {} + +func (t testAccessLoggerResponseWriterMock) Write(bytes []byte) (int, error) { + return 0, nil +} + +func (t testAccessLoggerResponseWriterMock) Flush() {} + +func (t testAccessLoggerResponseWriterMock) WrittenStatus() int { + return http.StatusOK +} + +func (t testAccessLoggerResponseWriterMock) WrittenSize() int { + return 123123 +} + +func TestAccessLogger(t *testing.T) { + setting.Log.AccessLogTemplate = `{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"` + recorder := newAccessLogRecorder() + mockLogger := &testAccessLoggerMock{} + recorder.logger = mockLogger + req := &http.Request{ + RemoteAddr: "remote-addr", + Method: http.MethodGet, + Proto: "https", + URL: &url.URL{Path: "/path"}, + } + req.Header = http.Header{} + req.Header.Add("Referer", "referer") + req.Header.Add("User-Agent", "user-agent") + recorder.record(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), &testAccessLoggerResponseWriterMock{}, req) + assert.Equal(t, []string{`remote-addr - - [02/Jan/2000:03:04:05 +0000] "GET /path https" 200 123123 "referer" "user-agent"`}, mockLogger.logs) +} diff --git a/services/context/api.go b/services/context/api.go index 3a3cbe670e..ab50a360f4 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -5,23 +5,31 @@ package context import ( + "errors" "fmt" "net/http" "net/url" + "slices" + "strconv" "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/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" ) // APIContext is a specific context for API service +// ATTENTION: This struct should never be manually constructed in routes/services, +// it has many internal details which should be carefully prepared by the framework. +// If it is abused, it would cause strange bugs like panic/resource-leak. type APIContext struct { *Base @@ -103,14 +111,28 @@ type APIRepoArchivedError struct { APIError } -// ServerError responds with error message, status is 500 -func (ctx *APIContext) ServerError(title string, err error) { - ctx.Error(http.StatusInternalServerError, title, err) +// APIErrorInternal responds with error message, status is 500 +func (ctx *APIContext) APIErrorInternal(err error) { + ctx.apiErrorInternal(1, err) } -// Error responds with an error message to client with given obj as the message. +func (ctx *APIContext) apiErrorInternal(skip int, err error) { + log.ErrorWithSkip(skip+1, "InternalServerError: %v", err) + + var message string + if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) { + message = err.Error() + } + + ctx.JSON(http.StatusInternalServerError, APIError{ + Message: message, + URL: setting.API.SwaggerURL, + }) +} + +// 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) Error(status int, title string, obj any) { +func (ctx *APIContext) APIError(status int, obj any) { var message string if err, ok := obj.(error); ok { message = err.Error() @@ -119,7 +141,7 @@ func (ctx *APIContext) Error(status int, title string, obj any) { } if status == http.StatusInternalServerError { - log.ErrorWithSkip(1, "%s: %s", title, message) + log.ErrorWithSkip(1, "APIError: %s", message) if setting.IsProd && !(ctx.Doer != nil && ctx.Doer.IsAdmin) { message = "" @@ -132,22 +154,6 @@ func (ctx *APIContext) Error(status int, title string, obj any) { }) } -// InternalServerError responds with an error message to the client with the error as a message -// and the file and line of the caller. -func (ctx *APIContext) InternalServerError(err error) { - log.ErrorWithSkip(1, "InternalServerError: %v", err) - - var message string - if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) { - message = err.Error() - } - - ctx.JSON(http.StatusInternalServerError, APIError{ - Message: message, - URL: setting.API.SwaggerURL, - }) -} - type apiContextKeyType struct{} var apiContextKey = apiContextKeyType{} @@ -165,7 +171,7 @@ func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string { if paginater.HasNext() { u := *curURL queries := u.Query() - queries.Set("page", fmt.Sprintf("%d", paginater.Next())) + queries.Set("page", strconv.Itoa(paginater.Next())) u.RawQuery = queries.Encode() links = append(links, fmt.Sprintf("<%s%s>; rel=\"next\"", setting.AppURL, u.RequestURI()[1:])) @@ -173,7 +179,7 @@ func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string { if !paginater.IsLast() { u := *curURL queries := u.Query() - queries.Set("page", fmt.Sprintf("%d", paginater.TotalPages())) + queries.Set("page", strconv.Itoa(paginater.TotalPages())) u.RawQuery = queries.Encode() links = append(links, fmt.Sprintf("<%s%s>; rel=\"last\"", setting.AppURL, u.RequestURI()[1:])) @@ -189,7 +195,7 @@ func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string { if paginater.HasPrevious() { u := *curURL queries := u.Query() - queries.Set("page", fmt.Sprintf("%d", paginater.Previous())) + queries.Set("page", strconv.Itoa(paginater.Previous())) u.RawQuery = queries.Encode() links = append(links, fmt.Sprintf("<%s%s>; rel=\"prev\"", setting.AppURL, u.RequestURI()[1:])) @@ -207,7 +213,7 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) { } } -// APIContexter returns apicontext as middleware +// APIContexter returns APIContext middleware func APIContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { @@ -222,14 +228,14 @@ func APIContexter() func(http.Handler) http.Handler { ctx.SetContextValue(apiContextKey, ctx) // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. - if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { + if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } } - httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform") + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true}) ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) next.ServeHTTP(ctx.Resp, ctx.Req) @@ -237,11 +243,11 @@ func APIContexter() func(http.Handler) http.Handler { } } -// NotFound handles 404s for APIContext +// APIErrorNotFound handles 404s for APIContext // String will replace message, errors will be added to a slice -func (ctx *APIContext) NotFound(objs ...any) { - message := ctx.Locale.TrString("error.not_found") - var errors []string +func (ctx *APIContext) APIErrorNotFound(objs ...any) { + var message string + var errs []string for _, obj := range objs { // Ignore nil if obj == nil { @@ -249,16 +255,15 @@ func (ctx *APIContext) NotFound(objs ...any) { } if err, ok := obj.(error); ok { - errors = append(errors, err.Error()) + errs = append(errs, err.Error()) } else { message = obj.(string) } } - ctx.JSON(http.StatusNotFound, map[string]any{ - "message": message, + "message": util.IfZero(message, "not found"), // do not use locale in API "url": setting.API.SwaggerURL, - "errors": errors, + "errors": errs, }) } @@ -276,7 +281,7 @@ func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) { var err error ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) + ctx.APIErrorInternal(err) return } } @@ -288,40 +293,33 @@ func RepoRefForAPI(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx := GetAPIContext(req) - if ctx.Repo.GitRepo == nil { - ctx.InternalServerError(fmt.Errorf("no open git repo")) + if ctx.Repo.Repository.IsEmpty { + ctx.APIErrorNotFound("repository is empty") return } - refName, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.PathParam("*"), ctx.FormTrim("ref")) - var err error + if ctx.Repo.GitRepo == nil { + panic("no GitRepo, forgot to call the middleware?") // it is a programming error + } - if ctx.Repo.GitRepo.IsBranchExist(refName) { + refName, refType, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.PathParam("*"), ctx.FormTrim("ref")) + var err error + switch refType { + case git.RefTypeBranch: ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) - if err != nil { - ctx.InternalServerError(err) - return - } - ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if ctx.Repo.GitRepo.IsTagExist(refName) { + case git.RefTypeTag: ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName) - if err != nil { - ctx.InternalServerError(err) - return - } - ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if len(refName) == ctx.Repo.GetObjectFormat().FullLength() { - ctx.Repo.CommitID = refName + case git.RefTypeCommit: ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) - if err != nil { - ctx.NotFound("GetCommit", err) - return - } - } else { - ctx.NotFound(fmt.Errorf("not exist: '%s'", ctx.PathParam("*"))) + } + if ctx.Repo.Commit == nil || errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound("unable to find a git ref") + return + } else if err != nil { + ctx.APIErrorInternal(err) return } - + ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() next.ServeHTTP(w, req) }) } @@ -347,12 +345,12 @@ func (ctx *APIContext) GetErrMsg() string { // 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(logMsg string, errCheck func(error) bool, logErr error) { - if errCheck(logErr) { +func (ctx *APIContext) NotFoundOrServerError(err error) { + if errors.Is(err, util.ErrNotExist) { ctx.JSON(http.StatusNotFound, nil) return } - ctx.Error(http.StatusInternalServerError, "NotFoundOrServerError", logMsg) + ctx.APIErrorInternal(err) } // IsUserSiteAdmin returns true if current user is a site admin @@ -365,13 +363,7 @@ func (ctx *APIContext) IsUserRepoAdmin() bool { return ctx.Repo.IsAdmin() } -// IsUserRepoWriter returns true if current user has write privilege in current repo +// IsUserRepoWriter returns true if current user has "write" privilege in current repo func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool { - for _, unitType := range unitTypes { - if ctx.Repo.CanWrite(unitType) { - return true - } - } - - return false + return slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite) } diff --git a/services/context/api_test.go b/services/context/api_test.go index 911a49949e..87d74004db 100644 --- a/services/context/api_test.go +++ b/services/context/api_test.go @@ -45,6 +45,6 @@ func TestGenAPILinks(t *testing.T) { links := genAPILinks(u, 100, 20, curPage) - assert.EqualValues(t, links, response) + assert.Equal(t, links, response) } } diff --git a/services/context/base.go b/services/context/base.go index 5db84f42a5..f3f92b7eeb 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -8,6 +8,7 @@ import ( "html/template" "io" "net/http" + "strconv" "strings" "code.gitea.io/gitea/modules/httplib" @@ -23,6 +24,10 @@ type BaseContextKeyType struct{} var BaseContextKey BaseContextKeyType +// Base is the base context for all web handlers +// ATTENTION: This struct should never be manually constructed in routes/services, +// it has many internal details which should be carefully prepared by the framework. +// If it is abused, it would cause strange bugs like panic/resource-leak. type Base struct { reqctx.RequestContext @@ -49,7 +54,7 @@ func (b *Base) AppendAccessControlExposeHeaders(names ...string) { // SetTotalCountHeader set "X-Total-Count" header func (b *Base) SetTotalCountHeader(total int64) { - b.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) + b.RespHeader().Set("X-Total-Count", strconv.FormatInt(total, 10)) b.AppendAccessControlExposeHeaders("X-Total-Count") } @@ -77,8 +82,8 @@ func (b *Base) RespHeader() http.Header { return b.Resp.Header() } -// Error returned an error to web browser -func (b *Base) Error(status int, contents ...string) { +// HTTPError returned an error to web browser +func (b *Base) HTTPError(status int, contents ...string) { v := http.StatusText(status) if len(contents) > 0 { v = contents[0] diff --git a/services/context/base_form.go b/services/context/base_form.go index 5b8cae9e99..81fd7cd328 100644 --- a/services/context/base_form.go +++ b/services/context/base_form.go @@ -12,6 +12,8 @@ import ( ) // FormString returns the first value matching the provided key in the form as a string +// It works the same as http.Request.FormValue: +// try urlencoded request body first, then query string, then multipart form body func (b *Base) FormString(key string, def ...string) string { s := b.Req.FormValue(key) if s == "" { @@ -20,7 +22,7 @@ func (b *Base) FormString(key string, def ...string) string { return s } -// FormStrings returns a string slice for the provided key from the form +// FormStrings returns a values for the key in the form (including query parameters), similar to FormString func (b *Base) FormStrings(key string) []string { if b.Req.Form == nil { if err := b.Req.ParseMultipartForm(32 << 20); err != nil { diff --git a/services/context/base_test.go b/services/context/base_test.go index b936b76f58..2a4f86dddf 100644 --- a/services/context/base_test.go +++ b/services/context/base_test.go @@ -15,7 +15,7 @@ import ( func TestRedirect(t *testing.T) { setting.IsInTesting = true - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) cases := []struct { url string @@ -36,7 +36,7 @@ func TestRedirect(t *testing.T) { assert.Equal(t, c.keep, has, "url = %q", c.url) } - req, _ = http.NewRequest("GET", "/", nil) + req, _ = http.NewRequest(http.MethodGet, "/", nil) resp := httptest.NewRecorder() req.Header.Add("HX-Request", "true") b := NewBaseContextForTest(resp, req) diff --git a/services/context/context.go b/services/context/context.go index 6715c5663d..32ec260aab 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -23,6 +23,7 @@ import ( "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" @@ -34,7 +35,10 @@ type Render interface { HTML(w io.Writer, status int, name templates.TplName, data any, templateCtx context.Context) error } -// Context represents context of a request. +// Context represents context of a web request. +// ATTENTION: This struct should never be manually constructed in routes/services, +// it has many internal details which should be carefully prepared by the framework. +// If it is abused, it would cause strange bugs like panic/resource-leak. type Context struct { *Base @@ -76,9 +80,9 @@ type webContextKeyType struct{} var WebContextKey = webContextKeyType{} -func GetWebContext(req *http.Request) *Context { - ctx, _ := req.Context().Value(WebContextKey).(*Context) - return ctx +func GetWebContext(ctx context.Context) *Context { + webCtx, _ := ctx.Value(WebContextKey).(*Context) + return webCtx } // ValidateContext is a special context for form validation middleware. It may be different from other contexts. @@ -132,6 +136,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { } ctx.TemplateContext = NewTemplateContextForWeb(ctx) ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} + ctx.SetContextValue(WebContextKey, ctx) return ctx } @@ -162,21 +167,12 @@ func Contexter() func(next http.Handler) http.Handler { ctx.PageData = map[string]any{} ctx.Data["PageData"] = ctx.PageData - ctx.Base.SetContextValue(WebContextKey, ctx) ctx.Csrf = NewCSRFProtector(csrfOpts) - // Get the last flash message from cookie - lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash) + // get the last flash message from cookie + lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash) if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 { - // store last Flash message into the template data, to render it - ctx.Data["Flash"] = &middleware.Flash{ - DataStore: ctx, - Values: vals, - ErrorMsg: vals.Get("error"), - SuccessMsg: vals.Get("success"), - InfoMsg: vals.Get("info"), - WarningMsg: vals.Get("warning"), - } + ctx.Data["Flash"] = lastFlashMsg // store last Flash message into the template data, to render it } // if there are new messages in the ctx.Flash, write them into cookie @@ -189,18 +185,20 @@ func Contexter() func(next http.Handler) http.Handler { }) // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. - if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { + if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size ctx.ServerError("ParseMultipartForm", err) return } } - httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform") + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true}) ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) ctx.Data["SystemConfig"] = setting.Config() + ctx.Data["ShowTwoFactorRequiredMessage"] = ctx.DoerNeedTwoFactorAuth() + // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["DisableStars"] = setting.Repository.DisableStars @@ -214,17 +212,27 @@ func Contexter() func(next http.Handler) http.Handler { } } +func (ctx *Context) DoerNeedTwoFactorAuth() bool { + if !setting.TwoFactorAuthEnforced { + return false + } + return ctx.Session.Get(session.KeyUserHasTwoFactorAuth) == false +} + // HasError returns true if error occurs in form validation. // Attention: this function changes ctx.Data and ctx.Flash // If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again. func (ctx *Context) HasError() bool { - hasErr, ok := ctx.Data["HasError"] - if !ok { + hasErr, _ := ctx.Data["HasError"].(bool) + hasErr = hasErr || ctx.Flash.ErrorMsg != "" + if !hasErr { return false } - ctx.Flash.ErrorMsg = ctx.GetErrMsg() + if ctx.Flash.ErrorMsg == "" { + ctx.Flash.ErrorMsg = ctx.GetErrMsg() + } ctx.Data["Flash"] = ctx.Flash - return hasErr.(bool) + return hasErr } // GetErrMsg returns error message in form validation. @@ -254,3 +262,11 @@ func (ctx *Context) JSONError(msg any) { panic(fmt.Sprintf("unsupported type: %T", msg)) } } + +func (ctx *Context) JSONErrorNotFound(optMsg ...string) { + msg := util.OptionalArg(optMsg) + if msg == "" { + msg = ctx.Locale.TrString("error.not_found") + } + ctx.JSON(http.StatusNotFound, map[string]any{"errorMessage": msg, "renderFormat": "text"}) +} diff --git a/services/context/context_response.go b/services/context/context_response.go index c7044791eb..4e11e29b69 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -28,7 +28,7 @@ import ( func RedirectToUser(ctx *Base, userName string, redirectUserID int64) { user, err := user_model.GetUserByID(ctx, redirectUserID) if err != nil { - ctx.Error(http.StatusInternalServerError, "unable to get user") + ctx.HTTPError(http.StatusInternalServerError, "unable to get user") return } @@ -122,8 +122,8 @@ func (ctx *Context) RenderWithErr(msg any, tpl templates.TplName, form any) { } // NotFound displays a 404 (Not Found) page and prints the given error, if any. -func (ctx *Context) NotFound(logMsg string, logErr error) { - ctx.notFoundInternal(logMsg, logErr) +func (ctx *Context) NotFound(logErr error) { + ctx.notFoundInternal("", logErr) } func (ctx *Context) notFoundInternal(logMsg string, logErr error) { @@ -150,7 +150,7 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) { ctx.Data["IsRepo"] = ctx.Repo.Repository != nil ctx.Data["Title"] = "Page Not Found" - ctx.HTML(http.StatusNotFound, templates.TplName("status/404")) + ctx.HTML(http.StatusNotFound, "status/404") } // ServerError displays a 500 (Internal Server Error) page and prints the given error, if any. diff --git a/services/context/org.go b/services/context/org.go index be87cef7a3..c8b6ed09b7 100644 --- a/services/context/org.go +++ b/services/context/org.go @@ -51,7 +51,7 @@ func GetOrganizationByParams(ctx *Context) { if err == nil { RedirectToUser(ctx.Base, orgName, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.NotFound("GetUserByName", err) + ctx.NotFound(err) } else { ctx.ServerError("LookupUserRedirect", err) } @@ -62,215 +62,193 @@ func GetOrganizationByParams(ctx *Context) { } } -// HandleOrgAssignment handles organization assignment -func HandleOrgAssignment(ctx *Context, args ...bool) { - var ( - requireMember bool - requireOwner bool - requireTeamMember bool - requireTeamAdmin bool - ) - if len(args) >= 1 { - requireMember = args[0] - } - if len(args) >= 2 { - requireOwner = args[1] - } - if len(args) >= 3 { - requireTeamMember = args[2] - } - if len(args) >= 4 { - requireTeamAdmin = args[3] - } - - var err error +type OrgAssignmentOptions struct { + RequireMember bool + RequireOwner bool + RequireTeamMember bool + RequireTeamAdmin bool +} - if ctx.ContextUser == nil { - // if Organization is not defined, get it from params - if ctx.Org.Organization == nil { - GetOrganizationByParams(ctx) - if ctx.Written() { - return +// OrgAssignment returns a middleware to handle organization assignment +func OrgAssignment(opts OrgAssignmentOptions) func(ctx *Context) { + return func(ctx *Context) { + var err error + if ctx.ContextUser == nil { + // if Organization is not defined, get it from params + if ctx.Org.Organization == nil { + GetOrganizationByParams(ctx) + if ctx.Written() { + return + } } + } else if ctx.ContextUser.IsOrganization() { + ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) + } else { + // ContextUser is an individual User + return } - } else if ctx.ContextUser.IsOrganization() { - if ctx.Org == nil { - ctx.Org = &Organization{} - } - ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) - } else { - // ContextUser is an individual User - return - } - org := ctx.Org.Organization + org := ctx.Org.Organization - // Handle Visibility - if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned { - // We must be signed in to see limited or private organizations - ctx.NotFound("OrgAssignment", err) - return - } - - if org.Visibility == structs.VisibleTypePrivate { - requireMember = true - } else if ctx.IsSigned && ctx.Doer.IsRestricted { - requireMember = true - } - - ctx.ContextUser = org.AsUser() - ctx.Data["Org"] = org - - // Admin has super access. - if ctx.IsSigned && ctx.Doer.IsAdmin { - ctx.Org.IsOwner = true - ctx.Org.IsMember = true - ctx.Org.IsTeamMember = true - ctx.Org.IsTeamAdmin = true - ctx.Org.CanCreateOrgRepo = true - } else if ctx.IsSigned { - ctx.Org.IsOwner, err = org.IsOwnedBy(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("IsOwnedBy", err) + // Handle Visibility + if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned { + // We must be signed in to see limited or private organizations + ctx.NotFound(err) return } - if ctx.Org.IsOwner { + if org.Visibility == structs.VisibleTypePrivate { + opts.RequireMember = true + } else if ctx.IsSigned && ctx.Doer.IsRestricted { + opts.RequireMember = true + } + + ctx.ContextUser = org.AsUser() + ctx.Data["Org"] = org + + // Admin has super access. + if ctx.IsSigned && ctx.Doer.IsAdmin { + ctx.Org.IsOwner = true ctx.Org.IsMember = true ctx.Org.IsTeamMember = true ctx.Org.IsTeamAdmin = true ctx.Org.CanCreateOrgRepo = true - } else { - ctx.Org.IsMember, err = org.IsOrgMember(ctx, ctx.Doer.ID) + } else if ctx.IsSigned { + ctx.Org.IsOwner, err = org.IsOwnedBy(ctx, ctx.Doer.ID) if err != nil { - ctx.ServerError("IsOrgMember", err) + ctx.ServerError("IsOwnedBy", err) return } - ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("CanCreateOrgRepo", err) - return + + if ctx.Org.IsOwner { + ctx.Org.IsMember = true + ctx.Org.IsTeamMember = true + ctx.Org.IsTeamAdmin = true + ctx.Org.CanCreateOrgRepo = true + } else { + ctx.Org.IsMember, err = org.IsOrgMember(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("IsOrgMember", err) + return + } + ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("CanCreateOrgRepo", err) + return + } } + } else { + // Fake data. + ctx.Data["SignedUser"] = &user_model.User{} } - } else { - // Fake data. - ctx.Data["SignedUser"] = &user_model.User{} - } - if (requireMember && !ctx.Org.IsMember) || - (requireOwner && !ctx.Org.IsOwner) { - ctx.NotFound("OrgAssignment", err) - return - } - ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner - ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember - ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - ctx.Data["IsPublicMember"] = func(uid int64) bool { - is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid) - return is - } - ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo + if (opts.RequireMember && !ctx.Org.IsMember) || (opts.RequireOwner && !ctx.Org.IsOwner) { + ctx.NotFound(err) + return + } + ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner + ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember + ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + ctx.Data["IsPublicMember"] = func(uid int64) bool { + is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid) + return is + } + ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo - ctx.Org.OrgLink = org.AsUser().OrganisationLink() - ctx.Data["OrgLink"] = ctx.Org.OrgLink + ctx.Org.OrgLink = org.AsUser().OrganisationLink() + ctx.Data["OrgLink"] = ctx.Org.OrgLink - // Member - opts := &organization.FindOrgMembersOpts{ - Doer: ctx.Doer, - OrgID: org.ID, - IsDoerMember: ctx.Org.IsMember, - } - ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, opts) - if err != nil { - ctx.ServerError("CountOrgMembers", err) - return - } + // Member + findMembersOpts := &organization.FindOrgMembersOpts{ + Doer: ctx.Doer, + OrgID: org.ID, + IsDoerMember: ctx.Org.IsMember, + } + ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, findMembersOpts) + if err != nil { + ctx.ServerError("CountOrgMembers", err) + return + } - // Team. - 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 + // Team. + 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 + } + } } - for _, team := range teams { - if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin { - 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) } - if shouldSeeAllTeams { - ctx.Org.Teams, err = org.LoadTeams(ctx) - if err != nil { - ctx.ServerError("LoadTeams", err) - return + + teamName := ctx.PathParam("team") + if len(teamName) > 0 { + teamExists := false + for _, team := range ctx.Org.Teams { + if team.LowerName == strings.ToLower(teamName) { + teamExists = true + ctx.Org.Team = team + ctx.Org.IsTeamMember = true + ctx.Data["Team"] = ctx.Org.Team + break + } } - } else { - ctx.Org.Teams, err = org.GetUserTeams(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("GetUserTeams", err) + + if !teamExists { + ctx.NotFound(err) return } - } - ctx.Data["NumTeams"] = len(ctx.Org.Teams) - } - teamName := ctx.PathParam("team") - if len(teamName) > 0 { - teamExists := false - for _, team := range ctx.Org.Teams { - if team.LowerName == strings.ToLower(teamName) { - teamExists = true - ctx.Org.Team = team - ctx.Org.IsTeamMember = true - ctx.Data["Team"] = ctx.Org.Team - break + ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember + if opts.RequireTeamMember && !ctx.Org.IsTeamMember { + ctx.NotFound(err) + return } - } - - if !teamExists { - ctx.NotFound("OrgAssignment", err) - return - } - ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember - if requireTeamMember && !ctx.Org.IsTeamMember { - ctx.NotFound("OrgAssignment", err) - return + ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.HasAdminAccess() + ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin + if opts.RequireTeamAdmin && !ctx.Org.IsTeamAdmin { + ctx.NotFound(err) + return + } } + ctx.Data["ContextUser"] = ctx.ContextUser - ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.AccessMode >= perm.AccessModeAdmin - ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin - if requireTeamAdmin && !ctx.Org.IsTeamAdmin { - ctx.NotFound("OrgAssignment", err) - return - } - } - ctx.Data["ContextUser"] = ctx.ContextUser + ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) + ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) + ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode) - ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) - ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) - ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode) - - ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) - if len(ctx.ContextUser.Description) != 0 { - content, err := markdown.RenderString(markup.NewRenderContext(ctx), ctx.ContextUser.Description) - if err != nil { - ctx.ServerError("RenderString", err) - return + ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + if len(ctx.ContextUser.Description) != 0 { + content, err := markdown.RenderString(markup.NewRenderContext(ctx), ctx.ContextUser.Description) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + ctx.Data["RenderedDescription"] = content } - ctx.Data["RenderedDescription"] = content - } -} - -// OrgAssignment returns a middleware to handle organization assignment -func OrgAssignment(args ...bool) func(ctx *Context) { - return func(ctx *Context) { - HandleOrgAssignment(ctx, args...) } } diff --git a/services/context/package.go b/services/context/package.go index e98e01acbb..8b722932b1 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -33,15 +33,15 @@ 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, title string, obj any) { + errorFn := func(status int, obj any) { err, ok := obj.(error) if !ok { err = fmt.Errorf("%s", obj) } if status == http.StatusNotFound { - ctx.NotFound(title, err) + ctx.NotFound(err) } else { - ctx.ServerError(title, err) + ctx.ServerError("PackageAssignment", err) } } paCtx := &packageAssignmentCtx{Base: ctx.Base, Doer: ctx.Doer, ContextUser: ctx.ContextUser} @@ -53,18 +53,18 @@ func PackageAssignment() func(ctx *Context) { func PackageAssignmentAPI() func(ctx *APIContext) { return func(ctx *APIContext) { paCtx := &packageAssignmentCtx{Base: ctx.Base, Doer: ctx.Doer, ContextUser: ctx.ContextUser} - ctx.Package = packageAssignment(paCtx, ctx.Error) + ctx.Package = packageAssignment(paCtx, ctx.APIError) } } -func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any)) *Package { +func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package { pkg := &Package{ Owner: ctx.ContextUser, } var err error pkg.AccessMode, err = determineAccessMode(ctx.Base, pkg, ctx.Doer) if err != nil { - errCb(http.StatusInternalServerError, "determineAccessMode", err) + errCb(http.StatusInternalServerError, fmt.Errorf("determineAccessMode: %w", err)) return pkg } @@ -75,16 +75,16 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any)) pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version) if err != nil { if err == packages_model.ErrPackageNotExist { - errCb(http.StatusNotFound, "GetVersionByNameAndVersion", err) + errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) } else { - errCb(http.StatusInternalServerError, "GetVersionByNameAndVersion", err) + errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) } return pkg } pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv) if err != nil { - errCb(http.StatusInternalServerError, "GetPackageDescriptor", err) + errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageDescriptor: %w", err)) return pkg } } @@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any)) } func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) { - if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) { + if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) { return perm.AccessModeNone, nil } @@ -154,9 +154,9 @@ func PackageContexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { base := NewBaseContext(resp, req) - // it is still needed when rendering 500 page in a package handler + // FIXME: web Context is still needed when rendering 500 page in a package handler + // It should be refactored to use new error handling mechanisms ctx := NewWebContext(base, renderer, nil) - ctx.SetContextValue(WebContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/services/context/pagination.go b/services/context/pagination.go index d33dd217d0..25a9298e01 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -21,12 +21,18 @@ type Pagination struct { // NewPagination creates a new instance of the Pagination struct. // "pagingNum" is "page size" or "limit", "current" is "page" +// total=-1 means only showing prev/next func NewPagination(total, pagingNum, current, numPages int) *Pagination { p := &Pagination{} p.Paginater = paginator.New(total, pagingNum, current, numPages) return p } +func (p *Pagination) WithCurRows(n int) *Pagination { + p.Paginater.SetCurRows(n) + return p +} + func (p *Pagination) AddParamFromRequest(req *http.Request) { for key, values := range req.URL.Query() { if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") { diff --git a/services/context/permission.go b/services/context/permission.go index 0d69ccc4a4..c0a5a98724 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -5,6 +5,7 @@ package context import ( "net/http" + "slices" auth_model "code.gitea.io/gitea/models/auth" repo_model "code.gitea.io/gitea/models/repo" @@ -15,7 +16,7 @@ import ( func RequireRepoAdmin() func(ctx *Context) { return func(ctx *Context) { if !ctx.IsSigned || !ctx.Repo.IsAdmin() { - ctx.NotFound("RequireRepoAdmin denies the request", nil) + ctx.NotFound(nil) return } } @@ -25,7 +26,7 @@ func RequireRepoAdmin() func(ctx *Context) { func CanWriteToBranch() func(ctx *Context) { return func(ctx *Context) { if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.NotFound("CanWriteToBranch denies permission", nil) + ctx.NotFound(nil) return } } @@ -34,12 +35,10 @@ 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) { - for _, unitType := range unitTypes { - if ctx.Repo.CanWrite(unitType) { - return - } + if slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite) { + return } - ctx.NotFound("RequireUnitWriter denies the request", nil) + ctx.NotFound(nil) } } @@ -54,7 +53,7 @@ func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) { return } } - ctx.NotFound("RequireUnitReader denies the request", nil) + ctx.NotFound(nil) } } @@ -78,7 +77,7 @@ func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_ } if publicOnly && repo.IsPrivate { - ctx.Error(http.StatusForbidden) + ctx.HTTPError(http.StatusForbidden) return } @@ -89,7 +88,7 @@ func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_ } if !scopeMatched { - ctx.Error(http.StatusForbidden) + ctx.HTTPError(http.StatusForbidden) return } } diff --git a/services/context/private.go b/services/context/private.go index 51857da8fe..d20e49f588 100644 --- a/services/context/private.go +++ b/services/context/private.go @@ -5,7 +5,6 @@ package context import ( "context" - "fmt" "net/http" "time" @@ -29,7 +28,6 @@ func init() { }) } -// Deadline is part of the interface for context.Context and we pass this to the request context func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) { if ctx.Override != nil { return ctx.Override.Deadline() @@ -37,7 +35,6 @@ func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) { return ctx.Base.Deadline() } -// Done is part of the interface for context.Context and we pass this to the request context func (ctx *PrivateContext) Done() <-chan struct{} { if ctx.Override != nil { return ctx.Override.Done() @@ -45,7 +42,6 @@ func (ctx *PrivateContext) Done() <-chan struct{} { return ctx.Base.Done() } -// Err is part of the interface for context.Context and we pass this to the request context func (ctx *PrivateContext) Err() error { if ctx.Override != nil { return ctx.Override.Err() @@ -53,14 +49,14 @@ func (ctx *PrivateContext) Err() error { return ctx.Base.Err() } -var privateContextKey any = "default_private_context" +type privateContextKeyType struct{} + +var privateContextKey privateContextKeyType -// GetPrivateContext returns a context for Private routes func GetPrivateContext(req *http.Request) *PrivateContext { return req.Context().Value(privateContextKey).(*PrivateContext) } -// PrivateContexter returns apicontext as middleware func PrivateContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { @@ -82,7 +78,7 @@ func OverrideContext() func(http.Handler) http.Handler { // We now need to override the request context as the base for our work because even if the request is cancelled we have to continue this work ctx := GetPrivateContext(req) var finished func() - ctx.Override, _, finished = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true) + ctx.Override, _, finished = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "PrivateContext: "+ctx.Req.RequestURI, process.RequestProcessType, true) defer finished() next.ServeHTTP(ctx.Resp, ctx.Req) }) diff --git a/services/context/repo.go b/services/context/repo.go index 6cd70d139b..572211712b 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user) } -// CanEnableEditor returns true if repository is editable and user has proper access level. -func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool { - return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived -} - // CanCreateBranch returns true if repository is editable and user has proper access level. func (r *Repository) CanCreateBranch() bool { return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch() @@ -89,63 +84,105 @@ func (r *Repository) GetObjectFormat() git.ObjectFormat { func RepoMustNotBeArchived() func(ctx *Context) { return func(ctx *Context) { if ctx.Repo.Repository.IsArchived { - ctx.NotFound("IsArchived", errors.New(ctx.Locale.TrString("repo.archive.title"))) + ctx.NotFound(errors.New(ctx.Locale.TrString("repo.archive.title"))) } } } -// CanCommitToBranchResults represents the results of CanCommitToBranch -type CanCommitToBranchResults struct { - CanCommitToBranch bool - EditorEnabled bool - UserCanPush bool - RequireSigned bool - WillSign bool - SigningKey string - WontSignReason string +type CommitFormOptions struct { + NeedFork bool + + TargetRepo *repo_model.Repository + TargetFormAction string + WillSubmitToFork bool + CanCommitToBranch bool + UserCanPush bool + RequireSigned bool + WillSign bool + SigningKey *git.SigningKey + WontSignReason string + CanCreatePullRequest bool + CanCreateBasePullRequest bool } -// CanCommitToBranch returns true if repository is editable and user has proper access level -// -// and branch is not protected for push -func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) { - protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName) +func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) { + if !refName.IsBranch() { + // it shouldn't happen because middleware already checks + return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName) + } + + originRepo := targetRepo + branchName := refName.ShortName() + // TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does + if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) { + targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID) + if targetRepo == nil { + return &CommitFormOptions{NeedFork: true}, nil + } + // now, we get our own forked repo; it must be writable by us. + } + submitToForkedRepo := targetRepo.ID != originRepo.ID + err := targetRepo.GetBaseRepo(ctx) if err != nil { - return CanCommitToBranchResults{}, err + return nil, err } - userCanPush := true - requireSigned := false + + protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName) + if err != nil { + return nil, err + } + canPushWithProtection := true + protectionRequireSigned := false if protectedBranch != nil { - protectedBranch.Repo = r.Repository - userCanPush = protectedBranch.CanUserPush(ctx, doer) - requireSigned = protectedBranch.RequireSignedCommits + protectedBranch.Repo = targetRepo + canPushWithProtection = protectedBranch.CanUserPush(ctx, doer) + protectionRequireSigned = protectedBranch.RequireSignedCommits } - sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) - - canCommit := r.CanEnableEditor(ctx, doer) && userCanPush - if requireSigned { - canCommit = canCommit && sign - } + willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String()) wontSignReason := "" - if err != nil { - if asymkey_service.IsErrWontSign(err) { - wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) - err = nil - } else { - wontSignReason = "error" - } + if asymkey_service.IsErrWontSign(err) { + wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) + } else if err != nil { + return nil, err + } + + canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection + if protectionRequireSigned { + canCommitToBranch = canCommitToBranch && willSign } - return CanCommitToBranchResults{ - CanCommitToBranch: canCommit, - EditorEnabled: r.CanEnableEditor(ctx, doer), - UserCanPush: userCanPush, - RequireSigned: requireSigned, - WillSign: sign, - SigningKey: keyID, + canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) + canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest + + opts := &CommitFormOptions{ + TargetRepo: targetRepo, + WillSubmitToFork: submitToForkedRepo, + CanCommitToBranch: canCommitToBranch, + UserCanPush: canPushWithProtection, + RequireSigned: protectionRequireSigned, + WillSign: willSign, + SigningKey: signKeyID, WontSignReason: wontSignReason, - }, err + + CanCreatePullRequest: canCreatePullRequest, + CanCreateBasePullRequest: canCreateBasePullRequest, + } + editorAction := ctx.PathParam("editor_action") + editorPathParamRemaining := util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + if submitToForkedRepo { + // there is only "default branch" in forked repo, we will use "from_base_branch" to get a new branch from base repo + editorPathParamRemaining = util.PathEscapeSegments(targetRepo.DefaultBranch) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + "?from_base_branch=" + url.QueryEscape(branchName) + } + if editorAction == "_cherrypick" { + opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + ctx.PathParam("sha") + "/" + editorPathParamRemaining + } else { + opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + editorPathParamRemaining + } + if ctx.Req.URL.RawQuery != "" { + opts.TargetFormAction += util.Iif(strings.Contains(opts.TargetFormAction, "?"), "&", "?") + ctx.Req.URL.RawQuery + } + return opts, nil } // CanUseTimetracker returns whether a user can use the timetracker. @@ -315,7 +352,7 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) { repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID) if err != nil { log.Error("GetRepositoryByID: %v", err) - ctx.Error(http.StatusInternalServerError, "GetRepositoryByID") + ctx.HTTPError(http.StatusInternalServerError, "GetRepositoryByID") return } @@ -328,7 +365,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) { if ctx.Req.URL.RawQuery != "" { redirectPath += "?" + ctx.Req.URL.RawQuery } - ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect) + // Git client needs a 301 redirect by default to follow the new location + // It's not documentated in git documentation, but it's the behavior of git client + ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently) } func repoAssignment(ctx *Context, repo *repo_model.Repository) { @@ -338,18 +377,22 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { return } - ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return + if ctx.DoerNeedTwoFactorAuth() { + ctx.Repo.Permission = access_model.PermissionNoAccess() + } else { + ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } } - if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() && !canWriteAsMaintainer(ctx) { + if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) return } - ctx.NotFound("no access right", nil) + ctx.NotFound(nil) return } ctx.Data["Permission"] = &ctx.Repo.Permission @@ -402,7 +445,7 @@ func RepoAssignment(ctx *Context) { if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { RedirectToUser(ctx.Base, userName, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.NotFound("GetUserByName", nil) + ctx.NotFound(nil) } else { ctx.ServerError("LookupUserRedirect", err) } @@ -447,7 +490,7 @@ func RepoAssignment(ctx *Context) { EarlyResponseForGoGetMeta(ctx) return } - ctx.NotFound("GetRepositoryByName", nil) + ctx.NotFound(nil) } else { ctx.ServerError("LookupRepoRedirect", err) } @@ -653,7 +696,7 @@ func RepoAssignment(ctx *Context) { ctx.Data["RepoTransfer"] = repoTransfer if ctx.Doer != nil { - ctx.Data["CanUserAcceptTransfer"] = repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) + ctx.Data["CanUserAcceptOrRejectTransfer"] = repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer) } } @@ -667,12 +710,6 @@ func RepoAssignment(ctx *Context) { const headRefName = "HEAD" -func RepoRef() func(*Context) { - // old code does: return RepoRefByType(git.RefTypeBranch) - // in most cases, it is an abuse, so we just disable it completely and fix the abuses one by one (if there is anything wrong) - return nil -} - func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool) string { refName := "" parts := strings.Split(path, "/") @@ -686,24 +723,24 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool return "" } -func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (string, git.RefType) { +func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (refName string, refType git.RefType, fallbackDefaultBranch bool) { reqRefPath := path.Join(extraRef, reqPath) reqRefPathParts := strings.Split(reqRefPath, "/") if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeBranch); refName != "" { - return refName, git.RefTypeBranch + return refName, git.RefTypeBranch, false } if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeTag); refName != "" { - return refName, git.RefTypeTag + return refName, git.RefTypeTag, false } if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), reqRefPathParts[0]) { // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists repo.TreePath = strings.Join(reqRefPathParts[1:], "/") - return reqRefPathParts[0], git.RefTypeCommit + return reqRefPathParts[0], git.RefTypeCommit, false } // FIXME: the old code falls back to default branch if "ref" doesn't exist, there could be an edge case: // "README?ref=no-such" would read the README file from the default branch, but the user might expect a 404 repo.TreePath = reqPath - return repo.Repository.DefaultBranch, git.RefTypeBranch + return repo.Repository.DefaultBranch, git.RefTypeBranch, true } func getRefName(ctx *Base, repo *Repository, path string, refType git.RefType) string { @@ -795,6 +832,9 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { return func(ctx *Context) { var err error refType := detectRefType + if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() { + return // no git repo, so do nothing, users will see a "migrating" UI provided by "migrate/migrating.tmpl", or empty repo guide + } // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty { // assume the user is viewing the (non-existent) default branch @@ -811,10 +851,10 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { reqPath := ctx.PathParam("*") if reqPath == "" { refShortName = ctx.Repo.Repository.DefaultBranch - if !ctx.Repo.GitRepo.IsBranchExist(refShortName) { - brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 1) + if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, refShortName) { + brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 1) if err == nil && len(brs) != 0 { - refShortName = brs[0].Name + refShortName = brs[0] } else if len(brs) == 0 { log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) } else { @@ -835,8 +875,9 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { } } else { // there is a path in request guessLegacyPath := refType == "" + fallbackDefaultBranch := false if guessLegacyPath { - refShortName, refType = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "") + refShortName, refType, fallbackDefaultBranch = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "") } else { refShortName = getRefName(ctx.Base, ctx.Repo, reqPath, refType) } @@ -850,7 +891,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { return } - if refType == git.RefTypeBranch && ctx.Repo.GitRepo.IsBranchExist(refShortName) { + if refType == git.RefTypeBranch && gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, refShortName) { ctx.Repo.BranchName = refShortName ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName) @@ -860,13 +901,13 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if refType == git.RefTypeTag && ctx.Repo.GitRepo.IsTagExist(refShortName) { + } else if refType == git.RefTypeTag && gitrepo.IsTagExist(ctx, ctx.Repo.Repository, refShortName) { ctx.Repo.RefFullName = git.RefNameFromTag(refShortName) ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refShortName) if err != nil { if git.IsErrNotExist(err) { - ctx.NotFound("GetTagCommit", err) + ctx.NotFound(err) return } ctx.ServerError("GetTagCommit", err) @@ -879,7 +920,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refShortName) if err != nil { - ctx.NotFound("GetCommit", err) + ctx.NotFound(err) return } // If short commit ID add canonical link header @@ -888,18 +929,30 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL)) } } else { - ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refShortName)) + ctx.NotFound(fmt.Errorf("branch or tag not exist: %s", refShortName)) return } if guessLegacyPath { // redirect from old URL scheme to new URL scheme - prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.PathParam("*"))), strings.ToLower(ctx.Repo.RepoLink)) - redirect := path.Join( - ctx.Repo.RepoLink, - util.PathEscapeSegments(prefix), - ctx.Repo.RefTypeNameSubURL(), - util.PathEscapeSegments(ctx.Repo.TreePath)) + // * /user2/repo1/commits/master => /user2/repo1/commits/branch/master + // * /user2/repo1/src/master => /user2/repo1/src/branch/master + // * /user2/repo1/src/README.md => /user2/repo1/src/branch/master/README.md (fallback to default branch) + var redirect string + refSubPath := "src" + // remove the "/subpath/owner/repo/" prefix, the names are case-insensitive + remainingLowerPath, cut := strings.CutPrefix(setting.AppSubURL+strings.ToLower(ctx.Req.URL.Path), strings.ToLower(ctx.Repo.RepoLink)+"/") + if cut { + refSubPath, _, _ = strings.Cut(remainingLowerPath, "/") // it could be "src" or "commits" + } + if fallbackDefaultBranch { + redirect = fmt.Sprintf("%s/%s/%s/%s/%s", ctx.Repo.RepoLink, refSubPath, refType, util.PathEscapeSegments(refShortName), ctx.PathParamRaw("*")) + } else { + redirect = fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, refSubPath, refType, ctx.PathParamRaw("*")) + } + if ctx.Req.URL.RawQuery != "" { + redirect += "?" + ctx.Req.URL.RawQuery + } ctx.Redirect(redirect) return } @@ -920,6 +973,15 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { ctx.ServerError("GetCommitsCount", err) return } + if ctx.Repo.RefFullName.IsTag() { + rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Repo.RefFullName.TagName()) + if err == nil && rel.NumCommits <= 0 { + rel.NumCommits = ctx.Repo.CommitsCount + if err := repo_model.UpdateReleaseNumCommits(ctx, rel); err != nil { + log.Error("UpdateReleaseNumCommits", err) + } + } + } ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) } @@ -929,7 +991,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { func GitHookService() func(ctx *Context) { return func(ctx *Context) { if !ctx.Doer.CanEditGitHook() { - ctx.NotFound("GitHookService", nil) + ctx.NotFound(nil) return } } diff --git a/services/context/response.go b/services/context/response.go index 2f271f211b..c7368ebc6f 100644 --- a/services/context/response.go +++ b/services/context/response.go @@ -11,31 +11,29 @@ import ( // ResponseWriter represents a response writer for HTTP type ResponseWriter interface { - http.ResponseWriter - http.Flusher - web_types.ResponseStatusProvider - - Before(func(ResponseWriter)) + http.ResponseWriter // provides Header/Write/WriteHeader + http.Flusher // provides Flush + web_types.ResponseStatusProvider // provides WrittenStatus - Status() int // used by access logger template - Size() int // used by access logger template + Before(fn func(ResponseWriter)) + WrittenSize() int } -var _ ResponseWriter = &Response{} +var _ ResponseWriter = (*Response)(nil) // Response represents a response type Response struct { http.ResponseWriter written int status int - befores []func(ResponseWriter) + beforeFuncs []func(ResponseWriter) beforeExecuted bool } // Write writes bytes to HTTP endpoint func (r *Response) Write(bs []byte) (int, error) { if !r.beforeExecuted { - for _, before := range r.befores { + for _, before := range r.beforeFuncs { before(r) } r.beforeExecuted = true @@ -51,18 +49,14 @@ func (r *Response) Write(bs []byte) (int, error) { return size, nil } -func (r *Response) Status() int { - return r.status -} - -func (r *Response) Size() int { +func (r *Response) WrittenSize() int { return r.written } // WriteHeader write status code func (r *Response) WriteHeader(statusCode int) { if !r.beforeExecuted { - for _, before := range r.befores { + for _, before := range r.beforeFuncs { before(r) } r.beforeExecuted = true @@ -87,17 +81,13 @@ func (r *Response) WrittenStatus() int { // Before allows for a function to be called before the ResponseWriter has been written to. This is // useful for setting headers or any other operations that must happen before a response has been written. -func (r *Response) Before(f func(ResponseWriter)) { - r.befores = append(r.befores, f) +func (r *Response) Before(fn func(ResponseWriter)) { + r.beforeFuncs = append(r.beforeFuncs, fn) } func WrapResponseWriter(resp http.ResponseWriter) *Response { if v, ok := resp.(*Response); ok { return v } - return &Response{ - ResponseWriter: resp, - status: 0, - befores: make([]func(ResponseWriter), 0), - } + return &Response{ResponseWriter: resp} } diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index da4370a433..23707950d4 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -11,7 +11,9 @@ 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" ) @@ -39,7 +41,7 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error { allowedTypesStr = strings.ReplaceAll(allowedTypesStr, "|", ",") // compat for old config format allowedTypes := []string{} - for _, entry := range strings.Split(allowedTypesStr, ",") { + for entry := range strings.SplitSeq(allowedTypesStr, ",") { entry = strings.ToLower(strings.TrimSpace(entry)) if entry != "" { allowedTypes = append(allowedTypes, entry) @@ -87,14 +89,15 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error { // AddUploadContext renders template values for dropzone func AddUploadContext(ctx *context.Context, uploadType string) { - if uploadType == "release" { + switch uploadType { + case "release": ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/releases/attachments" ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/releases/attachments/remove" ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/releases/attachments" ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Release.AllowedTypes, "|", ",") ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize - } else if uploadType == "comment" { + case "comment": ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments" ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove" if len(ctx.PathParam("index")) > 0 { @@ -105,12 +108,17 @@ func AddUploadContext(ctx *context.Context, uploadType string) { ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",") ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize - } else if uploadType == "repo" { - ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file" - ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove" - ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file" - ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",") - ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles - ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize + default: + setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType) } } + +func AddUploadContextForRepo(ctx reqctx.RequestContext, repo *repo_model.Repository) { + ctxData, repoLink := ctx.GetData(), repo.Link() + ctxData["UploadUrl"] = repoLink + "/upload-file" + ctxData["UploadRemoveUrl"] = repoLink + "/upload-remove" + ctxData["UploadLinkUrl"] = repoLink + "/upload-file" + ctxData["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",") + ctxData["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles + ctxData["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize +} diff --git a/services/context/user.go b/services/context/user.go index dbc35e198d..c09ded8339 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -14,15 +14,15 @@ import ( // 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, title string, obj any) { + errorFn := func(status int, obj any) { err, ok := obj.(error) if !ok { err = fmt.Errorf("%s", obj) } if status == http.StatusNotFound { - ctx.NotFound(title, err) + ctx.NotFound(err) } else { - ctx.ServerError(title, err) + ctx.ServerError("UserAssignmentWeb", err) } } ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, errorFn) @@ -42,9 +42,9 @@ func UserIDAssignmentAPI() func(ctx *APIContext) { ctx.ContextUser, err = user_model.GetUserByID(ctx, userID) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusNotFound, "GetUserByID", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByID", err) + ctx.APIErrorInternal(err) } } } @@ -54,11 +54,11 @@ func UserIDAssignmentAPI() func(ctx *APIContext) { // UserAssignmentAPI returns a middleware to handle context-user assignment for api routes func UserAssignmentAPI() func(ctx *APIContext) { return func(ctx *APIContext) { - ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, ctx.Error) + ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, ctx.APIError) } } -func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) { +func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (contextUser *user_model.User) { username := ctx.PathParam("username") if doer != nil && doer.LowerName == strings.ToLower(username) { @@ -71,12 +71,12 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, an if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil { RedirectToUser(ctx, username, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - errCb(http.StatusNotFound, "GetUserByName", err) + errCb(http.StatusNotFound, err) } else { - errCb(http.StatusInternalServerError, "LookupUserRedirect", err) + errCb(http.StatusInternalServerError, fmt.Errorf("LookupUserRedirect: %w", err)) } } else { - errCb(http.StatusInternalServerError, "GetUserByName", err) + errCb(http.StatusInternalServerError, fmt.Errorf("GetUserByName: %w", err)) } } } |