Skip to content

Commit cb700ae

Browse files
authoredMay 8, 2023
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.
1 parent ff56292 commit cb700ae

18 files changed

+747
-676
lines changed
 

‎models/repo.go

-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ import (
3535
"xorm.io/builder"
3636
)
3737

38-
// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
39-
var ItemsPerPage = 40
40-
4138
// Init initialize model
4239
func Init(ctx context.Context) error {
4340
if err := unit.LoadUnitConfig(); err != nil {

‎models/repo/search.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import "code.gitea.io/gitea/models/db"
7+
8+
// SearchOrderByMap represents all possible search order
9+
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{
10+
"asc": {
11+
"alpha": db.SearchOrderByAlphabetically,
12+
"created": db.SearchOrderByOldest,
13+
"updated": db.SearchOrderByLeastUpdated,
14+
"size": db.SearchOrderBySize,
15+
"id": db.SearchOrderByID,
16+
},
17+
"desc": {
18+
"alpha": db.SearchOrderByAlphabeticallyReverse,
19+
"created": db.SearchOrderByNewest,
20+
"updated": db.SearchOrderByRecentUpdated,
21+
"size": db.SearchOrderBySizeReverse,
22+
"id": db.SearchOrderByIDReverse,
23+
},
24+
}

‎modules/context/context.go

+16-576
Large diffs are not rendered by default.

‎modules/context/context_cookie.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package context
5+
6+
import (
7+
"encoding/hex"
8+
"net/http"
9+
"strconv"
10+
"strings"
11+
12+
"code.gitea.io/gitea/modules/setting"
13+
"code.gitea.io/gitea/modules/util"
14+
"code.gitea.io/gitea/modules/web/middleware"
15+
16+
"github.com/minio/sha256-simd"
17+
"golang.org/x/crypto/pbkdf2"
18+
)
19+
20+
const CookieNameFlash = "gitea_flash"
21+
22+
func removeSessionCookieHeader(w http.ResponseWriter) {
23+
cookies := w.Header()["Set-Cookie"]
24+
w.Header().Del("Set-Cookie")
25+
for _, cookie := range cookies {
26+
if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") {
27+
continue
28+
}
29+
w.Header().Add("Set-Cookie", cookie)
30+
}
31+
}
32+
33+
// SetSiteCookie convenience function to set most cookies consistently
34+
// CSRF and a few others are the exception here
35+
func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
36+
middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
37+
}
38+
39+
// DeleteSiteCookie convenience function to delete most cookies consistently
40+
// CSRF and a few others are the exception here
41+
func (ctx *Context) DeleteSiteCookie(name string) {
42+
middleware.SetSiteCookie(ctx.Resp, name, "", -1)
43+
}
44+
45+
// GetSiteCookie returns given cookie value from request header.
46+
func (ctx *Context) GetSiteCookie(name string) string {
47+
return middleware.GetSiteCookie(ctx.Req, name)
48+
}
49+
50+
// GetSuperSecureCookie returns given cookie value from request header with secret string.
51+
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
52+
val := ctx.GetSiteCookie(name)
53+
return ctx.CookieDecrypt(secret, val)
54+
}
55+
56+
// CookieDecrypt returns given value from with secret string.
57+
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
58+
if val == "" {
59+
return "", false
60+
}
61+
62+
text, err := hex.DecodeString(val)
63+
if err != nil {
64+
return "", false
65+
}
66+
67+
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
68+
text, err = util.AESGCMDecrypt(key, text)
69+
return string(text), err == nil
70+
}
71+
72+
// SetSuperSecureCookie sets given cookie value to response header with secret string.
73+
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) {
74+
text := ctx.CookieEncrypt(secret, value)
75+
ctx.SetSiteCookie(name, text, maxAge)
76+
}
77+
78+
// CookieEncrypt encrypts a given value using the provided secret
79+
func (ctx *Context) CookieEncrypt(secret, value string) string {
80+
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
81+
text, err := util.AESGCMEncrypt(key, []byte(value))
82+
if err != nil {
83+
panic("error encrypting cookie: " + err.Error())
84+
}
85+
86+
return hex.EncodeToString(text)
87+
}
88+
89+
// GetCookieInt returns cookie result in int type.
90+
func (ctx *Context) GetCookieInt(name string) int {
91+
r, _ := strconv.Atoi(ctx.GetSiteCookie(name))
92+
return r
93+
}
94+
95+
// GetCookieInt64 returns cookie result in int64 type.
96+
func (ctx *Context) GetCookieInt64(name string) int64 {
97+
r, _ := strconv.ParseInt(ctx.GetSiteCookie(name), 10, 64)
98+
return r
99+
}
100+
101+
// GetCookieFloat64 returns cookie result in float64 type.
102+
func (ctx *Context) GetCookieFloat64(name string) float64 {
103+
v, _ := strconv.ParseFloat(ctx.GetSiteCookie(name), 64)
104+
return v
105+
}

