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.go169
-rw-r--r--services/context/api_test.go2
-rw-r--r--services/context/base.go189
-rw-r--r--services/context/base_form.go79
-rw-r--r--services/context/base_path.go47
-rw-r--r--services/context/base_test.go11
-rw-r--r--services/context/context.go70
-rw-r--r--services/context/context_model.go20
-rw-r--r--services/context/context_response.go12
-rw-r--r--services/context/context_test.go4
-rw-r--r--services/context/org.go334
-rw-r--r--services/context/package.go28
-rw-r--r--services/context/pagination.go34
-rw-r--r--services/context/permission.go89
-rw-r--r--services/context/private.go31
-rw-r--r--services/context/repo.go653
-rw-r--r--services/context/response.go36
-rw-r--r--services/context/upload/upload.go32
-rw-r--r--services/context/user.go26
21 files changed, 982 insertions, 1061 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 b45e80a329..ab50a360f4 100644
--- a/services/context/api.go
+++ b/services/context/api.go
@@ -5,24 +5,31 @@
package context
import (
- "context"
+ "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
@@ -104,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()
@@ -120,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 = ""
@@ -133,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{}
@@ -166,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:]))
@@ -174,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:]))
@@ -190,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:]))
@@ -208,31 +213,29 @@ 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) {
- base, baseCleanUp := NewBaseContext(w, req)
+ base := NewBaseContext(w, req)
ctx := &APIContext{
Base: base,
Cache: cache.GetCache(),
Repo: &Repository{PullRequest: &PullRequest{}},
Org: &APIOrganization{},
}
- defer baseCleanUp()
- ctx.Base.AppendContextValue(apiContextKey, ctx)
- ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
+ 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)
@@ -240,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 {
@@ -252,46 +255,36 @@ 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,
})
}
// ReferencesGitRepo injects the GitRepo into the Context
// you can optional skip the IsEmpty check
-func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) {
- return func(ctx *APIContext) (cancel context.CancelFunc) {
+func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) {
+ return func(ctx *APIContext) {
// Empty repository does not have reference information.
if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) {
- return nil
+ return
}
// For API calls.
if ctx.Repo.GitRepo == nil {
- gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
+ 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)
- return cancel
- }
- ctx.Repo.GitRepo = gitRepo
- // We opened it, we should close it
- return func() {
- // If it's been set to nil then assume someone else has closed it.
- if ctx.Repo.GitRepo != nil {
- _ = ctx.Repo.GitRepo.Close()
- }
+ ctx.APIErrorInternal(err)
+ return
}
}
-
- return cancel
}
}
@@ -300,41 +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
}
- // NOTICE: the "ref" here for internal usage only (e.g. woodpecker)
- refName, _ := getRefNameLegacy(ctx.Base, ctx.Repo, 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)
})
}
@@ -360,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
@@ -378,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 d627095584..f3f92b7eeb 100644
--- a/services/context/base.go
+++ b/services/context/base.go
@@ -4,86 +4,44 @@
package context
import (
- "context"
"fmt"
"html/template"
"io"
"net/http"
- "net/url"
"strconv"
"strings"
- "time"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web/middleware"
-
- "github.com/go-chi/chi/v5"
)
-type contextValuePair struct {
- key any
- valueFn func() any
-}
-
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 {
- originCtx context.Context
- contextValues []contextValuePair
+ reqctx.RequestContext
Resp ResponseWriter
Req *http.Request
// Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData.
// Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler
- Data middleware.ContextData
+ Data reqctx.ContextData
// Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation
Locale translation.Locale
}
-func (b *Base) Deadline() (deadline time.Time, ok bool) {
- return b.originCtx.Deadline()
-}
-
-func (b *Base) Done() <-chan struct{} {
- return b.originCtx.Done()
-}
-
-func (b *Base) Err() error {
- return b.originCtx.Err()
-}
-
-func (b *Base) Value(key any) any {
- for _, pair := range b.contextValues {
- if pair.key == key {
- return pair.valueFn()
- }
- }
- return b.originCtx.Value(key)
-}
-
-func (b *Base) AppendContextValueFunc(key any, valueFn func() any) any {
- b.contextValues = append(b.contextValues, contextValuePair{key, valueFn})
- return b
-}
-
-func (b *Base) AppendContextValue(key, value any) any {
- b.contextValues = append(b.contextValues, contextValuePair{key, func() any { return value }})
- return b
-}
-
-func (b *Base) GetData() middleware.ContextData {
- return b.Data
-}
-
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
func (b *Base) AppendAccessControlExposeHeaders(names ...string) {
val := b.RespHeader().Get("Access-Control-Expose-Headers")
@@ -96,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")
}
@@ -124,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]
@@ -147,93 +105,6 @@ func (b *Base) RemoteAddr() string {
return b.Req.RemoteAddr
}
-// PathParam returns the param in request path, eg: "/{var}" => "/a%2fb", then `var == "a/b"`
-func (b *Base) PathParam(name string) string {
- s, err := url.PathUnescape(b.PathParamRaw(name))
- if err != nil && !setting.IsProd {
- panic("Failed to unescape path param: " + err.Error() + ", there seems to be a double-unescaping bug")
- }
- return s
-}
-
-// PathParamRaw returns the raw param in request path, eg: "/{var}" => "/a%2fb", then `var == "a%2fb"`
-func (b *Base) PathParamRaw(name string) string {
- return chi.URLParam(b.Req, strings.TrimPrefix(name, ":"))
-}
-
-// PathParamInt64 returns the param in request path as int64
-func (b *Base) PathParamInt64(p string) int64 {
- v, _ := strconv.ParseInt(b.PathParam(p), 10, 64)
- return v
-}
-
-// SetPathParam set request path params into routes
-func (b *Base) SetPathParam(k, v string) {
- chiCtx := chi.RouteContext(b)
- chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
-}
-
-// FormString returns the first value matching the provided key in the form as a string
-func (b *Base) FormString(key string) string {
- return b.Req.FormValue(key)
-}
-
-// FormStrings returns a string slice for the provided key from the form
-func (b *Base) FormStrings(key string) []string {
- if b.Req.Form == nil {
- if err := b.Req.ParseMultipartForm(32 << 20); err != nil {
- return nil
- }
- }
- if v, ok := b.Req.Form[key]; ok {
- return v
- }
- return nil
-}
-
-// FormTrim returns the first value for the provided key in the form as a space trimmed string
-func (b *Base) FormTrim(key string) string {
- return strings.TrimSpace(b.Req.FormValue(key))
-}
-
-// FormInt returns the first value for the provided key in the form as an int
-func (b *Base) FormInt(key string) int {
- v, _ := strconv.Atoi(b.Req.FormValue(key))
- return v
-}
-
-// FormInt64 returns the first value for the provided key in the form as an int64
-func (b *Base) FormInt64(key string) int64 {
- v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64)
- return v
-}
-
-// FormBool returns true if the value for the provided key in the form is "1", "true" or "on"
-func (b *Base) FormBool(key string) bool {
- s := b.Req.FormValue(key)
- v, _ := strconv.ParseBool(s)
- v = v || strings.EqualFold(s, "on")
- return v
-}
-
-// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value
-// for the provided key exists in the form else it returns optional.None[bool]()
-func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
- value := b.Req.FormValue(key)
- if len(value) == 0 {
- return optional.None[bool]()
- }
- s := b.Req.FormValue(key)
- v, _ := strconv.ParseBool(s)
- v = v || strings.EqualFold(s, "on")
- return optional.Some(v)
-}
-
-func (b *Base) SetFormString(key, value string) {
- _ = b.Req.FormValue(key) // force parse form
- b.Req.Form.Set(key, value)
-}
-
// PlainTextBytes renders bytes as plain text
func (b *Base) plainTextInternal(skip, status int, bs []byte) {
statusPrefix := status / 100
@@ -295,13 +166,6 @@ func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r)
}
-// Close frees all resources hold by Context
-func (b *Base) cleanUp() {
- if b.Req != nil && b.Req.MultipartForm != nil {
- _ = b.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
- }
-}
-
func (b *Base) Tr(msg string, args ...any) template.HTML {
return b.Locale.Tr(msg, args...)
}
@@ -310,17 +174,28 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return b.Locale.TrN(cnt, key1, keyN, args...)
}
-func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) {
- b = &Base{
- originCtx: req.Context(),
- Req: req,
- Resp: WrapResponseWriter(resp),
- Locale: middleware.Locale(resp, req),
- Data: middleware.GetContextData(req.Context()),
+func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base {
+ reqCtx := reqctx.FromContext(req.Context())
+ b := &Base{
+ RequestContext: reqCtx,
+
+ Req: req,
+ Resp: WrapResponseWriter(resp),
+ Locale: middleware.Locale(resp, req),
+ Data: reqCtx.GetData(),
}
b.Req = b.Req.WithContext(b)
- b.AppendContextValue(BaseContextKey, b)
- b.AppendContextValue(translation.ContextKey, b.Locale)
- b.AppendContextValue(httplib.RequestContextKey, b.Req)
- return b, b.cleanUp
+ reqCtx.SetContextValue(BaseContextKey, b)
+ reqCtx.SetContextValue(translation.ContextKey, b.Locale)
+ reqCtx.SetContextValue(httplib.RequestContextKey, b.Req)
+ return b
+}
+
+func NewBaseContextForTest(resp http.ResponseWriter, req *http.Request) *Base {
+ if !setting.IsInTesting {
+ panic("This function is only for testing")
+ }
+ ctx := reqctx.NewRequestContextForTest(req.Context())
+ *req = *req.WithContext(ctx)
+ return NewBaseContext(resp, req)
}
diff --git a/services/context/base_form.go b/services/context/base_form.go
new file mode 100644
index 0000000000..81fd7cd328
--- /dev/null
+++ b/services/context/base_form.go
@@ -0,0 +1,79 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// 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 == "" {
+ s = util.OptionalArg(def)
+ }
+ return s
+}
+
+// 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 {
+ return nil
+ }
+ }
+ if v, ok := b.Req.Form[key]; ok {
+ return v
+ }
+ return nil
+}
+
+// FormTrim returns the first value for the provided key in the form as a space trimmed string
+func (b *Base) FormTrim(key string) string {
+ return strings.TrimSpace(b.Req.FormValue(key))
+}
+
+// FormInt returns the first value for the provided key in the form as an int
+func (b *Base) FormInt(key string) int {
+ v, _ := strconv.Atoi(b.Req.FormValue(key))
+ return v
+}
+
+// FormInt64 returns the first value for the provided key in the form as an int64
+func (b *Base) FormInt64(key string) int64 {
+ v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64)
+ return v
+}
+
+// FormBool returns true if the value for the provided key in the form is "1", "true" or "on"
+func (b *Base) FormBool(key string) bool {
+ s := b.Req.FormValue(key)
+ v, _ := strconv.ParseBool(s)
+ v = v || strings.EqualFold(s, "on")
+ return v
+}
+
+// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value
+// for the provided key exists in the form else it returns optional.None[bool]()
+func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
+ value := b.Req.FormValue(key)
+ if len(value) == 0 {
+ return optional.None[bool]()
+ }
+ s := b.Req.FormValue(key)
+ v, _ := strconv.ParseBool(s)
+ v = v || strings.EqualFold(s, "on")
+ return optional.Some(v)
+}
+
+func (b *Base) SetFormString(key, value string) {
+ _ = b.Req.FormValue(key) // force parse form
+ b.Req.Form.Set(key, value)
+}
diff --git a/services/context/base_path.go b/services/context/base_path.go
new file mode 100644
index 0000000000..3678deaff9
--- /dev/null
+++ b/services/context/base_path.go
@@ -0,0 +1,47 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+ "net/url"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/go-chi/chi/v5"
+)
+
+// PathParam returns the param in request path, eg: "/{var}" => "/a%2fb", then `var == "a/b"`
+func (b *Base) PathParam(name string) string {
+ s, err := url.PathUnescape(b.PathParamRaw(name))
+ if err != nil && !setting.IsProd {
+ panic("Failed to unescape path param: " + err.Error() + ", there seems to be a double-unescaping bug")
+ }
+ return s
+}
+
+// PathParamRaw returns the raw param in request path, eg: "/{var}" => "/a%2fb", then `var == "a%2fb"`
+func (b *Base) PathParamRaw(name string) string {
+ if strings.HasPrefix(name, ":") {
+ setting.PanicInDevOrTesting("path param should not start with ':'")
+ name = name[1:]
+ }
+ return chi.URLParam(b.Req, name)
+}
+
+// PathParamInt64 returns the param in request path as int64
+func (b *Base) PathParamInt64(p string) int64 {
+ v, _ := strconv.ParseInt(b.PathParam(p), 10, 64)
+ return v
+}
+
+// SetPathParam set request path params into routes
+func (b *Base) SetPathParam(name, value string) {
+ if strings.HasPrefix(name, ":") {
+ setting.PanicInDevOrTesting("path param should not start with ':'")
+ name = name[1:]
+ }
+ chi.RouteContext(b).URLParams.Add(name, url.PathEscape(value))
+}
diff --git a/services/context/base_test.go b/services/context/base_test.go
index 823f20e00b..2a4f86dddf 100644
--- a/services/context/base_test.go
+++ b/services/context/base_test.go
@@ -14,7 +14,8 @@ import (
)
func TestRedirect(t *testing.T) {
- req, _ := http.NewRequest("GET", "/", nil)
+ setting.IsInTesting = true
+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
cases := []struct {
url string
@@ -28,20 +29,18 @@ func TestRedirect(t *testing.T) {
}
for _, c := range cases {
resp := httptest.NewRecorder()
- b, cleanup := NewBaseContext(resp, req)
+ b := NewBaseContextForTest(resp, req)
resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String())
b.Redirect(c.url)
- cleanup()
has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
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, cleanup := NewBaseContext(resp, req)
+ b := NewBaseContextForTest(resp, req)
b.Redirect("/other")
- cleanup()
assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
assert.Equal(t, http.StatusNoContent, resp.Code)
}
diff --git a/services/context/context.go b/services/context/context.go
index 0d5429e366..32ec260aab 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -18,12 +18,12 @@ import (
"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/gitrepo"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
web_types "code.gitea.io/gitea/modules/web/types"
@@ -35,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
@@ -77,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.
@@ -133,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
}
@@ -153,14 +157,9 @@ func Contexter() func(next http.Handler) http.Handler {
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
- base, baseCleanUp := NewBaseContext(resp, req)
- defer baseCleanUp()
+ base := NewBaseContext(resp, req)
ctx := NewWebContext(base, rnd, session.GetContextSession(req))
-
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
- if setting.IsProd && !setting.IsInTesting {
- ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
- }
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
ctx.Data["Link"] = ctx.Link
@@ -168,23 +167,12 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.PageData = map[string]any{}
ctx.Data["PageData"] = ctx.PageData
- ctx.Base.AppendContextValue(WebContextKey, ctx)
- ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
-
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
@@ -197,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
@@ -222,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.
@@ -262,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_model.go b/services/context/context_model.go
index 4f70aac516..3a1776102f 100644
--- a/services/context/context_model.go
+++ b/services/context/context_model.go
@@ -3,27 +3,7 @@
package context
-import (
- "code.gitea.io/gitea/models/unit"
-)
-
// IsUserSiteAdmin returns true if current user is a site admin
func (ctx *Context) IsUserSiteAdmin() bool {
return ctx.IsSigned && ctx.Doer.IsAdmin
}
-
-// IsUserRepoAdmin returns true if current user is admin in current repo
-func (ctx *Context) IsUserRepoAdmin() bool {
- return ctx.Repo.IsAdmin()
-}
-
-// IsUserRepoWriter returns true if current user has write privilege in current repo
-func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool {
- for _, unitType := range unitTypes {
- if ctx.Repo.CanWrite(unitType) {
- return true
- }
- }
-
- return false
-}
diff --git a/services/context/context_response.go b/services/context/context_response.go
index 4c086ea9f5..3f64fc7352 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
}
@@ -92,7 +92,7 @@ func (ctx *Context) HTML(status int, name templates.TplName) {
}
// JSONTemplate renders the template as JSON response
-// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape
+// keep in mind that the template is processed in HTML context, so JSON things should be handled carefully, e.g.: use JSEscape
func (ctx *Context) JSONTemplate(tmpl templates.TplName) {
t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
if err != nil {
@@ -106,7 +106,7 @@ func (ctx *Context) JSONTemplate(tmpl templates.TplName) {
}
// RenderToHTML renders the template content to a HTML string
-func (ctx *Context) RenderToHTML(name templates.TplName, data map[string]any) (template.HTML, error) {
+func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTML, error) {
var buf strings.Builder
err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext)
return template.HTML(buf.String()), err
@@ -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/context_test.go b/services/context/context_test.go
index 984593398d..54044644f0 100644
--- a/services/context/context_test.go
+++ b/services/context/context_test.go
@@ -26,6 +26,7 @@ func TestRemoveSessionCookieHeader(t *testing.T) {
}
func TestRedirectToCurrentSite(t *testing.T) {
+ setting.IsInTesting = true
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
cases := []struct {
@@ -40,8 +41,7 @@ func TestRedirectToCurrentSite(t *testing.T) {
t.Run(c.location, func(t *testing.T) {
req := &http.Request{URL: &url.URL{Path: "/"}}
resp := httptest.NewRecorder()
- base, baseCleanUp := NewBaseContext(resp, req)
- defer baseCleanUp()
+ base := NewBaseContextForTest(resp, req)
ctx := NewWebContext(base, nil, nil)
ctx.RedirectToCurrentSite(c.location)
redirect := test.RedirectURL(resp)
diff --git a/services/context/org.go b/services/context/org.go
index bf482fa754..1cd8923178 100644
--- a/services/context/org.go
+++ b/services/context/org.go
@@ -40,7 +40,7 @@ func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool {
}
func GetOrganizationByParams(ctx *Context) {
- orgName := ctx.PathParam(":org")
+ orgName := ctx.PathParam("org")
var err error
@@ -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 strings.EqualFold(team.LowerName, 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 271b61e99c..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
}
@@ -153,12 +153,10 @@ func PackageContexter() func(next http.Handler) http.Handler {
renderer := templates.HTMLRenderer()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
- base, baseCleanUp := NewBaseContext(resp, req)
- defer baseCleanUp()
-
- // it is still needed when rendering 500 page in a package handler
+ base := NewBaseContext(resp, req)
+ // 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.Base.AppendContextValue(WebContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
diff --git a/services/context/pagination.go b/services/context/pagination.go
index 42117cf96d..2a9805db05 100644
--- a/services/context/pagination.go
+++ b/services/context/pagination.go
@@ -21,45 +21,35 @@ 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
}
-// AddParamString adds a string parameter directly
-func (p *Pagination) AddParamString(key, value string) {
- urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value))
- p.urlParams = append(p.urlParams, urlParam)
+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 {
+func (p *Pagination) AddParamFromQuery(q url.Values) {
+ for key, values := range q {
+ if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") {
continue
}
for _, value := range values {
- urlParam := fmt.Sprintf("%s=%v", key, url.QueryEscape(value))
+ urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value))
p.urlParams = append(p.urlParams, urlParam)
}
}
}
+func (p *Pagination) AddParamFromRequest(req *http.Request) {
+ p.AddParamFromQuery(req.URL.Query())
+}
+
// GetParams returns the configured URL params
func (p *Pagination) GetParams() template.URL {
return template.URL(strings.Join(p.urlParams, "&"))
}
-
-// SetDefaultParams sets common pagination params that are often used
-func (p *Pagination) SetDefaultParams(ctx *Context) {
- if v, ok := ctx.Data["SortType"].(string); ok {
- p.AddParamString("sort", v)
- }
- if v, ok := ctx.Data["Keyword"].(string); ok {
- p.AddParamString("q", v)
- }
- if v, ok := ctx.Data["IsFuzzy"].(bool); ok {
- p.AddParamString("fuzzy", fmt.Sprint(v))
- }
- // do not add any more uncommon params here!
-}
diff --git a/services/context/permission.go b/services/context/permission.go
index 9338587257..c0a5a98724 100644
--- a/services/context/permission.go
+++ b/services/context/permission.go
@@ -5,112 +5,55 @@ package context
import (
"net/http"
+ "slices"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/log"
)
// RequireRepoAdmin returns a middleware for requiring repository admin permission
func RequireRepoAdmin() func(ctx *Context) {
return func(ctx *Context) {
if !ctx.IsSigned || !ctx.Repo.IsAdmin() {
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
+ ctx.NotFound(nil)
return
}
}
}
-// RequireRepoWriter returns a middleware for requiring repository write to the specify unitType
-func RequireRepoWriter(unitType unit.Type) func(ctx *Context) {
- return func(ctx *Context) {
- if !ctx.Repo.CanWrite(unitType) {
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
- return
- }
- }
-}
-
-// CanEnableEditor checks if the user is allowed to write to the branch of the repo
-func CanEnableEditor() func(ctx *Context) {
+// CanWriteToBranch checks if the user is allowed to write to the branch of the repo
+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
}
}
}
-// RequireRepoWriterOr returns a middleware for requiring repository write to one of the unit permission
-func RequireRepoWriterOr(unitTypes ...unit.Type) 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
- }
- }
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
- }
-}
-
-// RequireRepoReader returns a middleware for requiring repository read to the specify unitType
-func RequireRepoReader(unitType unit.Type) func(ctx *Context) {
- return func(ctx *Context) {
- if !ctx.Repo.CanRead(unitType) {
- if unitType == unit.TypeCode && canWriteAsMaintainer(ctx) {
- return
- }
- if log.IsTrace() {
- if ctx.IsSigned {
- log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
- "User in Repo has Permissions: %-+v",
- ctx.Doer,
- unitType,
- ctx.Repo.Repository,
- ctx.Repo.Permission)
- } else {
- log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
- "Anonymous user in Repo has Permissions: %-+v",
- unitType,
- ctx.Repo.Repository,
- ctx.Repo.Permission)
- }
- }
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
+ if slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite) {
return
}
+ ctx.NotFound(nil)
}
}
-// RequireRepoReaderOr returns a middleware for requiring repository write to one of the unit permission
-func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
+// RequireUnitReader returns a middleware for requiring repository write to one of the unit permission
+func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
return func(ctx *Context) {
for _, unitType := range unitTypes {
if ctx.Repo.CanRead(unitType) {
return
}
- }
- if log.IsTrace() {
- var format string
- var args []any
- if ctx.IsSigned {
- format = "Permission Denied: User %-v cannot read ["
- args = append(args, ctx.Doer)
- } else {
- format = "Permission Denied: Anonymous user cannot read ["
- }
- for _, unit := range unitTypes {
- format += "%-v, "
- args = append(args, unit)
+ if unitType == unit.TypeCode && canWriteAsMaintainer(ctx) {
+ return
}
-
- format = format[:len(format)-2] + "] in Repo %-v\n" +
- "User in Repo has Permissions: %-+v"
- args = append(args, ctx.Repo.Repository, ctx.Repo.Permission)
- log.Trace(format, args...)
}
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
+ ctx.NotFound(nil)
}
}
@@ -134,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
}
@@ -145,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 8b41949f60..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,22 +49,20 @@ 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) {
- base, baseCleanUp := NewBaseContext(w, req)
+ base := NewBaseContext(w, req)
ctx := &PrivateContext{Base: base}
- defer baseCleanUp()
- ctx.Base.AppendContextValue(privateContextKey, ctx)
-
+ ctx.SetContextValue(privateContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
@@ -78,8 +72,15 @@ func PrivateContexter() func(http.Handler) http.Handler {
// This function should be used when there is a need for work to continue even if the request has been cancelled.
// Primarily this affects hook/post-receive and hook/proc-receive both of which need to continue working even if
// the underlying request has timed out from the ssh/http push
-func OverrideContext(ctx *PrivateContext) (cancel context.CancelFunc) {
- // 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.Override, _, cancel = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true)
- return cancel
+func OverrideContext() func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ // 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(), "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 e96916ca42..afc6de9b16 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -46,22 +46,21 @@ type PullRequest struct {
// Repository contains information to operate a repository
type Repository struct {
access_model.Permission
- IsWatching bool
- IsViewBranch bool
- IsViewTag bool
- IsViewCommit bool
- Repository *repo_model.Repository
- Owner *user_model.User
+
+ Repository *repo_model.Repository
+ Owner *user_model.User
+
+ RepoLink string
+ GitRepo *git.Repository
+
+ // RefFullName is the full ref name that the user is viewing
+ RefFullName git.RefName
+ BranchName string // it is the RefFullName's short name if its type is "branch"
+ TreePath string
+
+ // Commit it is always set to the commit for the branch or tag, or just the commit that the user is viewing
Commit *git.Commit
- Tag *git.Tag
- GitRepo *git.Repository
- RefName string
- BranchName string
- TagName string
- TreePath string
CommitID string
- RepoLink string
- CloneLink repo_model.CloneLink
CommitsCount int64
PullRequest *PullRequest
@@ -72,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.IsViewBranch && 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()
@@ -90,66 +84,108 @@ 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 nil, err
+ }
+
+ protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName)
if err != nil {
- return CanCommitToBranchResults{}, err
+ return nil, err
}
- userCanPush := true
- requireSigned := false
+ 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 or not a user can use the timetracker.
+// CanUseTimetracker returns whether a user can use the timetracker.
func (r *Repository) CanUseTimetracker(ctx context.Context, issue *issues_model.Issue, user *user_model.User) bool {
// Checking for following:
// 1. Is timetracker enabled
@@ -169,15 +205,9 @@ func (r *Repository) GetCommitsCount() (int64, error) {
if r.Commit == nil {
return 0, nil
}
- var contextName string
- if r.IsViewBranch {
- contextName = r.BranchName
- } else if r.IsViewTag {
- contextName = r.TagName
- } else {
- contextName = r.CommitID
- }
- return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, r.IsViewBranch || r.IsViewTag), func() (int64, error) {
+ contextName := r.RefFullName.ShortName()
+ isRef := r.RefFullName.IsBranch() || r.RefFullName.IsTag()
+ return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, isRef), func() (int64, error) {
return r.Commit.CommitsCount()
})
}
@@ -199,33 +229,13 @@ func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool,
})
}
-// BranchNameSubURL sub-URL for the BranchName field
-func (r *Repository) BranchNameSubURL() string {
- switch {
- case r.IsViewBranch:
- return "branch/" + util.PathEscapeSegments(r.BranchName)
- case r.IsViewTag:
- return "tag/" + util.PathEscapeSegments(r.TagName)
- case r.IsViewCommit:
- return "commit/" + util.PathEscapeSegments(r.CommitID)
- }
- log.Error("Unknown view type for repo: %v", r)
- return ""
-}
-
-// FileExists returns true if a file exists in the given repo branch
-func (r *Repository) FileExists(path, branch string) (bool, error) {
- if branch == "" {
- branch = r.Repository.DefaultBranch
- }
- commit, err := r.GitRepo.GetBranchCommit(branch)
- if err != nil {
- return false, err
- }
- if _, err := commit.GetTreeEntryByPath(path); err != nil {
- return false, err
- }
- return true, nil
+// RefTypeNameSubURL makes a sub-url for the current ref (branch/tag/commit) field, for example:
+// * "branch/master"
+// * "tag/v1.0.0"
+// * "commit/123456"
+// It is usually used to construct a link like ".../src/{{RefTypeNameSubURL}}/{{PathEscapeSegments TreePath}}"
+func (r *Repository) RefTypeNameSubURL() string {
+ return r.RefFullName.RefWebLinkPath()
}
// GetEditorconfig returns the .editorconfig definition if found in the
@@ -316,8 +326,8 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string {
// This is particular a workaround for "go get" command which does not respect
// .netrc file.
func EarlyResponseForGoGetMeta(ctx *Context) {
- username := ctx.PathParam(":username")
- reponame := strings.TrimSuffix(ctx.PathParam(":reponame"), ".git")
+ username := ctx.PathParam("username")
+ reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
if username == "" || reponame == "" {
ctx.PlainText(http.StatusBadRequest, "invalid repository path")
return
@@ -325,9 +335,9 @@ func EarlyResponseForGoGetMeta(ctx *Context) {
var cloneURL string
if setting.Repository.GoGetCloneURLProtocol == "ssh" {
- cloneURL = repo_model.ComposeSSHCloneURL(username, reponame)
+ cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame)
} else {
- cloneURL = repo_model.ComposeHTTPSCloneURL(username, reponame)
+ cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame)
}
goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL)
htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent))
@@ -336,13 +346,13 @@ func EarlyResponseForGoGetMeta(ctx *Context) {
// RedirectToRepo redirect to a differently-named repository
func RedirectToRepo(ctx *Base, redirectRepoID int64) {
- ownerName := ctx.PathParam(":username")
- previousRepoName := ctx.PathParam(":reponame")
+ ownerName := ctx.PathParam("username")
+ previousRepoName := ctx.PathParam("reponame")
repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID)
if err != nil {
log.Error("GetRepositoryByID: %v", err)
- ctx.Error(http.StatusInternalServerError, "GetRepositoryByID")
+ ctx.HTTPError(http.StatusInternalServerError, "GetRepositoryByID")
return
}
@@ -355,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) {
@@ -365,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
@@ -397,64 +413,56 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
}
// RepoAssignment returns a middleware to handle repository assignment
-func RepoAssignment(ctx *Context) context.CancelFunc {
- if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce {
- // FIXME: it should panic in dev/test modes to have a clear behavior
- log.Trace("RepoAssignment was exec already, skipping second call ...")
- return nil
+func RepoAssignment(ctx *Context) {
+ if ctx.Data["Repository"] != nil {
+ setting.PanicInDevOrTesting("RepoAssignment should not be executed twice")
}
- ctx.Data["repoAssignmentExecuted"] = true
-
- var (
- owner *user_model.User
- err error
- )
- userName := ctx.PathParam(":username")
- repoName := ctx.PathParam(":reponame")
+ var err error
+ userName := ctx.PathParam("username")
+ repoName := ctx.PathParam("reponame")
repoName = strings.TrimSuffix(repoName, ".git")
if setting.Other.EnableFeed {
+ ctx.Data["EnableFeed"] = true
repoName = strings.TrimSuffix(repoName, ".rss")
repoName = strings.TrimSuffix(repoName, ".atom")
}
// Check if the user is the same as the repository owner
- if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) {
- owner = ctx.Doer
+ if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, userName) {
+ ctx.Repo.Owner = ctx.Doer
} else {
- owner, err = user_model.GetUserByName(ctx, userName)
+ ctx.Repo.Owner, err = user_model.GetUserByName(ctx, userName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
// go-get does not support redirects
// https://github.com/golang/go/issues/19760
if ctx.FormString("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx)
- return nil
+ return
}
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)
}
} else {
ctx.ServerError("GetUserByName", err)
}
- return nil
+ return
}
}
- ctx.Repo.Owner = owner
- ctx.ContextUser = owner
+ ctx.ContextUser = ctx.Repo.Owner
ctx.Data["ContextUser"] = ctx.ContextUser
- ctx.Data["Username"] = ctx.Repo.Owner.Name
// redirect link to wiki
if strings.HasSuffix(repoName, ".wiki") {
// ctx.Req.URL.Path does not have the preceding appSubURL - any redirect must have this added
// Now we happen to know that all of our paths are: /:username/:reponame/whatever_else
- originalRepoName := ctx.PathParam(":reponame")
+ originalRepoName := ctx.PathParam("reponame")
redirectRepoName := strings.TrimSuffix(repoName, ".wiki")
redirectRepoName += originalRepoName[len(redirectRepoName)+5:]
redirectPath := strings.Replace(
@@ -467,45 +475,40 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
redirectPath += "?" + ctx.Req.URL.RawQuery
}
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath))
- return nil
+ return
}
// Get repository.
- repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName)
+ repo, err := repo_model.GetRepositoryByName(ctx, ctx.Repo.Owner.ID, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
- redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, repoName)
+ redirectRepoID, err := repo_model.LookupRedirect(ctx, ctx.Repo.Owner.ID, repoName)
if err == nil {
RedirectToRepo(ctx.Base, redirectRepoID)
} else if repo_model.IsErrRedirectNotExist(err) {
if ctx.FormString("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx)
- return nil
+ return
}
- ctx.NotFound("GetRepositoryByName", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("LookupRepoRedirect", err)
}
} else {
ctx.ServerError("GetRepositoryByName", err)
}
- return nil
+ return
}
- repo.Owner = owner
+ repo.Owner = ctx.Repo.Owner
repoAssignment(ctx, repo)
if ctx.Written() {
- return nil
+ return
}
ctx.Repo.RepoLink = repo.Link()
ctx.Data["RepoLink"] = ctx.Repo.RepoLink
- ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
-
- if setting.Other.EnableFeed {
- ctx.Data["EnableFeed"] = true
- ctx.Data["FeedURL"] = ctx.Repo.RepoLink
- }
+ ctx.Data["FeedURL"] = ctx.Repo.RepoLink
unit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeExternalTracker)
if err == nil {
@@ -520,7 +523,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
})
if err != nil {
ctx.ServerError("GetReleaseCountByRepoID", err)
- return nil
+ return
}
ctx.Data["NumReleases"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
@@ -529,15 +532,12 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
})
if err != nil {
ctx.ServerError("GetReleaseCountByRepoID", err)
- return nil
+ return
}
- ctx.Data["Title"] = owner.Name + "/" + repo.Name
+ ctx.Data["Title"] = repo.Owner.Name + "/" + repo.Name
ctx.Data["Repository"] = repo
ctx.Data["Owner"] = ctx.Repo.Repository.Owner
- ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner()
- ctx.Data["IsRepositoryAdmin"] = ctx.Repo.IsAdmin()
- ctx.Data["RepoOwnerIsOrganization"] = repo.Owner.IsOrganization()
ctx.Data["CanWriteCode"] = ctx.Repo.CanWrite(unit_model.TypeCode)
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues)
ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests)
@@ -546,14 +546,14 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
canSignedUserFork, err := repo_module.CanUserForkRepo(ctx, ctx.Doer, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("CanUserForkRepo", err)
- return nil
+ return
}
ctx.Data["CanSignedUserFork"] = canSignedUserFork
userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GetForksByUserAndOrgs", err)
- return nil
+ return
}
ctx.Data["UserAndOrgForks"] = userAndOrgForks
@@ -562,7 +562,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
// If multiple forks are available or if the user can fork to another account, but there is already a fork: open selection dialog
ctx.Data["ShowForkModal"] = len(userAndOrgForks) > 1 || (canSignedUserFork && len(userAndOrgForks) > 0)
- ctx.Data["RepoCloneLink"] = repo.CloneLink()
+ ctx.Data["RepoCloneLink"] = repo.CloneLink(ctx, ctx.Doer)
cloneButtonShowHTTPS := !setting.Repository.DisableHTTPGit
cloneButtonShowSSH := !setting.SSH.Disabled && (ctx.IsSigned || setting.SSH.ExposeAnonymous)
@@ -587,14 +587,14 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
if repo.IsFork {
RetrieveBaseRepo(ctx, repo)
if ctx.Written() {
- return nil
+ return
}
}
if repo.IsGenerated() {
RetrieveTemplateRepo(ctx, repo)
if ctx.Written() {
- return nil
+ return
}
}
@@ -605,45 +605,36 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
// Disable everything when the repo is being created
if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() {
- ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch
if !isHomeOrSettings {
ctx.Redirect(ctx.Repo.RepoLink)
}
- return nil
+ return
+ }
+
+ if ctx.Repo.GitRepo != nil {
+ setting.PanicInDevOrTesting("RepoAssignment: GitRepo should be nil")
+ _ = ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
}
- gitRepo, err := gitrepo.OpenRepository(ctx, repo)
+ ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") {
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err)
ctx.Repo.Repository.MarkAsBrokenEmpty()
- ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch
// Only allow access to base of repo or settings
if !isHomeOrSettings {
ctx.Redirect(ctx.Repo.RepoLink)
}
- return nil
+ return
}
ctx.ServerError("RepoAssignment Invalid repo "+repo.FullName(), err)
- return nil
- }
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- }
- ctx.Repo.GitRepo = gitRepo
-
- // We opened it, we should close it
- cancel := func() {
- // If it's been set to nil then assume someone else has closed it.
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- }
+ return
}
// Stop at this point when the repo is empty.
if ctx.Repo.Repository.IsEmpty {
- ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch
- return cancel
+ return
}
branchOpts := git_model.FindBranchOptions{
@@ -654,7 +645,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
if err != nil {
ctx.ServerError("CountBranches", err)
- return cancel
+ return
}
// non-empty repo should have at least 1 branch, so this repository's branches haven't been synced yet
@@ -662,28 +653,12 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0)
if err != nil {
ctx.ServerError("SyncRepoBranches", err)
- return cancel
+ return
}
}
ctx.Data["BranchesCount"] = branchesTotal
- // If no branch is set in the request URL, try to guess a default one.
- if len(ctx.Repo.BranchName) == 0 {
- if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) {
- ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
- } else {
- ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository)
- if ctx.Repo.BranchName == "" {
- // If it still can't get a default branch, fall back to default branch from setting.
- // Something might be wrong. Either site admin should fix the repo sync or Gitea should fix a potential bug.
- ctx.Repo.BranchName = setting.Repository.DefaultBranch
- }
- }
- ctx.Repo.RefName = ctx.Repo.BranchName
- }
- ctx.Data["BranchName"] = ctx.Repo.BranchName
-
// People who have push access or have forked repository can propose a new pull request.
canPush := ctx.Repo.CanWrite(unit_model.TypeCode) ||
(ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID))
@@ -711,50 +686,30 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GetPendingRepositoryTransfer", err)
- return cancel
+ return
}
if err := repoTransfer.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadRecipient", err)
- return cancel
+ return
}
ctx.Data["RepoTransfer"] = repoTransfer
if ctx.Doer != nil {
- ctx.Data["CanUserAcceptTransfer"] = repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer)
+ ctx.Data["CanUserAcceptOrRejectTransfer"] = repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer)
}
}
if ctx.FormString("go-get") == "1" {
- ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, owner.Name, repo.Name)
+ ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, repo.Owner.Name, repo.Name)
fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}"
ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}"
}
- return cancel
}
-// RepoRefType type of repo reference
-type RepoRefType int
-
-const (
- // RepoRefUnknown is for legacy support, makes the code to "guess" the ref type
- RepoRefUnknown RepoRefType = iota
- RepoRefBranch
- RepoRefTag
- RepoRefCommit
- RepoRefBlob
-)
-
const headRefName = "HEAD"
-// RepoRef handles repository reference names when the ref name is not
-// explicitly given
-func RepoRef() func(*Context) context.CancelFunc {
- // since no ref name is explicitly specified, ok to just use branch
- return RepoRefByType(RepoRefBranch)
-}
-
func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool) string {
refName := ""
parts := strings.Split(path, "/")
@@ -768,37 +723,29 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool
return ""
}
-func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) {
- extraRef := util.OptionalArg(optionalExtraRef)
- reqPath := ctx.PathParam("*")
- reqPath = path.Join(extraRef, reqPath)
-
- if refName := getRefName(ctx, repo, RepoRefBranch); refName != "" {
- return refName, RepoRefBranch
+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, false
}
- if refName := getRefName(ctx, repo, RepoRefTag); refName != "" {
- return refName, RepoRefTag
+ if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeTag); refName != "" {
+ return refName, git.RefTypeTag, false
}
-
- // For legacy support only full commit sha
- parts := strings.Split(reqPath, "/")
- if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) {
+ 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(parts[1:], "/")
- return parts[0], RepoRefCommit
- }
-
- if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 {
- return refName, RepoRefBlob
+ repo.TreePath = strings.Join(reqRefPathParts[1:], "/")
+ 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, RepoRefBranch
+ return repo.Repository.DefaultBranch, git.RefTypeBranch, true
}
-func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
- path := ctx.PathParam("*")
- switch pathType {
- case RepoRefBranch:
+func getRefName(ctx *Base, repo *Repository, path string, refType git.RefType) string {
+ switch refType {
+ case git.RefTypeBranch:
ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist)
if len(ref) == 0 {
// check if ref is HEAD
@@ -828,9 +775,9 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
}
return ref
- case RepoRefTag:
+ case git.RefTypeTag:
return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist)
- case RepoRefCommit:
+ case git.RefTypeCommit:
parts := strings.Split(path, "/")
if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) {
// FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists
@@ -847,182 +794,196 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
repo.TreePath = strings.Join(parts[1:], "/")
return commit.ID.String()
}
- case RepoRefBlob:
- _, err := repo.GitRepo.GetBlob(path)
- if err != nil {
- return ""
- }
- return path
default:
- panic(fmt.Sprintf("Unrecognized path type: %v", pathType))
+ panic(fmt.Sprintf("Unrecognized ref type: %v", refType))
}
return ""
}
-type RepoRefByTypeOptions struct {
- IgnoreNotExistErr bool
+func repoRefFullName(typ git.RefType, shortName string) git.RefName {
+ switch typ {
+ case git.RefTypeBranch:
+ return git.RefNameFromBranch(shortName)
+ case git.RefTypeTag:
+ return git.RefNameFromTag(shortName)
+ case git.RefTypeCommit:
+ return git.RefNameFromCommit(shortName)
+ default:
+ setting.PanicInDevOrTesting("Unknown RepoRefType: %v", typ)
+ return git.RefNameFromBranch("main") // just a dummy result, it shouldn't happen
+ }
+}
+
+func RepoRefByDefaultBranch() func(*Context) {
+ return func(ctx *Context) {
+ ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
+ ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
+ ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
+ ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount()
+ ctx.Data["RefFullName"] = ctx.Repo.RefFullName
+ ctx.Data["BranchName"] = ctx.Repo.BranchName
+ ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
+ }
}
// RepoRefByType handles repository reference name for a specific type
// of repository reference
-func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) context.CancelFunc {
- opt := util.OptionalArg(opts)
- return func(ctx *Context) (cancel context.CancelFunc) {
+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
- ctx.Repo.IsViewBranch = true
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
+ ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.BranchName)
+ // these variables are used by the template to "add/upload" new files
+ ctx.Data["BranchName"] = ctx.Repo.BranchName
ctx.Data["TreePath"] = ""
- return nil
- }
-
- var (
- refName string
- err error
- )
-
- if ctx.Repo.GitRepo == nil {
- ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
- if err != nil {
- ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err)
- return nil
- }
- // We opened it, we should close it
- cancel = func() {
- // If it's been set to nil then assume someone else has closed it.
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- }
- }
+ return
}
// Get default branch.
- if len(ctx.PathParam("*")) == 0 {
- refName = ctx.Repo.Repository.DefaultBranch
- if !ctx.Repo.GitRepo.IsBranchExist(refName) {
- brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 1)
+ var refShortName string
+ reqPath := ctx.PathParam("*")
+ if reqPath == "" {
+ refShortName = ctx.Repo.Repository.DefaultBranch
+ if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, refShortName) {
+ brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 1)
if err == nil && len(brs) != 0 {
- refName = brs[0].Name
+ refShortName = brs[0]
} else if len(brs) == 0 {
log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
- ctx.Repo.Repository.MarkAsBrokenEmpty()
} else {
log.Error("GetBranches error: %v", err)
- ctx.Repo.Repository.MarkAsBrokenEmpty()
}
}
- ctx.Repo.RefName = refName
- ctx.Repo.BranchName = refName
- ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
+ ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName)
+ ctx.Repo.BranchName = refShortName
+ ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName)
if err == nil {
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") {
// if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users
log.Error("GetBranchCommit: %v", err)
- ctx.Repo.Repository.MarkAsBrokenEmpty()
} else {
ctx.ServerError("GetBranchCommit", err)
- return cancel
+ return
}
- ctx.Repo.IsViewBranch = true
- } else {
- guessLegacyPath := refType == RepoRefUnknown
+ } else { // there is a path in request
+ guessLegacyPath := refType == ""
+ fallbackDefaultBranch := false
if guessLegacyPath {
- refName, refType = getRefNameLegacy(ctx.Base, ctx.Repo)
+ refShortName, refType, fallbackDefaultBranch = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "")
} else {
- refName = getRefName(ctx.Base, ctx.Repo, refType)
+ refShortName = getRefName(ctx.Base, ctx.Repo, reqPath, refType)
}
- ctx.Repo.RefName = refName
+ ctx.Repo.RefFullName = repoRefFullName(refType, refShortName)
isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool)
if isRenamedBranch && has {
renamedBranchName := ctx.Data["RenamedBranchName"].(string)
- ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName))
- link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refName), util.PathEscapeSegments(renamedBranchName), 1)
+ ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refShortName, renamedBranchName))
+ link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refShortName), util.PathEscapeSegments(renamedBranchName), 1)
ctx.Redirect(link)
- return cancel
+ return
}
- if refType == RepoRefBranch && ctx.Repo.GitRepo.IsBranchExist(refName) {
- ctx.Repo.IsViewBranch = true
- ctx.Repo.BranchName = refName
+ if refType == git.RefTypeBranch && gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, refShortName) {
+ ctx.Repo.BranchName = refShortName
+ ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName)
- ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
+ ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName)
if err != nil {
ctx.ServerError("GetBranchCommit", err)
- return cancel
+ return
}
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
- } else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refName) {
- ctx.Repo.IsViewTag = true
- ctx.Repo.TagName = refName
+ } 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(refName)
+ ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refShortName)
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("GetTagCommit", err)
- return cancel
+ ctx.NotFound(err)
+ return
}
ctx.ServerError("GetTagCommit", err)
- return cancel
+ return
}
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
- } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) {
- ctx.Repo.IsViewCommit = true
- ctx.Repo.CommitID = refName
+ } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refShortName, 7) {
+ ctx.Repo.RefFullName = git.RefNameFromCommit(refShortName)
+ ctx.Repo.CommitID = refShortName
- ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
+ ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refShortName)
if err != nil {
- ctx.NotFound("GetCommit", err)
- return cancel
+ ctx.NotFound(err)
+ return
}
// If short commit ID add canonical link header
- if len(refName) < ctx.Repo.GetObjectFormat().FullLength() {
- canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))
+ if len(refShortName) < ctx.Repo.GetObjectFormat().FullLength() {
+ canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))
ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL))
}
} else {
- if opt.IgnoreNotExistErr {
- return cancel
- }
- ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName))
- return cancel
+ 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.BranchNameSubURL(),
- 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 cancel
+ return
}
}
+ ctx.Data["RefFullName"] = ctx.Repo.RefFullName
+ ctx.Data["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
+ ctx.Data["TreePath"] = ctx.Repo.TreePath
+
ctx.Data["BranchName"] = ctx.Repo.BranchName
- ctx.Data["RefName"] = ctx.Repo.RefName
- ctx.Data["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
- ctx.Data["TagName"] = ctx.Repo.TagName
+
ctx.Data["CommitID"] = ctx.Repo.CommitID
- ctx.Data["TreePath"] = ctx.Repo.TreePath
- ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch
- ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag
- ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit
+
ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
if err != nil {
ctx.ServerError("GetCommitsCount", err)
- return cancel
+ 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())
-
- return cancel
}
}
@@ -1030,7 +991,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
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 cefd13ebb6..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,30 +89,36 @@ 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 {
- ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.PathParam(":index")) + "/attachments"
+ if len(ctx.PathParam("index")) > 0 {
+ ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.PathParam("index")) + "/attachments"
} else {
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/attachments"
}
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 b0e855e923..f1a3035ee9 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)
@@ -33,7 +33,7 @@ func UserAssignmentWeb() func(ctx *Context) {
// UserIDAssignmentAPI returns a middleware to handle context-user assignment for api routes
func UserIDAssignmentAPI() func(ctx *APIContext) {
return func(ctx *APIContext) {
- userID := ctx.PathParamInt64(":user-id")
+ userID := ctx.PathParamInt64("user-id")
if ctx.IsSigned && ctx.Doer.ID == userID {
ctx.ContextUser = ctx.Doer
@@ -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,14 +54,14 @@ 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) {
- username := ctx.PathParam(":username")
+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) {
+ if doer != nil && strings.EqualFold(doer.LowerName, username) {
contextUser = doer
} else {
var err error
@@ -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))
}
}
}