mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-22 17:02:41 +00:00
before: gitrepo vs git packages after: git package fully handle all git operations by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide path details. benefits: 1. remove all unnecessary wrappers, developers no need to struggle with "which package should be used" 2. simplify code, RepositoryFacade can (will) be used everywhere, all "path" details are (will be) hidden
340 lines
10 KiB
Go
340 lines
10 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 (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/cache"
|
|
"gitea.dev/modules/git"
|
|
"gitea.dev/modules/httpcache"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/util"
|
|
"gitea.dev/modules/web"
|
|
web_types "gitea.dev/modules/web/types"
|
|
)
|
|
|
|
// APIContext is a specific context for API service
|
|
// 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
|
|
|
|
Cache cache.StringCache
|
|
|
|
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
|
|
Org *APIOrganization
|
|
Package *Package
|
|
PublicOnly bool // Whether the request is for a public endpoint
|
|
}
|
|
|
|
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
|
|
// A public-only token cannot reach a private repo or a repo owned by a non-public (limited or
|
|
// private) owner; any other token is unrestricted by this check.
|
|
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
|
|
return !ctx.PublicOnly || !publicOnlyTokenDeniedRepo(ctx, repo)
|
|
}
|
|
|
|
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{}
|
|
|
|
// 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
|
|
}
|
|
|
|
// APIErrorInternal responds with error message, status is 500
|
|
func (ctx *APIContext) APIErrorInternal(err error) {
|
|
ctx.apiErrorInternal(1, err)
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
// APIErrorNotFound handles 404s for APIContext
|
|
func (ctx *APIContext) APIErrorNotFound(msg ...string) {
|
|
ctx.JSON(http.StatusNotFound, APIError{
|
|
Message: util.OptionalArg(msg, "not found"),
|
|
URL: setting.API.SwaggerURL,
|
|
})
|
|
}
|
|
|
|
// APIError responds with an error message to client.
|
|
// If status is 500, also it prints error to log.
|
|
func (ctx *APIContext) APIError(status int, msg string) {
|
|
message := msg
|
|
if status == http.StatusInternalServerError {
|
|
log.ErrorWithSkip(1, "APIError: %s", message)
|
|
|
|
if setting.IsProd && !(ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
|
message = ""
|
|
}
|
|
}
|
|
|
|
ctx.JSON(status, APIError{
|
|
Message: message,
|
|
URL: setting.API.SwaggerURL,
|
|
})
|
|
}
|
|
|
|
// APIErrorAuto use error check function to determine the response code
|
|
func (ctx *APIContext) APIErrorAuto(err error) {
|
|
switch {
|
|
case errors.Is(err, util.ErrInvalidArgument):
|
|
ctx.APIError(http.StatusBadRequest, err.Error())
|
|
case errors.Is(err, util.ErrPermissionDenied):
|
|
ctx.APIError(http.StatusForbidden, err.Error())
|
|
case errors.Is(err, util.ErrNotExist):
|
|
ctx.APIError(http.StatusNotFound, err.Error())
|
|
case errors.Is(err, util.ErrAlreadyExist):
|
|
ctx.APIError(http.StatusConflict, err.Error())
|
|
case errors.Is(err, util.ErrContentTooLarge):
|
|
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
|
|
case errors.Is(err, util.ErrUnprocessableContent):
|
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
|
default:
|
|
ctx.apiErrorInternal(1, err)
|
|
}
|
|
}
|
|
|
|
type apiContextKeyType struct{}
|
|
|
|
var apiContextKey = apiContextKeyType{}
|
|
|
|
// 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 int64, 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", strconv.Itoa(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", strconv.Itoa(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", strconv.Itoa(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.
|
|
// "count" is usually from database result "count int64", so it also uses int64,
|
|
func (ctx *APIContext) SetLinkHeader(total int64, 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 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 := NewBaseContext(w, req)
|
|
ctx := &APIContext{
|
|
Base: base,
|
|
Cache: cache.GetCache(),
|
|
Repo: &Repository{},
|
|
Org: &APIOrganization{},
|
|
}
|
|
|
|
ctx.SetContextValue(apiContextKey, ctx)
|
|
|
|
// FIXME: GLOBAL-PARSE-FORM: see more details in another FIXME comment
|
|
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
|
if !ctx.ParseMultipartForm() {
|
|
return
|
|
}
|
|
}
|
|
|
|
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{})
|
|
next.ServeHTTP(ctx.Resp, ctx.Req)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ReferencesGitRepo injects the GitRepo into the Context
|
|
// you can optional skip the IsEmpty check
|
|
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
|
|
}
|
|
|
|
// For API calls.
|
|
if ctx.Repo.GitRepo == nil {
|
|
var err error
|
|
ctx.Repo.GitRepo, err = git.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.Repository.IsEmpty {
|
|
ctx.APIErrorNotFound("repository is empty")
|
|
return
|
|
}
|
|
|
|
if ctx.Repo.GitRepo == nil {
|
|
panic("no GitRepo, forgot to call the middleware?") // it is a programming error
|
|
}
|
|
|
|
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(ctx, refName)
|
|
case git.RefTypeTag:
|
|
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(ctx, refName)
|
|
case git.RefTypeCommit:
|
|
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(ctx, refName)
|
|
}
|
|
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)
|
|
})
|
|
}
|
|
|
|
// 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.Permission.IsAdmin()
|
|
}
|
|
|
|
// IsUserRepoWriter returns true if current user has "write" privilege in current repo
|
|
func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool {
|
|
return slices.ContainsFunc(unitTypes, ctx.Repo.Permission.CanWrite)
|
|
}
|