‎modules/context/context_data.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package context
5+
6+
import "code.gitea.io/gitea/modules/web/middleware"
7+
8+
// GetData returns the data
9+
func (ctx *Context) GetData() middleware.ContextData {
10+
return ctx.Data
11+
}
12+
13+
// HasAPIError returns true if error occurs in form validation.
14+
func (ctx *Context) HasAPIError() bool {
15+
hasErr, ok := ctx.Data["HasError"]
16+
if !ok {
17+
return false
18+
}
19+
return hasErr.(bool)
20+
}
21+
22+
// GetErrMsg returns error message
23+
func (ctx *Context) GetErrMsg() string {
24+
return ctx.Data["ErrorMsg"].(string)
25+
}
26+
27+
// HasError returns true if error occurs in form validation.
28+
// Attention: this function changes ctx.Data and ctx.Flash
29+
func (ctx *Context) HasError() bool {
30+
hasErr, ok := ctx.Data["HasError"]
31+
if !ok {
32+
return false
33+
}
34+
ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
35+
ctx.Data["Flash"] = ctx.Flash
36+
return hasErr.(bool)
37+
}
38+
39+
// HasValue returns true if value of given name exists.
40+
func (ctx *Context) HasValue(name string) bool {
41+
_, ok := ctx.Data[name]
42+
return ok
43+
}
File renamed without changes.

