mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-06 04:53:22 +09:00
189 lines
7.2 KiB
Go
189 lines
7.2 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"context"
|
|
"html"
|
|
"html/template"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/httplib"
|
|
"gitea.dev/modules/public"
|
|
"gitea.dev/modules/reqctx"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/translation"
|
|
"gitea.dev/modules/web/middleware"
|
|
"gitea.dev/services/webtheme"
|
|
)
|
|
|
|
type TemplateContext map[string]any
|
|
|
|
var _ context.Context = TemplateContext(nil)
|
|
|
|
func NewTemplateContext(ctx reqctx.RequestContext, req *http.Request) TemplateContext {
|
|
return TemplateContext{"_ctx": ctx, "_req": req}
|
|
}
|
|
|
|
func (c TemplateContext) req() *http.Request {
|
|
return c["_req"].(*http.Request) //nolint:forcetypeassert // must exist
|
|
}
|
|
|
|
func (c TemplateContext) parentContext() reqctx.RequestContext {
|
|
return c["_ctx"].(reqctx.RequestContext) //nolint:forcetypeassert // must exist
|
|
}
|
|
|
|
func (c TemplateContext) Deadline() (deadline time.Time, ok bool) {
|
|
return c.parentContext().Deadline()
|
|
}
|
|
|
|
func (c TemplateContext) Done() <-chan struct{} {
|
|
return c.parentContext().Done()
|
|
}
|
|
|
|
func (c TemplateContext) Err() error {
|
|
return c.parentContext().Err()
|
|
}
|
|
|
|
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) ImpersonatedUser() *user_model.User {
|
|
webCtx := GetWebContext(c)
|
|
if webCtx == nil || webCtx.Doer == nil || !webCtx.DoerIsImpersonated() {
|
|
return nil
|
|
}
|
|
return webCtx.Doer
|
|
}
|
|
|
|
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
|
|
// 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], "/"))
|
|
}
|
|
|
|
func (c TemplateContext) ScriptImport(path string, typ ...string) template.HTML {
|
|
if len(typ) > 0 {
|
|
if typ[0] == "module" {
|
|
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" type="module" src="` + html.EscapeString(public.AssetURI(path)) + `"></script>`)
|
|
}
|
|
panic("unsupported script type: " + typ[0])
|
|
}
|
|
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" src="` + html.EscapeString(public.AssetURI(path)) + `"></script>`)
|
|
}
|
|
|
|
func (c TemplateContext) CspScriptNonce() (ret string) {
|
|
return CspScriptNonce(c.parentContext())
|
|
}
|
|
|
|
func WebContentSecurityPolicy(scriptNonce string) string {
|
|
if setting.Security.ContentSecurityPolicyGeneral == "unset" {
|
|
return "" // if site admin disables the general CSP, then we don't use it
|
|
}
|
|
// The CSP problem is more complicated than it looks.
|
|
// Gitea was designed to support various "customizations", including:
|
|
// * custom themes (custom CSS and JS)
|
|
// * custom assets URL (CDN)
|
|
// * custom plugins and external renders (e.g.: PlantUML render, and the renders might also load some JS/CSS assets)
|
|
// There is no easy way for end users to make the CSP "source" completely right.
|
|
//
|
|
// There can be 2 approaches in the future:
|
|
// A. Let end users to configure their reverse proxy to add CSP header
|
|
// * Browsers will merge and use the stricter rules between Gitea and reverse proxy
|
|
// B. Introduce some config options in "app.ini"
|
|
// * Maybe this approach should be avoided, don't make the config system too complex, just let users use A
|
|
|
|
// allow all by default (the same as old releases with no CSP)
|
|
// * maybe some images or markup (external) renders need "data:", need to investigate
|
|
// * avatar upload editor needs "blob:", at least "img-src" and "content-src"
|
|
return `default-src * data: blob:;` +
|
|
|
|
// enforce nonce for all scripts, disallow inline scripts
|
|
`script-src * 'nonce-` + scriptNonce + `';` +
|
|
|
|
// it seems that Vue needs the unsafe-inline, and our custom colors (e.g.: label) also need it
|
|
`style-src * 'unsafe-inline';`
|
|
}
|
|
|
|
func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
|
|
scriptNonce := c.CspScriptNonce()
|
|
csp := WebContentSecurityPolicy(scriptNonce)
|
|
if csp == "" {
|
|
return ""
|
|
}
|
|
return template.HTML(`<meta http-equiv="Content-Security-Policy" content="` + csp + `">`)
|
|
}
|
|
|
|
func (c TemplateContext) WindowConfig() map[string]any {
|
|
locale := c["Locale"].(translation.Locale) //nolint:forcetypeassert // must exist
|
|
return map[string]any{
|
|
"appUrl": c.AppFullLink("/"),
|
|
"appSubUrl": setting.AppSubURL,
|
|
"assetUrlPrefix": setting.StaticURLPrefix + "/assets",
|
|
"runModeIsProd": setting.IsProd,
|
|
"customEmojis": setting.UI.CustomEmojisMap,
|
|
"pageData": c.parentContext().GetData()["PageData"],
|
|
"enableTimeTracking": setting.Service.EnableTimetracking,
|
|
"mermaidMaxSourceCharacters": setting.MermaidMaxSourceCharacters,
|
|
"sharedWorkerUri": public.AssetURI("web_src/js/user-events.sharedworker.ts"),
|
|
"notificationSettings": map[string]any{
|
|
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
|
|
"TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
|
|
"MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
|
|
},
|
|
// This global i18n object should only contain general texts.
|
|
// for specialized texts, it should be provided inside the related modules by:
|
|
// (1) API response (2) HTML data-attribute (3) PageData
|
|
//
|
|
// Maybe (if really needed) in the future we can introduce versioned frontend i18n data,
|
|
// make frontend cache i18n data in local storage and only update when the version is changed,
|
|
// then we can fill more keys here.
|
|
"i18n": map[string]any{
|
|
"error_occurred": locale.Tr("error.occurred"),
|
|
"remove_label_str": locale.Tr("remove_label_str"),
|
|
"modal_confirm": locale.Tr("modal.confirm"),
|
|
"modal_cancel": locale.Tr("modal.cancel"),
|
|
"more_items": locale.Tr("more_items"),
|
|
"copy_success": locale.Tr("copy_success"),
|
|
"copy_error": locale.Tr("copy_error"),
|
|
},
|
|
}
|
|
}
|