gitea
Зеркало из https://github.com/go-gitea/gitea
1// Copyright 2016 The Gogs Authors. All rights reserved.
2// Copyright 2019 The Gitea Authors. All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package context
6
7import (
8"context"
9"fmt"
10"net/http"
11"net/url"
12"strings"
13
14"code.gitea.io/gitea/models/unit"
15user_model "code.gitea.io/gitea/models/user"
16"code.gitea.io/gitea/modules/cache"
17"code.gitea.io/gitea/modules/git"
18"code.gitea.io/gitea/modules/gitrepo"
19"code.gitea.io/gitea/modules/httpcache"
20"code.gitea.io/gitea/modules/log"
21"code.gitea.io/gitea/modules/setting"
22"code.gitea.io/gitea/modules/web"
23web_types "code.gitea.io/gitea/modules/web/types"
24)
25
26// APIContext is a specific context for API service
27type APIContext struct {
28*Base
29
30Cache cache.StringCache
31
32Doer *user_model.User // current signed-in user
33IsSigned bool
34IsBasicAuth bool
35
36ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
37
38Repo *Repository
39Org *APIOrganization
40Package *Package
41}
42
43func init() {
44web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
45return req.Context().Value(apiContextKey).(*APIContext)
46})
47}
48
49// Currently, we have the following common fields in error response:
50// * message: the message for end users (it shouldn't be used for error type detection)
51// if we need to indicate some errors, we should introduce some new fields like ErrorCode or ErrorType
52// * url: the swagger document URL
53
54// APIError is error format response
55// swagger:response error
56type APIError struct {
57Message string `json:"message"`
58URL string `json:"url"`
59}
60
61// APIValidationError is error format response related to input validation
62// swagger:response validationError
63type APIValidationError struct {
64Message string `json:"message"`
65URL string `json:"url"`
66}
67
68// APIInvalidTopicsError is error format response to invalid topics
69// swagger:response invalidTopicsError
70type APIInvalidTopicsError struct {
71Message string `json:"message"`
72InvalidTopics []string `json:"invalidTopics"`
73}
74
75// APIEmpty is an empty response
76// swagger:response empty
77type APIEmpty struct{}
78
79// APIForbiddenError is a forbidden error response
80// swagger:response forbidden
81type APIForbiddenError struct {
82APIError
83}
84
85// APINotFound is a not found empty response
86// swagger:response notFound
87type APINotFound struct{}
88
89// APIConflict is a conflict empty response
90// swagger:response conflict
91type APIConflict struct{}
92
93// APIRedirect is a redirect response
94// swagger:response redirect
95type APIRedirect struct{}
96
97// APIString is a string response
98// swagger:response string
99type APIString string
100
101// APIRepoArchivedError is an error that is raised when an archived repo should be modified
102// swagger:response repoArchivedError
103type APIRepoArchivedError struct {
104APIError
105}
106
107// ServerError responds with error message, status is 500
108func (ctx *APIContext) ServerError(title string, err error) {
109ctx.Error(http.StatusInternalServerError, title, err)
110}
111
112// Error responds with an error message to client with given obj as the message.
113// If status is 500, also it prints error to log.
114func (ctx *APIContext) Error(status int, title string, obj any) {
115var message string
116if err, ok := obj.(error); ok {
117message = err.Error()
118} else {
119message = fmt.Sprintf("%s", obj)
120}
121
122if status == http.StatusInternalServerError {
123log.ErrorWithSkip(1, "%s: %s", title, message)
124
125if setting.IsProd && !(ctx.Doer != nil && ctx.Doer.IsAdmin) {
126message = ""
127}
128}
129
130ctx.JSON(status, APIError{
131Message: message,
132URL: setting.API.SwaggerURL,
133})
134}
135
136// InternalServerError responds with an error message to the client with the error as a message
137// and the file and line of the caller.
138func (ctx *APIContext) InternalServerError(err error) {
139log.ErrorWithSkip(1, "InternalServerError: %v", err)
140
141var message string
142if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
143message = err.Error()
144}
145
146ctx.JSON(http.StatusInternalServerError, APIError{
147Message: message,
148URL: setting.API.SwaggerURL,
149})
150}
151
152type apiContextKeyType struct{}
153
154var apiContextKey = apiContextKeyType{}
155
156// GetAPIContext returns a context for API routes
157func GetAPIContext(req *http.Request) *APIContext {
158return req.Context().Value(apiContextKey).(*APIContext)
159}
160
161func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
162page := NewPagination(total, pageSize, curPage, 0)
163paginater := page.Paginater
164links := make([]string, 0, 4)
165
166if paginater.HasNext() {
167u := *curURL
168queries := u.Query()
169queries.Set("page", fmt.Sprintf("%d", paginater.Next()))
170u.RawQuery = queries.Encode()
171
172links = append(links, fmt.Sprintf("<%s%s>; rel=\"next\"", setting.AppURL, u.RequestURI()[1:]))
173}
174if !paginater.IsLast() {
175u := *curURL
176queries := u.Query()
177queries.Set("page", fmt.Sprintf("%d", paginater.TotalPages()))
178u.RawQuery = queries.Encode()
179
180links = append(links, fmt.Sprintf("<%s%s>; rel=\"last\"", setting.AppURL, u.RequestURI()[1:]))
181}
182if !paginater.IsFirst() {
183u := *curURL
184queries := u.Query()
185queries.Set("page", "1")
186u.RawQuery = queries.Encode()
187
188links = append(links, fmt.Sprintf("<%s%s>; rel=\"first\"", setting.AppURL, u.RequestURI()[1:]))
189}
190if paginater.HasPrevious() {
191u := *curURL
192queries := u.Query()
193queries.Set("page", fmt.Sprintf("%d", paginater.Previous()))
194u.RawQuery = queries.Encode()
195
196links = append(links, fmt.Sprintf("<%s%s>; rel=\"prev\"", setting.AppURL, u.RequestURI()[1:]))
197}
198return links
199}
200
201// SetLinkHeader sets pagination link header by given total number and page size.
202func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
203links := genAPILinks(ctx.Req.URL, total, pageSize, ctx.FormInt("page"))
204
205if len(links) > 0 {
206ctx.RespHeader().Set("Link", strings.Join(links, ","))
207ctx.AppendAccessControlExposeHeaders("Link")
208}
209}
210
211// APIContexter returns apicontext as middleware
212func APIContexter() func(http.Handler) http.Handler {
213return func(next http.Handler) http.Handler {
214return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
215base, baseCleanUp := NewBaseContext(w, req)
216ctx := &APIContext{
217Base: base,
218Cache: cache.GetCache(),
219Repo: &Repository{PullRequest: &PullRequest{}},
220Org: &APIOrganization{},
221}
222defer baseCleanUp()
223
224ctx.Base.AppendContextValue(apiContextKey, ctx)
225ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
226
227// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
228if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
229if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
230ctx.InternalServerError(err)
231return
232}
233}
234
235httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
236ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
237
238next.ServeHTTP(ctx.Resp, ctx.Req)
239})
240}
241}
242
243// NotFound handles 404s for APIContext
244// String will replace message, errors will be added to a slice
245func (ctx *APIContext) NotFound(objs ...any) {
246message := ctx.Locale.TrString("error.not_found")
247var errors []string
248for _, obj := range objs {
249// Ignore nil
250if obj == nil {
251continue
252}
253
254if err, ok := obj.(error); ok {
255errors = append(errors, err.Error())
256} else {
257message = obj.(string)
258}
259}
260
261ctx.JSON(http.StatusNotFound, map[string]any{
262"message": message,
263"url": setting.API.SwaggerURL,
264"errors": errors,
265})
266}
267
268// ReferencesGitRepo injects the GitRepo into the Context
269// you can optional skip the IsEmpty check
270func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) {
271return func(ctx *APIContext) (cancel context.CancelFunc) {
272// Empty repository does not have reference information.
273if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) {
274return nil
275}
276
277// For API calls.
278if ctx.Repo.GitRepo == nil {
279gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
280if err != nil {
281ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err)
282return cancel
283}
284ctx.Repo.GitRepo = gitRepo
285// We opened it, we should close it
286return func() {
287// If it's been set to nil then assume someone else has closed it.
288if ctx.Repo.GitRepo != nil {
289_ = ctx.Repo.GitRepo.Close()
290}
291}
292}
293
294return cancel
295}
296}
297
298// RepoRefForAPI handles repository reference names when the ref name is not explicitly given
299func RepoRefForAPI(next http.Handler) http.Handler {
300return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
301ctx := GetAPIContext(req)
302
303if ctx.Repo.GitRepo == nil {
304ctx.InternalServerError(fmt.Errorf("no open git repo"))
305return
306}
307
308if ref := ctx.FormTrim("ref"); len(ref) > 0 {
309commit, err := ctx.Repo.GitRepo.GetCommit(ref)
310if err != nil {
311if git.IsErrNotExist(err) {
312ctx.NotFound()
313} else {
314ctx.Error(http.StatusInternalServerError, "GetCommit", err)
315}
316return
317}
318ctx.Repo.Commit = commit
319ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
320ctx.Repo.TreePath = ctx.PathParam("*")
321next.ServeHTTP(w, req)
322return
323}
324
325refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
326var err error
327
328if ctx.Repo.GitRepo.IsBranchExist(refName) {
329ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
330if err != nil {
331ctx.InternalServerError(err)
332return
333}
334ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
335} else if ctx.Repo.GitRepo.IsTagExist(refName) {
336ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName)
337if err != nil {
338ctx.InternalServerError(err)
339return
340}
341ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
342} else if len(refName) == ctx.Repo.GetObjectFormat().FullLength() {
343ctx.Repo.CommitID = refName
344ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
345if err != nil {
346ctx.NotFound("GetCommit", err)
347return
348}
349} else {
350ctx.NotFound(fmt.Errorf("not exist: '%s'", ctx.PathParam("*")))
351return
352}
353
354next.ServeHTTP(w, req)
355})
356}
357
358// HasAPIError returns true if error occurs in form validation.
359func (ctx *APIContext) HasAPIError() bool {
360hasErr, ok := ctx.Data["HasError"]
361if !ok {
362return false
363}
364return hasErr.(bool)
365}
366
367// GetErrMsg returns error message in form validation.
368func (ctx *APIContext) GetErrMsg() string {
369msg, _ := ctx.Data["ErrorMsg"].(string)
370if msg == "" {
371msg = "invalid form data"
372}
373return msg
374}
375
376// NotFoundOrServerError use error check function to determine if the error
377// is about not found. It responds with 404 status code for not found error,
378// or error context description for logging purpose of 500 server error.
379func (ctx *APIContext) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
380if errCheck(logErr) {
381ctx.JSON(http.StatusNotFound, nil)
382return
383}
384ctx.Error(http.StatusInternalServerError, "NotFoundOrServerError", logMsg)
385}
386
387// IsUserSiteAdmin returns true if current user is a site admin
388func (ctx *APIContext) IsUserSiteAdmin() bool {
389return ctx.IsSigned && ctx.Doer.IsAdmin
390}
391
392// IsUserRepoAdmin returns true if current user is admin in current repo
393func (ctx *APIContext) IsUserRepoAdmin() bool {
394return ctx.Repo.IsAdmin()
395}
396
397// IsUserRepoWriter returns true if current user has write privilege in current repo
398func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool {
399for _, unitType := range unitTypes {
400if ctx.Repo.CanWrite(unitType) {
401return true
402}
403}
404
405return false
406}
407