‎modules/context/context_model.go

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package context
5+
6+
import (
7+
"path"
8+
"strings"
9+
10+
"code.gitea.io/gitea/models/unit"
11+
"code.gitea.io/gitea/modules/git"
12+
"code.gitea.io/gitea/modules/issue/template"
13+
"code.gitea.io/gitea/modules/log"
14+
api "code.gitea.io/gitea/modules/structs"
15+
)
16+
17+
// IsUserSiteAdmin returns true if current user is a site admin
18+
func (ctx *Context) IsUserSiteAdmin() bool {
19+
return ctx.IsSigned && ctx.Doer.IsAdmin
20+
}
21+
22+
// IsUserRepoOwner returns true if current user owns current repo
23+
func (ctx *Context) IsUserRepoOwner() bool {
24+
return ctx.Repo.IsOwner()
25+
}
26+
27+
// IsUserRepoAdmin returns true if current user is admin in current repo
28+
func (ctx *Context) IsUserRepoAdmin() bool {
29+
return ctx.Repo.IsAdmin()
30+
}
31+
32+
// IsUserRepoWriter returns true if current user has write privilege in current repo
33+
func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool {
34+
for _, unitType := range unitTypes {
35+
if ctx.Repo.CanWrite(unitType) {
36+
return true
37+
}
38+
}
39+
40+
return false
41+
}
42+
43+
// IsUserRepoReaderSpecific returns true if current user can read current repo's specific part
44+
func (ctx *Context) IsUserRepoReaderSpecific(unitType unit.Type) bool {
45+
return ctx.Repo.CanRead(unitType)
46+
}
47+
48+
// IsUserRepoReaderAny returns true if current user can read any part of current repo
49+
func (ctx *Context) IsUserRepoReaderAny() bool {
50+
return ctx.Repo.HasAccess()
51+
}
52+
53+
// IssueTemplatesFromDefaultBranch checks for valid issue templates in the repo's default branch,
54+
func (ctx *Context) IssueTemplatesFromDefaultBranch() []*api.IssueTemplate {
55+
ret, _ := ctx.IssueTemplatesErrorsFromDefaultBranch()
56+
return ret
57+
}
58+
59+
// IssueTemplatesErrorsFromDefaultBranch checks for issue templates in the repo's default branch,
60+
// returns valid templates and the errors of invalid template files.
61+
func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplate, map[string]error) {
62+
var issueTemplates []*api.IssueTemplate
63+
64+
if ctx.Repo.Repository.IsEmpty {
65+
return issueTemplates, nil
66+
}
67+
68+
if ctx.Repo.Commit == nil {
69+
var err error
70+
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
71+
if err != nil {
72+
return issueTemplates, nil
73+
}
74+
}
75+
76+
invalidFiles := map[string]error{}
77+
for _, dirName := range IssueTemplateDirCandidates {
78+
tree, err := ctx.Repo.Commit.SubTree(dirName)
79+
if err != nil {
80+
log.Debug("get sub tree of %s: %v", dirName, err)
81+
continue
82+
}
83+
entries, err := tree.ListEntries()
84+
if err != nil {
85+
log.Debug("list entries in %s: %v", dirName, err)
86+
return issueTemplates, nil
87+
}
88+
for _, entry := range entries {
89+
if !template.CouldBe(entry.Name()) {
90+
continue
91+
}
92+
fullName := path.Join(dirName, entry.Name())
93+
if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
94+
invalidFiles[fullName] = err
95+
} else {
96+
if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
97+
it.Ref = git.BranchPrefix + it.Ref
98+
}
99+
issueTemplates = append(issueTemplates, it)
100+
}
101+
}
102+
}
103+
return issueTemplates, invalidFiles
104+
}
105+
106+
// IssueConfigFromDefaultBranch returns the issue config for this repo.
107+
// It never returns a nil config.
108+
func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) {
109+
if ctx.Repo.Repository.IsEmpty {
110+
return GetDefaultIssueConfig(), nil
111+
}
112+
113+
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
114+
if err != nil {
115+
return GetDefaultIssueConfig(), err
116+
}
117+
118+
for _, configName := range IssueConfigCandidates {
119+
if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil {
120+
return ctx.Repo.GetIssueConfig(configName+".yaml", commit)
121+
}
122+
123+
if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil {
124+
return ctx.Repo.GetIssueConfig(configName+".yml", commit)
125+
}
126+
}
127+
128+
return GetDefaultIssueConfig(), nil
129+
}
130+
131+
func (ctx *Context) HasIssueTemplatesOrContactLinks() bool {
132+
if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 {
133+
return true
134+
}
135+
136+
issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
137+
return len(issueConfig.ContactLinks) > 0
138+
}

