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