mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Split "modules/context.go" to separate files (#24569)
The "modules/context.go" is too large to maintain. This PR splits it to separate files, eg: context_request.go, context_response.go, context_serve.go This PR will help: 1. The future refactoring for Gitea's web context (eg: simplify the context) 2. Introduce proper "range request" support 3. Introduce context function This PR only moves code, doesn't change any logic.
This commit is contained in:
		| @@ -35,9 +35,6 @@ import ( | |||||||
| 	"xorm.io/builder" | 	"xorm.io/builder" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // ItemsPerPage maximum items per page in forks, watchers and stars of a repo |  | ||||||
| var ItemsPerPage = 40 |  | ||||||
|  |  | ||||||
| // Init initialize model | // Init initialize model | ||||||
| func Init(ctx context.Context) error { | func Init(ctx context.Context) error { | ||||||
| 	if err := unit.LoadUnitConfig(); err != nil { | 	if err := unit.LoadUnitConfig(); err != nil { | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								models/repo/search.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								models/repo/search.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||||
|  | 	}, | ||||||
|  | } | ||||||
| @@ -6,45 +6,28 @@ package context | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/hex" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"html" | 	"html" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" |  | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/base" |  | ||||||
| 	mc "code.gitea.io/gitea/modules/cache" | 	mc "code.gitea.io/gitea/modules/cache" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/httpcache" | 	"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/setting" | ||||||
| 	"code.gitea.io/gitea/modules/templates" | 	"code.gitea.io/gitea/modules/templates" | ||||||
| 	"code.gitea.io/gitea/modules/translation" | 	"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" | 	"code.gitea.io/gitea/modules/web/middleware" | ||||||
|  |  | ||||||
| 	"gitea.com/go-chi/cache" | 	"gitea.com/go-chi/cache" | ||||||
| 	"gitea.com/go-chi/session" | 	"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 | // Render represents a template render | ||||||
| type Render interface { | type Render interface { | ||||||
| 	TemplateLookup(tmpl string) (templates.TemplateExecutor, error) | 	TemplateLookup(tmpl string) (templates.TemplateExecutor, error) | ||||||
| @@ -56,9 +39,9 @@ type Context struct { | |||||||
| 	Resp     ResponseWriter | 	Resp     ResponseWriter | ||||||
| 	Req      *http.Request | 	Req      *http.Request | ||||||
| 	Data     middleware.ContextData // data used by MVC templates | 	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 | 	Render   Render | ||||||
| 	translation.Locale | 	Locale   translation.Locale | ||||||
| 	Cache    cache.Cache | 	Cache    cache.Cache | ||||||
| 	Csrf     CSRFProtector | 	Csrf     CSRFProtector | ||||||
| 	Flash    *middleware.Flash | 	Flash    *middleware.Flash | ||||||
| @@ -86,513 +69,22 @@ func (ctx *Context) Close() error { | |||||||
| 	return err | 	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. | // This is useful if the locale message is intended to only produce HTML content. | ||||||
| func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { | func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { | ||||||
| 	trArgs := make([]interface{}, len(args)) | 	trArgs := make([]interface{}, len(args)) | ||||||
| 	for i, arg := range args { | 	for i, arg := range args { | ||||||
| 		trArgs[i] = html.EscapeString(arg) | 		trArgs[i] = html.EscapeString(arg) | ||||||
| 	} | 	} | ||||||
| 	return ctx.Tr(msg, trArgs...) | 	return ctx.Locale.Tr(msg, trArgs...) | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetData returns the data | func (ctx *Context) Tr(msg string, args ...any) string { | ||||||
| func (ctx *Context) GetData() middleware.ContextData { | 	return ctx.Locale.Tr(msg, args...) | ||||||
| 	return ctx.Data |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // IsUserSiteAdmin returns true if current user is a site admin | func (ctx *Context) TrN(cnt any, key1, keyN string, args ...any) string { | ||||||
| func (ctx *Context) IsUserSiteAdmin() bool { | 	return ctx.Locale.TrN(cnt, key1, keyN, args...) | ||||||
| 	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) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Deadline is part of the interface for context.Context and we pass this to the request context | // 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) | 	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{} | type contextKeyType struct{} | ||||||
|  |  | ||||||
| var contextKey interface{} = contextKeyType{} | var contextKey interface{} = contextKeyType{} | ||||||
| @@ -657,19 +130,10 @@ func GetContext(req *http.Request) *Context { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetContextUser returns context user | // Contexter initializes a classic context for a request. | ||||||
| func GetContextUser(req *http.Request) *user_model.User { | func Contexter() func(next http.Handler) http.Handler { | ||||||
| 	if apiContext, ok := req.Context().Value(apiContextKey).(*APIContext); ok { | 	rnd := templates.HTMLRenderer() | ||||||
| 		return apiContext.Doer | 	csrfOpts := CsrfOptions{ | ||||||
| 	} |  | ||||||
| 	if ctx, ok := req.Context().Value(contextKey).(*Context); ok { |  | ||||||
| 		return ctx.Doer |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func getCsrfOpts() CsrfOptions { |  | ||||||
| 	return CsrfOptions{ |  | ||||||
| 		Secret:         setting.SecretKey, | 		Secret:         setting.SecretKey, | ||||||
| 		Cookie:         setting.CSRFCookieName, | 		Cookie:         setting.CSRFCookieName, | ||||||
| 		SetCookie:      true, | 		SetCookie:      true, | ||||||
| @@ -680,12 +144,6 @@ func getCsrfOpts() CsrfOptions { | |||||||
| 		CookiePath:     setting.SessionConfig.CookiePath, | 		CookiePath:     setting.SessionConfig.CookiePath, | ||||||
| 		SameSite:       setting.SessionConfig.SameSite, | 		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 { | 	if !setting.IsProd { | ||||||
| 		CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose | 		CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose | ||||||
| 	} | 	} | ||||||
| @@ -776,21 +234,3 @@ func Contexter() func(next http.Handler) http.Handler { | |||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // SearchOrderByMap represents all possible search order |  | ||||||
| var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ |  | ||||||
| 	"asc": { |  | ||||||
| 		"alpha":   db.SearchOrderByAlphabetically, |  | ||||||
| 		"created": db.SearchOrderByOldest, |  | ||||||
| 		"updated": db.SearchOrderByLeastUpdated, |  | ||||||
| 		"size":    db.SearchOrderBySize, |  | ||||||
| 		"id":      db.SearchOrderByID, |  | ||||||
| 	}, |  | ||||||
| 	"desc": { |  | ||||||
| 		"alpha":   db.SearchOrderByAlphabeticallyReverse, |  | ||||||
| 		"created": db.SearchOrderByNewest, |  | ||||||
| 		"updated": db.SearchOrderByRecentUpdated, |  | ||||||
| 		"size":    db.SearchOrderBySizeReverse, |  | ||||||
| 		"id":      db.SearchOrderByIDReverse, |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										105
									
								
								modules/context/context_cookie.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								modules/context/context_cookie.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package context | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | 	"code.gitea.io/gitea/modules/web/middleware" | ||||||
|  |  | ||||||
|  | 	"github.com/minio/sha256-simd" | ||||||
|  | 	"golang.org/x/crypto/pbkdf2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const CookieNameFlash = "gitea_flash" | ||||||
|  |  | ||||||
|  | func removeSessionCookieHeader(w http.ResponseWriter) { | ||||||
|  | 	cookies := w.Header()["Set-Cookie"] | ||||||
|  | 	w.Header().Del("Set-Cookie") | ||||||
|  | 	for _, cookie := range cookies { | ||||||
|  | 		if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		w.Header().Add("Set-Cookie", cookie) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetSiteCookie convenience function to set most cookies consistently | ||||||
|  | // CSRF and a few others are the exception here | ||||||
|  | func (ctx *Context) SetSiteCookie(name, value string, maxAge int) { | ||||||
|  | 	middleware.SetSiteCookie(ctx.Resp, name, value, maxAge) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteSiteCookie convenience function to delete most cookies consistently | ||||||
|  | // CSRF and a few others are the exception here | ||||||
|  | func (ctx *Context) DeleteSiteCookie(name string) { | ||||||
|  | 	middleware.SetSiteCookie(ctx.Resp, name, "", -1) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSiteCookie returns given cookie value from request header. | ||||||
|  | func (ctx *Context) GetSiteCookie(name string) string { | ||||||
|  | 	return middleware.GetSiteCookie(ctx.Req, name) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetSuperSecureCookie returns given cookie value from request header with secret string. | ||||||
|  | func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) { | ||||||
|  | 	val := ctx.GetSiteCookie(name) | ||||||
|  | 	return ctx.CookieDecrypt(secret, val) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CookieDecrypt returns given value from with secret string. | ||||||
|  | func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) { | ||||||
|  | 	if val == "" { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	text, err := hex.DecodeString(val) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) | ||||||
|  | 	text, err = util.AESGCMDecrypt(key, text) | ||||||
|  | 	return string(text), err == nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetSuperSecureCookie sets given cookie value to response header with secret string. | ||||||
|  | func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) { | ||||||
|  | 	text := ctx.CookieEncrypt(secret, value) | ||||||
|  | 	ctx.SetSiteCookie(name, text, maxAge) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CookieEncrypt encrypts a given value using the provided secret | ||||||
|  | func (ctx *Context) CookieEncrypt(secret, value string) string { | ||||||
|  | 	key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) | ||||||
|  | 	text, err := util.AESGCMEncrypt(key, []byte(value)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic("error encrypting cookie: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return hex.EncodeToString(text) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetCookieInt returns cookie result in int type. | ||||||
|  | func (ctx *Context) GetCookieInt(name string) int { | ||||||
|  | 	r, _ := strconv.Atoi(ctx.GetSiteCookie(name)) | ||||||
|  | 	return r | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetCookieInt64 returns cookie result in int64 type. | ||||||
|  | func (ctx *Context) GetCookieInt64(name string) int64 { | ||||||
|  | 	r, _ := strconv.ParseInt(ctx.GetSiteCookie(name), 10, 64) | ||||||
|  | 	return r | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetCookieFloat64 returns cookie result in float64 type. | ||||||
|  | func (ctx *Context) GetCookieFloat64(name string) float64 { | ||||||
|  | 	v, _ := strconv.ParseFloat(ctx.GetSiteCookie(name), 64) | ||||||
|  | 	return v | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								modules/context/context_data.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								modules/context/context_data.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package context | ||||||
|  |  | ||||||
|  | import "code.gitea.io/gitea/modules/web/middleware" | ||||||
|  |  | ||||||
|  | // GetData returns the data | ||||||
|  | func (ctx *Context) GetData() middleware.ContextData { | ||||||
|  | 	return ctx.Data | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HasAPIError returns true if error occurs in form validation. | ||||||
|  | func (ctx *Context) HasAPIError() bool { | ||||||
|  | 	hasErr, ok := ctx.Data["HasError"] | ||||||
|  | 	if !ok { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return hasErr.(bool) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetErrMsg returns error message | ||||||
|  | func (ctx *Context) GetErrMsg() string { | ||||||
|  | 	return ctx.Data["ErrorMsg"].(string) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HasError returns true if error occurs in form validation. | ||||||
|  | // Attention: this function changes ctx.Data and ctx.Flash | ||||||
|  | func (ctx *Context) HasError() bool { | ||||||
|  | 	hasErr, ok := ctx.Data["HasError"] | ||||||
|  | 	if !ok { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string) | ||||||
|  | 	ctx.Data["Flash"] = ctx.Flash | ||||||
|  | 	return hasErr.(bool) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HasValue returns true if value of given name exists. | ||||||
|  | func (ctx *Context) HasValue(name string) bool { | ||||||
|  | 	_, ok := ctx.Data[name] | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
							
								
								
									
										138
									
								
								modules/context/context_model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								modules/context/context_model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package context | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"path" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/unit" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/issue/template" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // IsUserSiteAdmin returns true if current user is a site admin | ||||||
|  | func (ctx *Context) IsUserSiteAdmin() bool { | ||||||
|  | 	return ctx.IsSigned && ctx.Doer.IsAdmin | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsUserRepoOwner returns true if current user owns current repo | ||||||
|  | func (ctx *Context) IsUserRepoOwner() bool { | ||||||
|  | 	return ctx.Repo.IsOwner() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsUserRepoAdmin returns true if current user is admin in current repo | ||||||
|  | func (ctx *Context) IsUserRepoAdmin() bool { | ||||||
|  | 	return ctx.Repo.IsAdmin() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsUserRepoWriter returns true if current user has write privilege in current repo | ||||||
|  | func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool { | ||||||
|  | 	for _, unitType := range unitTypes { | ||||||
|  | 		if ctx.Repo.CanWrite(unitType) { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsUserRepoReaderSpecific returns true if current user can read current repo's specific part | ||||||
|  | func (ctx *Context) IsUserRepoReaderSpecific(unitType unit.Type) bool { | ||||||
|  | 	return ctx.Repo.CanRead(unitType) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsUserRepoReaderAny returns true if current user can read any part of current repo | ||||||
|  | func (ctx *Context) IsUserRepoReaderAny() bool { | ||||||
|  | 	return ctx.Repo.HasAccess() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IssueTemplatesFromDefaultBranch checks for valid issue templates in the repo's default branch, | ||||||
|  | func (ctx *Context) IssueTemplatesFromDefaultBranch() []*api.IssueTemplate { | ||||||
|  | 	ret, _ := ctx.IssueTemplatesErrorsFromDefaultBranch() | ||||||
|  | 	return ret | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IssueTemplatesErrorsFromDefaultBranch checks for issue templates in the repo's default branch, | ||||||
|  | // returns valid templates and the errors of invalid template files. | ||||||
|  | func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplate, map[string]error) { | ||||||
|  | 	var issueTemplates []*api.IssueTemplate | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.Repository.IsEmpty { | ||||||
|  | 		return issueTemplates, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.Commit == nil { | ||||||
|  | 		var err error | ||||||
|  | 		ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return issueTemplates, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	invalidFiles := map[string]error{} | ||||||
|  | 	for _, dirName := range IssueTemplateDirCandidates { | ||||||
|  | 		tree, err := ctx.Repo.Commit.SubTree(dirName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Debug("get sub tree of %s: %v", dirName, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		entries, err := tree.ListEntries() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Debug("list entries in %s: %v", dirName, err) | ||||||
|  | 			return issueTemplates, nil | ||||||
|  | 		} | ||||||
|  | 		for _, entry := range entries { | ||||||
|  | 			if !template.CouldBe(entry.Name()) { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			fullName := path.Join(dirName, entry.Name()) | ||||||
|  | 			if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil { | ||||||
|  | 				invalidFiles[fullName] = err | ||||||
|  | 			} else { | ||||||
|  | 				if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref> | ||||||
|  | 					it.Ref = git.BranchPrefix + it.Ref | ||||||
|  | 				} | ||||||
|  | 				issueTemplates = append(issueTemplates, it) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return issueTemplates, invalidFiles | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IssueConfigFromDefaultBranch returns the issue config for this repo. | ||||||
|  | // It never returns a nil config. | ||||||
|  | func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) { | ||||||
|  | 	if ctx.Repo.Repository.IsEmpty { | ||||||
|  | 		return GetDefaultIssueConfig(), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return GetDefaultIssueConfig(), err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, configName := range IssueConfigCandidates { | ||||||
|  | 		if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil { | ||||||
|  | 			return ctx.Repo.GetIssueConfig(configName+".yaml", commit) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil { | ||||||
|  | 			return ctx.Repo.GetIssueConfig(configName+".yml", commit) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return GetDefaultIssueConfig(), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ctx *Context) HasIssueTemplatesOrContactLinks() bool { | ||||||
|  | 	if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	issueConfig, _ := ctx.IssueConfigFromDefaultBranch() | ||||||
|  | 	return len(issueConfig.ContactLinks) > 0 | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								modules/context/context_request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								modules/context/context_request.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package context | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/go-chi/chi/v5" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // RemoteAddr returns the client machine ip address | ||||||
|  | func (ctx *Context) RemoteAddr() string { | ||||||
|  | 	return ctx.Req.RemoteAddr | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Params returns the param on route | ||||||
|  | func (ctx *Context) Params(p string) string { | ||||||
|  | 	s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":"))) | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ParamsInt64 returns the param on route as int64 | ||||||
|  | func (ctx *Context) ParamsInt64(p string) int64 { | ||||||
|  | 	v, _ := strconv.ParseInt(ctx.Params(p), 10, 64) | ||||||
|  | 	return v | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetParams set params into routes | ||||||
|  | func (ctx *Context) SetParams(k, v string) { | ||||||
|  | 	chiCtx := chi.RouteContext(ctx) | ||||||
|  | 	chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UploadStream returns the request body or the first form file | ||||||
|  | // Only form files need to get closed. | ||||||
|  | func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) { | ||||||
|  | 	contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type")) | ||||||
|  | 	if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") { | ||||||
|  | 		if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil { | ||||||
|  | 			return nil, false, err | ||||||
|  | 		} | ||||||
|  | 		if ctx.Req.MultipartForm.File == nil { | ||||||
|  | 			return nil, false, http.ErrMissingFile | ||||||
|  | 		} | ||||||
|  | 		for _, files := range ctx.Req.MultipartForm.File { | ||||||
|  | 			if len(files) > 0 { | ||||||
|  | 				r, err := files[0].Open() | ||||||
|  | 				return r, true, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return nil, false, http.ErrMissingFile | ||||||
|  | 	} | ||||||
|  | 	return ctx.Req.Body, false, nil | ||||||
|  | } | ||||||
							
								
								
									
										279
									
								
								modules/context/context_response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								modules/context/context_response.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,279 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package context | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"path" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/base" | ||||||
|  | 	"code.gitea.io/gitea/modules/json" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/templates" | ||||||
|  | 	"code.gitea.io/gitea/modules/web/middleware" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // SetTotalCountHeader set "X-Total-Count" header | ||||||
|  | func (ctx *Context) SetTotalCountHeader(total int64) { | ||||||
|  | 	ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) | ||||||
|  | 	ctx.AppendAccessControlExposeHeaders("X-Total-Count") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header | ||||||
|  | func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) { | ||||||
|  | 	val := ctx.RespHeader().Get("Access-Control-Expose-Headers") | ||||||
|  | 	if len(val) != 0 { | ||||||
|  | 		ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) | ||||||
|  | 	} else { | ||||||
|  | 		ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Written returns true if there are something sent to web browser | ||||||
|  | func (ctx *Context) Written() bool { | ||||||
|  | 	return ctx.Resp.Status() > 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Status writes status code | ||||||
|  | func (ctx *Context) Status(status int) { | ||||||
|  | 	ctx.Resp.WriteHeader(status) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Write writes data to web browser | ||||||
|  | func (ctx *Context) Write(bs []byte) (int, error) { | ||||||
|  | 	return ctx.Resp.Write(bs) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RedirectToUser redirect to a differently-named user | ||||||
|  | func RedirectToUser(ctx *Context, userName string, redirectUserID int64) { | ||||||
|  | 	user, err := user_model.GetUserByID(ctx, redirectUserID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("GetUserByID", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	redirectPath := strings.Replace( | ||||||
|  | 		ctx.Req.URL.EscapedPath(), | ||||||
|  | 		url.PathEscape(userName), | ||||||
|  | 		url.PathEscape(user.Name), | ||||||
|  | 		1, | ||||||
|  | 	) | ||||||
|  | 	if ctx.Req.URL.RawQuery != "" { | ||||||
|  | 		redirectPath += "?" + ctx.Req.URL.RawQuery | ||||||
|  | 	} | ||||||
|  | 	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RedirectToFirst redirects to first not empty URL | ||||||
|  | func (ctx *Context) RedirectToFirst(location ...string) { | ||||||
|  | 	for _, loc := range location { | ||||||
|  | 		if len(loc) == 0 { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Unfortunately browsers consider a redirect Location with preceding "//" and "/\" as meaning redirect to "http(s)://REST_OF_PATH" | ||||||
|  | 		// Therefore we should ignore these redirect locations to prevent open redirects | ||||||
|  | 		if len(loc) > 1 && loc[0] == '/' && (loc[1] == '/' || loc[1] == '\\') { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		u, err := url.Parse(loc) | ||||||
|  | 		if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(loc), strings.ToLower(setting.AppURL))) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		ctx.Redirect(loc) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Redirect(setting.AppSubURL + "/") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const tplStatus500 base.TplName = "status/500" | ||||||
|  |  | ||||||
|  | // HTML calls Context.HTML and renders the template to HTTP response | ||||||
|  | func (ctx *Context) HTML(status int, name base.TplName) { | ||||||
|  | 	log.Debug("Template: %s", name) | ||||||
|  |  | ||||||
|  | 	tmplStartTime := time.Now() | ||||||
|  | 	if !setting.IsProd { | ||||||
|  | 		ctx.Data["TemplateName"] = name | ||||||
|  | 	} | ||||||
|  | 	ctx.Data["TemplateLoadTimes"] = func() string { | ||||||
|  | 		return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data) | ||||||
|  | 	if err == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// if rendering fails, show error page | ||||||
|  | 	if name != tplStatus500 { | ||||||
|  | 		err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err)) | ||||||
|  | 		ctx.ServerError("Render failed", err) // show the 500 error page | ||||||
|  | 	} else { | ||||||
|  | 		ctx.PlainText(http.StatusInternalServerError, "Unable to render status/500 page, the template system is broken, or Gitea can't find your template files.") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RenderToString renders the template content to a string | ||||||
|  | func (ctx *Context) RenderToString(name base.TplName, data map[string]interface{}) (string, error) { | ||||||
|  | 	var buf strings.Builder | ||||||
|  | 	err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data) | ||||||
|  | 	return buf.String(), err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RenderWithErr used for page has form validation but need to prompt error to users. | ||||||
|  | func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form interface{}) { | ||||||
|  | 	if form != nil { | ||||||
|  | 		middleware.AssignForm(form, ctx.Data) | ||||||
|  | 	} | ||||||
|  | 	ctx.Flash.ErrorMsg = msg | ||||||
|  | 	ctx.Data["Flash"] = ctx.Flash | ||||||
|  | 	ctx.HTML(http.StatusOK, tpl) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NotFound displays a 404 (Not Found) page and prints the given error, if any. | ||||||
|  | func (ctx *Context) NotFound(logMsg string, logErr error) { | ||||||
|  | 	ctx.notFoundInternal(logMsg, logErr) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ctx *Context) notFoundInternal(logMsg string, logErr error) { | ||||||
|  | 	if logErr != nil { | ||||||
|  | 		log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr) | ||||||
|  | 		if !setting.IsProd { | ||||||
|  | 			ctx.Data["ErrorMsg"] = logErr | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// response simple message if Accept isn't text/html | ||||||
|  | 	showHTML := false | ||||||
|  | 	for _, part := range ctx.Req.Header["Accept"] { | ||||||
|  | 		if strings.Contains(part, "text/html") { | ||||||
|  | 			showHTML = true | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !showHTML { | ||||||
|  | 		ctx.plainTextInternal(3, http.StatusNotFound, []byte("Not found.\n")) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Data["IsRepo"] = ctx.Repo.Repository != nil | ||||||
|  | 	ctx.Data["Title"] = "Page Not Found" | ||||||
|  | 	ctx.HTML(http.StatusNotFound, base.TplName("status/404")) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ServerError displays a 500 (Internal Server Error) page and prints the given error, if any. | ||||||
|  | func (ctx *Context) ServerError(logMsg string, logErr error) { | ||||||
|  | 	ctx.serverErrorInternal(logMsg, logErr) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ctx *Context) serverErrorInternal(logMsg string, logErr error) { | ||||||
|  | 	if logErr != nil { | ||||||
|  | 		log.ErrorWithSkip(2, "%s: %v", logMsg, logErr) | ||||||
|  | 		if _, ok := logErr.(*net.OpError); ok || errors.Is(logErr, &net.OpError{}) { | ||||||
|  | 			// This is an error within the underlying connection | ||||||
|  | 			// and further rendering will not work so just return | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// it's safe to show internal error to admin users, and it helps | ||||||
|  | 		if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) { | ||||||
|  | 			ctx.Data["ErrorMsg"] = fmt.Sprintf("%s, %s", logMsg, logErr) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Data["Title"] = "Internal Server Error" | ||||||
|  | 	ctx.HTML(http.StatusInternalServerError, tplStatus500) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NotFoundOrServerError use error check function to determine if the error | ||||||
|  | // is about not found. It responds with 404 status code for not found error, | ||||||
|  | // or error context description for logging purpose of 500 server error. | ||||||
|  | func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) { | ||||||
|  | 	if errCheck(logErr) { | ||||||
|  | 		ctx.notFoundInternal(logMsg, logErr) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ctx.serverErrorInternal(logMsg, logErr) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // PlainTextBytes renders bytes as plain text | ||||||
|  | func (ctx *Context) plainTextInternal(skip, status int, bs []byte) { | ||||||
|  | 	statusPrefix := status / 100 | ||||||
|  | 	if statusPrefix == 4 || statusPrefix == 5 { | ||||||
|  | 		log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs)) | ||||||
|  | 	} | ||||||
|  | 	ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") | ||||||
|  | 	ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff") | ||||||
|  | 	ctx.Resp.WriteHeader(status) | ||||||
|  | 	if _, err := ctx.Resp.Write(bs); err != nil { | ||||||
|  | 		log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // PlainTextBytes renders bytes as plain text | ||||||
|  | func (ctx *Context) PlainTextBytes(status int, bs []byte) { | ||||||
|  | 	ctx.plainTextInternal(2, status, bs) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // PlainText renders content as plain text | ||||||
|  | func (ctx *Context) PlainText(status int, text string) { | ||||||
|  | 	ctx.plainTextInternal(2, status, []byte(text)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RespHeader returns the response header | ||||||
|  | func (ctx *Context) RespHeader() http.Header { | ||||||
|  | 	return ctx.Resp.Header() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Error returned an error to web browser | ||||||
|  | func (ctx *Context) Error(status int, contents ...string) { | ||||||
|  | 	v := http.StatusText(status) | ||||||
|  | 	if len(contents) > 0 { | ||||||
|  | 		v = contents[0] | ||||||
|  | 	} | ||||||
|  | 	http.Error(ctx.Resp, v, status) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // JSON render content as JSON | ||||||
|  | func (ctx *Context) JSON(status int, content interface{}) { | ||||||
|  | 	ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") | ||||||
|  | 	ctx.Resp.WriteHeader(status) | ||||||
|  | 	if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil { | ||||||
|  | 		ctx.ServerError("Render JSON failed", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Redirect redirects the request | ||||||
|  | func (ctx *Context) Redirect(location string, status ...int) { | ||||||
|  | 	code := http.StatusSeeOther | ||||||
|  | 	if len(status) == 1 { | ||||||
|  | 		code = status[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if strings.Contains(location, "://") || strings.HasPrefix(location, "//") { | ||||||
|  | 		// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path | ||||||
|  | 		// 1. the first request to "/my-path" contains cookie | ||||||
|  | 		// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking) | ||||||
|  | 		// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser | ||||||
|  | 		// 4. then the browser accepts the empty session, then the user is logged out | ||||||
|  | 		// So in this case, we should remove the session cookie from the response header | ||||||
|  | 		removeSessionCookieHeader(ctx.Resp) | ||||||
|  | 	} | ||||||
|  | 	http.Redirect(ctx.Resp, ctx.Req, location, code) | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								modules/context/context_serve.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								modules/context/context_serve.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package context | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/httpcache" | ||||||
|  | 	"code.gitea.io/gitea/modules/typesniffer" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type ServeHeaderOptions struct { | ||||||
|  | 	ContentType        string // defaults to "application/octet-stream" | ||||||
|  | 	ContentTypeCharset string | ||||||
|  | 	ContentLength      *int64 | ||||||
|  | 	Disposition        string // defaults to "attachment" | ||||||
|  | 	Filename           string | ||||||
|  | 	CacheDuration      time.Duration // defaults to 5 minutes | ||||||
|  | 	LastModified       time.Time | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetServeHeaders sets necessary content serve headers | ||||||
|  | func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) { | ||||||
|  | 	header := ctx.Resp.Header() | ||||||
|  |  | ||||||
|  | 	contentType := typesniffer.ApplicationOctetStream | ||||||
|  | 	if opts.ContentType != "" { | ||||||
|  | 		if opts.ContentTypeCharset != "" { | ||||||
|  | 			contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset) | ||||||
|  | 		} else { | ||||||
|  | 			contentType = opts.ContentType | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	header.Set("Content-Type", contentType) | ||||||
|  | 	header.Set("X-Content-Type-Options", "nosniff") | ||||||
|  |  | ||||||
|  | 	if opts.ContentLength != nil { | ||||||
|  | 		header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if opts.Filename != "" { | ||||||
|  | 		disposition := opts.Disposition | ||||||
|  | 		if disposition == "" { | ||||||
|  | 			disposition = "attachment" | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \" | ||||||
|  | 		header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename))) | ||||||
|  | 		header.Set("Access-Control-Expose-Headers", "Content-Disposition") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	duration := opts.CacheDuration | ||||||
|  | 	if duration == 0 { | ||||||
|  | 		duration = 5 * time.Minute | ||||||
|  | 	} | ||||||
|  | 	httpcache.SetCacheControlInHeader(header, duration) | ||||||
|  |  | ||||||
|  | 	if !opts.LastModified.IsZero() { | ||||||
|  | 		header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ServeContent serves content to http request | ||||||
|  | func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { | ||||||
|  | 	ctx.SetServeHeaders(opts) | ||||||
|  | 	http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r) | ||||||
|  | } | ||||||
| @@ -25,7 +25,6 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/cache" | 	"code.gitea.io/gitea/modules/cache" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	code_indexer "code.gitea.io/gitea/modules/indexer/code" | 	code_indexer "code.gitea.io/gitea/modules/indexer/code" | ||||||
| 	"code.gitea.io/gitea/modules/issue/template" |  | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | 	repo_module "code.gitea.io/gitea/modules/repository" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"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 { | func GetDefaultIssueConfig() api.IssueConfig { | ||||||
| 	return api.IssueConfig{ | 	return api.IssueConfig{ | ||||||
| 		BlankIssuesEnabled: true, | 		BlankIssuesEnabled: true, | ||||||
| @@ -1177,31 +1123,6 @@ func (r *Repository) GetIssueConfig(path string, commit *git.Commit) (api.IssueC | |||||||
| 	return issueConfig, nil | 	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. | // IsIssueConfig returns if the given path is a issue config file. | ||||||
| func (r *Repository) IsIssueConfig(path string) bool { | func (r *Repository) IsIssueConfig(path string) bool { | ||||||
| 	for _, configName := range IssueConfigCandidates { | 	for _, configName := range IssueConfigCandidates { | ||||||
| @@ -1211,12 +1132,3 @@ func (r *Repository) IsIssueConfig(path string) bool { | |||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| func (ctx *Context) HasIssueTemplatesOrContactLinks() bool { |  | ||||||
| 	if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 { |  | ||||||
| 		return true |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	issueConfig, _ := ctx.IssueConfigFromDefaultBranch() |  | ||||||
| 	return len(issueConfig.ContactLinks) > 0 |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -178,7 +178,7 @@ func Search(ctx *context.APIContext) { | |||||||
| 		if len(sortOrder) == 0 { | 		if len(sortOrder) == 0 { | ||||||
| 			sortOrder = "asc" | 			sortOrder = "asc" | ||||||
| 		} | 		} | ||||||
| 		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok { | 		if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { | ||||||
| 			if orderBy, ok := searchModeMap[sortMode]; ok { | 			if orderBy, ok := searchModeMap[sortMode]; ok { | ||||||
| 				opts.OrderBy = orderBy | 				opts.OrderBy = orderBy | ||||||
| 			} else { | 			} else { | ||||||
|   | |||||||
| @@ -148,7 +148,7 @@ func NewUserPost(ctx *context.Context) { | |||||||
| 		} | 		} | ||||||
| 		if !password.IsComplexEnough(form.Password) { | 		if !password.IsComplexEnough(form.Password) { | ||||||
| 			ctx.Data["Err_Password"] = true | 			ctx.Data["Err_Password"] = true | ||||||
| 			ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form) | 			ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		pwned, err := password.IsPwned(ctx, form.Password) | 		pwned, err := password.IsPwned(ctx, form.Password) | ||||||
| @@ -301,7 +301,7 @@ func EditUserPost(ctx *context.Context) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		if !password.IsComplexEnough(form.Password) { | 		if !password.IsComplexEnough(form.Password) { | ||||||
| 			ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form) | 			ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		pwned, err := password.IsPwned(ctx, form.Password) | 		pwned, err := password.IsPwned(ctx, form.Password) | ||||||
|   | |||||||
| @@ -444,7 +444,7 @@ func SignUpPost(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
| 	if !password.IsComplexEnough(form.Password) { | 	if !password.IsComplexEnough(form.Password) { | ||||||
| 		ctx.Data["Err_Password"] = true | 		ctx.Data["Err_Password"] = true | ||||||
| 		ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form) | 		ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	pwned, err := password.IsPwned(ctx, form.Password) | 	pwned, err := password.IsPwned(ctx, form.Password) | ||||||
|   | |||||||
| @@ -176,7 +176,7 @@ func ResetPasswdPost(ctx *context.Context) { | |||||||
| 	} else if !password.IsComplexEnough(passwd) { | 	} else if !password.IsComplexEnough(passwd) { | ||||||
| 		ctx.Data["IsResetForm"] = true | 		ctx.Data["IsResetForm"] = true | ||||||
| 		ctx.Data["Err_Password"] = true | 		ctx.Data["Err_Password"] = true | ||||||
| 		ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil) | 		ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil) | ||||||
| 		return | 		return | ||||||
| 	} else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil { | 	} else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil { | ||||||
| 		errMsg := ctx.Tr("auth.password_pwned") | 		errMsg := ctx.Tr("auth.password_pwned") | ||||||
| @@ -305,7 +305,7 @@ func MustChangePasswordPost(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	if !password.IsComplexEnough(form.Password) { | 	if !password.IsComplexEnough(form.Password) { | ||||||
| 		ctx.Data["Err_Password"] = true | 		ctx.Data["Err_Password"] = true | ||||||
| 		ctx.RenderWithErr(password.BuildComplexityError(ctx), tplMustChangePassword, &form) | 		ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	pwned, err := password.IsPwned(ctx, form.Password) | 	pwned, err := password.IsPwned(ctx, form.Password) | ||||||
|   | |||||||
| @@ -546,7 +546,7 @@ func SearchRepo(ctx *context.Context) { | |||||||
| 		if len(sortOrder) == 0 { | 		if len(sortOrder) == 0 { | ||||||
| 			sortOrder = "asc" | 			sortOrder = "asc" | ||||||
| 		} | 		} | ||||||
| 		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok { | 		if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { | ||||||
| 			if orderBy, ok := searchModeMap[sortMode]; ok { | 			if orderBy, ok := searchModeMap[sortMode]; ok { | ||||||
| 				opts.OrderBy = orderBy | 				opts.OrderBy = orderBy | ||||||
| 			} else { | 			} else { | ||||||
|   | |||||||
| @@ -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 | 	// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md | ||||||
| 	// 2. Txt files - e.g. README.txt | 	// 2. Txt files - e.g. README.txt | ||||||
| 	// 3. No extension - e.g. README | 	// 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) | 	extCount := len(exts) | ||||||
| 	readmeFiles := make([]*git.TreeEntry, extCount+1) | 	readmeFiles := make([]*git.TreeEntry, extCount+1) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ func AccountPost(ctx *context.Context) { | |||||||
| 	} else if form.Password != form.Retype { | 	} else if form.Password != form.Retype { | ||||||
| 		ctx.Flash.Error(ctx.Tr("form.password_not_match")) | 		ctx.Flash.Error(ctx.Tr("form.password_not_match")) | ||||||
| 	} else if !password.IsComplexEnough(form.Password) { | 	} 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 { | 	} else if pwned, err := password.IsPwned(ctx, form.Password); pwned || err != nil { | ||||||
| 		errMsg := ctx.Tr("auth.password_pwned") | 		errMsg := ctx.Tr("auth.password_pwned") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 wxiaoguang
					wxiaoguang