‎modules/context/context_request.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package context
5+
6+
import (
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"strconv"
11+
"strings"
12+
13+
"github.com/go-chi/chi/v5"
14+
)
15+
16+
// RemoteAddr returns the client machine ip address
17+
func (ctx *Context) RemoteAddr() string {
18+
return ctx.Req.RemoteAddr
19+
}
20+
21+
// Params returns the param on route
22+
func (ctx *Context) Params(p string) string {
23+
s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":")))
24+
return s
25+
}
26+
27+
// ParamsInt64 returns the param on route as int64
28+
func (ctx *Context) ParamsInt64(p string) int64 {
29+
v, _ := strconv.ParseInt(ctx.Params(p), 10, 64)
30+
return v
31+
}
32+
33+
// SetParams set params into routes
34+
func (ctx *Context) SetParams(k, v string) {
35+
chiCtx := chi.RouteContext(ctx)
36+
chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
37+
}
38+
39+
// UploadStream returns the request body or the first form file
40+
// Only form files need to get closed.
41+
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {
42+
contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type"))
43+
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") {
44+
if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil {
45+
return nil, false, err
46+
}
47+
if ctx.Req.MultipartForm.File == nil {
48+
return nil, false, http.ErrMissingFile
49+
}
50+
for _, files := range ctx.Req.MultipartForm.File {
51+
if len(files) > 0 {
52+
r, err := files[0].Open()
53+
return r, true, err
54+
}
55+
}
56+
return nil, false, http.ErrMissingFile
57+
}
58+
return ctx.Req.Body, false, nil
59+
}

‎modules/context/context_response.go

