From cb700aedd1e670fb47b8cf0cd67fb117a1ad88a2 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 8 May 2023 17:36:54 +0800
Subject: [PATCH] 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.
---
 models/repo.go                               |   3 -
 models/repo/search.go                        |  24 +
 modules/context/context.go                   | 592 +------------------
 modules/context/context_cookie.go            | 105 ++++
 modules/context/context_data.go              |  43 ++
 modules/context/{form.go => context_form.go} |   0
 modules/context/context_model.go             | 138 +++++
 modules/context/context_request.go           |  59 ++
 modules/context/context_response.go          | 279 +++++++++
 modules/context/context_serve.go             |  74 +++
 modules/context/repo.go                      |  88 ---
 routers/api/v1/repo/repo.go                  |   2 +-
 routers/web/admin/users.go                   |   4 +-
 routers/web/auth/auth.go                     |   2 +-
 routers/web/auth/password.go                 |   4 +-
 routers/web/repo/repo.go                     |   2 +-
 routers/web/repo/view.go                     |   2 +-
 routers/web/user/setting/account.go          |   2 +-
 18 files changed, 747 insertions(+), 676 deletions(-)
 create mode 100644 models/repo/search.go
 create mode 100644 modules/context/context_cookie.go
 create mode 100644 modules/context/context_data.go
 rename modules/context/{form.go => context_form.go} (100%)
 create mode 100644 modules/context/context_model.go
 create mode 100644 modules/context/context_request.go
 create mode 100644 modules/context/context_response.go
 create mode 100644 modules/context/context_serve.go

diff --git a/models/repo.go b/models/repo.go
index d2a3311c8b..5903132862 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -35,9 +35,6 @@ import (
 	"xorm.io/builder"
 )
 
-// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
-var ItemsPerPage = 40
-
 // Init initialize model
 func Init(ctx context.Context) error {
 	if err := unit.LoadUnitConfig(); err != nil {
diff --git a/models/repo/search.go b/models/repo/search.go
new file mode 100644
index 0000000000..4d64acf8cf
--- /dev/null
+++ b/models/repo/search.go
@@ -0,0 +1,24 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import "code.gitea.io/gitea/models/db"
+
+// 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,
+	},
+}
diff --git a/modules/context/context.go b/modules/context/context.go
index d73a26e5b6..3e1b48dcde 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -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,
-	},
-}
diff --git a/modules/context/context_cookie.go b/modules/context/context_cookie.go
new file mode 100644
index 0000000000..5cb4ea0aca
--- /dev/null
+++ b/modules/context/context_cookie.go
@@ -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
+}
diff --git a/modules/context/context_data.go b/modules/context/context_data.go
new file mode 100644
index 0000000000..cdf4ff9afe
--- /dev/null
+++ b/modules/context/context_data.go
@@ -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
+}
diff --git a/modules/context/form.go b/modules/context/context_form.go
similarity index 100%
rename from modules/context/form.go
rename to modules/context/context_form.go
diff --git a/modules/context/context_model.go b/modules/context/context_model.go
new file mode 100644
index 0000000000..5ba98f7e01
--- /dev/null
+++ b/modules/context/context_model.go
@@ -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
+}
diff --git a/modules/context/context_request.go b/modules/context/context_request.go
new file mode 100644
index 0000000000..0b87552c08
--- /dev/null
+++ b/modules/context/context_request.go
@@ -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
+}
diff --git a/modules/context/context_response.go b/modules/context/context_response.go
new file mode 100644
index 0000000000..8adff96994
--- /dev/null
+++ b/modules/context/context_response.go
@@ -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)
+}
diff --git a/modules/context/context_serve.go b/modules/context/context_serve.go
new file mode 100644
index 0000000000..44dd739eff
--- /dev/null
+++ b/modules/context/context_serve.go
@@ -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)
+}
diff --git a/modules/context/repo.go b/modules/context/repo.go
index a1c8f43644..b33341c245 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -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
-}
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 36cc03f893..480ca397d4 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -178,7 +178,7 @@ func Search(ctx *context.APIContext) {
 		if len(sortOrder) == 0 {
 			sortOrder = "asc"
 		}
-		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
+		if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok {
 			if orderBy, ok := searchModeMap[sortMode]; ok {
 				opts.OrderBy = orderBy
 			} else {
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index bd31d9d632..3895bcfdb9 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -148,7 +148,7 @@ func NewUserPost(ctx *context.Context) {
 		}
 		if !password.IsComplexEnough(form.Password) {
 			ctx.Data["Err_Password"] = true
-			ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form)
+			ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
 			return
 		}
 		pwned, err := password.IsPwned(ctx, form.Password)
@@ -301,7 +301,7 @@ func EditUserPost(ctx *context.Context) {
 			return
 		}
 		if !password.IsComplexEnough(form.Password) {
-			ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form)
+			ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
 			return
 		}
 		pwned, err := password.IsPwned(ctx, form.Password)
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index d8042afecc..e0883a2696 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -444,7 +444,7 @@ func SignUpPost(ctx *context.Context) {
 	}
 	if !password.IsComplexEnough(form.Password) {
 		ctx.Data["Err_Password"] = true
-		ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form)
+		ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
 		return
 	}
 	pwned, err := password.IsPwned(ctx, form.Password)
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index ed0412d745..b34a1d8fce 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -176,7 +176,7 @@ func ResetPasswdPost(ctx *context.Context) {
 	} else if !password.IsComplexEnough(passwd) {
 		ctx.Data["IsResetForm"] = true
 		ctx.Data["Err_Password"] = true
-		ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil)
+		ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
 		return
 	} else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
 		errMsg := ctx.Tr("auth.password_pwned")
@@ -305,7 +305,7 @@ func MustChangePasswordPost(ctx *context.Context) {
 
 	if !password.IsComplexEnough(form.Password) {
 		ctx.Data["Err_Password"] = true
-		ctx.RenderWithErr(password.BuildComplexityError(ctx), tplMustChangePassword, &form)
+		ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
 		return
 	}
 	pwned, err := password.IsPwned(ctx, form.Password)
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index c4b5351eca..2f87e19022 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -546,7 +546,7 @@ func SearchRepo(ctx *context.Context) {
 		if len(sortOrder) == 0 {
 			sortOrder = "asc"
 		}
-		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
+		if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok {
 			if orderBy, ok := searchModeMap[sortMode]; ok {
 				opts.OrderBy = orderBy
 			} else {
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 9f2770a3ac..2bf293cbda 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -66,7 +66,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
 	// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
 	// 2. Txt files - e.g. README.txt
 	// 3. No extension - e.g. README
-	exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority
+	exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
 	extCount := len(exts)
 	readmeFiles := make([]*git.TreeEntry, extCount+1)
 
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 0e48013b04..a67c2398fb 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -60,7 +60,7 @@ func AccountPost(ctx *context.Context) {
 	} else if form.Password != form.Retype {
 		ctx.Flash.Error(ctx.Tr("form.password_not_match"))
 	} else if !password.IsComplexEnough(form.Password) {
-		ctx.Flash.Error(password.BuildComplexityError(ctx))
+		ctx.Flash.Error(password.BuildComplexityError(ctx.Locale))
 	} else if pwned, err := password.IsPwned(ctx, form.Password); pwned || err != nil {
 		errMsg := ctx.Tr("auth.password_pwned")
 		if err != nil {