code.gitea.io/gitea@v1.19.3/modules/context/context.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2020 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package context 6 7 import ( 8 "context" 9 "crypto/sha256" 10 "encoding/hex" 11 "errors" 12 "fmt" 13 "html" 14 "html/template" 15 "io" 16 "net" 17 "net/http" 18 "net/url" 19 "path" 20 "regexp" 21 "strconv" 22 "strings" 23 texttemplate "text/template" 24 "time" 25 26 "code.gitea.io/gitea/models/db" 27 "code.gitea.io/gitea/models/unit" 28 user_model "code.gitea.io/gitea/models/user" 29 "code.gitea.io/gitea/modules/base" 30 mc "code.gitea.io/gitea/modules/cache" 31 "code.gitea.io/gitea/modules/git" 32 "code.gitea.io/gitea/modules/httpcache" 33 "code.gitea.io/gitea/modules/json" 34 "code.gitea.io/gitea/modules/log" 35 "code.gitea.io/gitea/modules/setting" 36 "code.gitea.io/gitea/modules/templates" 37 "code.gitea.io/gitea/modules/translation" 38 "code.gitea.io/gitea/modules/typesniffer" 39 "code.gitea.io/gitea/modules/util" 40 "code.gitea.io/gitea/modules/web/middleware" 41 42 "gitea.com/go-chi/cache" 43 "gitea.com/go-chi/session" 44 chi "github.com/go-chi/chi/v5" 45 "github.com/unrolled/render" 46 "golang.org/x/crypto/pbkdf2" 47 ) 48 49 // Render represents a template render 50 type Render interface { 51 TemplateLookup(tmpl string) *template.Template 52 HTML(w io.Writer, status int, name string, binding interface{}, htmlOpt ...render.HTMLOptions) error 53 } 54 55 // Context represents context of a request. 56 type Context struct { 57 Resp ResponseWriter 58 Req *http.Request 59 Data map[string]interface{} // data used by MVC templates 60 PageData map[string]interface{} // data used by JavaScript modules in one page, it's `window.config.pageData` 61 Render Render 62 translation.Locale 63 Cache cache.Cache 64 csrf CSRFProtector 65 Flash *middleware.Flash 66 Session session.Store 67 68 Link string // current request URL 69 EscapedLink string 70 Doer *user_model.User 71 IsSigned bool 72 IsBasicAuth bool 73 74 ContextUser *user_model.User 75 Repo *Repository 76 Org *Organization 77 Package *Package 78 } 79 80 // Close frees all resources hold by Context 81 func (ctx *Context) Close() error { 82 var err error 83 if ctx.Req != nil && ctx.Req.MultipartForm != nil { 84 err = ctx.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory 85 } 86 // TODO: close opened repo, and more 87 return err 88 } 89 90 // TrHTMLEscapeArgs runs Tr but pre-escapes all arguments with html.EscapeString. 91 // This is useful if the locale message is intended to only produce HTML content. 92 func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { 93 trArgs := make([]interface{}, len(args)) 94 for i, arg := range args { 95 trArgs[i] = html.EscapeString(arg) 96 } 97 return ctx.Tr(msg, trArgs...) 98 } 99 100 // GetData returns the data 101 func (ctx *Context) GetData() map[string]interface{} { 102 return ctx.Data 103 } 104 105 // IsUserSiteAdmin returns true if current user is a site admin 106 func (ctx *Context) IsUserSiteAdmin() bool { 107 return ctx.IsSigned && ctx.Doer.IsAdmin 108 } 109 110 // IsUserRepoOwner returns true if current user owns current repo 111 func (ctx *Context) IsUserRepoOwner() bool { 112 return ctx.Repo.IsOwner() 113 } 114 115 // IsUserRepoAdmin returns true if current user is admin in current repo 116 func (ctx *Context) IsUserRepoAdmin() bool { 117 return ctx.Repo.IsAdmin() 118 } 119 120 // IsUserRepoWriter returns true if current user has write privilege in current repo 121 func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool { 122 for _, unitType := range unitTypes { 123 if ctx.Repo.CanWrite(unitType) { 124 return true 125 } 126 } 127 128 return false 129 } 130 131 // IsUserRepoReaderSpecific returns true if current user can read current repo's specific part 132 func (ctx *Context) IsUserRepoReaderSpecific(unitType unit.Type) bool { 133 return ctx.Repo.CanRead(unitType) 134 } 135 136 // IsUserRepoReaderAny returns true if current user can read any part of current repo 137 func (ctx *Context) IsUserRepoReaderAny() bool { 138 return ctx.Repo.HasAccess() 139 } 140 141 // RedirectToUser redirect to a differently-named user 142 func RedirectToUser(ctx *Context, userName string, redirectUserID int64) { 143 user, err := user_model.GetUserByID(ctx, redirectUserID) 144 if err != nil { 145 ctx.ServerError("GetUserByID", err) 146 return 147 } 148 149 redirectPath := strings.Replace( 150 ctx.Req.URL.EscapedPath(), 151 url.PathEscape(userName), 152 url.PathEscape(user.Name), 153 1, 154 ) 155 if ctx.Req.URL.RawQuery != "" { 156 redirectPath += "?" + ctx.Req.URL.RawQuery 157 } 158 ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect) 159 } 160 161 // HasAPIError returns true if error occurs in form validation. 162 func (ctx *Context) HasAPIError() bool { 163 hasErr, ok := ctx.Data["HasError"] 164 if !ok { 165 return false 166 } 167 return hasErr.(bool) 168 } 169 170 // GetErrMsg returns error message 171 func (ctx *Context) GetErrMsg() string { 172 return ctx.Data["ErrorMsg"].(string) 173 } 174 175 // HasError returns true if error occurs in form validation. 176 // Attention: this function changes ctx.Data and ctx.Flash 177 func (ctx *Context) HasError() bool { 178 hasErr, ok := ctx.Data["HasError"] 179 if !ok { 180 return false 181 } 182 ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string) 183 ctx.Data["Flash"] = ctx.Flash 184 return hasErr.(bool) 185 } 186 187 // HasValue returns true if value of given name exists. 188 func (ctx *Context) HasValue(name string) bool { 189 _, ok := ctx.Data[name] 190 return ok 191 } 192 193 // RedirectToFirst redirects to first not empty URL 194 func (ctx *Context) RedirectToFirst(location ...string) { 195 for _, loc := range location { 196 if len(loc) == 0 { 197 continue 198 } 199 200 // Unfortunately browsers consider a redirect Location with preceding "//" and "/\" as meaning redirect to "http(s)://REST_OF_PATH" 201 // Therefore we should ignore these redirect locations to prevent open redirects 202 if len(loc) > 1 && loc[0] == '/' && (loc[1] == '/' || loc[1] == '\\') { 203 continue 204 } 205 206 u, err := url.Parse(loc) 207 if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(loc), strings.ToLower(setting.AppURL))) { 208 continue 209 } 210 211 ctx.Redirect(loc) 212 return 213 } 214 215 ctx.Redirect(setting.AppSubURL + "/") 216 } 217 218 var templateExecutingErr = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): executing (?:"(.*)" at <(.*)>: )?`) 219 220 // HTML calls Context.HTML and renders the template to HTTP response 221 func (ctx *Context) HTML(status int, name base.TplName) { 222 log.Debug("Template: %s", name) 223 tmplStartTime := time.Now() 224 if !setting.IsProd { 225 ctx.Data["TemplateName"] = name 226 } 227 ctx.Data["TemplateLoadTimes"] = func() string { 228 return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms" 229 } 230 if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil { 231 if status == http.StatusInternalServerError && name == base.TplName("status/500") { 232 ctx.PlainText(http.StatusInternalServerError, "Unable to find status/500 template") 233 return 234 } 235 if execErr, ok := err.(texttemplate.ExecError); ok { 236 if groups := templateExecutingErr.FindStringSubmatch(err.Error()); len(groups) > 0 { 237 errorTemplateName, lineStr, posStr := groups[1], groups[2], groups[3] 238 target := "" 239 if len(groups) == 6 { 240 target = groups[5] 241 } 242 line, _ := strconv.Atoi(lineStr) // Cannot error out as groups[2] is [1-9][0-9]* 243 pos, _ := strconv.Atoi(posStr) // Cannot error out as groups[3] is [1-9][0-9]* 244 filename, filenameErr := templates.GetAssetFilename("templates/" + errorTemplateName + ".tmpl") 245 if filenameErr != nil { 246 filename = "(template) " + errorTemplateName 247 } 248 if errorTemplateName != string(name) { 249 filename += " (subtemplate of " + string(name) + ")" 250 } 251 err = fmt.Errorf("%w\nin template file %s:\n%s", err, filename, templates.GetLineFromTemplate(errorTemplateName, line, target, pos)) 252 } else { 253 filename, filenameErr := templates.GetAssetFilename("templates/" + execErr.Name + ".tmpl") 254 if filenameErr != nil { 255 filename = "(template) " + execErr.Name 256 } 257 if execErr.Name != string(name) { 258 filename += " (subtemplate of " + string(name) + ")" 259 } 260 err = fmt.Errorf("%w\nin template file %s", err, filename) 261 } 262 } 263 ctx.ServerError("Render failed", err) 264 } 265 } 266 267 // RenderToString renders the template content to a string 268 func (ctx *Context) RenderToString(name base.TplName, data map[string]interface{}) (string, error) { 269 var buf strings.Builder 270 err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data) 271 return buf.String(), err 272 } 273 274 // RenderWithErr used for page has form validation but need to prompt error to users. 275 func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form interface{}) { 276 if form != nil { 277 middleware.AssignForm(form, ctx.Data) 278 } 279 ctx.Flash.ErrorMsg = msg 280 ctx.Data["Flash"] = ctx.Flash 281 ctx.HTML(http.StatusOK, tpl) 282 } 283 284 // NotFound displays a 404 (Not Found) page and prints the given error, if any. 285 func (ctx *Context) NotFound(logMsg string, logErr error) { 286 ctx.notFoundInternal(logMsg, logErr) 287 } 288 289 func (ctx *Context) notFoundInternal(logMsg string, logErr error) { 290 if logErr != nil { 291 log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr) 292 if !setting.IsProd { 293 ctx.Data["ErrorMsg"] = logErr 294 } 295 } 296 297 // response simple message if Accept isn't text/html 298 showHTML := false 299 for _, part := range ctx.Req.Header["Accept"] { 300 if strings.Contains(part, "text/html") { 301 showHTML = true 302 break 303 } 304 } 305 306 if !showHTML { 307 ctx.plainTextInternal(3, http.StatusNotFound, []byte("Not found.\n")) 308 return 309 } 310 311 ctx.Data["IsRepo"] = ctx.Repo.Repository != nil 312 ctx.Data["Title"] = "Page Not Found" 313 ctx.HTML(http.StatusNotFound, base.TplName("status/404")) 314 } 315 316 // ServerError displays a 500 (Internal Server Error) page and prints the given error, if any. 317 func (ctx *Context) ServerError(logMsg string, logErr error) { 318 ctx.serverErrorInternal(logMsg, logErr) 319 } 320 321 func (ctx *Context) serverErrorInternal(logMsg string, logErr error) { 322 if logErr != nil { 323 log.ErrorWithSkip(2, "%s: %v", logMsg, logErr) 324 if _, ok := logErr.(*net.OpError); ok || errors.Is(logErr, &net.OpError{}) { 325 // This is an error within the underlying connection 326 // and further rendering will not work so just return 327 return 328 } 329 330 if !setting.IsProd { 331 ctx.Data["ErrorMsg"] = logErr 332 } 333 } 334 335 ctx.Data["Title"] = "Internal Server Error" 336 ctx.HTML(http.StatusInternalServerError, base.TplName("status/500")) 337 } 338 339 // NotFoundOrServerError use error check function to determine if the error 340 // is about not found. It responds with 404 status code for not found error, 341 // or error context description for logging purpose of 500 server error. 342 func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, err error) { 343 if errCheck(err) { 344 ctx.notFoundInternal(logMsg, err) 345 return 346 } 347 ctx.serverErrorInternal(logMsg, err) 348 } 349 350 // PlainTextBytes renders bytes as plain text 351 func (ctx *Context) plainTextInternal(skip, status int, bs []byte) { 352 statusPrefix := status / 100 353 if statusPrefix == 4 || statusPrefix == 5 { 354 log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs)) 355 } 356 ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") 357 ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff") 358 ctx.Resp.WriteHeader(status) 359 if _, err := ctx.Resp.Write(bs); err != nil { 360 log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err) 361 } 362 } 363 364 // PlainTextBytes renders bytes as plain text 365 func (ctx *Context) PlainTextBytes(status int, bs []byte) { 366 ctx.plainTextInternal(2, status, bs) 367 } 368 369 // PlainText renders content as plain text 370 func (ctx *Context) PlainText(status int, text string) { 371 ctx.plainTextInternal(2, status, []byte(text)) 372 } 373 374 // RespHeader returns the response header 375 func (ctx *Context) RespHeader() http.Header { 376 return ctx.Resp.Header() 377 } 378 379 type ServeHeaderOptions struct { 380 ContentType string // defaults to "application/octet-stream" 381 ContentTypeCharset string 382 ContentLength *int64 383 Disposition string // defaults to "attachment" 384 Filename string 385 CacheDuration time.Duration // defaults to 5 minutes 386 LastModified time.Time 387 } 388 389 // SetServeHeaders sets necessary content serve headers 390 func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) { 391 header := ctx.Resp.Header() 392 393 contentType := typesniffer.ApplicationOctetStream 394 if opts.ContentType != "" { 395 if opts.ContentTypeCharset != "" { 396 contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset) 397 } else { 398 contentType = opts.ContentType 399 } 400 } 401 header.Set("Content-Type", contentType) 402 header.Set("X-Content-Type-Options", "nosniff") 403 404 if opts.ContentLength != nil { 405 header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) 406 } 407 408 if opts.Filename != "" { 409 disposition := opts.Disposition 410 if disposition == "" { 411 disposition = "attachment" 412 } 413 414 backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \" 415 header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename))) 416 header.Set("Access-Control-Expose-Headers", "Content-Disposition") 417 } 418 419 duration := opts.CacheDuration 420 if duration == 0 { 421 duration = 5 * time.Minute 422 } 423 httpcache.SetCacheControlInHeader(header, duration) 424 425 if !opts.LastModified.IsZero() { 426 header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat)) 427 } 428 } 429 430 // ServeContent serves content to http request 431 func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { 432 ctx.SetServeHeaders(opts) 433 http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r) 434 } 435 436 // UploadStream returns the request body or the first form file 437 // Only form files need to get closed. 438 func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) { 439 contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type")) 440 if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") { 441 if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil { 442 return nil, false, err 443 } 444 if ctx.Req.MultipartForm.File == nil { 445 return nil, false, http.ErrMissingFile 446 } 447 for _, files := range ctx.Req.MultipartForm.File { 448 if len(files) > 0 { 449 r, err := files[0].Open() 450 return r, true, err 451 } 452 } 453 return nil, false, http.ErrMissingFile 454 } 455 return ctx.Req.Body, false, nil 456 } 457 458 // Error returned an error to web browser 459 func (ctx *Context) Error(status int, contents ...string) { 460 v := http.StatusText(status) 461 if len(contents) > 0 { 462 v = contents[0] 463 } 464 http.Error(ctx.Resp, v, status) 465 } 466 467 // JSON render content as JSON 468 func (ctx *Context) JSON(status int, content interface{}) { 469 ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") 470 ctx.Resp.WriteHeader(status) 471 if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil { 472 ctx.ServerError("Render JSON failed", err) 473 } 474 } 475 476 // Redirect redirects the request 477 func (ctx *Context) Redirect(location string, status ...int) { 478 code := http.StatusSeeOther 479 if len(status) == 1 { 480 code = status[0] 481 } 482 483 http.Redirect(ctx.Resp, ctx.Req, location, code) 484 } 485 486 // SetCookie convenience function to set most cookies consistently 487 // CSRF and a few others are the exception here 488 func (ctx *Context) SetCookie(name, value string, expiry int) { 489 middleware.SetCookie(ctx.Resp, name, value, 490 expiry, 491 setting.AppSubURL, 492 setting.SessionConfig.Domain, 493 setting.SessionConfig.Secure, 494 true, 495 middleware.SameSite(setting.SessionConfig.SameSite)) 496 } 497 498 // DeleteCookie convenience function to delete most cookies consistently 499 // CSRF and a few others are the exception here 500 func (ctx *Context) DeleteCookie(name string) { 501 middleware.SetCookie(ctx.Resp, name, "", 502 -1, 503 setting.AppSubURL, 504 setting.SessionConfig.Domain, 505 setting.SessionConfig.Secure, 506 true, 507 middleware.SameSite(setting.SessionConfig.SameSite)) 508 } 509 510 // GetCookie returns given cookie value from request header. 511 func (ctx *Context) GetCookie(name string) string { 512 return middleware.GetCookie(ctx.Req, name) 513 } 514 515 // GetSuperSecureCookie returns given cookie value from request header with secret string. 516 func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) { 517 val := ctx.GetCookie(name) 518 return ctx.CookieDecrypt(secret, val) 519 } 520 521 // CookieDecrypt returns given value from with secret string. 522 func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) { 523 if val == "" { 524 return "", false 525 } 526 527 text, err := hex.DecodeString(val) 528 if err != nil { 529 return "", false 530 } 531 532 key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) 533 text, err = util.AESGCMDecrypt(key, text) 534 return string(text), err == nil 535 } 536 537 // SetSuperSecureCookie sets given cookie value to response header with secret string. 538 func (ctx *Context) SetSuperSecureCookie(secret, name, value string, expiry int) { 539 text := ctx.CookieEncrypt(secret, value) 540 541 ctx.SetCookie(name, text, expiry) 542 } 543 544 // CookieEncrypt encrypts a given value using the provided secret 545 func (ctx *Context) CookieEncrypt(secret, value string) string { 546 key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) 547 text, err := util.AESGCMEncrypt(key, []byte(value)) 548 if err != nil { 549 panic("error encrypting cookie: " + err.Error()) 550 } 551 552 return hex.EncodeToString(text) 553 } 554 555 // GetCookieInt returns cookie result in int type. 556 func (ctx *Context) GetCookieInt(name string) int { 557 r, _ := strconv.Atoi(ctx.GetCookie(name)) 558 return r 559 } 560 561 // GetCookieInt64 returns cookie result in int64 type. 562 func (ctx *Context) GetCookieInt64(name string) int64 { 563 r, _ := strconv.ParseInt(ctx.GetCookie(name), 10, 64) 564 return r 565 } 566 567 // GetCookieFloat64 returns cookie result in float64 type. 568 func (ctx *Context) GetCookieFloat64(name string) float64 { 569 v, _ := strconv.ParseFloat(ctx.GetCookie(name), 64) 570 return v 571 } 572 573 // RemoteAddr returns the client machie ip address 574 func (ctx *Context) RemoteAddr() string { 575 return ctx.Req.RemoteAddr 576 } 577 578 // Params returns the param on route 579 func (ctx *Context) Params(p string) string { 580 s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":"))) 581 return s 582 } 583 584 // ParamsInt64 returns the param on route as int64 585 func (ctx *Context) ParamsInt64(p string) int64 { 586 v, _ := strconv.ParseInt(ctx.Params(p), 10, 64) 587 return v 588 } 589 590 // SetParams set params into routes 591 func (ctx *Context) SetParams(k, v string) { 592 chiCtx := chi.RouteContext(ctx) 593 chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v)) 594 } 595 596 // Write writes data to web browser 597 func (ctx *Context) Write(bs []byte) (int, error) { 598 return ctx.Resp.Write(bs) 599 } 600 601 // Written returns true if there are something sent to web browser 602 func (ctx *Context) Written() bool { 603 return ctx.Resp.Status() > 0 604 } 605 606 // Status writes status code 607 func (ctx *Context) Status(status int) { 608 ctx.Resp.WriteHeader(status) 609 } 610 611 // Deadline is part of the interface for context.Context and we pass this to the request context 612 func (ctx *Context) Deadline() (deadline time.Time, ok bool) { 613 return ctx.Req.Context().Deadline() 614 } 615 616 // Done is part of the interface for context.Context and we pass this to the request context 617 func (ctx *Context) Done() <-chan struct{} { 618 return ctx.Req.Context().Done() 619 } 620 621 // Err is part of the interface for context.Context and we pass this to the request context 622 func (ctx *Context) Err() error { 623 return ctx.Req.Context().Err() 624 } 625 626 // Value is part of the interface for context.Context and we pass this to the request context 627 func (ctx *Context) Value(key interface{}) interface{} { 628 if key == git.RepositoryContextKey && ctx.Repo != nil { 629 return ctx.Repo.GitRepo 630 } 631 632 return ctx.Req.Context().Value(key) 633 } 634 635 // SetTotalCountHeader set "X-Total-Count" header 636 func (ctx *Context) SetTotalCountHeader(total int64) { 637 ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) 638 ctx.AppendAccessControlExposeHeaders("X-Total-Count") 639 } 640 641 // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header 642 func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) { 643 val := ctx.RespHeader().Get("Access-Control-Expose-Headers") 644 if len(val) != 0 { 645 ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) 646 } else { 647 ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) 648 } 649 } 650 651 // Handler represents a custom handler 652 type Handler func(*Context) 653 654 type contextKeyType struct{} 655 656 var contextKey interface{} = contextKeyType{} 657 658 // WithContext set up install context in request 659 func WithContext(req *http.Request, ctx *Context) *http.Request { 660 return req.WithContext(context.WithValue(req.Context(), contextKey, ctx)) 661 } 662 663 // GetContext retrieves install context from request 664 func GetContext(req *http.Request) *Context { 665 return req.Context().Value(contextKey).(*Context) 666 } 667 668 // GetContextUser returns context user 669 func GetContextUser(req *http.Request) *user_model.User { 670 if apiContext, ok := req.Context().Value(apiContextKey).(*APIContext); ok { 671 return apiContext.Doer 672 } 673 if ctx, ok := req.Context().Value(contextKey).(*Context); ok { 674 return ctx.Doer 675 } 676 return nil 677 } 678 679 func getCsrfOpts() CsrfOptions { 680 return CsrfOptions{ 681 Secret: setting.SecretKey, 682 Cookie: setting.CSRFCookieName, 683 SetCookie: true, 684 Secure: setting.SessionConfig.Secure, 685 CookieHTTPOnly: setting.CSRFCookieHTTPOnly, 686 Header: "X-Csrf-Token", 687 CookieDomain: setting.SessionConfig.Domain, 688 CookiePath: setting.SessionConfig.CookiePath, 689 SameSite: setting.SessionConfig.SameSite, 690 } 691 } 692 693 // Contexter initializes a classic context for a request. 694 func Contexter(ctx context.Context) func(next http.Handler) http.Handler { 695 _, rnd := templates.HTMLRenderer(ctx) 696 csrfOpts := getCsrfOpts() 697 if !setting.IsProd { 698 CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose 699 } 700 return func(next http.Handler) http.Handler { 701 return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 702 locale := middleware.Locale(resp, req) 703 startTime := time.Now() 704 link := setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/") 705 706 ctx := Context{ 707 Resp: NewResponse(resp), 708 Cache: mc.GetCache(), 709 Locale: locale, 710 Link: link, 711 Render: rnd, 712 Session: session.GetSession(req), 713 Repo: &Repository{ 714 PullRequest: &PullRequest{}, 715 }, 716 Org: &Organization{}, 717 Data: map[string]interface{}{ 718 "CurrentURL": setting.AppSubURL + req.URL.RequestURI(), 719 "PageStartTime": startTime, 720 "Link": link, 721 "RunModeIsProd": setting.IsProd, 722 }, 723 } 724 defer ctx.Close() 725 726 // PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules 727 ctx.PageData = map[string]interface{}{} 728 ctx.Data["PageData"] = ctx.PageData 729 ctx.Data["Context"] = &ctx 730 731 ctx.Req = WithContext(req, &ctx) 732 ctx.csrf = PrepareCSRFProtector(csrfOpts, &ctx) 733 734 // Get flash. 735 flashCookie := ctx.GetCookie("macaron_flash") 736 vals, _ := url.ParseQuery(flashCookie) 737 if len(vals) > 0 { 738 f := &middleware.Flash{ 739 DataStore: &ctx, 740 Values: vals, 741 ErrorMsg: vals.Get("error"), 742 SuccessMsg: vals.Get("success"), 743 InfoMsg: vals.Get("info"), 744 WarningMsg: vals.Get("warning"), 745 } 746 ctx.Data["Flash"] = f 747 } 748 749 f := &middleware.Flash{ 750 DataStore: &ctx, 751 Values: url.Values{}, 752 ErrorMsg: "", 753 WarningMsg: "", 754 InfoMsg: "", 755 SuccessMsg: "", 756 } 757 ctx.Resp.Before(func(resp ResponseWriter) { 758 if flash := f.Encode(); len(flash) > 0 { 759 middleware.SetCookie(resp, "macaron_flash", flash, 0, 760 setting.SessionConfig.CookiePath, 761 middleware.Domain(setting.SessionConfig.Domain), 762 middleware.HTTPOnly(true), 763 middleware.Secure(setting.SessionConfig.Secure), 764 middleware.SameSite(setting.SessionConfig.SameSite), 765 ) 766 return 767 } 768 769 middleware.SetCookie(ctx.Resp, "macaron_flash", "", -1, 770 setting.SessionConfig.CookiePath, 771 middleware.Domain(setting.SessionConfig.Domain), 772 middleware.HTTPOnly(true), 773 middleware.Secure(setting.SessionConfig.Secure), 774 middleware.SameSite(setting.SessionConfig.SameSite), 775 ) 776 }) 777 778 ctx.Flash = f 779 780 // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. 781 if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { 782 if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size 783 ctx.ServerError("ParseMultipartForm", err) 784 return 785 } 786 } 787 788 httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform") 789 ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) 790 791 ctx.Data["CsrfToken"] = ctx.csrf.GetToken() 792 ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`) 793 794 // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these 795 ctx.Data["IsLandingPageHome"] = setting.LandingPageURL == setting.LandingPageHome 796 ctx.Data["IsLandingPageExplore"] = setting.LandingPageURL == setting.LandingPageExplore 797 ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations 798 799 ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton 800 ctx.Data["ShowMilestonesDashboardPage"] = setting.Service.ShowMilestonesDashboardPage 801 ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding 802 ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion 803 804 ctx.Data["EnableSwagger"] = setting.API.EnableSwagger 805 ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn 806 ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations 807 ctx.Data["DisableStars"] = setting.Repository.DisableStars 808 ctx.Data["EnableActions"] = setting.Actions.Enabled 809 810 ctx.Data["ManifestData"] = setting.ManifestData 811 812 ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled() 813 ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled() 814 ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled() 815 ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled() 816 ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled() 817 818 ctx.Data["locale"] = locale 819 ctx.Data["AllLangs"] = translation.AllLangs() 820 821 next.ServeHTTP(ctx.Resp, ctx.Req) 822 823 // Handle adding signedUserName to the context for the AccessLogger 824 usernameInterface := ctx.Data["SignedUserName"] 825 identityPtrInterface := ctx.Req.Context().Value(signedUserNameStringPointerKey) 826 if usernameInterface != nil && identityPtrInterface != nil { 827 username := usernameInterface.(string) 828 identityPtr := identityPtrInterface.(*string) 829 if identityPtr != nil && username != "" { 830 *identityPtr = username 831 } 832 } 833 }) 834 } 835 } 836 837 // SearchOrderByMap represents all possible search order 838 var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ 839 "asc": { 840 "alpha": db.SearchOrderByAlphabetically, 841 "created": db.SearchOrderByOldest, 842 "updated": db.SearchOrderByLeastUpdated, 843 "size": db.SearchOrderBySize, 844 "id": db.SearchOrderByID, 845 }, 846 "desc": { 847 "alpha": db.SearchOrderByAlphabeticallyReverse, 848 "created": db.SearchOrderByNewest, 849 "updated": db.SearchOrderByRecentUpdated, 850 "size": db.SearchOrderBySizeReverse, 851 "id": db.SearchOrderByIDReverse, 852 }, 853 }