+279
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package context
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"net"
10+
"net/http"
11+
"net/url"
12+
"path"
13+
"strconv"
14+
"strings"
15+
"time"
16+
17+
user_model "code.gitea.io/gitea/models/user"
18+
"code.gitea.io/gitea/modules/base"
19+
"code.gitea.io/gitea/modules/json"
20+
"code.gitea.io/gitea/modules/log"
21+
"code.gitea.io/gitea/modules/setting"
22+
"code.gitea.io/gitea/modules/templates"
23+
"code.gitea.io/gitea/modules/web/middleware"
24+
)
25+
26+
// SetTotalCountHeader set "X-Total-Count" header
27+
func (ctx *Context) SetTotalCountHeader(total int64) {
28+
ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total))
29+
ctx.AppendAccessControlExposeHeaders("X-Total-Count")
30+
}
31+
32+
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
33+
func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) {
34+
val := ctx.RespHeader().Get("Access-Control-Expose-Headers")
35+
if len(val) != 0 {
36+
ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", ")))
37+
} else {
38+
ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", "))
39+
}
40+
}
41+
42+
// Written returns true if there are something sent to web browser
43+
func (ctx *Context) Written() bool {
44+
return ctx.Resp.Status() > 0
45+
}
46+
47+
// Status writes status code
48+
func (ctx *Context) Status(status int) {
49+
ctx.Resp.WriteHeader(status)
50+
}
51+
52+
// Write writes data to web browser
53+
func (ctx *Context) Write(bs []byte) (int, error) {
54+
return ctx.Resp.Write(bs)
55+
}
56+
57+
// RedirectToUser redirect to a differently-named user
58+
func RedirectToUser(ctx *Context, userName string, redirectUserID int64) {
59+
user, err := user_model.GetUserByID(ctx, redirectUserID)
60+
if err != nil {
61+
ctx.ServerError("GetUserByID", err)
62+
return
63+
}
64+
65+
redirectPath := strings.Replace(
66+
ctx.Req.URL.EscapedPath(),
67+
url.PathEscape(userName),
68+
url.PathEscape(user.Name),
69+
1,
70+
)
71+
if ctx.Req.URL.RawQuery != "" {
72+
redirectPath += "?" + ctx.Req.URL.RawQuery
73+
}
74+
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
75+
}
76+
77+
// RedirectToFirst redirects to first not empty URL
78+
func (ctx *Context) RedirectToFirst(location ...string) {
79+
for _, loc := range location {
80+
if len(loc) == 0 {
81+
continue
82+
}
83+
84+
// Unfortunately browsers consider a redirect Location with preceding "//" and "/\" as meaning redirect to "http(s)://REST_OF_PATH"
85+
// Therefore we should ignore these redirect locations to prevent open redirects
86+
if len(loc) > 1 && loc[0] == '/' && (loc[1] == '/' || loc[1] == '\\') {
87+
continue
88+
}
89+
90+
u, err := url.Parse(loc)
91+
if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(loc), strings.ToLower(setting.AppURL))) {
92+
continue
93+
}
94+
95+
ctx.Redirect(loc)
96+
return
97+
}
98+
99+
ctx.Redirect(setting.AppSubURL + "/")
100+
}
101+
102+
const tplStatus500 base.TplName = "status/500"
103+
104+
// HTML calls Context.HTML and renders the template to HTTP response
105+
func (ctx *Context) HTML(status int, name base.TplName) {
106+
log.Debug("Template: %s", name)
107+
108+
tmplStartTime := time.Now()
109+
if !setting.IsProd {
110+
ctx.Data["TemplateName"] = name
111+
}
112+
ctx.Data["TemplateLoadTimes"] = func() string {
113+
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
114+
}
115+
116+
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data)
117+
if err == nil {
118+
return
119+
}
120+
121+
// if rendering fails, show error page
122+
if name != tplStatus500 {
123+
err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err))
124+
ctx.ServerError("Render failed", err) // show the 500 error page
125+
} else {
126+
ctx.PlainText(http.StatusInternalServerError, "Unable to render status/500 page, the template system is broken, or Gitea can't find your template files.")
127+
return
128+
}
129+
}
130+
131+
// RenderToString renders the template content to a string
132+
func (ctx *Context) RenderToString(name base.TplName, data map[string]interface{}) (string, error) {
133+
var buf strings.Builder
134+
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data)
135+
return buf.String(), err
136+
}
137+
138+
// RenderWithErr used for page has form validation but need to prompt error to users.
139+
func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form interface{}) {
140+
if form != nil {
141+
middleware.AssignForm(form, ctx.Data)
142+
}
143+
ctx.Flash.ErrorMsg = msg
144+
ctx.Data["Flash"] = ctx.Flash
145+
ctx.HTML(http.StatusOK, tpl)
146+
}
147+
148+
// NotFound displays a 404 (Not Found) page and prints the given error, if any.
149+
func (ctx *Context) NotFound(logMsg string, logErr error) {
150+
ctx.notFoundInternal(logMsg, logErr)
151+
}
152+
153+
func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
154+
if logErr != nil {
155+
log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr)
156+
if !setting.IsProd {
157+
ctx.Data["ErrorMsg"] = logErr
158+
}
159+
}
160+
161+
// response simple message if Accept isn't text/html
162+
showHTML := false
163+
for _, part := range ctx.Req.Header["Accept"] {
164+
if strings.Contains(part, "text/html") {
165+
showHTML = true
166+
break
167+
}
168+
}
169+
170+
if !showHTML {
171+
ctx.plainTextInternal(3, http.StatusNotFound, []byte("Not found.\n"))
172+
return
173+
}
174+
175+
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
176+
ctx.Data["Title"] = "Page Not Found"
177+
ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
178+
}
179+
180+
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
181+
func (ctx *Context) ServerError(logMsg string, logErr error) {
182+
ctx.serverErrorInternal(logMsg, logErr)
183+
}
184+
185+
func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
186+
if logErr != nil {
187+
log.ErrorWithSkip(2, "%s: %v", logMsg, logErr)
188+
if _, ok := logErr.(*net.OpError); ok || errors.Is(logErr, &net.OpError{}) {
189+
// This is an error within the underlying connection
190+
// and further rendering will not work so just return
191+
return
192+
}
193+
194+
// it's safe to show internal error to admin users, and it helps
195+
if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
196+
ctx.Data["ErrorMsg"] = fmt.Sprintf("%s, %s", logMsg, logErr)
197+
}
198+
}
199+
200+
ctx.Data["Title"] = "Internal Server Error"
201+
ctx.HTML(http.StatusInternalServerError, tplStatus500)
202+
}
203+
204+
// NotFoundOrServerError use error check function to determine if the error
205+
// is about not found. It responds with 404 status code for not found error,
206+
// or error context description for logging purpose of 500 server error.
207+
func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
208+
if errCheck(logErr) {
209+
ctx.notFoundInternal(logMsg, logErr)
210+
return
211+
}
212+
ctx.serverErrorInternal(logMsg, logErr)
213+
}
214+
215+
// PlainTextBytes renders bytes as plain text
216+
func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
217+
statusPrefix := status / 100
218+
if statusPrefix == 4 || statusPrefix == 5 {
219+
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
220+
}
221+
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
222+
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
223+
ctx.Resp.WriteHeader(status)
224+
if _, err := ctx.Resp.Write(bs); err != nil {
225+
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
226+
}
227+
}
228+
229+
// PlainTextBytes renders bytes as plain text
230+
func (ctx *Context) PlainTextBytes(status int, bs []byte) {
231+
ctx.plainTextInternal(2, status, bs)
232+
}
233+
234+
// PlainText renders content as plain text
235+
func (ctx *Context) PlainText(status int, text string) {
236+
ctx.plainTextInternal(2, status, []byte(text))
237+
}
238+
239+
// RespHeader returns the response header
240+
func (ctx *Context) RespHeader() http.Header {
241+
return ctx.Resp.Header()
242+
}
243+
244+
// Error returned an error to web browser
245+
func (ctx *Context) Error(status int, contents ...string) {
246+
v := http.StatusText(status)
247+
if len(contents) > 0 {
248+
v = contents[0]
249+
}
250+
http.Error(ctx.Resp, v, status)
251+
}
252+
253+
// JSON render content as JSON
254+
func (ctx *Context) JSON(status int, content interface{}) {
255+
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
256+
ctx.Resp.WriteHeader(status)
257+
if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil {
258+
ctx.ServerError("Render JSON failed", err)
259+
}
260+
}
261+
262+
// Redirect redirects the request
263+
func (ctx *Context) Redirect(location string, status ...int) {
264+
code := http.StatusSeeOther
265+
if len(status) == 1 {
266+
code = status[0]
267+
}
268+
269+
if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
270+
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
271+
// 1. the first request to "/my-path" contains cookie
272+
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
273+
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
274+
// 4. then the browser accepts the empty session, then the user is logged out
275+
// So in this case, we should remove the session cookie from the response header
276+
removeSessionCookieHeader(ctx.Resp)
277+
}
278+
http.Redirect(ctx.Resp, ctx.Req, location, code)
279+
}

