aboutsummaryrefslogtreecommitdiffstats
path: root/services/context
diff options
context:
space:
mode:
Diffstat (limited to 'services/context')
-rw-r--r--services/context/access_log.go106
-rw-r--r--services/context/access_log_test.go71
-rw-r--r--services/context/api.go140
-rw-r--r--services/context/api_test.go2
-rw-r--r--services/context/base.go11
-rw-r--r--services/context/base_form.go4
-rw-r--r--services/context/base_test.go4
-rw-r--r--services/context/context.go60
-rw-r--r--services/context/context_response.go8
-rw-r--r--services/context/org.go332
-rw-r--r--services/context/package.go24
-rw-r--r--services/context/pagination.go6
-rw-r--r--services/context/permission.go19
-rw-r--r--services/context/private.go12
-rw-r--r--services/context/repo.go232
-rw-r--r--services/context/response.go36
-rw-r--r--services/context/upload/upload.go28
-rw-r--r--services/context/user.go20
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))
}
}
}