mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-29 14:27:44 +01:00
280 lines
8.7 KiB
Go
280 lines
8.7 KiB
Go
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||
|
// SPDX-License-Identifier: MIT
|
||
|
|
||
|
package context
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"path"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
user_model "code.gitea.io/gitea/models/user"
|
||
|
"code.gitea.io/gitea/modules/base"
|
||
|
"code.gitea.io/gitea/modules/json"
|
||
|
"code.gitea.io/gitea/modules/log"
|
||
|
"code.gitea.io/gitea/modules/setting"
|
||
|
"code.gitea.io/gitea/modules/templates"
|
||
|
"code.gitea.io/gitea/modules/web/middleware"
|
||
|
)
|
||
|
|
||
|
// SetTotalCountHeader set "X-Total-Count" header
|
||
|
func (ctx *Context) SetTotalCountHeader(total int64) {
|
||
|
ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total))
|
||
|
ctx.AppendAccessControlExposeHeaders("X-Total-Count")
|
||
|
}
|
||
|
|
||
|
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
|
||
|
func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) {
|
||
|
val := ctx.RespHeader().Get("Access-Control-Expose-Headers")
|
||
|
if len(val) != 0 {
|
||
|
ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", ")))
|
||
|
} else {
|
||
|
ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", "))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Written returns true if there are something sent to web browser
|
||
|
func (ctx *Context) Written() bool {
|
||
|
return ctx.Resp.Status() > 0
|
||
|
}
|
||
|
|
||
|
// Status writes status code
|
||
|
func (ctx *Context) Status(status int) {
|
||
|
ctx.Resp.WriteHeader(status)
|
||
|
}
|
||
|
|
||
|
// Write writes data to web browser
|
||
|
func (ctx *Context) Write(bs []byte) (int, error) {
|
||
|
return ctx.Resp.Write(bs)
|
||
|
}
|
||
|
|
||
|
// RedirectToUser redirect to a differently-named user
|
||
|
func RedirectToUser(ctx *Context, userName string, redirectUserID int64) {
|
||
|
user, err := user_model.GetUserByID(ctx, redirectUserID)
|
||
|
if err != nil {
|
||
|
ctx.ServerError("GetUserByID", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
redirectPath := strings.Replace(
|
||
|
ctx.Req.URL.EscapedPath(),
|
||
|
url.PathEscape(userName),
|
||
|
url.PathEscape(user.Name),
|
||
|
1,
|
||
|
)
|
||
|
if ctx.Req.URL.RawQuery != "" {
|
||
|
redirectPath += "?" + ctx.Req.URL.RawQuery
|
||
|
}
|
||
|
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
|
||
|
}
|
||
|
|
||
|
// RedirectToFirst redirects to first not empty URL
|
||
|
func (ctx *Context) RedirectToFirst(location ...string) {
|
||
|
for _, loc := range location {
|
||
|
if len(loc) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Unfortunately browsers consider a redirect Location with preceding "//" and "/\" as meaning redirect to "http(s)://REST_OF_PATH"
|
||
|
// Therefore we should ignore these redirect locations to prevent open redirects
|
||
|
if len(loc) > 1 && loc[0] == '/' && (loc[1] == '/' || loc[1] == '\\') {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
u, err := url.Parse(loc)
|
||
|
if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(loc), strings.ToLower(setting.AppURL))) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
ctx.Redirect(loc)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
ctx.Redirect(setting.AppSubURL + "/")
|
||
|
}
|
||
|
|
||
|
const tplStatus500 base.TplName = "status/500"
|
||
|
|
||
|
// HTML calls Context.HTML and renders the template to HTTP response
|
||
|
func (ctx *Context) HTML(status int, name base.TplName) {
|
||
|
log.Debug("Template: %s", name)
|
||
|
|
||
|
tmplStartTime := time.Now()
|
||
|
if !setting.IsProd {
|
||
|
ctx.Data["TemplateName"] = name
|
||
|
}
|
||
|
ctx.Data["TemplateLoadTimes"] = func() string {
|
||
|
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
|
||
|
}
|
||
|
|
||
|
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data)
|
||
|
if err == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// if rendering fails, show error page
|
||
|
if name != tplStatus500 {
|
||
|
err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err))
|
||
|
ctx.ServerError("Render failed", err) // show the 500 error page
|
||
|
} else {
|
||
|
ctx.PlainText(http.StatusInternalServerError, "Unable to render status/500 page, the template system is broken, or Gitea can't find your template files.")
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// RenderToString renders the template content to a string
|
||
|
func (ctx *Context) RenderToString(name base.TplName, data map[string]interface{}) (string, error) {
|
||
|
var buf strings.Builder
|
||
|
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data)
|
||
|
return buf.String(), err
|
||
|
}
|
||
|
|
||
|
// RenderWithErr used for page has form validation but need to prompt error to users.
|
||
|
func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form interface{}) {
|
||
|
if form != nil {
|
||
|
middleware.AssignForm(form, ctx.Data)
|
||
|
}
|
||
|
ctx.Flash.ErrorMsg = msg
|
||
|
ctx.Data["Flash"] = ctx.Flash
|
||
|
ctx.HTML(http.StatusOK, tpl)
|
||
|
}
|
||
|
|
||
|
// NotFound displays a 404 (Not Found) page and prints the given error, if any.
|
||
|
func (ctx *Context) NotFound(logMsg string, logErr error) {
|
||
|
ctx.notFoundInternal(logMsg, logErr)
|
||
|
}
|
||
|
|
||
|
func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
||
|
if logErr != nil {
|
||
|
log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr)
|
||
|
if !setting.IsProd {
|
||
|
ctx.Data["ErrorMsg"] = logErr
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// response simple message if Accept isn't text/html
|
||
|
showHTML := false
|
||
|
for _, part := range ctx.Req.Header["Accept"] {
|
||
|
if strings.Contains(part, "text/html") {
|
||
|
showHTML = true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if !showHTML {
|
||
|
ctx.plainTextInternal(3, http.StatusNotFound, []byte("Not found.\n"))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||
|
ctx.Data["Title"] = "Page Not Found"
|
||
|
ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
|
||
|
}
|
||
|
|
||
|
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
|
||
|
func (ctx *Context) ServerError(logMsg string, logErr error) {
|
||
|
ctx.serverErrorInternal(logMsg, logErr)
|
||
|
}
|
||
|
|
||
|
func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
|
||
|
if logErr != nil {
|
||
|
log.ErrorWithSkip(2, "%s: %v", logMsg, logErr)
|
||
|
if _, ok := logErr.(*net.OpError); ok || errors.Is(logErr, &net.OpError{}) {
|
||
|
// This is an error within the underlying connection
|
||
|
// and further rendering will not work so just return
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// it's safe to show internal error to admin users, and it helps
|
||
|
if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
||
|
ctx.Data["ErrorMsg"] = fmt.Sprintf("%s, %s", logMsg, logErr)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ctx.Data["Title"] = "Internal Server Error"
|
||
|
ctx.HTML(http.StatusInternalServerError, tplStatus500)
|
||
|
}
|
||
|
|
||
|
// 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 *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
|
||
|
if errCheck(logErr) {
|
||
|
ctx.notFoundInternal(logMsg, logErr)
|
||
|
return
|
||
|
}
|
||
|
ctx.serverErrorInternal(logMsg, logErr)
|
||
|
}
|
||
|
|
||
|
// PlainTextBytes renders bytes as plain text
|
||
|
func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
|
||
|
statusPrefix := status / 100
|
||
|
if statusPrefix == 4 || statusPrefix == 5 {
|
||
|
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
|
||
|
}
|
||
|
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||
|
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||
|
ctx.Resp.WriteHeader(status)
|
||
|
if _, err := ctx.Resp.Write(bs); err != nil {
|
||
|
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// PlainTextBytes renders bytes as plain text
|
||
|
func (ctx *Context) PlainTextBytes(status int, bs []byte) {
|
||
|
ctx.plainTextInternal(2, status, bs)
|
||
|
}
|
||
|
|
||
|
// PlainText renders content as plain text
|
||
|
func (ctx *Context) PlainText(status int, text string) {
|
||
|
ctx.plainTextInternal(2, status, []byte(text))
|
||
|
}
|
||
|
|
||
|
// RespHeader returns the response header
|
||
|
func (ctx *Context) RespHeader() http.Header {
|
||
|
return ctx.Resp.Header()
|
||
|
}
|
||
|
|
||
|
// Error returned an error to web browser
|
||
|
func (ctx *Context) Error(status int, contents ...string) {
|
||
|
v := http.StatusText(status)
|
||
|
if len(contents) > 0 {
|
||
|
v = contents[0]
|
||
|
}
|
||
|
http.Error(ctx.Resp, v, status)
|
||
|
}
|
||
|
|
||
|
// JSON render content as JSON
|
||
|
func (ctx *Context) JSON(status int, content interface{}) {
|
||
|
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
|
||
|
ctx.Resp.WriteHeader(status)
|
||
|
if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil {
|
||
|
ctx.ServerError("Render JSON failed", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Redirect redirects the request
|
||
|
func (ctx *Context) Redirect(location string, status ...int) {
|
||
|
code := http.StatusSeeOther
|
||
|
if len(status) == 1 {
|
||
|
code = status[0]
|
||
|
}
|
||
|
|
||
|
if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
|
||
|
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
|
||
|
// 1. the first request to "/my-path" contains cookie
|
||
|
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
|
||
|
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
|
||
|
// 4. then the browser accepts the empty session, then the user is logged out
|
||
|
// So in this case, we should remove the session cookie from the response header
|
||
|
removeSessionCookieHeader(ctx.Resp)
|
||
|
}
|
||
|
http.Redirect(ctx.Resp, ctx.Req, location, code)
|
||
|
}
|