‎modules/context/context_serve.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package context
5+
6+
import (
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
"strconv"
12+
"strings"
13+
"time"
14+
15+
"code.gitea.io/gitea/modules/httpcache"
16+
"code.gitea.io/gitea/modules/typesniffer"
17+
)
18+
19+
type ServeHeaderOptions struct {
20+
ContentType string // defaults to "application/octet-stream"
21+
ContentTypeCharset string
22+
ContentLength *int64
23+
Disposition string // defaults to "attachment"
24+
Filename string
25+
CacheDuration time.Duration // defaults to 5 minutes
26+
LastModified time.Time
27+
}
28+
29+
// SetServeHeaders sets necessary content serve headers
30+
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
31+
header := ctx.Resp.Header()
32+
33+
contentType := typesniffer.ApplicationOctetStream
34+
if opts.ContentType != "" {
35+
if opts.ContentTypeCharset != "" {
36+
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
37+
} else {
38+
contentType = opts.ContentType
39+
}
40+
}
41+
header.Set("Content-Type", contentType)
42+
header.Set("X-Content-Type-Options", "nosniff")
43+
44+
if opts.ContentLength != nil {
45+
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
46+
}
47+
48+
if opts.Filename != "" {
49+
disposition := opts.Disposition
50+
if disposition == "" {
51+
disposition = "attachment"
52+
}
53+
54+
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
55+
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
56+
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
57+
}
58+
59+
duration := opts.CacheDuration
60+
if duration == 0 {
61+
duration = 5 * time.Minute
62+
}
63+
httpcache.SetCacheControlInHeader(header, duration)
64+
65+
if !opts.LastModified.IsZero() {
66+
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
67+
}
68+
}
69+
70+
// ServeContent serves content to http request
71+
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
72+
ctx.SetServeHeaders(opts)
73+
http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
74+
}

