mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-18 15:37:48 +00:00
This PR pertains to the client-side validation of the Website input on user, repo, and org profiles. #12962 extends `[service].VALID_SITE_URL_SCHEMES` to cover Website fields on repo and org profiles, where before that config key only applied to the one on user profiles. If that change merges, it will then be possible to construct an HTML [`pattern`](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/input#pattern) attribute for general use on any Website form input that the server validates this way, thus enabling browsers to catch errors early relating to URL scheme confusion. This PR (1) introduces such a `pattern` attribute, and (2) adds a new UI note to make clear to users which URL schemes are permitted. This change helps explain the browser's otherwise cryptic error messages regarding pattern mismatch, while also letting users know what URI schemes the Forgejo instance supports as Website links (e.g. gemini:// URLs).  This MUST NOT merge before #12962. To do so would introduce a regression wherein the UI may suggest and validate a different set of allowed URL schemes than the server actually permits. See also #5519 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12991 Reviewed-by: 0ko <0ko@noreply.codeberg.org>
264 lines
8.9 KiB
Go
264 lines
8.9 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// Copyright 2024 The Forgejo Authors.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"forgejo.org/models/unit"
|
|
user_model "forgejo.org/models/user"
|
|
mc "forgejo.org/modules/cache"
|
|
"forgejo.org/modules/gitrepo"
|
|
"forgejo.org/modules/httpcache"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/templates"
|
|
"forgejo.org/modules/translation"
|
|
"forgejo.org/modules/web"
|
|
"forgejo.org/modules/web/middleware"
|
|
web_types "forgejo.org/modules/web/types"
|
|
"forgejo.org/services/auth"
|
|
|
|
"code.forgejo.org/go-chi/cache"
|
|
"code.forgejo.org/go-chi/session"
|
|
)
|
|
|
|
// Render represents a template render
|
|
type Render interface {
|
|
TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error)
|
|
HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error
|
|
}
|
|
|
|
// Context represents context of a request.
|
|
type Context struct {
|
|
*Base
|
|
|
|
TemplateContext *templates.Context
|
|
|
|
Render Render
|
|
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
|
|
|
Cache cache.Cache
|
|
Flash *middleware.Flash
|
|
Session session.Store
|
|
|
|
Link string // current request URL (without query string)
|
|
|
|
Doer *user_model.User // current signed-in user
|
|
IsSigned bool
|
|
Authentication auth.AuthenticationResult
|
|
|
|
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
|
|
|
Repo *Repository
|
|
Org *Organization
|
|
Package *Package
|
|
}
|
|
|
|
func init() {
|
|
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
|
|
return req.Context().Value(WebContextKey).(*Context)
|
|
})
|
|
}
|
|
|
|
type webContextKeyType struct{}
|
|
|
|
var WebContextKey = webContextKeyType{}
|
|
|
|
func GetWebContext(req *http.Request) *Context {
|
|
ctx, _ := req.Context().Value(WebContextKey).(*Context)
|
|
return ctx
|
|
}
|
|
|
|
// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
|
|
type ValidateContext struct {
|
|
*Base
|
|
}
|
|
|
|
// GetValidateContext gets a context for middleware form validation
|
|
func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
|
|
if ctxAPI, ok := req.Context().Value(apiContextKey).(*APIContext); ok {
|
|
ctx = &ValidateContext{Base: ctxAPI.Base}
|
|
} else if ctxWeb, ok := req.Context().Value(WebContextKey).(*Context); ok {
|
|
ctx = &ValidateContext{Base: ctxWeb.Base}
|
|
} else {
|
|
panic("invalid context, expect either APIContext or Context")
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func NewTemplateContextForWeb(ctx *Context) *templates.Context {
|
|
tmplCtx := templates.NewContext(ctx)
|
|
tmplCtx.Locale = ctx.Locale
|
|
tmplCtx.AvatarUtils = templates.NewAvatarUtils(ctx)
|
|
tmplCtx.Data = ctx.Data
|
|
return tmplCtx
|
|
}
|
|
|
|
func NewWebContext(base *Base, render Render, session session.Store) *Context {
|
|
ctx := &Context{
|
|
Base: base,
|
|
Render: render,
|
|
Session: session,
|
|
|
|
Cache: mc.GetCache(),
|
|
Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"),
|
|
Repo: &Repository{PullRequest: &PullRequest{}},
|
|
Org: &Organization{},
|
|
|
|
Authentication: &auth.UnauthenticatedResult{},
|
|
}
|
|
ctx.TemplateContext = NewTemplateContextForWeb(ctx)
|
|
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
|
|
return ctx
|
|
}
|
|
|
|
func (ctx *Context) AddPluralStringsToPageData(keys []string) {
|
|
for _, key := range keys {
|
|
array, fallback := ctx.Locale.TrPluralStringAllForms(key)
|
|
|
|
ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)[key] = array
|
|
|
|
if fallback != nil {
|
|
ctx.PageData["PLURALSTRINGS_FALLBACK"].(map[string][]string)[key] = fallback
|
|
}
|
|
}
|
|
}
|
|
|
|
// Contexter initializes a classic context for a request.
|
|
func Contexter() func(next http.Handler) http.Handler {
|
|
rnd := templates.HTMLRenderer()
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
|
base, baseCleanUp := NewBaseContext(resp, req)
|
|
defer baseCleanUp()
|
|
ctx := NewWebContext(base, rnd, session.GetSession(req))
|
|
|
|
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
|
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
|
|
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
|
|
ctx.Data["Link"] = ctx.Link
|
|
ctx.Data["ValidSiteURLSchemes"] = setting.Service.ValidSiteURLSchemes
|
|
ctx.Data["ValidSiteURLPattern"] = setting.ValidSiteURLPattern()
|
|
|
|
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
|
|
ctx.PageData = map[string]any{}
|
|
ctx.Data["PageData"] = ctx.PageData
|
|
|
|
ctx.AppendContextValue(WebContextKey, ctx)
|
|
ctx.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
|
|
|
|
// Get the last flash message from cookie
|
|
lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
|
|
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
|
|
// store last Flash message into the template data, to render it
|
|
ctx.Data["Flash"] = &middleware.Flash{
|
|
DataStore: ctx,
|
|
Values: vals,
|
|
ErrorMsg: vals.Get("error"),
|
|
SuccessMsg: vals.Get("success"),
|
|
InfoMsg: vals.Get("info"),
|
|
WarningMsg: vals.Get("warning"),
|
|
}
|
|
}
|
|
|
|
// if there are new messages in the ctx.Flash, write them into cookie
|
|
ctx.Resp.Before(func(resp ResponseWriter) {
|
|
if val := ctx.Flash.Encode(); val != "" {
|
|
middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, val, 0)
|
|
} else if lastFlashCookie != "" {
|
|
middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, "", -1)
|
|
}
|
|
})
|
|
|
|
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0)
|
|
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
|
|
|
ctx.Data["SystemConfig"] = setting.Config()
|
|
|
|
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
|
|
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
|
ctx.Data["DisableStars"] = setting.Repository.DisableStars
|
|
ctx.Data["DisableForks"] = setting.Repository.DisableForks
|
|
ctx.Data["EnableActions"] = setting.Actions.Enabled
|
|
ctx.Data["EnableFederation"] = setting.Federation.Enabled
|
|
|
|
ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled()
|
|
ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled()
|
|
ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled()
|
|
ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled()
|
|
ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled()
|
|
|
|
ctx.Data["AllLangs"] = translation.AllLangs()
|
|
|
|
ctx.PageData["PLURAL_RULE_LANG"] = translation.GetPluralRule(ctx.Locale)
|
|
ctx.PageData["PLURAL_RULE_FALLBACK"] = translation.GetDefaultPluralRule()
|
|
ctx.PageData["PLURALSTRINGS_LANG"] = map[string][]string{}
|
|
ctx.PageData["PLURALSTRINGS_FALLBACK"] = map[string][]string{}
|
|
|
|
ctx.AddPluralStringsToPageData([]string{"relativetime.mins", "relativetime.hours", "relativetime.days", "relativetime.weeks", "relativetime.months", "relativetime.years"})
|
|
|
|
ctx.PageData["DATETIMESTRINGS"] = map[string]string{
|
|
"FUTURE": ctx.Locale.TrString("relativetime.future"),
|
|
"NOW": ctx.Locale.TrString("relativetime.now"),
|
|
}
|
|
for _, key := range []string{"relativetime.1day", "relativetime.1week", "relativetime.1month", "relativetime.1year"} {
|
|
// These keys are used for special-casing some time words. We only add keys that are actually translated, so that we
|
|
// can fall back to the generic pluralized time word in the correct language if the special case is untranslated.
|
|
if ctx.Locale.HasKey(key) {
|
|
ctx.PageData["DATETIMESTRINGS"].(map[string]string)[key] = ctx.Locale.TrString(key)
|
|
}
|
|
}
|
|
|
|
next.ServeHTTP(ctx.Resp, ctx.Req)
|
|
})
|
|
}
|
|
}
|
|
|
|
// HasError returns true if error occurs in form validation.
|
|
// Attention: this function changes ctx.Data and ctx.Flash
|
|
func (ctx *Context) HasError() bool {
|
|
hasErr, ok := ctx.Data["HasError"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
|
|
ctx.Data["Flash"] = ctx.Flash
|
|
return hasErr.(bool)
|
|
}
|
|
|
|
// GetErrMsg returns error message in form validation.
|
|
func (ctx *Context) GetErrMsg() string {
|
|
msg, _ := ctx.Data["ErrorMsg"].(string)
|
|
if msg == "" {
|
|
msg = "invalid form data"
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func (ctx *Context) JSONRedirect(redirect string) {
|
|
ctx.JSON(http.StatusOK, map[string]any{"redirect": redirect})
|
|
}
|
|
|
|
func (ctx *Context) JSONOK() {
|
|
ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
|
|
}
|
|
|
|
func (ctx *Context) JSONError(msg any) {
|
|
switch v := msg.(type) {
|
|
case string:
|
|
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
|
|
case template.HTML:
|
|
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
|
|
default:
|
|
panic(fmt.Sprintf("unsupported type: %T", msg))
|
|
}
|
|
}
|