mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-27 12:16:10 +01:00
e658a6a9cd
Instead of repeating the tests that verify the ID of a comment is related to the repository of the API endpoint, add the middleware function commentAssignment() to assign ctx.Comment if the ID of the comment is verified to be related to the repository. There already are integration tests for cases of potential unrelated comment IDs that cover some of the modified endpoints which covers the commentAssignment() function logic. * TestAPICommentReactions - GetIssueCommentReactions * TestAPICommentReactions - PostIssueCommentReaction * TestAPICommentReactions - DeleteIssueCommentReaction * TestAPIEditComment - EditIssueComment * TestAPIDeleteComment - DeleteIssueComment * TestAPIGetCommentAttachment - GetIssueCommentAttachment The other modified endpoints do not have tests to verify cases of potential unrelated comment IDs. They no longer need to because they no longer implement the logic to enforce this. They however all have integration tests that verify the commentAssignment() they now rely on does not introduce a regression. * TestAPIGetComment - GetIssueComment * TestAPIListCommentAttachments - ListIssueCommentAttachments * TestAPICreateCommentAttachment - CreateIssueCommentAttachment * TestAPIEditCommentAttachment - EditIssueCommentAttachment * TestAPIDeleteCommentAttachment - DeleteIssueCommentAttachment (cherry picked from commitd414376d74
) (cherry picked from commit09db07aeae
) (cherry picked from commitf44830c3cb
) Conflicts: modules/context/api.go https://codeberg.org/forgejo/forgejo/pulls/2249 (cherry picked from commit9d1bf7be15
)
416 lines
12 KiB
Go
416 lines
12 KiB
Go
// Copyright 2016 The Gogs Authors. All rights reserved.
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
|
"code.gitea.io/gitea/models/unit"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
mc "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/web"
|
|
web_types "code.gitea.io/gitea/modules/web/types"
|
|
|
|
"gitea.com/go-chi/cache"
|
|
)
|
|
|
|
// APIContext is a specific context for API service
|
|
type APIContext struct {
|
|
*Base
|
|
|
|
Cache cache.Cache
|
|
|
|
Doer *user_model.User // current signed-in user
|
|
IsSigned bool
|
|
IsBasicAuth bool
|
|
|
|
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
|
|
|
Repo *Repository
|
|
Comment *issues_model.Comment
|
|
Org *APIOrganization
|
|
Package *Package
|
|
}
|
|
|
|
func init() {
|
|
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
|
|
return req.Context().Value(apiContextKey).(*APIContext)
|
|
})
|
|
}
|
|
|
|
// Currently, we have the following common fields in error response:
|
|
// * message: the message for end users (it shouldn't be used for error type detection)
|
|
// if we need to indicate some errors, we should introduce some new fields like ErrorCode or ErrorType
|
|
// * url: the swagger document URL
|
|
|
|
// APIError is error format response
|
|
// swagger:response error
|
|
type APIError struct {
|
|
Message string `json:"message"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// APIValidationError is error format response related to input validation
|
|
// swagger:response validationError
|
|
type APIValidationError struct {
|
|
Message string `json:"message"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// APIInvalidTopicsError is error format response to invalid topics
|
|
// swagger:response invalidTopicsError
|
|
type APIInvalidTopicsError struct {
|
|
Message string `json:"message"`
|
|
InvalidTopics []string `json:"invalidTopics"`
|
|
}
|
|
|
|
// APIEmpty is an empty response
|
|
// swagger:response empty
|
|
type APIEmpty struct{}
|
|
|
|
// APIForbiddenError is a forbidden error response
|
|
// swagger:response forbidden
|
|
type APIForbiddenError struct {
|
|
APIError
|
|
}
|
|
|
|
// APINotFound is a not found empty response
|
|
// swagger:response notFound
|
|
type APINotFound struct{}
|
|
|
|
// APIConflict is a conflict empty response
|
|
// swagger:response conflict
|
|
type APIConflict struct{}
|
|
|
|
// APIRedirect is a redirect response
|
|
// swagger:response redirect
|
|
type APIRedirect struct{}
|
|
|
|
// APIString is a string response
|
|
// swagger:response string
|
|
type APIString string
|
|
|
|
// APIRepoArchivedError is an error that is raised when an archived repo should be modified
|
|
// swagger:response repoArchivedError
|
|
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)
|
|
}
|
|
|
|
// Error 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) {
|
|
var message string
|
|
if err, ok := obj.(error); ok {
|
|
message = err.Error()
|
|
} else {
|
|
message = fmt.Sprintf("%s", obj)
|
|
}
|
|
|
|
if status == http.StatusInternalServerError {
|
|
log.ErrorWithSkip(1, "%s: %s", title, message)
|
|
|
|
if setting.IsProd && !(ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
|
message = ""
|
|
}
|
|
}
|
|
|
|
ctx.JSON(status, APIError{
|
|
Message: message,
|
|
URL: setting.API.SwaggerURL,
|
|
})
|
|
}
|
|
|
|
// 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{}
|
|
|
|
// GetAPIContext returns a context for API routes
|
|
func GetAPIContext(req *http.Request) *APIContext {
|
|
return req.Context().Value(apiContextKey).(*APIContext)
|
|
}
|
|
|
|
func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
|
|
page := NewPagination(total, pageSize, curPage, 0)
|
|
paginater := page.Paginater
|
|
links := make([]string, 0, 4)
|
|
|
|
if paginater.HasNext() {
|
|
u := *curURL
|
|
queries := u.Query()
|
|
queries.Set("page", fmt.Sprintf("%d", paginater.Next()))
|
|
u.RawQuery = queries.Encode()
|
|
|
|
links = append(links, fmt.Sprintf("<%s%s>; rel=\"next\"", setting.AppURL, u.RequestURI()[1:]))
|
|
}
|
|
if !paginater.IsLast() {
|
|
u := *curURL
|
|
queries := u.Query()
|
|
queries.Set("page", fmt.Sprintf("%d", paginater.TotalPages()))
|
|
u.RawQuery = queries.Encode()
|
|
|
|
links = append(links, fmt.Sprintf("<%s%s>; rel=\"last\"", setting.AppURL, u.RequestURI()[1:]))
|
|
}
|
|
if !paginater.IsFirst() {
|
|
u := *curURL
|
|
queries := u.Query()
|
|
queries.Set("page", "1")
|
|
u.RawQuery = queries.Encode()
|
|
|
|
links = append(links, fmt.Sprintf("<%s%s>; rel=\"first\"", setting.AppURL, u.RequestURI()[1:]))
|
|
}
|
|
if paginater.HasPrevious() {
|
|
u := *curURL
|
|
queries := u.Query()
|
|
queries.Set("page", fmt.Sprintf("%d", paginater.Previous()))
|
|
u.RawQuery = queries.Encode()
|
|
|
|
links = append(links, fmt.Sprintf("<%s%s>; rel=\"prev\"", setting.AppURL, u.RequestURI()[1:]))
|
|
}
|
|
return links
|
|
}
|
|
|
|
// SetLinkHeader sets pagination link header by given total number and page size.
|
|
func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
|
|
links := genAPILinks(ctx.Req.URL, total, pageSize, ctx.FormInt("page"))
|
|
|
|
if len(links) > 0 {
|
|
ctx.RespHeader().Set("Link", strings.Join(links, ","))
|
|
ctx.AppendAccessControlExposeHeaders("Link")
|
|
}
|
|
}
|
|
|
|
// APIContexter returns apicontext as 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)
|
|
ctx := &APIContext{
|
|
Base: base,
|
|
Cache: mc.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 })
|
|
|
|
// 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 err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
|
|
ctx.InternalServerError(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
|
|
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
|
|
|
next.ServeHTTP(ctx.Resp, ctx.Req)
|
|
})
|
|
}
|
|
}
|
|
|
|
// NotFound handles 404s for APIContext
|
|
// String will replace message, errors will be added to a slice
|
|
func (ctx *APIContext) NotFound(objs ...any) {
|
|
message := ctx.Tr("error.not_found")
|
|
var errors []string
|
|
for _, obj := range objs {
|
|
// Ignore nil
|
|
if obj == nil {
|
|
continue
|
|
}
|
|
|
|
if err, ok := obj.(error); ok {
|
|
errors = append(errors, err.Error())
|
|
} else {
|
|
message = obj.(string)
|
|
}
|
|
}
|
|
|
|
ctx.JSON(http.StatusNotFound, map[string]any{
|
|
"message": message,
|
|
"url": setting.API.SwaggerURL,
|
|
"errors": errors,
|
|
})
|
|
}
|
|
|
|
// 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) {
|
|
// Empty repository does not have reference information.
|
|
if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) {
|
|
return nil
|
|
}
|
|
|
|
// For API calls.
|
|
if ctx.Repo.GitRepo == nil {
|
|
gitRepo, err := gitrepo.OpenRepository(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()
|
|
}
|
|
}
|
|
}
|
|
|
|
return cancel
|
|
}
|
|
}
|
|
|
|
// RepoRefForAPI handles repository reference names when the ref name is not explicitly given
|
|
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"))
|
|
return
|
|
}
|
|
|
|
objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
|
|
return
|
|
}
|
|
|
|
if ref := ctx.FormTrim("ref"); len(ref) > 0 {
|
|
commit, err := ctx.Repo.GitRepo.GetCommit(ref)
|
|
if err != nil {
|
|
if git.IsErrNotExist(err) {
|
|
ctx.NotFound()
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Repo.Commit = commit
|
|
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
|
ctx.Repo.TreePath = ctx.Params("*")
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
|
|
|
|
if ctx.Repo.GitRepo.IsBranchExist(refName) {
|
|
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) {
|
|
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) == objectFormat.FullLength() {
|
|
ctx.Repo.CommitID = refName
|
|
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.Params("*")))
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
|
|
// HasAPIError returns true if error occurs in form validation.
|
|
func (ctx *APIContext) HasAPIError() bool {
|
|
hasErr, ok := ctx.Data["HasError"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
return hasErr.(bool)
|
|
}
|
|
|
|
// GetErrMsg returns error message in form validation.
|
|
func (ctx *APIContext) GetErrMsg() string {
|
|
msg, _ := ctx.Data["ErrorMsg"].(string)
|
|
if msg == "" {
|
|
msg = "invalid form data"
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// NotFoundOrServerError use error check function to determine if the error
|
|
// is about not found. It responds with 404 status code for not found error,
|
|
// or error context description for logging purpose of 500 server error.
|
|
func (ctx *APIContext) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
|
|
if errCheck(logErr) {
|
|
ctx.JSON(http.StatusNotFound, nil)
|
|
return
|
|
}
|
|
ctx.Error(http.StatusInternalServerError, "NotFoundOrServerError", logMsg)
|
|
}
|
|
|
|
// IsUserSiteAdmin returns true if current user is a site admin
|
|
func (ctx *APIContext) IsUserSiteAdmin() bool {
|
|
return ctx.IsSigned && ctx.Doer.IsAdmin
|
|
}
|
|
|
|
// IsUserRepoAdmin returns true if current user is admin in current repo
|
|
func (ctx *APIContext) IsUserRepoAdmin() bool {
|
|
return ctx.Repo.IsAdmin()
|
|
}
|
|
|
|
// 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
|
|
}
|