feat: vendor gitea 1.16.2
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -37,6 +38,16 @@ const keyOfRequestIDInTemplate = ".RequestID"
|
||||
// So, we accept a Request ID with a maximum character length of 40
|
||||
const maxRequestIDByteLength = 40
|
||||
|
||||
func isSafeRequestID(id string) bool {
|
||||
for _, r := range id {
|
||||
safe := unicode.IsPrint(r)
|
||||
if !safe {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseRequestIDFromRequestHeader(req *http.Request) string {
|
||||
requestID := "-"
|
||||
for _, key := range setting.Log.RequestIDHeaders {
|
||||
@@ -45,6 +56,9 @@ func parseRequestIDFromRequestHeader(req *http.Request) string {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isSafeRequestID(requestID) {
|
||||
return "-"
|
||||
}
|
||||
if len(requestID) > maxRequestIDByteLength {
|
||||
requestID = requestID[:maxRequestIDByteLength] + "..."
|
||||
}
|
||||
|
||||
@@ -69,3 +69,8 @@ func TestAccessLogger(t *testing.T) {
|
||||
recorder.record(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), &testAccessLoggerResponseWriterMock{}, req)
|
||||
assert.Equal(t, []string{`remote-addr - - [02/Jan/2000:03:04:05 +0000] "GET /path https" 200 123123 "referer" "user-agent"`}, mockLogger.logs)
|
||||
}
|
||||
|
||||
func TestAccessLoggerRequestID(t *testing.T) {
|
||||
assert.False(t, isSafeRequestID("\x00"))
|
||||
assert.True(t, isSafeRequestID("a b-c"))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
@@ -47,6 +48,12 @@ type APIContext struct {
|
||||
PublicOnly bool // Whether the request is for a public endpoint
|
||||
}
|
||||
|
||||
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
|
||||
// A public-only token cannot reach a private repo; any other token is unrestricted by this check.
|
||||
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
|
||||
return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
|
||||
}
|
||||
|
||||
func init() {
|
||||
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||
return req.Context().Value(apiContextKey).(*APIContext)
|
||||
@@ -163,7 +170,7 @@ func GetAPIContext(req *http.Request) *APIContext {
|
||||
return req.Context().Value(apiContextKey).(*APIContext)
|
||||
}
|
||||
|
||||
func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
|
||||
func genAPILinks(curURL *url.URL, total int64, pageSize, curPage int) []string {
|
||||
page := NewPagination(total, pageSize, curPage, 0)
|
||||
paginater := page.Paginater
|
||||
links := make([]string, 0, 4)
|
||||
@@ -204,7 +211,8 @@ func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
|
||||
}
|
||||
|
||||
// SetLinkHeader sets pagination link header by given total number and page size.
|
||||
func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
|
||||
// "count" is usually from database result "count int64", so it also uses int64,
|
||||
func (ctx *APIContext) SetLinkHeader(total int64, pageSize int) {
|
||||
links := genAPILinks(ctx.Req.URL, total, pageSize, ctx.FormInt("page"))
|
||||
|
||||
if len(links) > 0 {
|
||||
@@ -221,13 +229,13 @@ func APIContexter() func(http.Handler) http.Handler {
|
||||
ctx := &APIContext{
|
||||
Base: base,
|
||||
Cache: cache.GetCache(),
|
||||
Repo: &Repository{PullRequest: &PullRequest{}},
|
||||
Repo: &Repository{},
|
||||
Org: &APIOrganization{},
|
||||
}
|
||||
|
||||
ctx.SetContextValue(apiContextKey, ctx)
|
||||
|
||||
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
||||
// FIXME: GLOBAL-PARSE-FORM: see more details in another FIXME comment
|
||||
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if !ctx.ParseMultipartForm() {
|
||||
return
|
||||
@@ -235,8 +243,6 @@ func APIContexter() func(http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
@@ -147,10 +148,7 @@ func (b *Base) PlainText(status int, text string) {
|
||||
|
||||
// Redirect redirects the request
|
||||
func (b *Base) Redirect(location string, status ...int) {
|
||||
code := http.StatusSeeOther
|
||||
if len(status) == 1 {
|
||||
code = status[0]
|
||||
}
|
||||
code := util.OptionalArg(status, http.StatusSeeOther)
|
||||
|
||||
if !httplib.IsRelativeURL(location) {
|
||||
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
|
||||
@@ -172,15 +170,15 @@ func (b *Base) Redirect(location string, status ...int) {
|
||||
http.Redirect(b.Resp, b.Req, location, code)
|
||||
}
|
||||
|
||||
type ServeHeaderOptions httplib.ServeHeaderOptions
|
||||
type ServeHeaderOptions = httplib.ServeHeaderOptions
|
||||
|
||||
func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt))
|
||||
func (b *Base) SetServeHeaders(opts ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(b.Resp, opts)
|
||||
}
|
||||
|
||||
// ServeContent serves content to http request
|
||||
func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts))
|
||||
func (b *Base) ServeContent(r io.ReadSeeker, opts ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(b.Resp, opts)
|
||||
http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,11 +37,16 @@ func (b *Base) PathParamInt64(p string) int64 {
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *Base) PathParamInt(p string) int {
|
||||
v, _ := strconv.Atoi(b.PathParam(p))
|
||||
return v
|
||||
}
|
||||
|
||||
// SetPathParam set request path params into routes
|
||||
func (b *Base) SetPathParam(name, value string) {
|
||||
if strings.HasPrefix(name, ":") {
|
||||
setting.PanicInDevOrTesting("path param should not start with ':'")
|
||||
name = name[1:]
|
||||
}
|
||||
chi.RouteContext(b).URLParams.Add(name, url.PathEscape(value))
|
||||
}
|
||||
|
||||
func (b *Base) SetPathParamRaw(name, value string) {
|
||||
chi.RouteContext(b).URLParams.Add(name, value)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
@@ -29,32 +30,25 @@ func GetImageCaptcha() *captcha.Captcha {
|
||||
imageCaptchaOnce.Do(func() {
|
||||
cpt = captcha.NewCaptcha(captcha.Options{
|
||||
SubURL: setting.AppSubURL,
|
||||
// Use a color palette with high contrast colors suitable for both light and dark modes
|
||||
// These colors provide good visibility and readability in both themes
|
||||
ColorPalette: color.Palette{
|
||||
color.RGBA{R: 234, G: 67, B: 53, A: 255}, // Bright red
|
||||
color.RGBA{R: 66, G: 133, B: 244, A: 255}, // Medium blue
|
||||
color.RGBA{R: 52, G: 168, B: 83, A: 255}, // Green
|
||||
color.RGBA{R: 251, G: 188, B: 5, A: 255}, // Yellow/gold
|
||||
color.RGBA{R: 171, G: 71, B: 188, A: 255}, // Purple
|
||||
},
|
||||
})
|
||||
cpt.Store = cache.GetCache().ChiCache()
|
||||
})
|
||||
return cpt
|
||||
}
|
||||
|
||||
// SetCaptchaData sets common captcha data
|
||||
func SetCaptchaData(ctx *Context) {
|
||||
if !setting.Service.EnableCaptcha {
|
||||
return
|
||||
}
|
||||
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
|
||||
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
|
||||
ctx.Data["Captcha"] = GetImageCaptcha()
|
||||
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
|
||||
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
|
||||
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
|
||||
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
|
||||
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
|
||||
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
|
||||
}
|
||||
|
||||
const (
|
||||
gRecaptchaResponseField = "g-recaptcha-response"
|
||||
hCaptchaResponseField = "h-captcha-response"
|
||||
mCaptchaResponseField = "m-captcha-response"
|
||||
mCaptchaResponseField = "mcaptcha__token" // this form key is hard-coded in the mcaptcha frontend library
|
||||
cfTurnstileResponseField = "cf-turnstile-response"
|
||||
)
|
||||
|
||||
@@ -88,6 +82,6 @@ func VerifyCaptcha(ctx *Context, tpl templates.TplName, form any) {
|
||||
|
||||
if !valid {
|
||||
ctx.Data["Err_Captcha"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.captcha_incorrect"), tpl, form)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,18 @@ package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
@@ -48,7 +47,6 @@ type Context struct {
|
||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
||||
|
||||
Cache cache.StringCache
|
||||
Csrf CSRFProtector
|
||||
Flash *middleware.Flash
|
||||
Session session.Store
|
||||
|
||||
@@ -102,12 +100,13 @@ func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func NewTemplateContextForWeb(ctx *Context) TemplateContext {
|
||||
tmplCtx := NewTemplateContext(ctx)
|
||||
tmplCtx["Locale"] = ctx.Base.Locale
|
||||
func NewTemplateContextForWeb(ctx reqctx.RequestContext, req *http.Request, locale translation.Locale) TemplateContext {
|
||||
tmplCtx := NewTemplateContext(ctx, req)
|
||||
tmplCtx["Locale"] = locale
|
||||
tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
|
||||
tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx)
|
||||
tmplCtx["RootData"] = ctx.Data
|
||||
tmplCtx["MiscUtils"] = templates.NewMiscUtils(ctx)
|
||||
tmplCtx["RootData"] = ctx.GetData()
|
||||
tmplCtx["Consts"] = map[string]any{
|
||||
"RepoUnitTypeCode": unit.TypeCode,
|
||||
"RepoUnitTypeIssues": unit.TypeIssues,
|
||||
@@ -131,30 +130,36 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
|
||||
|
||||
Cache: cache.GetCache(),
|
||||
Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"),
|
||||
Repo: &Repository{PullRequest: &PullRequest{}},
|
||||
Repo: &Repository{},
|
||||
Org: &Organization{},
|
||||
}
|
||||
ctx.TemplateContext = NewTemplateContextForWeb(ctx)
|
||||
ctx.TemplateContext = NewTemplateContextForWeb(ctx, ctx.Base.Req, ctx.Base.Locale)
|
||||
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
|
||||
ctx.SetContextValue(WebContextKey, ctx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func ContexterInstallPage(data map[string]any) func(next http.Handler) http.Handler {
|
||||
rnd := templates.PageRenderer()
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
base := NewBaseContext(resp, req)
|
||||
ctx := NewWebContext(base, rnd, session.GetContextSession(req))
|
||||
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
||||
ctx.Data.MergeFrom(reqctx.ContextData{
|
||||
"Title": ctx.Locale.Tr("install.install"),
|
||||
"PageIsInstall": true,
|
||||
"AllLangs": translation.AllLangs(),
|
||||
})
|
||||
ctx.Data.MergeFrom(data)
|
||||
next.ServeHTTP(resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Contexter initializes a classic context for a request.
|
||||
func Contexter() func(next http.Handler) http.Handler {
|
||||
rnd := templates.HTMLRenderer()
|
||||
csrfOpts := CsrfOptions{
|
||||
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
|
||||
Cookie: setting.CSRFCookieName,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
|
||||
CookieDomain: setting.SessionConfig.Domain,
|
||||
CookiePath: setting.SessionConfig.CookiePath,
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
}
|
||||
if !setting.IsProd {
|
||||
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
|
||||
}
|
||||
rnd := templates.PageRenderer()
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
base := NewBaseContext(resp, req)
|
||||
@@ -167,8 +172,6 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
ctx.PageData = map[string]any{}
|
||||
ctx.Data["PageData"] = ctx.PageData
|
||||
|
||||
ctx.Csrf = NewCSRFProtector(csrfOpts)
|
||||
|
||||
// get the last flash message from cookie
|
||||
lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash)
|
||||
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
|
||||
@@ -184,7 +187,10 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
}
|
||||
})
|
||||
|
||||
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
||||
// FIXME: GLOBAL-PARSE-FORM: this ParseMultipartForm was used for parsing the csrf token from multipart/form-data
|
||||
// We have dropped the csrf token, so ideally this global ParseMultipartForm should be removed.
|
||||
// When removing this, we need to avoid regressions in the handler functions because Golang's http framework is quite fragile
|
||||
// and developers sometimes need to manually parse the form before accessing some values.
|
||||
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if !ctx.ParseMultipartForm() {
|
||||
return
|
||||
@@ -192,7 +198,10 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
if setting.Security.XFrameOptions != "unset" {
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
|
||||
}
|
||||
|
||||
ctx.Data["SystemConfig"] = setting.Config()
|
||||
|
||||
|
||||
@@ -25,13 +25,11 @@ func removeSessionCookieHeader(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
@@ -124,8 +125,12 @@ func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTM
|
||||
return template.HTML(buf.String()), err
|
||||
}
|
||||
|
||||
// RenderWithErr used for page has form validation but need to prompt error to users.
|
||||
func (ctx *Context) RenderWithErr(msg any, tpl templates.TplName, form any) {
|
||||
// RenderWithErrDeprecated render the page with form validation when it needs to prompt error to users.
|
||||
// Deprecated: use "form-fetch-action" and JSON response instead.
|
||||
// WARNING: in many cases, this function is not able to render the page or recover the form fields correctly.
|
||||
// And it is very difficult to test the page rendered by this function.
|
||||
// DO NOT USE IT ANYMORE.
|
||||
func (ctx *Context) RenderWithErrDeprecated(msg any, tpl templates.TplName, form any) {
|
||||
if form != nil {
|
||||
middleware.AssignForm(form, ctx.Data)
|
||||
}
|
||||
@@ -139,11 +144,9 @@ func (ctx *Context) NotFound(logErr error) {
|
||||
}
|
||||
|
||||
func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
||||
// TODO: it's safe to show the error message to end users if the error is fully controlled by our error system
|
||||
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
|
||||
@@ -162,11 +165,17 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
||||
|
||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||
ctx.Data["Title"] = "Page Not Found"
|
||||
ctx.Data["ErrorMsg"] = "" // FIXME: the template never renders this message, need to fix in the future (and show safe messages to end users)
|
||||
ctx.HTML(http.StatusNotFound, "status/404")
|
||||
}
|
||||
|
||||
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
|
||||
// If the error is controlled by our error system, a related 404 page can be displayed instead.
|
||||
func (ctx *Context) ServerError(logMsg string, logErr error) {
|
||||
if errors.Is(logErr, util.ErrNotExist) {
|
||||
ctx.notFoundInternal(logMsg, logErr)
|
||||
return
|
||||
}
|
||||
ctx.serverErrorInternal(logMsg, logErr)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,26 @@ package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/webtheme"
|
||||
)
|
||||
|
||||
var _ context.Context = TemplateContext(nil)
|
||||
|
||||
func NewTemplateContext(ctx context.Context) TemplateContext {
|
||||
return TemplateContext{"_ctx": ctx}
|
||||
func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext {
|
||||
return TemplateContext{"_ctx": ctx, "_req": req}
|
||||
}
|
||||
|
||||
func (c TemplateContext) req() *http.Request {
|
||||
return c["_req"].(*http.Request)
|
||||
}
|
||||
|
||||
func (c TemplateContext) parentContext() context.Context {
|
||||
@@ -33,3 +46,40 @@ func (c TemplateContext) Err() error {
|
||||
func (c TemplateContext) Value(key any) any {
|
||||
return c.parentContext().Value(key)
|
||||
}
|
||||
|
||||
func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo {
|
||||
var themeName string
|
||||
if webCtx := GetWebContext(c); webCtx != nil {
|
||||
if webCtx.Doer != nil {
|
||||
themeName = webCtx.Doer.Theme
|
||||
}
|
||||
}
|
||||
if themeName == "" {
|
||||
themeName = middleware.GetSiteCookie(c.req(), middleware.CookieTheme)
|
||||
}
|
||||
return webtheme.GuaranteeGetThemeMetaInfo(themeName)
|
||||
}
|
||||
|
||||
func (c TemplateContext) CurrentWebBanner() *setting.WebBannerType {
|
||||
// Using revision as a simple approach to determine if the banner has been changed after the user dismissed it.
|
||||
// There could be some false-positives because revision can be changed even if the banner isn't.
|
||||
// While it should be still good enough (no admin would keep changing the settings) and doesn't really harm end users (just a few more times to see the banner)
|
||||
// So it doesn't need to make it more complicated by allocating unique IDs or using hashes.
|
||||
dismissedBannerRevision, _ := strconv.Atoi(middleware.GetSiteCookie(c.req(), middleware.CookieWebBannerDismissed))
|
||||
banner, revision, _ := setting.Config().Instance.WebBanner.ValueRevision(c)
|
||||
if banner.ShouldDisplay() && dismissedBannerRevision != revision {
|
||||
return &banner
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppFullLink returns a full URL link with AppSubURL for the given app link (no AppSubURL)
|
||||
// If no link is given, it returns the current app full URL with sub-path but without trailing slash (that's why it is not named as AppURL)
|
||||
func (c TemplateContext) AppFullLink(link ...string) template.URL {
|
||||
s := httplib.GuessCurrentAppURL(c.parentContext())
|
||||
s = strings.TrimSuffix(s, "/")
|
||||
if len(link) == 0 {
|
||||
return template.URL(s)
|
||||
}
|
||||
return template.URL(s + "/" + strings.TrimPrefix(link[0], "/"))
|
||||
}
|
||||
|
||||
@@ -49,3 +49,17 @@ func TestRedirectToCurrentSite(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppFullLink(t *testing.T) {
|
||||
setting.IsInTesting = true
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://gitea.example.com/sub/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
|
||||
defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLNever)()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://gitea.example.com/sub/", nil)
|
||||
tmplCtx := NewTemplateContext(req.Context(), req)
|
||||
|
||||
assert.Equal(t, "https://gitea.example.com/sub", string(tmplCtx.AppFullLink()))
|
||||
assert.Equal(t, "https://gitea.example.com/sub/user/repo", string(tmplCtx.AppFullLink("user/repo")))
|
||||
assert.Equal(t, "https://gitea.example.com/sub/user/repo", string(tmplCtx.AppFullLink("/user/repo")))
|
||||
}
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2021 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// a middleware that generates and validates CSRF tokens.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
CsrfHeaderName = "X-Csrf-Token"
|
||||
CsrfFormName = "_csrf"
|
||||
)
|
||||
|
||||
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
|
||||
type CSRFProtector interface {
|
||||
// PrepareForSessionUser prepares the csrf protector for the current session user.
|
||||
PrepareForSessionUser(ctx *Context)
|
||||
// Validate validates the csrf token in http context.
|
||||
Validate(ctx *Context)
|
||||
// DeleteCookie deletes the csrf cookie
|
||||
DeleteCookie(ctx *Context)
|
||||
}
|
||||
|
||||
type csrfProtector struct {
|
||||
opt CsrfOptions
|
||||
// id must be unique per user.
|
||||
id string
|
||||
// token is the valid one which will be used by end user and passed via header, cookie, or hidden form value.
|
||||
token string
|
||||
}
|
||||
|
||||
// CsrfOptions maintains options to manage behavior of Generate.
|
||||
type CsrfOptions struct {
|
||||
// The global secret value used to generate Tokens.
|
||||
Secret string
|
||||
// Cookie value used to set and get token.
|
||||
Cookie string
|
||||
// Cookie domain.
|
||||
CookieDomain string
|
||||
// Cookie path.
|
||||
CookiePath string
|
||||
CookieHTTPOnly bool
|
||||
// SameSite set the cookie SameSite type
|
||||
SameSite http.SameSite
|
||||
// Set the Secure flag to true on the cookie.
|
||||
Secure bool
|
||||
// sessionKey is the key used for getting the unique ID per user.
|
||||
sessionKey string
|
||||
// oldSessionKey saves old value corresponding to sessionKey.
|
||||
oldSessionKey string
|
||||
}
|
||||
|
||||
func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: opt.Cookie,
|
||||
Value: value,
|
||||
Path: opt.CookiePath,
|
||||
Domain: opt.CookieDomain,
|
||||
MaxAge: int(CsrfTokenTimeout.Seconds()),
|
||||
Secure: opt.Secure,
|
||||
HttpOnly: opt.CookieHTTPOnly,
|
||||
SameSite: opt.SameSite,
|
||||
}
|
||||
}
|
||||
|
||||
func NewCSRFProtector(opt CsrfOptions) CSRFProtector {
|
||||
if opt.Secret == "" {
|
||||
panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code
|
||||
}
|
||||
opt.Cookie = util.IfZero(opt.Cookie, "_csrf")
|
||||
opt.CookiePath = util.IfZero(opt.CookiePath, "/")
|
||||
opt.sessionKey = "uid"
|
||||
opt.oldSessionKey = "_old_" + opt.sessionKey
|
||||
return &csrfProtector{opt: opt}
|
||||
}
|
||||
|
||||
func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
|
||||
c.id = "0"
|
||||
if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil {
|
||||
switch uidVal := uidAny.(type) {
|
||||
case string:
|
||||
c.id = uidVal
|
||||
case int64:
|
||||
c.id = strconv.FormatInt(uidVal, 10)
|
||||
default:
|
||||
log.Error("invalid uid type in session: %T", uidAny)
|
||||
}
|
||||
}
|
||||
|
||||
oldUID := ctx.Session.Get(c.opt.oldSessionKey)
|
||||
uidChanged := oldUID == nil || oldUID.(string) != c.id
|
||||
cookieToken := ctx.GetSiteCookie(c.opt.Cookie)
|
||||
|
||||
needsNew := true
|
||||
if uidChanged {
|
||||
_ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
|
||||
} else if cookieToken != "" {
|
||||
// If cookie token present, re-use existing unexpired token, else generate a new one.
|
||||
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
|
||||
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
|
||||
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
|
||||
c.token = cookieToken
|
||||
needsNew = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needsNew {
|
||||
c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now())
|
||||
ctx.Resp.Header().Add("Set-Cookie", newCsrfCookie(&c.opt, c.token).String())
|
||||
}
|
||||
|
||||
ctx.Data["CsrfToken"] = c.token
|
||||
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(c.token) + `">`)
|
||||
}
|
||||
|
||||
func (c *csrfProtector) validateToken(ctx *Context, token string) {
|
||||
if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) {
|
||||
c.DeleteCookie(ctx)
|
||||
// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
|
||||
// FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch)
|
||||
http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
|
||||
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
|
||||
// If this validation fails, http.StatusBadRequest is sent.
|
||||
func (c *csrfProtector) Validate(ctx *Context) {
|
||||
if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" {
|
||||
c.validateToken(ctx, token)
|
||||
return
|
||||
}
|
||||
if token := ctx.Req.FormValue(CsrfFormName); token != "" {
|
||||
c.validateToken(ctx, token)
|
||||
return
|
||||
}
|
||||
c.validateToken(ctx, "") // no csrf token, use an empty token to respond error
|
||||
}
|
||||
|
||||
func (c *csrfProtector) DeleteCookie(ctx *Context) {
|
||||
cookie := newCsrfCookie(&c.opt, "")
|
||||
cookie.MaxAge = -1
|
||||
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
@@ -132,10 +132,12 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
|
||||
ctx.ServerError("IsOrgMember", err)
|
||||
return
|
||||
}
|
||||
ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanCreateOrgRepo", err)
|
||||
return
|
||||
if ctx.Org.IsMember {
|
||||
ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanCreateOrgRepo", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
@@ -58,23 +59,28 @@ func PackageAssignmentAPI() func(ctx *APIContext) {
|
||||
}
|
||||
|
||||
func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package {
|
||||
pkg := &Package{
|
||||
Owner: ctx.ContextUser,
|
||||
}
|
||||
var err error
|
||||
pkg.AccessMode, err = determineAccessMode(ctx.Base, pkg, ctx.Doer)
|
||||
pkgOwner := ctx.ContextUser
|
||||
accessMode, err := determineAccessMode(ctx.Base, pkgOwner, ctx.Doer)
|
||||
if err != nil {
|
||||
errCb(http.StatusInternalServerError, fmt.Errorf("determineAccessMode: %w", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
pkg := &Package{
|
||||
Owner: pkgOwner,
|
||||
AccessMode: accessMode,
|
||||
}
|
||||
packageType := ctx.PathParam("type")
|
||||
name := ctx.PathParam("name")
|
||||
if packageType == "" || name == "" {
|
||||
return pkg
|
||||
}
|
||||
|
||||
packageType := ctx.PathParam("type")
|
||||
name := ctx.PathParam("name")
|
||||
version := ctx.PathParam("version")
|
||||
if packageType != "" && name != "" && version != "" {
|
||||
if version != "" {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err))
|
||||
} else {
|
||||
errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err))
|
||||
@@ -87,12 +93,27 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package
|
||||
errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageDescriptor: %w", err))
|
||||
return pkg
|
||||
}
|
||||
} else {
|
||||
p, err := packages_model.GetPackageByName(ctx, pkg.Owner.ID, packages_model.Type(packageType), name)
|
||||
if err != nil {
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
errCb(http.StatusNotFound, fmt.Errorf("GetPackageByName: %w", err))
|
||||
} else {
|
||||
errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageByName: %w", err))
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
|
||||
pkg.Descriptor = &packages_model.PackageDescriptor{
|
||||
Package: p,
|
||||
Owner: pkg.Owner,
|
||||
}
|
||||
}
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
|
||||
func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
@@ -103,8 +124,8 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A
|
||||
|
||||
// TODO: ActionUser permission check
|
||||
accessMode := perm.AccessModeNone
|
||||
if pkg.Owner.IsOrganization() {
|
||||
org := organization.OrgFromUser(pkg.Owner)
|
||||
if pkgOwner.IsOrganization() {
|
||||
org := organization.OrgFromUser(pkgOwner)
|
||||
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. If user is logged in, check all team packages permissions
|
||||
@@ -128,19 +149,19 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A
|
||||
}
|
||||
}
|
||||
}
|
||||
if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkg.Owner, doer) {
|
||||
if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkgOwner, doer) {
|
||||
// 2. If user is unauthorized or no org member, check if org is visible
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else {
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. Check if user is package owner
|
||||
if doer.ID == pkg.Owner.ID {
|
||||
if doer.ID == pkgOwner.ID {
|
||||
accessMode = perm.AccessModeOwner
|
||||
} else if pkg.Owner.Visibility == structs.VisibleTypePublic || pkg.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
|
||||
} else if pkgOwner.Visibility == structs.VisibleTypePublic || pkgOwner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else if pkg.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
|
||||
} else if pkgOwner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
}
|
||||
@@ -150,7 +171,7 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A
|
||||
|
||||
// PackageContexter initializes a package context for a request.
|
||||
func PackageContexter() func(next http.Handler) http.Handler {
|
||||
renderer := templates.HTMLRenderer()
|
||||
renderer := templates.PageRenderer()
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
base := NewBaseContext(resp, req)
|
||||
|
||||
@@ -6,6 +6,7 @@ package context
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -22,11 +23,13 @@ type Pagination struct {
|
||||
}
|
||||
|
||||
// NewPagination creates a new instance of the Pagination struct.
|
||||
// "total" is usually from database result "count int64", so it also uses int64
|
||||
// "pagingNum" is "page size" or "limit", "current" is "page"
|
||||
// total=-1 means only showing prev/next
|
||||
func NewPagination(total, pagingNum, current, numPages int) *Pagination {
|
||||
func NewPagination(total int64, pagingNum, current, numPages int) *Pagination {
|
||||
totalInt := int(min(total, int64(math.MaxInt)))
|
||||
p := &Pagination{}
|
||||
p.Paginater = paginator.New(total, pagingNum, current, numPages)
|
||||
p.Paginater = paginator.New(totalInt, pagingNum, current, numPages)
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,39 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
)
|
||||
|
||||
// CheckTokenScopes checks whether the authenticated API token contains any of the given scopes.
|
||||
func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_model.AccessTokenScope) {
|
||||
if ctx.Data["IsApiToken"] != true {
|
||||
return
|
||||
}
|
||||
|
||||
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.ServerError("PublicOnly", err)
|
||||
return
|
||||
}
|
||||
|
||||
if publicOnly && repo != nil && repo.IsPrivate {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
scopeMatched, err := scope.HasAnyScope(scopes...)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasAnyScope", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !scopeMatched {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRepoAdmin returns a middleware for requiring repository admin permission
|
||||
func RequireRepoAdmin() func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
@@ -57,39 +90,7 @@ func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// CheckRepoScopedToken check whether personal access token has repo scope
|
||||
// CheckRepoScopedToken checks whether the authenticated API token has repo scope.
|
||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
|
||||
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
|
||||
return
|
||||
}
|
||||
|
||||
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if ok { // it's a personal access token but not oauth2 token
|
||||
var scopeMatched bool
|
||||
|
||||
requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)
|
||||
|
||||
// check if scope only applies to public resources
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
|
||||
if publicOnly && repo.IsPrivate {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
scopeMatched, err = scope.HasScope(requiredScopes...)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !scopeMatched {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
CheckTokenScopes(ctx, repo, auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)...)
|
||||
}
|
||||
|
||||
@@ -37,11 +37,46 @@ import (
|
||||
"github.com/editorconfig/editorconfig-core-go/v2"
|
||||
)
|
||||
|
||||
// PullRequest contains information to make a pull request
|
||||
type PullRequest struct {
|
||||
BaseRepo *repo_model.Repository
|
||||
Allowed bool // it only used by the web tmpl: "PullRequestCtx.Allowed"
|
||||
SameRepo bool // it only used by the web tmpl: "PullRequestCtx.SameRepo"
|
||||
// PullRequestContext contains context information for making a new pull request
|
||||
type PullRequestContext struct {
|
||||
ctx *Context
|
||||
|
||||
baseRepo, headRepo *repo_model.Repository
|
||||
|
||||
canCreateNewPull *bool
|
||||
defaultTargetBranch *string
|
||||
}
|
||||
|
||||
func (prc *PullRequestContext) SameRepo() bool {
|
||||
return prc.baseRepo != nil && prc.headRepo != nil && prc.baseRepo.ID == prc.headRepo.ID
|
||||
}
|
||||
|
||||
func (prc *PullRequestContext) CanCreateNewPull() bool {
|
||||
if prc.canCreateNewPull != nil {
|
||||
return *prc.canCreateNewPull
|
||||
}
|
||||
ctx := prc.ctx
|
||||
// People who have push access or have forked repository can propose a new pull request.
|
||||
can := prc.baseRepo.CanContentChange() &&
|
||||
(ctx.Repo.CanWrite(unit_model.TypeCode) || (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)))
|
||||
prc.canCreateNewPull = &can
|
||||
return can
|
||||
}
|
||||
|
||||
func (prc *PullRequestContext) MakeDefaultCompareLink(headBranch string) string {
|
||||
return prc.baseRepo.Link() + "/compare/" +
|
||||
util.PathEscapeSegments(prc.DefaultTargetBranch()) + "..." +
|
||||
util.Iif(prc.SameRepo(), "", util.PathEscapeSegments(prc.headRepo.OwnerName)+":") +
|
||||
util.PathEscapeSegments(headBranch)
|
||||
}
|
||||
|
||||
func (prc *PullRequestContext) DefaultTargetBranch() string {
|
||||
if prc.defaultTargetBranch != nil {
|
||||
return *prc.defaultTargetBranch
|
||||
}
|
||||
branchName := prc.baseRepo.GetPullRequestTargetBranch(prc.ctx)
|
||||
prc.defaultTargetBranch = &branchName
|
||||
return branchName
|
||||
}
|
||||
|
||||
// Repository contains information to operate a repository
|
||||
@@ -64,7 +99,7 @@ type Repository struct {
|
||||
CommitID string
|
||||
CommitsCount int64
|
||||
|
||||
PullRequest *PullRequest
|
||||
PullRequestCtx *PullRequestContext
|
||||
}
|
||||
|
||||
// CanWriteToBranch checks if the branch is writable by the user
|
||||
@@ -138,9 +173,25 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
|
||||
protectedBranch.Repo = targetRepo
|
||||
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
|
||||
protectionRequireSigned = protectedBranch.RequireSignedCommits
|
||||
// If branch-wide push is restricted, allow direct commit when the
|
||||
// URL-derived tree path matches an unprotected file pattern. The
|
||||
// pre-receive hook re-checks every path the commit actually touches
|
||||
// (e.g. rename source and destination).
|
||||
if !canPushWithProtection && ctx.Repo.TreePath != "" && protectedBranch.UnprotectedFilePatterns != "" {
|
||||
globs := protectedBranch.GetUnprotectedFilePatterns()
|
||||
if protectedBranch.IsUnprotectedFile(globs, ctx.Repo.TreePath) {
|
||||
canPushWithProtection = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
|
||||
targetGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, targetRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, doer, targetGitRepo, refName.String())
|
||||
wontSignReason := ""
|
||||
if asymkey_service.IsErrWontSign(err) {
|
||||
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
|
||||
@@ -202,14 +253,14 @@ func (r *Repository) CanCreateIssueDependencies(ctx context.Context, user *user_
|
||||
}
|
||||
|
||||
// GetCommitsCount returns cached commit count for current view
|
||||
func (r *Repository) GetCommitsCount() (int64, error) {
|
||||
func (r *Repository) GetCommitsCount(ctx context.Context) (int64, error) {
|
||||
if r.Commit == nil {
|
||||
return 0, nil
|
||||
}
|
||||
contextName := r.RefFullName.ShortName()
|
||||
isRef := r.RefFullName.IsBranch() || r.RefFullName.IsTag()
|
||||
return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, isRef), func() (int64, error) {
|
||||
return r.Commit.CommitsCount()
|
||||
return gitrepo.CommitsCountOfCommit(ctx, r.Repository, r.Commit.ID.String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -219,11 +270,10 @@ func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool,
|
||||
|
||||
return cache.GetInt64(cacheKey, func() (int64, error) {
|
||||
if len(branches) == 0 {
|
||||
return git.AllCommitsCount(ctx, r.Repository.RepoPath(), hidePRRefs, files...)
|
||||
return gitrepo.AllCommitsCount(ctx, r.Repository, hidePRRefs, files...)
|
||||
}
|
||||
return git.CommitsCount(ctx,
|
||||
git.CommitsCountOptions{
|
||||
RepoPath: r.Repository.RepoPath(),
|
||||
return gitrepo.CommitsCount(ctx, r.Repository,
|
||||
gitrepo.CommitsCountOptions{
|
||||
Revision: branches,
|
||||
RelPath: files,
|
||||
})
|
||||
@@ -304,9 +354,9 @@ func RetrieveTemplateRepo(ctx *Context, repo *repo_model.Repository) {
|
||||
return
|
||||
}
|
||||
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, templateRepo, ctx.Doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, templateRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -381,9 +431,9 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
if ctx.DoerNeedTwoFactorAuth() {
|
||||
ctx.Repo.Permission = access_model.PermissionNoAccess()
|
||||
} else {
|
||||
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
ctx.Repo.Permission, err = access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -413,6 +463,12 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
|
||||
}
|
||||
|
||||
func InitRepoPullRequestCtx(ctx *Context, base, head *repo_model.Repository) {
|
||||
ctx.Repo.PullRequestCtx = &PullRequestContext{ctx: ctx}
|
||||
ctx.Repo.PullRequestCtx.baseRepo, ctx.Repo.PullRequestCtx.headRepo = base, head
|
||||
ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequestCtx
|
||||
}
|
||||
|
||||
// RepoAssignment returns a middleware to handle repository assignment
|
||||
func RepoAssignment(ctx *Context) {
|
||||
if ctx.Data["Repository"] != nil {
|
||||
@@ -622,7 +678,7 @@ func RepoAssignment(ctx *Context) {
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") {
|
||||
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err)
|
||||
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RelativePath(), err)
|
||||
ctx.Repo.Repository.MarkAsBrokenEmpty()
|
||||
// Only allow access to base of repo or settings
|
||||
if !isHomeOrSettings {
|
||||
@@ -661,28 +717,16 @@ func RepoAssignment(ctx *Context) {
|
||||
|
||||
ctx.Data["BranchesCount"] = branchesTotal
|
||||
|
||||
// People who have push access or have forked repository can propose a new pull request.
|
||||
canPush := ctx.Repo.CanWrite(unit_model.TypeCode) ||
|
||||
(ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID))
|
||||
canCompare := false
|
||||
|
||||
// Pull request is allowed if this is a fork repository
|
||||
// and base repository accepts pull requests.
|
||||
// Pull request is allowed if this is a fork repository, and base repository accepts pull requests.
|
||||
if repo.BaseRepo != nil && repo.BaseRepo.AllowsPulls(ctx) {
|
||||
canCompare = true
|
||||
// TODO: this (and below) "BaseRepo" var is not clear and should be removed in the future
|
||||
ctx.Data["BaseRepo"] = repo.BaseRepo
|
||||
ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo
|
||||
ctx.Repo.PullRequest.Allowed = canPush
|
||||
InitRepoPullRequestCtx(ctx, repo.BaseRepo, repo)
|
||||
} else if repo.AllowsPulls(ctx) {
|
||||
// Or, this is repository accepts pull requests between branches.
|
||||
canCompare = true
|
||||
ctx.Data["BaseRepo"] = repo
|
||||
ctx.Repo.PullRequest.BaseRepo = repo
|
||||
ctx.Repo.PullRequest.Allowed = canPush
|
||||
ctx.Repo.PullRequest.SameRepo = true
|
||||
InitRepoPullRequestCtx(ctx, repo, repo)
|
||||
}
|
||||
ctx.Data["CanCompareOrPull"] = canCompare
|
||||
ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest
|
||||
|
||||
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
|
||||
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
|
||||
@@ -821,7 +865,7 @@ func RepoRefByDefaultBranch() func(*Context) {
|
||||
ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
|
||||
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
|
||||
ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
|
||||
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount()
|
||||
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount(ctx)
|
||||
ctx.Data["RefFullName"] = ctx.Repo.RefFullName
|
||||
ctx.Data["BranchName"] = ctx.Repo.BranchName
|
||||
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
|
||||
@@ -858,7 +902,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
|
||||
if err == nil && len(brs) != 0 {
|
||||
refShortName = brs[0]
|
||||
} else if len(brs) == 0 {
|
||||
log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
|
||||
log.Error("No branches in non-empty repository %s", ctx.Repo.Repository.RelativePath())
|
||||
} else {
|
||||
log.Error("GetBranches error: %v", err)
|
||||
}
|
||||
@@ -927,7 +971,8 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
|
||||
}
|
||||
// If short commit ID add canonical link header
|
||||
if len(refShortName) < ctx.Repo.GetObjectFormat().FullLength() {
|
||||
canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))
|
||||
// FIXME: the dirty hack of "strings.Replace" should be fixed
|
||||
canonicalURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)
|
||||
ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL))
|
||||
}
|
||||
} else {
|
||||
@@ -970,7 +1015,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
|
||||
|
||||
ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef
|
||||
|
||||
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
|
||||
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommitsCount", err)
|
||||
return
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
web_types "code.gitea.io/gitea/modules/web/types"
|
||||
@@ -67,6 +69,15 @@ func (r *Response) WriteHeader(statusCode int) {
|
||||
}
|
||||
}
|
||||
|
||||
// Hijack implements http.Hijacker, delegating to the underlying ResponseWriter.
|
||||
// This is needed for WebSocket upgrades through reverse proxies.
|
||||
func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := r.ResponseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, http.ErrNotSupported
|
||||
}
|
||||
|
||||
// Flush flushes cached data
|
||||
func (r *Response) Flush() {
|
||||
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||
|
||||
@@ -95,8 +95,8 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
|
||||
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/releases/attachments/remove"
|
||||
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/releases/attachments"
|
||||
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Release.AllowedTypes, "|", ",")
|
||||
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
|
||||
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
|
||||
ctx.Data["UploadMaxFiles"] = setting.Repository.Release.MaxFiles
|
||||
ctx.Data["UploadMaxSize"] = setting.Repository.Release.FileMaxSize
|
||||
case "comment":
|
||||
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments"
|
||||
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove"
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2020 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CsrfTokenTimeout represents the duration that XSRF tokens are valid.
|
||||
// It is exported so clients may set cookie timeouts that match generated tokens.
|
||||
const CsrfTokenTimeout = 24 * time.Hour
|
||||
|
||||
// CsrfTokenRegenerationInterval is the interval between token generations, old tokens are still valid before CsrfTokenTimeout
|
||||
var CsrfTokenRegenerationInterval = 10 * time.Minute
|
||||
|
||||
var csrfTokenSep = []byte(":")
|
||||
|
||||
// GenerateCsrfToken returns a URL-safe secure XSRF token that expires in CsrfTokenTimeout hours.
|
||||
// key is a secret key for your application.
|
||||
// userID is a unique identifier for the user.
|
||||
// actionID is the action the user is taking (e.g. POSTing to a particular path).
|
||||
func GenerateCsrfToken(key, userID, actionID string, now time.Time) string {
|
||||
nowUnixNano := now.UnixNano()
|
||||
nowUnixNanoStr := strconv.FormatInt(nowUnixNano, 10)
|
||||
h := hmac.New(sha1.New, []byte(key))
|
||||
h.Write([]byte(strings.ReplaceAll(userID, ":", "_")))
|
||||
h.Write(csrfTokenSep)
|
||||
h.Write([]byte(strings.ReplaceAll(actionID, ":", "_")))
|
||||
h.Write(csrfTokenSep)
|
||||
h.Write([]byte(nowUnixNanoStr))
|
||||
tok := fmt.Sprintf("%s:%s", h.Sum(nil), nowUnixNanoStr)
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(tok))
|
||||
}
|
||||
|
||||
func ParseCsrfToken(token string) (issueTime time.Time, ok bool) {
|
||||
data, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
pos := bytes.LastIndex(data, csrfTokenSep)
|
||||
if pos == -1 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
nanos, err := strconv.ParseInt(string(data[pos+1:]), 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return time.Unix(0, nanos), true
|
||||
}
|
||||
|
||||
// ValidCsrfToken returns true if token is a valid and unexpired token returned by Generate.
|
||||
func ValidCsrfToken(token, key, userID, actionID string, now time.Time) bool {
|
||||
issueTime, ok := ParseCsrfToken(token)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the token is not expired.
|
||||
if now.Sub(issueTime) >= CsrfTokenTimeout {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the token is not from the future.
|
||||
// Allow 1-minute grace period in case the token is being verified on a
|
||||
// machine whose clock is behind the machine that issued the token.
|
||||
if issueTime.After(now.Add(1 * time.Minute)) {
|
||||
return false
|
||||
}
|
||||
|
||||
expected := GenerateCsrfToken(key, userID, actionID, issueTime)
|
||||
|
||||
// Check that the token matches the expected value.
|
||||
// Use constant time comparison to avoid timing attacks.
|
||||
return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2020 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
key = "quay"
|
||||
userID = "12345678"
|
||||
actionID = "POST /form"
|
||||
)
|
||||
|
||||
var (
|
||||
now = time.Now()
|
||||
oneMinuteFromNow = now.Add(1 * time.Minute)
|
||||
)
|
||||
|
||||
func Test_ValidToken(t *testing.T) {
|
||||
t.Run("Validate token", func(t *testing.T) {
|
||||
tok := GenerateCsrfToken(key, userID, actionID, now)
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, oneMinuteFromNow))
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(CsrfTokenTimeout-1*time.Nanosecond)))
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(-1*time.Minute)))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_SeparatorReplacement tests that separators are being correctly substituted
|
||||
func Test_SeparatorReplacement(t *testing.T) {
|
||||
t.Run("Test two separator replacements", func(t *testing.T) {
|
||||
assert.NotEqual(t, GenerateCsrfToken("foo:bar", "baz", "wah", now),
|
||||
GenerateCsrfToken("foo", "bar:baz", "wah", now))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_InvalidToken(t *testing.T) {
|
||||
t.Run("Test invalid tokens", func(t *testing.T) {
|
||||
invalidTokenTests := []struct {
|
||||
name, key, userID, actionID string
|
||||
t time.Time
|
||||
}{
|
||||
{"Bad key", "foobar", userID, actionID, oneMinuteFromNow},
|
||||
{"Bad userID", key, "foobar", actionID, oneMinuteFromNow},
|
||||
{"Bad actionID", key, userID, "foobar", oneMinuteFromNow},
|
||||
{"Expired", key, userID, actionID, now.Add(CsrfTokenTimeout)},
|
||||
{"More than 1 minute from the future", key, userID, actionID, now.Add(-1*time.Nanosecond - 1*time.Minute)},
|
||||
}
|
||||
|
||||
tok := GenerateCsrfToken(key, userID, actionID, now)
|
||||
for _, itt := range invalidTokenTests {
|
||||
assert.False(t, ValidCsrfToken(tok, itt.key, itt.userID, itt.actionID, itt.t))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_ValidateBadData primarily tests that no unexpected panics are triggered during parsing
|
||||
func Test_ValidateBadData(t *testing.T) {
|
||||
t.Run("Validate bad data", func(t *testing.T) {
|
||||
badDataTests := []struct {
|
||||
name, tok string
|
||||
}{
|
||||
{"Invalid Base64", "ASDab24(@)$*=="},
|
||||
{"No delimiter", base64.URLEncoding.EncodeToString([]byte("foobar12345678"))},
|
||||
{"Invalid time", base64.URLEncoding.EncodeToString([]byte("foobar:foobar"))},
|
||||
}
|
||||
|
||||
for _, bdt := range badDataTests {
|
||||
assert.False(t, ValidCsrfToken(bdt.tok, key, userID, actionID, oneMinuteFromNow))
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user