mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-29 21:59:37 +03: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:
parent
ff5629268c
commit
cb700aedd1
18 changed files with 747 additions and 676 deletions
|
@ -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,13 +39,13 @@ 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
|
||||||
Session session.Store
|
Session session.Store
|
||||||
|
|
||||||
Link string // current request URL
|
Link string // current request URL
|
||||||
EscapedLink string
|
EscapedLink string
|
||||||
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue