From cb700aedd1e670fb47b8cf0cd67fb117a1ad88a2 Mon Sep 17 00:00:00 2001 From: wxiaoguang 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 d2a3311c8b75..590313286236 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 000000000000..4d64acf8cfe3 --- /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 d73a26e5b6f3..3e1b48dcde9b 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...) -} - -// GetData returns the data -func (ctx *Context) GetData() middleware.ContextData { - return ctx.Data -} - -// 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) + return ctx.Locale.Tr(msg, trArgs...) } -// 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 + "/") +func (ctx *Context) Tr(msg string, args ...any) string { + return ctx.Locale.Tr(msg, args...) } -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 000000000000..5cb4ea0aca38 --- /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 000000000000..cdf4ff9afe3d --- /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 000000000000..5ba98f7e01a5 --- /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/ + 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 000000000000..0b87552c085b --- /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 000000000000..8adff96994bb --- /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 000000000000..44dd739eff72 --- /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 a1c8f43644d5..b33341c2456a 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/ - 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 36cc03f8938e..480ca397d498 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 bd31d9d632f2..3895bcfdb929 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 d8042afeccb2..e0883a269656 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 ed0412d74500..b34a1d8fce16 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 c4b5351eca28..2f87e190228a 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 9f2770a3acb9..2bf293cbda83 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 0e48013b04da..a67c2398fb0b 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 {