2014-03-15 15:01:50 +04:00
|
|
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
2020-01-10 00:34:25 +03:00
|
|
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
2022-11-27 21:20:29 +03:00
|
|
|
// SPDX-License-Identifier: MIT
|
2014-03-15 15:01:50 +04:00
|
|
|
|
2016-03-11 19:56:52 +03:00
|
|
|
package context
|
2014-03-15 15:01:50 +04:00
|
|
|
|
|
|
|
import (
|
2021-01-26 18:36:53 +03:00
|
|
|
"context"
|
|
|
|
"encoding/hex"
|
2022-02-06 22:28:25 +03:00
|
|
|
"errors"
|
2022-04-07 21:59:56 +03:00
|
|
|
"fmt"
|
2016-11-30 00:49:06 +03:00
|
|
|
"html"
|
2014-03-22 21:44:02 +04:00
|
|
|
"html/template"
|
2014-04-15 20:27:29 +04:00
|
|
|
"io"
|
2022-02-06 22:28:25 +03:00
|
|
|
"net"
|
2014-03-15 15:01:50 +04:00
|
|
|
"net/http"
|
2018-03-16 00:13:34 +03:00
|
|
|
"net/url"
|
2017-06-26 04:06:40 +03:00
|
|
|
"path"
|
2021-01-26 18:36:53 +03:00
|
|
|
"strconv"
|
2014-03-23 00:40:09 +04:00
|
|
|
"strings"
|
2014-03-19 17:57:55 +04:00
|
|
|
"time"
|
2014-03-15 15:01:50 +04:00
|
|
|
|
2022-04-07 21:59:56 +03:00
|
|
|
"code.gitea.io/gitea/models/db"
|
2021-11-09 22:57:58 +03:00
|
|
|
"code.gitea.io/gitea/models/unit"
|
2021-11-24 12:49:20 +03:00
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
2016-11-10 19:24:48 +03:00
|
|
|
"code.gitea.io/gitea/modules/base"
|
2021-01-27 17:56:54 +03:00
|
|
|
mc "code.gitea.io/gitea/modules/cache"
|
2022-01-20 02:26:57 +03:00
|
|
|
"code.gitea.io/gitea/modules/git"
|
2022-07-23 09:38:03 +03:00
|
|
|
"code.gitea.io/gitea/modules/httpcache"
|
2021-07-24 19:03:58 +03:00
|
|
|
"code.gitea.io/gitea/modules/json"
|
2016-11-10 19:24:48 +03:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2021-01-26 18:36:53 +03:00
|
|
|
"code.gitea.io/gitea/modules/templates"
|
|
|
|
"code.gitea.io/gitea/modules/translation"
|
2022-11-17 20:55:15 +03:00
|
|
|
"code.gitea.io/gitea/modules/typesniffer"
|
2022-04-01 11:47:50 +03:00
|
|
|
"code.gitea.io/gitea/modules/util"
|
2021-01-30 11:55:53 +03:00
|
|
|
"code.gitea.io/gitea/modules/web/middleware"
|
2019-08-23 19:40:30 +03:00
|
|
|
|
2021-01-26 18:36:53 +03:00
|
|
|
"gitea.com/go-chi/cache"
|
|
|
|
"gitea.com/go-chi/session"
|
2021-10-14 05:50:23 +03:00
|
|
|
chi "github.com/go-chi/chi/v5"
|
2023-02-22 22:21:46 +03:00
|
|
|
"github.com/minio/sha256-simd"
|
2021-01-26 18:36:53 +03:00
|
|
|
"github.com/unrolled/render"
|
|
|
|
"golang.org/x/crypto/pbkdf2"
|
2014-03-15 15:01:50 +04:00
|
|
|
)
|
|
|
|
|
2021-01-26 18:36:53 +03:00
|
|
|
// Render represents a template render
|
|
|
|
type Render interface {
|
|
|
|
TemplateLookup(tmpl string) *template.Template
|
|
|
|
HTML(w io.Writer, status int, name string, binding interface{}, htmlOpt ...render.HTMLOptions) error
|
|
|
|
}
|
|
|
|
|
2014-03-15 17:17:16 +04:00
|
|
|
// Context represents context of a request.
|
2014-03-15 15:01:50 +04:00
|
|
|
type Context struct {
|
2021-10-12 21:11:35 +03:00
|
|
|
Resp ResponseWriter
|
|
|
|
Req *http.Request
|
|
|
|
Data map[string]interface{} // data used by MVC templates
|
2021-10-15 05:35:26 +03:00
|
|
|
PageData map[string]interface{} // data used by JavaScript modules in one page, it's `window.config.pageData`
|
2021-10-12 21:11:35 +03:00
|
|
|
Render Render
|
2021-01-26 18:36:53 +03:00
|
|
|
translation.Locale
|
2014-08-01 01:25:34 +04:00
|
|
|
Cache cache.Cache
|
2022-04-08 08:21:05 +03:00
|
|
|
csrf CSRFProtector
|
2021-01-30 11:55:53 +03:00
|
|
|
Flash *middleware.Flash
|
2014-07-26 08:24:27 +04:00
|
|
|
Session session.Store
|
|
|
|
|
2017-06-26 04:06:40 +03:00
|
|
|
Link string // current request URL
|
2017-11-28 12:43:51 +03:00
|
|
|
EscapedLink string
|
2022-03-22 10:03:22 +03:00
|
|
|
Doer *user_model.User
|
2014-11-18 19:07:16 +03:00
|
|
|
IsSigned bool
|
|
|
|
IsBasicAuth bool
|
2014-03-15 20:03:23 +04:00
|
|
|
|
2022-03-26 12:04:22 +03:00
|
|
|
ContextUser *user_model.User
|
|
|
|
Repo *Repository
|
|
|
|
Org *Organization
|
2022-03-30 11:42:47 +03:00
|
|
|
Package *Package
|
2014-03-15 15:01:50 +04:00
|
|
|
}
|
|
|
|
|
2022-05-05 17:13:23 +03:00
|
|
|
// Close frees all resources hold by Context
|
|
|
|
func (ctx *Context) Close() error {
|
|
|
|
var err error
|
|
|
|
if ctx.Req != nil && ctx.Req.MultipartForm != nil {
|
|
|
|
err = ctx.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
|
|
|
}
|
|
|
|
// TODO: close opened repo, and more
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-11-16 21:18:25 +03:00
|
|
|
// TrHTMLEscapeArgs runs Tr but pre-escapes all arguments with html.EscapeString.
|
|
|
|
// This is useful if the locale message is intended to only produce HTML content.
|
|
|
|
func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
|
|
|
|
trArgs := make([]interface{}, len(args))
|
|
|
|
for i, arg := range args {
|
|
|
|
trArgs[i] = html.EscapeString(arg)
|
|
|
|
}
|
|
|
|
return ctx.Tr(msg, trArgs...)
|
|
|
|
}
|
|
|
|
|
2021-01-05 16:05:40 +03:00
|
|
|
// GetData returns the data
|
|
|
|
func (ctx *Context) GetData() map[string]interface{} {
|
|
|
|
return ctx.Data
|
|
|
|
}
|
|
|
|
|
2019-04-08 01:49:34 +03:00
|
|
|
// IsUserSiteAdmin returns true if current user is a site admin
|
|
|
|
func (ctx *Context) IsUserSiteAdmin() bool {
|
2022-03-22 10:03:22 +03:00
|
|
|
return ctx.IsSigned && ctx.Doer.IsAdmin
|
2019-04-08 01:49:34 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2021-11-09 22:57:58 +03:00
|
|
|
func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool {
|
2019-04-08 01:49:34 +03:00
|
|
|
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
|
2021-11-09 22:57:58 +03:00
|
|
|
func (ctx *Context) IsUserRepoReaderSpecific(unitType unit.Type) bool {
|
2019-04-08 01:49:34 +03:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2021-01-24 18:23:05 +03:00
|
|
|
// RedirectToUser redirect to a differently-named user
|
|
|
|
func RedirectToUser(ctx *Context, userName string, redirectUserID int64) {
|
2022-12-03 05:48:26 +03:00
|
|
|
user, err := user_model.GetUserByID(ctx, redirectUserID)
|
2021-01-24 18:23:05 +03:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("GetUserByID", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
redirectPath := strings.Replace(
|
2021-11-16 21:18:25 +03:00
|
|
|
ctx.Req.URL.EscapedPath(),
|
|
|
|
url.PathEscape(userName),
|
|
|
|
url.PathEscape(user.Name),
|
2021-01-24 18:23:05 +03:00
|
|
|
1,
|
|
|
|
)
|
|
|
|
if ctx.Req.URL.RawQuery != "" {
|
|
|
|
redirectPath += "?" + ctx.Req.URL.RawQuery
|
|
|
|
}
|
2022-03-23 07:54:07 +03:00
|
|
|
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
|
2021-01-24 18:23:05 +03:00
|
|
|
}
|
|
|
|
|
2016-11-25 09:51:01 +03:00
|
|
|
// HasAPIError returns true if error occurs in form validation.
|
|
|
|
func (ctx *Context) HasAPIError() bool {
|
2014-05-05 21:08:01 +04:00
|
|
|
hasErr, ok := ctx.Data["HasError"]
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return hasErr.(bool)
|
|
|
|
}
|
|
|
|
|
2016-11-25 09:51:01 +03:00
|
|
|
// GetErrMsg returns error message
|
2014-05-05 21:08:01 +04:00
|
|
|
func (ctx *Context) GetErrMsg() string {
|
|
|
|
return ctx.Data["ErrorMsg"].(string)
|
|
|
|
}
|
|
|
|
|
2014-03-15 18:52:14 +04:00
|
|
|
// HasError returns true if error occurs in form validation.
|
2021-12-15 09:59:57 +03:00
|
|
|
// Attention: this function changes ctx.Data and ctx.Flash
|
2014-03-15 18:52:14 +04:00
|
|
|
func (ctx *Context) HasError() bool {
|
|
|
|
hasErr, ok := ctx.Data["HasError"]
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
2014-04-14 02:12:07 +04:00
|
|
|
ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
|
|
|
|
ctx.Data["Flash"] = ctx.Flash
|
2014-03-15 18:52:14 +04:00
|
|
|
return hasErr.(bool)
|
|
|
|
}
|
|
|
|
|
2015-07-08 14:47:56 +03:00
|
|
|
// HasValue returns true if value of given name exists.
|
|
|
|
func (ctx *Context) HasValue(name string) bool {
|
|
|
|
_, ok := ctx.Data[name]
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
2018-03-16 00:13:34 +03:00
|
|
|
// RedirectToFirst redirects to first not empty URL
|
|
|
|
func (ctx *Context) RedirectToFirst(location ...string) {
|
|
|
|
for _, loc := range location {
|
|
|
|
if len(loc) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-03-23 19:12:36 +03:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2018-03-16 00:13:34 +03:00
|
|
|
u, err := url.Parse(loc)
|
2020-01-10 00:34:25 +03:00
|
|
|
if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(loc), strings.ToLower(setting.AppURL))) {
|
2018-03-16 00:13:34 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Redirect(loc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Redirect(setting.AppSubURL + "/")
|
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// HTML calls Context.HTML and renders the template to HTTP response
|
2014-07-26 08:24:27 +04:00
|
|
|
func (ctx *Context) HTML(status int, name base.TplName) {
|
2015-12-20 09:06:54 +03:00
|
|
|
log.Debug("Template: %s", name)
|
2021-12-15 09:59:57 +03:00
|
|
|
tmplStartTime := time.Now()
|
2022-02-11 20:08:22 +03:00
|
|
|
if !setting.IsProd {
|
|
|
|
ctx.Data["TemplateName"] = name
|
|
|
|
}
|
|
|
|
ctx.Data["TemplateLoadTimes"] = func() string {
|
2021-12-15 09:59:57 +03:00
|
|
|
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
|
2021-01-29 07:33:47 +03:00
|
|
|
}
|
2022-08-08 16:42:36 +03:00
|
|
|
if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
|
2021-02-20 07:26:57 +03:00
|
|
|
if status == http.StatusInternalServerError && name == base.TplName("status/500") {
|
2021-12-15 09:59:57 +03:00
|
|
|
ctx.PlainText(http.StatusInternalServerError, "Unable to find status/500 template")
|
2021-02-20 07:26:57 +03:00
|
|
|
return
|
|
|
|
}
|
2021-01-26 18:36:53 +03:00
|
|
|
ctx.ServerError("Render failed", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// RenderToString renders the template content to a string
|
|
|
|
func (ctx *Context) RenderToString(name base.TplName, data map[string]interface{}) (string, error) {
|
2021-01-26 18:36:53 +03:00
|
|
|
var buf strings.Builder
|
2022-03-23 07:54:07 +03:00
|
|
|
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data)
|
2021-01-26 18:36:53 +03:00
|
|
|
return buf.String(), err
|
2014-03-20 15:50:26 +04:00
|
|
|
}
|
|
|
|
|
2014-03-15 18:52:14 +04:00
|
|
|
// RenderWithErr used for page has form validation but need to prompt error to users.
|
2014-07-26 08:24:27 +04:00
|
|
|
func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form interface{}) {
|
2014-04-03 23:50:55 +04:00
|
|
|
if form != nil {
|
2021-01-30 11:55:53 +03:00
|
|
|
middleware.AssignForm(form, ctx.Data)
|
2014-04-03 23:50:55 +04:00
|
|
|
}
|
2014-04-11 00:36:50 +04:00
|
|
|
ctx.Flash.ErrorMsg = msg
|
|
|
|
ctx.Data["Flash"] = ctx.Flash
|
2021-04-05 18:30:52 +03:00
|
|
|
ctx.HTML(http.StatusOK, tpl)
|
2014-03-15 18:52:14 +04:00
|
|
|
}
|
|
|
|
|
2018-01-11 00:34:17 +03:00
|
|
|
// NotFound displays a 404 (Not Found) page and prints the given error, if any.
|
2021-12-15 09:59:57 +03:00
|
|
|
func (ctx *Context) NotFound(logMsg string, logErr error) {
|
|
|
|
ctx.notFoundInternal(logMsg, logErr)
|
2019-04-09 21:10:42 +03:00
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
|
|
|
if logErr != nil {
|
2022-01-29 23:52:37 +03:00
|
|
|
log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr)
|
2021-10-20 17:37:19 +03:00
|
|
|
if !setting.IsProd {
|
2021-12-15 09:59:57 +03:00
|
|
|
ctx.Data["ErrorMsg"] = logErr
|
2014-05-02 02:53:41 +04:00
|
|
|
}
|
2014-03-19 12:48:45 +04:00
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// 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
|
2021-04-01 18:11:42 +03:00
|
|
|
}
|
2021-12-15 09:59:57 +03:00
|
|
|
}
|
2021-04-01 18:11:42 +03:00
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
if !showHTML {
|
2022-01-29 23:52:37 +03:00
|
|
|
ctx.plainTextInternal(3, http.StatusNotFound, []byte("Not found.\n"))
|
2021-12-15 09:59:57 +03:00
|
|
|
return
|
2021-04-01 18:11:42 +03:00
|
|
|
}
|
|
|
|
|
2019-02-20 02:09:47 +03:00
|
|
|
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
2018-01-11 00:34:17 +03:00
|
|
|
ctx.Data["Title"] = "Page Not Found"
|
|
|
|
ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
|
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// 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)
|
2019-04-09 21:10:42 +03:00
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
|
|
|
|
if logErr != nil {
|
|
|
|
log.ErrorWithSkip(2, "%s: %v", logMsg, logErr)
|
2022-03-10 23:23:15 +03:00
|
|
|
if _, ok := logErr.(*net.OpError); ok || errors.Is(logErr, &net.OpError{}) {
|
2022-02-06 22:28:25 +03:00
|
|
|
// This is an error within the underlying connection
|
|
|
|
// and further rendering will not work so just return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-10-20 17:37:19 +03:00
|
|
|
if !setting.IsProd {
|
2021-12-15 09:59:57 +03:00
|
|
|
ctx.Data["ErrorMsg"] = logErr
|
2018-01-11 00:34:17 +03:00
|
|
|
}
|
2014-05-02 02:53:41 +04:00
|
|
|
}
|
2018-01-11 00:34:17 +03:00
|
|
|
|
|
|
|
ctx.Data["Title"] = "Internal Server Error"
|
2019-01-31 01:00:00 +03:00
|
|
|
ctx.HTML(http.StatusInternalServerError, base.TplName("status/500"))
|
2014-03-15 15:01:50 +04:00
|
|
|
}
|
|
|
|
|
2016-08-30 12:08:38 +03:00
|
|
|
// NotFoundOrServerError use error check function to determine if the error
|
2021-12-15 09:59:57 +03:00
|
|
|
// is about not found. It responds with 404 status code for not found error,
|
2016-08-30 12:08:38 +03:00
|
|
|
// or error context description for logging purpose of 500 server error.
|
2021-12-15 09:59:57 +03:00
|
|
|
func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, err error) {
|
|
|
|
if errCheck(err) {
|
|
|
|
ctx.notFoundInternal(logMsg, err)
|
2016-07-25 21:48:17 +03:00
|
|
|
return
|
|
|
|
}
|
2021-12-15 09:59:57 +03:00
|
|
|
ctx.serverErrorInternal(logMsg, err)
|
|
|
|
}
|
2016-07-25 21:48:17 +03:00
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// PlainTextBytes renders bytes as plain text
|
2022-01-29 23:52:37 +03:00
|
|
|
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))
|
2021-12-15 09:59:57 +03:00
|
|
|
}
|
|
|
|
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
2022-01-22 21:32:35 +03:00
|
|
|
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
2022-11-17 20:55:15 +03:00
|
|
|
ctx.Resp.WriteHeader(status)
|
2021-12-15 09:59:57 +03:00
|
|
|
if _, err := ctx.Resp.Write(bs); err != nil {
|
2022-01-29 23:52:37 +03:00
|
|
|
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
|
2021-12-15 09:59:57 +03:00
|
|
|
}
|
2016-07-25 21:48:17 +03:00
|
|
|
}
|
|
|
|
|
2022-01-29 23:52:37 +03:00
|
|
|
// PlainTextBytes renders bytes as plain text
|
|
|
|
func (ctx *Context) PlainTextBytes(status int, bs []byte) {
|
|
|
|
ctx.plainTextInternal(2, status, bs)
|
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// PlainText renders content as plain text
|
|
|
|
func (ctx *Context) PlainText(status int, text string) {
|
2022-01-29 23:52:37 +03:00
|
|
|
ctx.plainTextInternal(2, status, []byte(text))
|
2021-01-26 18:36:53 +03:00
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// RespHeader returns the response header
|
|
|
|
func (ctx *Context) RespHeader() http.Header {
|
|
|
|
return ctx.Resp.Header()
|
2015-03-28 17:30:05 +03:00
|
|
|
}
|
|
|
|
|
2022-11-17 20:55:15 +03:00
|
|
|
type ServeHeaderOptions struct {
|
|
|
|
ContentType string // defaults to "application/octet-stream"
|
|
|
|
ContentTypeCharset string
|
2022-11-24 17:25:13 +03:00
|
|
|
ContentLength *int64
|
2022-11-17 20:55:15 +03:00
|
|
|
Disposition string // defaults to "attachment"
|
|
|
|
Filename string
|
|
|
|
CacheDuration time.Duration // defaults to 5 minutes
|
2022-11-24 17:25:13 +03:00
|
|
|
LastModified time.Time
|
2022-11-17 20:55:15 +03:00
|
|
|
}
|
|
|
|
|
2022-03-30 11:42:47 +03:00
|
|
|
// SetServeHeaders sets necessary content serve headers
|
2022-11-17 20:55:15 +03:00
|
|
|
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")
|
|
|
|
|
2022-11-24 17:25:13 +03:00
|
|
|
if opts.ContentLength != nil {
|
|
|
|
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
|
|
|
|
}
|
|
|
|
|
2022-11-17 20:55:15 +03:00
|
|
|
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.AddCacheControlToHeader(header, duration)
|
2022-11-24 17:25:13 +03:00
|
|
|
|
|
|
|
if !opts.LastModified.IsZero() {
|
|
|
|
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
|
|
|
}
|
2022-03-30 11:42:47 +03:00
|
|
|
}
|
|
|
|
|
2016-11-25 09:51:01 +03:00
|
|
|
// ServeContent serves content to http request
|
2022-11-24 17:25:13 +03:00
|
|
|
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
|
|
|
|
ctx.SetServeHeaders(opts)
|
|
|
|
http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
|
2021-01-26 18:36:53 +03:00
|
|
|
}
|
|
|
|
|
2022-03-30 11:42:47 +03:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-01-26 18:36:53 +03:00
|
|
|
// Error returned an error to web browser
|
|
|
|
func (ctx *Context) Error(status int, contents ...string) {
|
2022-01-20 20:46:10 +03:00
|
|
|
v := http.StatusText(status)
|
2021-01-26 18:36:53 +03:00
|
|
|
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{}) {
|
2021-01-29 16:42:47 +03:00
|
|
|
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
|
2021-01-28 00:46:22 +03:00
|
|
|
ctx.Resp.WriteHeader(status)
|
2021-01-26 18:36:53 +03:00
|
|
|
if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil {
|
|
|
|
ctx.ServerError("Render JSON failed", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// Redirect redirects the request
|
2021-01-26 18:36:53 +03:00
|
|
|
func (ctx *Context) Redirect(location string, status ...int) {
|
2022-03-23 07:54:07 +03:00
|
|
|
code := http.StatusSeeOther
|
2021-01-26 18:36:53 +03:00
|
|
|
if len(status) == 1 {
|
|
|
|
code = status[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
http.Redirect(ctx.Resp, ctx.Req, location, code)
|
|
|
|
}
|
|
|
|
|
2021-03-07 11:12:43 +03:00
|
|
|
// SetCookie convenience function to set most cookies consistently
|
|
|
|
// CSRF and a few others are the exception here
|
|
|
|
func (ctx *Context) SetCookie(name, value string, expiry int) {
|
|
|
|
middleware.SetCookie(ctx.Resp, name, value,
|
|
|
|
expiry,
|
|
|
|
setting.AppSubURL,
|
|
|
|
setting.SessionConfig.Domain,
|
|
|
|
setting.SessionConfig.Secure,
|
|
|
|
true,
|
|
|
|
middleware.SameSite(setting.SessionConfig.SameSite))
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteCookie convenience function to delete most cookies consistently
|
|
|
|
// CSRF and a few others are the exception here
|
|
|
|
func (ctx *Context) DeleteCookie(name string) {
|
|
|
|
middleware.SetCookie(ctx.Resp, name, "",
|
|
|
|
-1,
|
|
|
|
setting.AppSubURL,
|
|
|
|
setting.SessionConfig.Domain,
|
|
|
|
setting.SessionConfig.Secure,
|
|
|
|
true,
|
|
|
|
middleware.SameSite(setting.SessionConfig.SameSite))
|
2021-01-26 18:36:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetCookie returns given cookie value from request header.
|
|
|
|
func (ctx *Context) GetCookie(name string) string {
|
2021-01-30 11:55:53 +03:00
|
|
|
return middleware.GetCookie(ctx.Req, name)
|
2021-01-26 18:36:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetSuperSecureCookie returns given cookie value from request header with secret string.
|
|
|
|
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
|
|
|
|
val := ctx.GetCookie(name)
|
2021-03-07 11:12:43 +03:00
|
|
|
return ctx.CookieDecrypt(secret, val)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CookieDecrypt returns given value from with secret string.
|
|
|
|
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
|
2021-01-26 18:36:53 +03:00
|
|
|
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)
|
2022-04-01 11:47:50 +03:00
|
|
|
text, err = util.AESGCMDecrypt(key, text)
|
2021-01-26 18:36:53 +03:00
|
|
|
return string(text), err == nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetSuperSecureCookie sets given cookie value to response header with secret string.
|
2021-03-07 11:12:43 +03:00
|
|
|
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, expiry int) {
|
|
|
|
text := ctx.CookieEncrypt(secret, value)
|
|
|
|
|
|
|
|
ctx.SetCookie(name, text, expiry)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CookieEncrypt encrypts a given value using the provided secret
|
|
|
|
func (ctx *Context) CookieEncrypt(secret, value string) string {
|
2021-01-26 18:36:53 +03:00
|
|
|
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
|
2022-04-01 11:47:50 +03:00
|
|
|
text, err := util.AESGCMEncrypt(key, []byte(value))
|
2021-01-26 18:36:53 +03:00
|
|
|
if err != nil {
|
|
|
|
panic("error encrypting cookie: " + err.Error())
|
|
|
|
}
|
|
|
|
|
2021-03-07 11:12:43 +03:00
|
|
|
return hex.EncodeToString(text)
|
2021-01-26 18:36:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetCookieInt returns cookie result in int type.
|
|
|
|
func (ctx *Context) GetCookieInt(name string) int {
|
|
|
|
r, _ := strconv.Atoi(ctx.GetCookie(name))
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCookieInt64 returns cookie result in int64 type.
|
|
|
|
func (ctx *Context) GetCookieInt64(name string) int64 {
|
|
|
|
r, _ := strconv.ParseInt(ctx.GetCookie(name), 10, 64)
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCookieFloat64 returns cookie result in float64 type.
|
|
|
|
func (ctx *Context) GetCookieFloat64(name string) float64 {
|
|
|
|
v, _ := strconv.ParseFloat(ctx.GetCookie(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) {
|
2021-05-31 09:18:11 +03:00
|
|
|
chiCtx := chi.RouteContext(ctx)
|
2021-01-26 18:36:53 +03:00
|
|
|
chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
|
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
// Write writes data to web browser
|
2021-01-26 18:36:53 +03:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-05-31 09:18:11 +03:00
|
|
|
// Deadline is part of the interface for context.Context and we pass this to the request context
|
|
|
|
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
|
|
|
|
return ctx.Req.Context().Deadline()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Done is part of the interface for context.Context and we pass this to the request context
|
|
|
|
func (ctx *Context) Done() <-chan struct{} {
|
|
|
|
return ctx.Req.Context().Done()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Err is part of the interface for context.Context and we pass this to the request context
|
|
|
|
func (ctx *Context) Err() error {
|
|
|
|
return ctx.Req.Context().Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Value is part of the interface for context.Context and we pass this to the request context
|
|
|
|
func (ctx *Context) Value(key interface{}) interface{} {
|
2022-01-20 02:26:57 +03:00
|
|
|
if key == git.RepositoryContextKey && ctx.Repo != nil {
|
|
|
|
return ctx.Repo.GitRepo
|
|
|
|
}
|
|
|
|
|
2021-05-31 09:18:11 +03:00
|
|
|
return ctx.Req.Context().Value(key)
|
|
|
|
}
|
|
|
|
|
2022-04-07 21:59:56 +03:00
|
|
|
// 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, ", "))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-26 18:36:53 +03:00
|
|
|
// Handler represents a custom handler
|
|
|
|
type Handler func(*Context)
|
|
|
|
|
2021-12-15 09:59:57 +03:00
|
|
|
type contextKeyType struct{}
|
|
|
|
|
|
|
|
var contextKey interface{} = contextKeyType{}
|
2021-01-26 18:36:53 +03:00
|
|
|
|
|
|
|
// WithContext set up install context in request
|
|
|
|
func WithContext(req *http.Request, ctx *Context) *http.Request {
|
|
|
|
return req.WithContext(context.WithValue(req.Context(), contextKey, ctx))
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetContext retrieves install context from request
|
|
|
|
func GetContext(req *http.Request) *Context {
|
|
|
|
return req.Context().Value(contextKey).(*Context)
|
|
|
|
}
|
|
|
|
|
2021-09-28 16:13:04 +03:00
|
|
|
// GetContextUser returns context user
|
2021-11-24 12:49:20 +03:00
|
|
|
func GetContextUser(req *http.Request) *user_model.User {
|
2021-09-28 16:13:04 +03:00
|
|
|
if apiContext, ok := req.Context().Value(apiContextKey).(*APIContext); ok {
|
2022-03-22 10:03:22 +03:00
|
|
|
return apiContext.Doer
|
2021-09-28 16:13:04 +03:00
|
|
|
}
|
|
|
|
if ctx, ok := req.Context().Value(contextKey).(*Context); ok {
|
2022-03-22 10:03:22 +03:00
|
|
|
return ctx.Doer
|
2021-09-28 16:13:04 +03:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-01-26 18:36:53 +03:00
|
|
|
func getCsrfOpts() CsrfOptions {
|
|
|
|
return CsrfOptions{
|
|
|
|
Secret: setting.SecretKey,
|
|
|
|
Cookie: setting.CSRFCookieName,
|
|
|
|
SetCookie: true,
|
|
|
|
Secure: setting.SessionConfig.Secure,
|
|
|
|
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
|
|
|
|
Header: "X-Csrf-Token",
|
|
|
|
CookieDomain: setting.SessionConfig.Domain,
|
|
|
|
CookiePath: setting.SessionConfig.CookiePath,
|
2021-03-07 11:12:43 +03:00
|
|
|
SameSite: setting.SessionConfig.SameSite,
|
2021-01-26 18:36:53 +03:00
|
|
|
}
|
2014-04-10 22:37:43 +04:00
|
|
|
}
|
|
|
|
|
2014-07-26 08:24:27 +04:00
|
|
|
// Contexter initializes a classic context for a request.
|
2022-08-28 12:43:25 +03:00
|
|
|
func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
|
|
|
|
_, rnd := templates.HTMLRenderer(ctx)
|
2022-01-20 20:46:10 +03:00
|
|
|
csrfOpts := getCsrfOpts()
|
2022-04-08 08:21:05 +03:00
|
|
|
if !setting.IsProd {
|
|
|
|
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
|
|
|
|
}
|
2021-01-26 18:36:53 +03:00
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
2022-01-20 20:46:10 +03:00
|
|
|
locale := middleware.Locale(resp, req)
|
|
|
|
startTime := time.Now()
|
|
|
|
link := setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/")
|
2021-12-16 20:40:18 +03:00
|
|
|
|
2022-01-20 20:46:10 +03:00
|
|
|
ctx := Context{
|
2021-01-26 18:36:53 +03:00
|
|
|
Resp: NewResponse(resp),
|
2021-01-27 17:56:54 +03:00
|
|
|
Cache: mc.GetCache(),
|
2021-01-26 18:36:53 +03:00
|
|
|
Locale: locale,
|
|
|
|
Link: link,
|
|
|
|
Render: rnd,
|
|
|
|
Session: session.GetSession(req),
|
|
|
|
Repo: &Repository{
|
|
|
|
PullRequest: &PullRequest{},
|
|
|
|
},
|
|
|
|
Org: &Organization{},
|
|
|
|
Data: map[string]interface{}{
|
|
|
|
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
|
|
|
|
"PageStartTime": startTime,
|
2021-01-29 07:33:47 +03:00
|
|
|
"Link": link,
|
2021-10-21 10:37:43 +03:00
|
|
|
"RunModeIsProd": setting.IsProd,
|
2021-01-26 18:36:53 +03:00
|
|
|
},
|
|
|
|
}
|
2022-05-05 17:13:23 +03:00
|
|
|
defer ctx.Close()
|
|
|
|
|
2021-10-15 05:35:26 +03:00
|
|
|
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
|
2021-10-12 21:11:35 +03:00
|
|
|
ctx.PageData = map[string]interface{}{}
|
|
|
|
ctx.Data["PageData"] = ctx.PageData
|
2022-01-20 02:26:57 +03:00
|
|
|
ctx.Data["Context"] = &ctx
|
2021-01-26 18:36:53 +03:00
|
|
|
|
|
|
|
ctx.Req = WithContext(req, &ctx)
|
2022-04-08 08:21:05 +03:00
|
|
|
ctx.csrf = PrepareCSRFProtector(csrfOpts, &ctx)
|
2021-01-26 18:36:53 +03:00
|
|
|
|
|
|
|
// Get flash.
|
|
|
|
flashCookie := ctx.GetCookie("macaron_flash")
|
|
|
|
vals, _ := url.ParseQuery(flashCookie)
|
|
|
|
if len(vals) > 0 {
|
2021-01-30 11:55:53 +03:00
|
|
|
f := &middleware.Flash{
|
2021-01-26 18:36:53 +03:00
|
|
|
DataStore: &ctx,
|
|
|
|
Values: vals,
|
|
|
|
ErrorMsg: vals.Get("error"),
|
|
|
|
SuccessMsg: vals.Get("success"),
|
|
|
|
InfoMsg: vals.Get("info"),
|
|
|
|
WarningMsg: vals.Get("warning"),
|
|
|
|
}
|
|
|
|
ctx.Data["Flash"] = f
|
|
|
|
}
|
|
|
|
|
2021-01-30 11:55:53 +03:00
|
|
|
f := &middleware.Flash{
|
2021-01-26 18:36:53 +03:00
|
|
|
DataStore: &ctx,
|
|
|
|
Values: url.Values{},
|
|
|
|
ErrorMsg: "",
|
|
|
|
WarningMsg: "",
|
|
|
|
InfoMsg: "",
|
|
|
|
SuccessMsg: "",
|
|
|
|
}
|
|
|
|
ctx.Resp.Before(func(resp ResponseWriter) {
|
|
|
|
if flash := f.Encode(); len(flash) > 0 {
|
2021-01-30 11:55:53 +03:00
|
|
|
middleware.SetCookie(resp, "macaron_flash", flash, 0,
|
2021-01-27 17:56:54 +03:00
|
|
|
setting.SessionConfig.CookiePath,
|
2021-01-30 11:55:53 +03:00
|
|
|
middleware.Domain(setting.SessionConfig.Domain),
|
|
|
|
middleware.HTTPOnly(true),
|
|
|
|
middleware.Secure(setting.SessionConfig.Secure),
|
2021-03-07 11:12:43 +03:00
|
|
|
middleware.SameSite(setting.SessionConfig.SameSite),
|
2021-01-27 17:56:54 +03:00
|
|
|
)
|
|
|
|
return
|
2021-01-26 18:36:53 +03:00
|
|
|
}
|
|
|
|
|
2021-03-07 11:12:43 +03:00
|
|
|
middleware.SetCookie(ctx.Resp, "macaron_flash", "", -1,
|
2021-01-26 18:36:53 +03:00
|
|
|
setting.SessionConfig.CookiePath,
|
2021-01-30 11:55:53 +03:00
|
|
|
middleware.Domain(setting.SessionConfig.Domain),
|
|
|
|
middleware.HTTPOnly(true),
|
|
|
|
middleware.Secure(setting.SessionConfig.Secure),
|
2021-03-07 11:12:43 +03:00
|
|
|
middleware.SameSite(setting.SessionConfig.SameSite),
|
2021-01-26 18:36:53 +03:00
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
ctx.Flash = f
|
|
|
|
|
|
|
|
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
|
|
|
if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
|
|
|
if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
|
|
|
|
ctx.ServerError("ParseMultipartForm", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2018-08-27 05:23:27 +03:00
|
|
|
|
2022-07-23 09:38:03 +03:00
|
|
|
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform")
|
2021-08-06 23:47:10 +03:00
|
|
|
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
2021-01-26 18:36:53 +03:00
|
|
|
|
2022-04-08 08:21:05 +03:00
|
|
|
ctx.Data["CsrfToken"] = ctx.csrf.GetToken()
|
2021-01-26 18:36:53 +03:00
|
|
|
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
|
|
|
|
|
2021-05-05 00:48:31 +03:00
|
|
|
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
|
2021-01-26 18:36:53 +03:00
|
|
|
ctx.Data["IsLandingPageHome"] = setting.LandingPageURL == setting.LandingPageHome
|
|
|
|
ctx.Data["IsLandingPageExplore"] = setting.LandingPageURL == setting.LandingPageExplore
|
|
|
|
ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations
|
|
|
|
|
|
|
|
ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton
|
|
|
|
ctx.Data["ShowMilestonesDashboardPage"] = setting.Service.ShowMilestonesDashboardPage
|
|
|
|
ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding
|
|
|
|
ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion
|
|
|
|
|
|
|
|
ctx.Data["EnableSwagger"] = setting.API.EnableSwagger
|
|
|
|
ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn
|
|
|
|
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
2021-04-15 19:53:57 +03:00
|
|
|
ctx.Data["DisableStars"] = setting.Repository.DisableStars
|
Implement actions (#21937)
Close #13539.
Co-authored by: @lunny @appleboy @fuxiaohei and others.
Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner
### Summary
The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:
- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.
**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.
### ⚠️ Breaking
`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.
### Some important reviews
- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868
### What it looks like
<details>
#### Manage runners
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">
#### List runs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">
#### View logs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">
</details>
### How to try it
<details>
#### 1. Start Gitea
Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).
Add additional configurations in `app.ini` to enable Actions:
```ini
[actions]
ENABLED = true
```
Start it.
If all is well, you'll see the management page of runners:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">
#### 2. Start runner
Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.
If all is well, you'll see a new runner has been added:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">
#### 3. Enable actions for a repo
Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">
If all is well, you'll see a new tab "Actions":
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">
#### 4. Upload workflow files
Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
If all is well, you'll see a new run in `Actions` tab:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">
#### 5. Check the logs of jobs
Click a run and you'll see the logs:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">
#### 6. Go on
You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.
Come on, PRs are welcome.
</details>
See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)
---------
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-01-31 04:45:19 +03:00
|
|
|
ctx.Data["EnableActions"] = setting.Actions.Enabled
|
2021-01-26 18:36:53 +03:00
|
|
|
|
|
|
|
ctx.Data["ManifestData"] = setting.ManifestData
|
|
|
|
|
2021-11-09 22:57:58 +03:00
|
|
|
ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled()
|
|
|
|
ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled()
|
|
|
|
ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled()
|
|
|
|
ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled()
|
Implement actions (#21937)
Close #13539.
Co-authored by: @lunny @appleboy @fuxiaohei and others.
Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner
### Summary
The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:
- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.
**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.
### ⚠️ Breaking
`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.
### Some important reviews
- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868
### What it looks like
<details>
#### Manage runners
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">
#### List runs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">
#### View logs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">
</details>
### How to try it
<details>
#### 1. Start Gitea
Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).
Add additional configurations in `app.ini` to enable Actions:
```ini
[actions]
ENABLED = true
```
Start it.
If all is well, you'll see the management page of runners:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">
#### 2. Start runner
Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.
If all is well, you'll see a new runner has been added:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">
#### 3. Enable actions for a repo
Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">
If all is well, you'll see a new tab "Actions":
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">
#### 4. Upload workflow files
Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
If all is well, you'll see a new run in `Actions` tab:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">
#### 5. Check the logs of jobs
Click a run and you'll see the logs:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">
#### 6. Go on
You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.
Come on, PRs are welcome.
</details>
See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)
---------
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-01-31 04:45:19 +03:00
|
|
|
ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled()
|
2021-05-05 00:48:31 +03:00
|
|
|
|
2022-06-27 23:58:46 +03:00
|
|
|
ctx.Data["locale"] = locale
|
2021-01-26 18:36:53 +03:00
|
|
|
ctx.Data["AllLangs"] = translation.AllLangs()
|
2020-12-22 14:13:50 +03:00
|
|
|
|
2021-01-26 18:36:53 +03:00
|
|
|
next.ServeHTTP(ctx.Resp, ctx.Req)
|
2021-08-04 20:26:30 +03:00
|
|
|
|
|
|
|
// Handle adding signedUserName to the context for the AccessLogger
|
|
|
|
usernameInterface := ctx.Data["SignedUserName"]
|
|
|
|
identityPtrInterface := ctx.Req.Context().Value(signedUserNameStringPointerKey)
|
|
|
|
if usernameInterface != nil && identityPtrInterface != nil {
|
|
|
|
username := usernameInterface.(string)
|
|
|
|
identityPtr := identityPtrInterface.(*string)
|
|
|
|
if identityPtr != nil && username != "" {
|
|
|
|
*identityPtr = username
|
|
|
|
}
|
|
|
|
}
|
2021-01-26 18:36:53 +03:00
|
|
|
})
|
2014-03-15 15:01:50 +04:00
|
|
|
}
|
|
|
|
}
|
2022-04-07 21:59:56 +03:00
|
|
|
|
|
|
|
// 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,
|
|
|
|
},
|
|
|
|
}
|