mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Split "modules/context.go" to separate files (#24569)
The "modules/context.go" is too large to maintain. This PR splits it to separate files, eg: context_request.go, context_response.go, context_serve.go This PR will help: 1. The future refactoring for Gitea's web context (eg: simplify the context) 2. Introduce proper "range request" support 3. Introduce context function This PR only moves code, doesn't change any logic.
This commit is contained in:
		@@ -6,45 +6,28 @@ package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	mc "code.gitea.io/gitea/modules/cache"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/httpcache"
 | 
			
		||||
	"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/translation"
 | 
			
		||||
	"code.gitea.io/gitea/modules/typesniffer"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
			
		||||
 | 
			
		||||
	"gitea.com/go-chi/cache"
 | 
			
		||||
	"gitea.com/go-chi/session"
 | 
			
		||||
	chi "github.com/go-chi/chi/v5"
 | 
			
		||||
	"github.com/minio/sha256-simd"
 | 
			
		||||
	"golang.org/x/crypto/pbkdf2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const CookieNameFlash = "gitea_flash"
 | 
			
		||||
 | 
			
		||||
// Render represents a template render
 | 
			
		||||
type Render interface {
 | 
			
		||||
	TemplateLookup(tmpl string) (templates.TemplateExecutor, error)
 | 
			
		||||
@@ -56,13 +39,13 @@ type Context struct {
 | 
			
		||||
	Resp     ResponseWriter
 | 
			
		||||
	Req      *http.Request
 | 
			
		||||
	Data     middleware.ContextData // data used by MVC templates
 | 
			
		||||
	PageData map[string]interface{} // data used by JavaScript modules in one page, it's `window.config.pageData`
 | 
			
		||||
	PageData map[string]any         // data used by JavaScript modules in one page, it's `window.config.pageData`
 | 
			
		||||
	Render   Render
 | 
			
		||||
	translation.Locale
 | 
			
		||||
	Cache   cache.Cache
 | 
			
		||||
	Csrf    CSRFProtector
 | 
			
		||||
	Flash   *middleware.Flash
 | 
			
		||||
	Session session.Store
 | 
			
		||||
	Locale   translation.Locale
 | 
			
		||||
	Cache    cache.Cache
 | 
			
		||||
	Csrf     CSRFProtector
 | 
			
		||||
	Flash    *middleware.Flash
 | 
			
		||||
	Session  session.Store
 | 
			
		||||
 | 
			
		||||
	Link        string // current request URL
 | 
			
		||||
	EscapedLink string
 | 
			
		||||
@@ -86,513 +69,22 @@ func (ctx *Context) Close() error {
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TrHTMLEscapeArgs runs Tr but pre-escapes all arguments with html.EscapeString.
 | 
			
		||||
// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
 | 
			
		||||
// This is useful if the locale message is intended to only produce HTML content.
 | 
			
		||||
func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
 | 
			
		||||
	trArgs := make([]interface{}, len(args))
 | 
			
		||||
	for i, arg := range args {
 | 
			
		||||
		trArgs[i] = html.EscapeString(arg)
 | 
			
		||||
	}
 | 
			
		||||
	return ctx.Tr(msg, trArgs...)
 | 
			
		||||
	return ctx.Locale.Tr(msg, trArgs...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetData returns the data
 | 
			
		||||
func (ctx *Context) GetData() middleware.ContextData {
 | 
			
		||||
	return ctx.Data
 | 
			
		||||
func (ctx *Context) Tr(msg string, args ...any) string {
 | 
			
		||||
	return ctx.Locale.Tr(msg, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserSiteAdmin returns true if current user is a site admin
 | 
			
		||||
func (ctx *Context) IsUserSiteAdmin() bool {
 | 
			
		||||
	return ctx.IsSigned && ctx.Doer.IsAdmin
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoOwner returns true if current user owns current repo
 | 
			
		||||
func (ctx *Context) IsUserRepoOwner() bool {
 | 
			
		||||
	return ctx.Repo.IsOwner()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoAdmin returns true if current user is admin in current repo
 | 
			
		||||
func (ctx *Context) IsUserRepoAdmin() bool {
 | 
			
		||||
	return ctx.Repo.IsAdmin()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoWriter returns true if current user has write privilege in current repo
 | 
			
		||||
func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool {
 | 
			
		||||
	for _, unitType := range unitTypes {
 | 
			
		||||
		if ctx.Repo.CanWrite(unitType) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoReaderSpecific returns true if current user can read current repo's specific part
 | 
			
		||||
func (ctx *Context) IsUserRepoReaderSpecific(unitType unit.Type) bool {
 | 
			
		||||
	return ctx.Repo.CanRead(unitType)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoReaderAny returns true if current user can read any part of current repo
 | 
			
		||||
func (ctx *Context) IsUserRepoReaderAny() bool {
 | 
			
		||||
	return ctx.Repo.HasAccess()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasAPIError returns true if error occurs in form validation.
 | 
			
		||||
func (ctx *Context) HasAPIError() bool {
 | 
			
		||||
	hasErr, ok := ctx.Data["HasError"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return hasErr.(bool)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetErrMsg returns error message
 | 
			
		||||
func (ctx *Context) GetErrMsg() string {
 | 
			
		||||
	return ctx.Data["ErrorMsg"].(string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasError returns true if error occurs in form validation.
 | 
			
		||||
// Attention: this function changes ctx.Data and ctx.Flash
 | 
			
		||||
func (ctx *Context) HasError() bool {
 | 
			
		||||
	hasErr, ok := ctx.Data["HasError"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
 | 
			
		||||
	ctx.Data["Flash"] = ctx.Flash
 | 
			
		||||
	return hasErr.(bool)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasValue returns true if value of given name exists.
 | 
			
		||||
func (ctx *Context) HasValue(name string) bool {
 | 
			
		||||
	_, ok := ctx.Data[name]
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ServeHeaderOptions struct {
 | 
			
		||||
	ContentType        string // defaults to "application/octet-stream"
 | 
			
		||||
	ContentTypeCharset string
 | 
			
		||||
	ContentLength      *int64
 | 
			
		||||
	Disposition        string // defaults to "attachment"
 | 
			
		||||
	Filename           string
 | 
			
		||||
	CacheDuration      time.Duration // defaults to 5 minutes
 | 
			
		||||
	LastModified       time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetServeHeaders sets necessary content serve headers
 | 
			
		||||
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
 | 
			
		||||
	header := ctx.Resp.Header()
 | 
			
		||||
 | 
			
		||||
	contentType := typesniffer.ApplicationOctetStream
 | 
			
		||||
	if opts.ContentType != "" {
 | 
			
		||||
		if opts.ContentTypeCharset != "" {
 | 
			
		||||
			contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
 | 
			
		||||
		} else {
 | 
			
		||||
			contentType = opts.ContentType
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	header.Set("Content-Type", contentType)
 | 
			
		||||
	header.Set("X-Content-Type-Options", "nosniff")
 | 
			
		||||
 | 
			
		||||
	if opts.ContentLength != nil {
 | 
			
		||||
		header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Filename != "" {
 | 
			
		||||
		disposition := opts.Disposition
 | 
			
		||||
		if disposition == "" {
 | 
			
		||||
			disposition = "attachment"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
 | 
			
		||||
		header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
 | 
			
		||||
		header.Set("Access-Control-Expose-Headers", "Content-Disposition")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	duration := opts.CacheDuration
 | 
			
		||||
	if duration == 0 {
 | 
			
		||||
		duration = 5 * time.Minute
 | 
			
		||||
	}
 | 
			
		||||
	httpcache.SetCacheControlInHeader(header, duration)
 | 
			
		||||
 | 
			
		||||
	if !opts.LastModified.IsZero() {
 | 
			
		||||
		header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServeContent serves content to http request
 | 
			
		||||
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
 | 
			
		||||
	ctx.SetServeHeaders(opts)
 | 
			
		||||
	http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadStream returns the request body or the first form file
 | 
			
		||||
// Only form files need to get closed.
 | 
			
		||||
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {
 | 
			
		||||
	contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type"))
 | 
			
		||||
	if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") {
 | 
			
		||||
		if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil {
 | 
			
		||||
			return nil, false, err
 | 
			
		||||
		}
 | 
			
		||||
		if ctx.Req.MultipartForm.File == nil {
 | 
			
		||||
			return nil, false, http.ErrMissingFile
 | 
			
		||||
		}
 | 
			
		||||
		for _, files := range ctx.Req.MultipartForm.File {
 | 
			
		||||
			if len(files) > 0 {
 | 
			
		||||
				r, err := files[0].Open()
 | 
			
		||||
				return r, true, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return nil, false, http.ErrMissingFile
 | 
			
		||||
	}
 | 
			
		||||
	return ctx.Req.Body, false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func removeSessionCookieHeader(w http.ResponseWriter) {
 | 
			
		||||
	cookies := w.Header()["Set-Cookie"]
 | 
			
		||||
	w.Header().Del("Set-Cookie")
 | 
			
		||||
	for _, cookie := range cookies {
 | 
			
		||||
		if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		w.Header().Add("Set-Cookie", cookie)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSiteCookie convenience function to set most cookies consistently
 | 
			
		||||
// CSRF and a few others are the exception here
 | 
			
		||||
func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
 | 
			
		||||
	middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteSiteCookie convenience function to delete most cookies consistently
 | 
			
		||||
// CSRF and a few others are the exception here
 | 
			
		||||
func (ctx *Context) DeleteSiteCookie(name string) {
 | 
			
		||||
	middleware.SetSiteCookie(ctx.Resp, name, "", -1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSiteCookie returns given cookie value from request header.
 | 
			
		||||
func (ctx *Context) GetSiteCookie(name string) string {
 | 
			
		||||
	return middleware.GetSiteCookie(ctx.Req, name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSuperSecureCookie returns given cookie value from request header with secret string.
 | 
			
		||||
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
 | 
			
		||||
	val := ctx.GetSiteCookie(name)
 | 
			
		||||
	return ctx.CookieDecrypt(secret, val)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CookieDecrypt returns given value from with secret string.
 | 
			
		||||
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
 | 
			
		||||
	if val == "" {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	text, err := hex.DecodeString(val)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
 | 
			
		||||
	text, err = util.AESGCMDecrypt(key, text)
 | 
			
		||||
	return string(text), err == nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSuperSecureCookie sets given cookie value to response header with secret string.
 | 
			
		||||
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) {
 | 
			
		||||
	text := ctx.CookieEncrypt(secret, value)
 | 
			
		||||
	ctx.SetSiteCookie(name, text, maxAge)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CookieEncrypt encrypts a given value using the provided secret
 | 
			
		||||
func (ctx *Context) CookieEncrypt(secret, value string) string {
 | 
			
		||||
	key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
 | 
			
		||||
	text, err := util.AESGCMEncrypt(key, []byte(value))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic("error encrypting cookie: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return hex.EncodeToString(text)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCookieInt returns cookie result in int type.
 | 
			
		||||
func (ctx *Context) GetCookieInt(name string) int {
 | 
			
		||||
	r, _ := strconv.Atoi(ctx.GetSiteCookie(name))
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCookieInt64 returns cookie result in int64 type.
 | 
			
		||||
func (ctx *Context) GetCookieInt64(name string) int64 {
 | 
			
		||||
	r, _ := strconv.ParseInt(ctx.GetSiteCookie(name), 10, 64)
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCookieFloat64 returns cookie result in float64 type.
 | 
			
		||||
func (ctx *Context) GetCookieFloat64(name string) float64 {
 | 
			
		||||
	v, _ := strconv.ParseFloat(ctx.GetSiteCookie(name), 64)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RemoteAddr returns the client machie ip address
 | 
			
		||||
func (ctx *Context) RemoteAddr() string {
 | 
			
		||||
	return ctx.Req.RemoteAddr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Params returns the param on route
 | 
			
		||||
func (ctx *Context) Params(p string) string {
 | 
			
		||||
	s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":")))
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParamsInt64 returns the param on route as int64
 | 
			
		||||
func (ctx *Context) ParamsInt64(p string) int64 {
 | 
			
		||||
	v, _ := strconv.ParseInt(ctx.Params(p), 10, 64)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetParams set params into routes
 | 
			
		||||
func (ctx *Context) SetParams(k, v string) {
 | 
			
		||||
	chiCtx := chi.RouteContext(ctx)
 | 
			
		||||
	chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Write writes data to web browser
 | 
			
		||||
func (ctx *Context) Write(bs []byte) (int, error) {
 | 
			
		||||
	return ctx.Resp.Write(bs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
func (ctx *Context) TrN(cnt any, key1, keyN string, args ...any) string {
 | 
			
		||||
	return ctx.Locale.TrN(cnt, key1, keyN, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deadline is part of the interface for context.Context and we pass this to the request context
 | 
			
		||||
@@ -621,25 +113,6 @@ func (ctx *Context) Value(key interface{}) interface{} {
 | 
			
		||||
	return ctx.Req.Context().Value(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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, ", "))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handler represents a custom handler
 | 
			
		||||
type Handler func(*Context)
 | 
			
		||||
 | 
			
		||||
type contextKeyType struct{}
 | 
			
		||||
 | 
			
		||||
var contextKey interface{} = contextKeyType{}
 | 
			
		||||
@@ -657,19 +130,10 @@ func GetContext(req *http.Request) *Context {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetContextUser returns context user
 | 
			
		||||
func GetContextUser(req *http.Request) *user_model.User {
 | 
			
		||||
	if apiContext, ok := req.Context().Value(apiContextKey).(*APIContext); ok {
 | 
			
		||||
		return apiContext.Doer
 | 
			
		||||
	}
 | 
			
		||||
	if ctx, ok := req.Context().Value(contextKey).(*Context); ok {
 | 
			
		||||
		return ctx.Doer
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getCsrfOpts() CsrfOptions {
 | 
			
		||||
	return CsrfOptions{
 | 
			
		||||
// Contexter initializes a classic context for a request.
 | 
			
		||||
func Contexter() func(next http.Handler) http.Handler {
 | 
			
		||||
	rnd := templates.HTMLRenderer()
 | 
			
		||||
	csrfOpts := CsrfOptions{
 | 
			
		||||
		Secret:         setting.SecretKey,
 | 
			
		||||
		Cookie:         setting.CSRFCookieName,
 | 
			
		||||
		SetCookie:      true,
 | 
			
		||||
@@ -680,12 +144,6 @@ func getCsrfOpts() CsrfOptions {
 | 
			
		||||
		CookiePath:     setting.SessionConfig.CookiePath,
 | 
			
		||||
		SameSite:       setting.SessionConfig.SameSite,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Contexter initializes a classic context for a request.
 | 
			
		||||
func Contexter() func(next http.Handler) http.Handler {
 | 
			
		||||
	rnd := templates.HTMLRenderer()
 | 
			
		||||
	csrfOpts := getCsrfOpts()
 | 
			
		||||
	if !setting.IsProd {
 | 
			
		||||
		CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
 | 
			
		||||
	}
 | 
			
		||||
@@ -776,21 +234,3 @@ func Contexter() func(next http.Handler) http.Handler {
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchOrderByMap represents all possible search order
 | 
			
		||||
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{
 | 
			
		||||
	"asc": {
 | 
			
		||||
		"alpha":   db.SearchOrderByAlphabetically,
 | 
			
		||||
		"created": db.SearchOrderByOldest,
 | 
			
		||||
		"updated": db.SearchOrderByLeastUpdated,
 | 
			
		||||
		"size":    db.SearchOrderBySize,
 | 
			
		||||
		"id":      db.SearchOrderByID,
 | 
			
		||||
	},
 | 
			
		||||
	"desc": {
 | 
			
		||||
		"alpha":   db.SearchOrderByAlphabeticallyReverse,
 | 
			
		||||
		"created": db.SearchOrderByNewest,
 | 
			
		||||
		"updated": db.SearchOrderByRecentUpdated,
 | 
			
		||||
		"size":    db.SearchOrderBySizeReverse,
 | 
			
		||||
		"id":      db.SearchOrderByIDReverse,
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										105
									
								
								modules/context/context_cookie.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								modules/context/context_cookie.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
			
		||||
 | 
			
		||||
	"github.com/minio/sha256-simd"
 | 
			
		||||
	"golang.org/x/crypto/pbkdf2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const CookieNameFlash = "gitea_flash"
 | 
			
		||||
 | 
			
		||||
func removeSessionCookieHeader(w http.ResponseWriter) {
 | 
			
		||||
	cookies := w.Header()["Set-Cookie"]
 | 
			
		||||
	w.Header().Del("Set-Cookie")
 | 
			
		||||
	for _, cookie := range cookies {
 | 
			
		||||
		if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		w.Header().Add("Set-Cookie", cookie)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSiteCookie convenience function to set most cookies consistently
 | 
			
		||||
// CSRF and a few others are the exception here
 | 
			
		||||
func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
 | 
			
		||||
	middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteSiteCookie convenience function to delete most cookies consistently
 | 
			
		||||
// CSRF and a few others are the exception here
 | 
			
		||||
func (ctx *Context) DeleteSiteCookie(name string) {
 | 
			
		||||
	middleware.SetSiteCookie(ctx.Resp, name, "", -1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSiteCookie returns given cookie value from request header.
 | 
			
		||||
func (ctx *Context) GetSiteCookie(name string) string {
 | 
			
		||||
	return middleware.GetSiteCookie(ctx.Req, name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSuperSecureCookie returns given cookie value from request header with secret string.
 | 
			
		||||
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
 | 
			
		||||
	val := ctx.GetSiteCookie(name)
 | 
			
		||||
	return ctx.CookieDecrypt(secret, val)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CookieDecrypt returns given value from with secret string.
 | 
			
		||||
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
 | 
			
		||||
	if val == "" {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	text, err := hex.DecodeString(val)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
 | 
			
		||||
	text, err = util.AESGCMDecrypt(key, text)
 | 
			
		||||
	return string(text), err == nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSuperSecureCookie sets given cookie value to response header with secret string.
 | 
			
		||||
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) {
 | 
			
		||||
	text := ctx.CookieEncrypt(secret, value)
 | 
			
		||||
	ctx.SetSiteCookie(name, text, maxAge)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CookieEncrypt encrypts a given value using the provided secret
 | 
			
		||||
func (ctx *Context) CookieEncrypt(secret, value string) string {
 | 
			
		||||
	key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
 | 
			
		||||
	text, err := util.AESGCMEncrypt(key, []byte(value))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic("error encrypting cookie: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return hex.EncodeToString(text)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCookieInt returns cookie result in int type.
 | 
			
		||||
func (ctx *Context) GetCookieInt(name string) int {
 | 
			
		||||
	r, _ := strconv.Atoi(ctx.GetSiteCookie(name))
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCookieInt64 returns cookie result in int64 type.
 | 
			
		||||
func (ctx *Context) GetCookieInt64(name string) int64 {
 | 
			
		||||
	r, _ := strconv.ParseInt(ctx.GetSiteCookie(name), 10, 64)
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCookieFloat64 returns cookie result in float64 type.
 | 
			
		||||
func (ctx *Context) GetCookieFloat64(name string) float64 {
 | 
			
		||||
	v, _ := strconv.ParseFloat(ctx.GetSiteCookie(name), 64)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								modules/context/context_data.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								modules/context/context_data.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import "code.gitea.io/gitea/modules/web/middleware"
 | 
			
		||||
 | 
			
		||||
// GetData returns the data
 | 
			
		||||
func (ctx *Context) GetData() middleware.ContextData {
 | 
			
		||||
	return ctx.Data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasAPIError returns true if error occurs in form validation.
 | 
			
		||||
func (ctx *Context) HasAPIError() bool {
 | 
			
		||||
	hasErr, ok := ctx.Data["HasError"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return hasErr.(bool)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetErrMsg returns error message
 | 
			
		||||
func (ctx *Context) GetErrMsg() string {
 | 
			
		||||
	return ctx.Data["ErrorMsg"].(string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasError returns true if error occurs in form validation.
 | 
			
		||||
// Attention: this function changes ctx.Data and ctx.Flash
 | 
			
		||||
func (ctx *Context) HasError() bool {
 | 
			
		||||
	hasErr, ok := ctx.Data["HasError"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
 | 
			
		||||
	ctx.Data["Flash"] = ctx.Flash
 | 
			
		||||
	return hasErr.(bool)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasValue returns true if value of given name exists.
 | 
			
		||||
func (ctx *Context) HasValue(name string) bool {
 | 
			
		||||
	_, ok := ctx.Data[name]
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										138
									
								
								modules/context/context_model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								modules/context/context_model.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/issue/template"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IsUserSiteAdmin returns true if current user is a site admin
 | 
			
		||||
func (ctx *Context) IsUserSiteAdmin() bool {
 | 
			
		||||
	return ctx.IsSigned && ctx.Doer.IsAdmin
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoOwner returns true if current user owns current repo
 | 
			
		||||
func (ctx *Context) IsUserRepoOwner() bool {
 | 
			
		||||
	return ctx.Repo.IsOwner()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoAdmin returns true if current user is admin in current repo
 | 
			
		||||
func (ctx *Context) IsUserRepoAdmin() bool {
 | 
			
		||||
	return ctx.Repo.IsAdmin()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoWriter returns true if current user has write privilege in current repo
 | 
			
		||||
func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool {
 | 
			
		||||
	for _, unitType := range unitTypes {
 | 
			
		||||
		if ctx.Repo.CanWrite(unitType) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoReaderSpecific returns true if current user can read current repo's specific part
 | 
			
		||||
func (ctx *Context) IsUserRepoReaderSpecific(unitType unit.Type) bool {
 | 
			
		||||
	return ctx.Repo.CanRead(unitType)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUserRepoReaderAny returns true if current user can read any part of current repo
 | 
			
		||||
func (ctx *Context) IsUserRepoReaderAny() bool {
 | 
			
		||||
	return ctx.Repo.HasAccess()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueTemplatesFromDefaultBranch checks for valid issue templates in the repo's default branch,
 | 
			
		||||
func (ctx *Context) IssueTemplatesFromDefaultBranch() []*api.IssueTemplate {
 | 
			
		||||
	ret, _ := ctx.IssueTemplatesErrorsFromDefaultBranch()
 | 
			
		||||
	return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueTemplatesErrorsFromDefaultBranch checks for issue templates in the repo's default branch,
 | 
			
		||||
// returns valid templates and the errors of invalid template files.
 | 
			
		||||
func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplate, map[string]error) {
 | 
			
		||||
	var issueTemplates []*api.IssueTemplate
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.Repository.IsEmpty {
 | 
			
		||||
		return issueTemplates, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.Commit == nil {
 | 
			
		||||
		var err error
 | 
			
		||||
		ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return issueTemplates, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	invalidFiles := map[string]error{}
 | 
			
		||||
	for _, dirName := range IssueTemplateDirCandidates {
 | 
			
		||||
		tree, err := ctx.Repo.Commit.SubTree(dirName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Debug("get sub tree of %s: %v", dirName, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		entries, err := tree.ListEntries()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Debug("list entries in %s: %v", dirName, err)
 | 
			
		||||
			return issueTemplates, nil
 | 
			
		||||
		}
 | 
			
		||||
		for _, entry := range entries {
 | 
			
		||||
			if !template.CouldBe(entry.Name()) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			fullName := path.Join(dirName, entry.Name())
 | 
			
		||||
			if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
 | 
			
		||||
				invalidFiles[fullName] = err
 | 
			
		||||
			} else {
 | 
			
		||||
				if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
 | 
			
		||||
					it.Ref = git.BranchPrefix + it.Ref
 | 
			
		||||
				}
 | 
			
		||||
				issueTemplates = append(issueTemplates, it)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return issueTemplates, invalidFiles
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueConfigFromDefaultBranch returns the issue config for this repo.
 | 
			
		||||
// It never returns a nil config.
 | 
			
		||||
func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) {
 | 
			
		||||
	if ctx.Repo.Repository.IsEmpty {
 | 
			
		||||
		return GetDefaultIssueConfig(), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return GetDefaultIssueConfig(), err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, configName := range IssueConfigCandidates {
 | 
			
		||||
		if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil {
 | 
			
		||||
			return ctx.Repo.GetIssueConfig(configName+".yaml", commit)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil {
 | 
			
		||||
			return ctx.Repo.GetIssueConfig(configName+".yml", commit)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return GetDefaultIssueConfig(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) HasIssueTemplatesOrContactLinks() bool {
 | 
			
		||||
	if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
 | 
			
		||||
	return len(issueConfig.ContactLinks) > 0
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								modules/context/context_request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								modules/context/context_request.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-chi/chi/v5"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RemoteAddr returns the client machine ip address
 | 
			
		||||
func (ctx *Context) RemoteAddr() string {
 | 
			
		||||
	return ctx.Req.RemoteAddr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Params returns the param on route
 | 
			
		||||
func (ctx *Context) Params(p string) string {
 | 
			
		||||
	s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":")))
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParamsInt64 returns the param on route as int64
 | 
			
		||||
func (ctx *Context) ParamsInt64(p string) int64 {
 | 
			
		||||
	v, _ := strconv.ParseInt(ctx.Params(p), 10, 64)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetParams set params into routes
 | 
			
		||||
func (ctx *Context) SetParams(k, v string) {
 | 
			
		||||
	chiCtx := chi.RouteContext(ctx)
 | 
			
		||||
	chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadStream returns the request body or the first form file
 | 
			
		||||
// Only form files need to get closed.
 | 
			
		||||
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {
 | 
			
		||||
	contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type"))
 | 
			
		||||
	if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") {
 | 
			
		||||
		if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil {
 | 
			
		||||
			return nil, false, err
 | 
			
		||||
		}
 | 
			
		||||
		if ctx.Req.MultipartForm.File == nil {
 | 
			
		||||
			return nil, false, http.ErrMissingFile
 | 
			
		||||
		}
 | 
			
		||||
		for _, files := range ctx.Req.MultipartForm.File {
 | 
			
		||||
			if len(files) > 0 {
 | 
			
		||||
				r, err := files[0].Open()
 | 
			
		||||
				return r, true, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return nil, false, http.ErrMissingFile
 | 
			
		||||
	}
 | 
			
		||||
	return ctx.Req.Body, false, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										279
									
								
								modules/context/context_response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								modules/context/context_response.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,279 @@
 | 
			
		||||
// 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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								modules/context/context_serve.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								modules/context/context_serve.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/httpcache"
 | 
			
		||||
	"code.gitea.io/gitea/modules/typesniffer"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ServeHeaderOptions struct {
 | 
			
		||||
	ContentType        string // defaults to "application/octet-stream"
 | 
			
		||||
	ContentTypeCharset string
 | 
			
		||||
	ContentLength      *int64
 | 
			
		||||
	Disposition        string // defaults to "attachment"
 | 
			
		||||
	Filename           string
 | 
			
		||||
	CacheDuration      time.Duration // defaults to 5 minutes
 | 
			
		||||
	LastModified       time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetServeHeaders sets necessary content serve headers
 | 
			
		||||
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
 | 
			
		||||
	header := ctx.Resp.Header()
 | 
			
		||||
 | 
			
		||||
	contentType := typesniffer.ApplicationOctetStream
 | 
			
		||||
	if opts.ContentType != "" {
 | 
			
		||||
		if opts.ContentTypeCharset != "" {
 | 
			
		||||
			contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
 | 
			
		||||
		} else {
 | 
			
		||||
			contentType = opts.ContentType
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	header.Set("Content-Type", contentType)
 | 
			
		||||
	header.Set("X-Content-Type-Options", "nosniff")
 | 
			
		||||
 | 
			
		||||
	if opts.ContentLength != nil {
 | 
			
		||||
		header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Filename != "" {
 | 
			
		||||
		disposition := opts.Disposition
 | 
			
		||||
		if disposition == "" {
 | 
			
		||||
			disposition = "attachment"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
 | 
			
		||||
		header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
 | 
			
		||||
		header.Set("Access-Control-Expose-Headers", "Content-Disposition")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	duration := opts.CacheDuration
 | 
			
		||||
	if duration == 0 {
 | 
			
		||||
		duration = 5 * time.Minute
 | 
			
		||||
	}
 | 
			
		||||
	httpcache.SetCacheControlInHeader(header, duration)
 | 
			
		||||
 | 
			
		||||
	if !opts.LastModified.IsZero() {
 | 
			
		||||
		header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ServeContent serves content to http request
 | 
			
		||||
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
 | 
			
		||||
	ctx.SetServeHeaders(opts)
 | 
			
		||||
	http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
 | 
			
		||||
}
 | 
			
		||||
@@ -25,7 +25,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 | 
			
		||||
	"code.gitea.io/gitea/modules/issue/template"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
@@ -1063,59 +1062,6 @@ func UnitTypes() func(ctx *Context) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueTemplatesFromDefaultBranch checks for valid issue templates in the repo's default branch,
 | 
			
		||||
func (ctx *Context) IssueTemplatesFromDefaultBranch() []*api.IssueTemplate {
 | 
			
		||||
	ret, _ := ctx.IssueTemplatesErrorsFromDefaultBranch()
 | 
			
		||||
	return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueTemplatesErrorsFromDefaultBranch checks for issue templates in the repo's default branch,
 | 
			
		||||
// returns valid templates and the errors of invalid template files.
 | 
			
		||||
func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplate, map[string]error) {
 | 
			
		||||
	var issueTemplates []*api.IssueTemplate
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.Repository.IsEmpty {
 | 
			
		||||
		return issueTemplates, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.Commit == nil {
 | 
			
		||||
		var err error
 | 
			
		||||
		ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return issueTemplates, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	invalidFiles := map[string]error{}
 | 
			
		||||
	for _, dirName := range IssueTemplateDirCandidates {
 | 
			
		||||
		tree, err := ctx.Repo.Commit.SubTree(dirName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Debug("get sub tree of %s: %v", dirName, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		entries, err := tree.ListEntries()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Debug("list entries in %s: %v", dirName, err)
 | 
			
		||||
			return issueTemplates, nil
 | 
			
		||||
		}
 | 
			
		||||
		for _, entry := range entries {
 | 
			
		||||
			if !template.CouldBe(entry.Name()) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			fullName := path.Join(dirName, entry.Name())
 | 
			
		||||
			if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
 | 
			
		||||
				invalidFiles[fullName] = err
 | 
			
		||||
			} else {
 | 
			
		||||
				if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
 | 
			
		||||
					it.Ref = git.BranchPrefix + it.Ref
 | 
			
		||||
				}
 | 
			
		||||
				issueTemplates = append(issueTemplates, it)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return issueTemplates, invalidFiles
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetDefaultIssueConfig() api.IssueConfig {
 | 
			
		||||
	return api.IssueConfig{
 | 
			
		||||
		BlankIssuesEnabled: true,
 | 
			
		||||
@@ -1177,31 +1123,6 @@ func (r *Repository) GetIssueConfig(path string, commit *git.Commit) (api.IssueC
 | 
			
		||||
	return issueConfig, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueConfigFromDefaultBranch returns the issue config for this repo.
 | 
			
		||||
// It never returns a nil config.
 | 
			
		||||
func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) {
 | 
			
		||||
	if ctx.Repo.Repository.IsEmpty {
 | 
			
		||||
		return GetDefaultIssueConfig(), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return GetDefaultIssueConfig(), err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, configName := range IssueConfigCandidates {
 | 
			
		||||
		if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil {
 | 
			
		||||
			return ctx.Repo.GetIssueConfig(configName+".yaml", commit)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil {
 | 
			
		||||
			return ctx.Repo.GetIssueConfig(configName+".yml", commit)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return GetDefaultIssueConfig(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsIssueConfig returns if the given path is a issue config file.
 | 
			
		||||
func (r *Repository) IsIssueConfig(path string) bool {
 | 
			
		||||
	for _, configName := range IssueConfigCandidates {
 | 
			
		||||
@@ -1211,12 +1132,3 @@ func (r *Repository) IsIssueConfig(path string) bool {
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) HasIssueTemplatesOrContactLinks() bool {
 | 
			
		||||
	if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
 | 
			
		||||
	return len(issueConfig.ContactLinks) > 0
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user