‎modules/context/repo.go

-88
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"code.gitea.io/gitea/modules/cache"
2626
"code.gitea.io/gitea/modules/git"
2727
code_indexer "code.gitea.io/gitea/modules/indexer/code"
28-
"code.gitea.io/gitea/modules/issue/template"
2928
"code.gitea.io/gitea/modules/log"
3029
repo_module "code.gitea.io/gitea/modules/repository"
3130
"code.gitea.io/gitea/modules/setting"
@@ -1063,59 +1062,6 @@ func UnitTypes() func(ctx *Context) {
10631062
}
10641063
}
10651064

1066-
// IssueTemplatesFromDefaultBranch checks for valid issue templates in the repo's default branch,
1067-
func (ctx *Context) IssueTemplatesFromDefaultBranch() []*api.IssueTemplate {
1068-
ret, _ := ctx.IssueTemplatesErrorsFromDefaultBranch()
1069-
return ret
1070-
}
1071-
1072-
// IssueTemplatesErrorsFromDefaultBranch checks for issue templates in the repo's default branch,
1073-
// returns valid templates and the errors of invalid template files.
1074-
func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplate, map[string]error) {
1075-
var issueTemplates []*api.IssueTemplate
1076-
1077-
if ctx.Repo.Repository.IsEmpty {
1078-
return issueTemplates, nil
1079-
}
1080-
1081-
if ctx.Repo.Commit == nil {
1082-
var err error
1083-
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
1084-
if err != nil {
1085-
return issueTemplates, nil
1086-
}
1087-
}
1088-
1089-
invalidFiles := map[string]error{}
1090-
for _, dirName := range IssueTemplateDirCandidates {
1091-
tree, err := ctx.Repo.Commit.SubTree(dirName)
1092-
if err != nil {
1093-
log.Debug("get sub tree of %s: %v", dirName, err)
1094-
continue
1095-
}
1096-
entries, err := tree.ListEntries()
1097-
if err != nil {
1098-
log.Debug("list entries in %s: %v", dirName, err)
1099-
return issueTemplates, nil
1100-
}
1101-
for _, entry := range entries {
1102-
if !template.CouldBe(entry.Name()) {
1103-
continue
1104-
}
1105-
fullName := path.Join(dirName, entry.Name())
1106-
if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
1107-
invalidFiles[fullName] = err
1108-
} else {
1109-
if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
1110-
it.Ref = git.BranchPrefix + it.Ref
1111-
}
1112-
issueTemplates = append(issueTemplates, it)
1113-
}
1114-
}
1115-
}
1116-
return issueTemplates, invalidFiles
1117-
}
1118-
11191065
func GetDefaultIssueConfig() api.IssueConfig {
11201066
return api.IssueConfig{
11211067
BlankIssuesEnabled: true,
@@ -1177,31 +1123,6 @@ func (r *Repository) GetIssueConfig(path string, commit *git.Commit) (api.IssueC
11771123
return issueConfig, nil
11781124
}
11791125

1180-
// IssueConfigFromDefaultBranch returns the issue config for this repo.
1181-
// It never returns a nil config.
1182-
func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) {
1183-
if ctx.Repo.Repository.IsEmpty {
1184-
return GetDefaultIssueConfig(), nil
1185-
}
1186-
1187-
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
1188-
if err != nil {
1189-
return GetDefaultIssueConfig(), err
1190-
}
1191-
1192-
for _, configName := range IssueConfigCandidates {
1193-
if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil {
1194-
return ctx.Repo.GetIssueConfig(configName+".yaml", commit)
1195-
}
1196-
1197-
if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil {
1198-
return ctx.Repo.GetIssueConfig(configName+".yml", commit)
1199-
}
1200-
}
1201-
1202-
return GetDefaultIssueConfig(), nil
1203-
}
1204-
12051126
// IsIssueConfig returns if the given path is a issue config file.
12061127
func (r *Repository) IsIssueConfig(path string) bool {
12071128
for _, configName := range IssueConfigCandidates {
@@ -1211,12 +1132,3 @@ func (r *Repository) IsIssueConfig(path string) bool {
12111132
}
12121133
return false
12131134
}
1214-
1215-
func (ctx *Context) HasIssueTemplatesOrContactLinks() bool {
1216-
if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 {
1217-
return true
1218-
}
1219-
1220-
issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
1221-
return len(issueConfig.ContactLinks) > 0
1222-
}

‎routers/api/v1/repo/repo.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func Search(ctx *context.APIContext) {
178178
if len(sortOrder) == 0 {
179179
sortOrder = "asc"
180180
}
181-
if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
181+
if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok {
182182
if orderBy, ok := searchModeMap[sortMode]; ok {
183183
opts.OrderBy = orderBy
184184
} else {

‎routers/web/admin/users.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func NewUserPost(ctx *context.Context) {
148148
}
149149
if !password.IsComplexEnough(form.Password) {
150150
ctx.Data["Err_Password"] = true
151-
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form)
151+
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
152152
return
153153
}
154154
pwned, err := password.IsPwned(ctx, form.Password)
@@ -301,7 +301,7 @@ func EditUserPost(ctx *context.Context) {
301301
return
302302
}
303303
if !password.IsComplexEnough(form.Password) {
304-
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form)
304+
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
305305
return
306306
}
307307
pwned, err := password.IsPwned(ctx, form.Password)

‎routers/web/auth/auth.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ func SignUpPost(ctx *context.Context) {
444444
}
445445
if !password.IsComplexEnough(form.Password) {
446446
ctx.Data["Err_Password"] = true
447-
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form)
447+
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
448448
return
449449
}
450450
pwned, err := password.IsPwned(ctx, form.Password)

‎routers/web/auth/password.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func ResetPasswdPost(ctx *context.Context) {
176176
} else if !password.IsComplexEnough(passwd) {
177177
ctx.Data["IsResetForm"] = true
178178
ctx.Data["Err_Password"] = true
179-
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil)
179+
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
180180
return
181181
} else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
182182
errMsg := ctx.Tr("auth.password_pwned")
@@ -305,7 +305,7 @@ func MustChangePasswordPost(ctx *context.Context) {
305305

306306
if !password.IsComplexEnough(form.Password) {
307307
ctx.Data["Err_Password"] = true
308-
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplMustChangePassword, &form)
308+
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
309309
return
310310
}
311311
pwned, err := password.IsPwned(ctx, form.Password)

‎routers/web/repo/repo.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ func SearchRepo(ctx *context.Context) {
546546
if len(sortOrder) == 0 {
547547
sortOrder = "asc"
548548
}
549-
if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
549+
if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok {
550550
if orderBy, ok := searchModeMap[sortMode]; ok {
551551
opts.OrderBy = orderBy
552552
} else {

‎routers/web/repo/view.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
6666
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
6767
// 2. Txt files - e.g. README.txt
6868
// 3. No extension - e.g. README
69-
exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority
69+
exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
7070
extCount := len(exts)
7171
readmeFiles := make([]*git.TreeEntry, extCount+1)
7272

‎routers/web/user/setting/account.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func AccountPost(ctx *context.Context) {
6060
} else if form.Password != form.Retype {
6161
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
6262
} else if !password.IsComplexEnough(form.Password) {
63-
ctx.Flash.Error(password.BuildComplexityError(ctx))
63+
ctx.Flash.Error(password.BuildComplexityError(ctx.Locale))
6464
} else if pwned, err := password.IsPwned(ctx, form.Password); pwned || err != nil {
6565
errMsg := ctx.Tr("auth.password_pwned")
6666
if err != nil {

0 commit comments

Comments
 (0)
Please sign in to comment.