From eb501f6b19f8b8dd325c6e29831b2ca0f4669e73 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 5 Sep 2026 10:50:43 +0200 Subject: [PATCH] enhance: move window.config to JSON, improve CSP format (#39236) Co-authored-by: wxiaoguang --- modules/templates/helper.go | 13 --------- services/context/context.go | 7 +++-- services/context/context_template.go | 40 ++++++++++++++++++++++++++-- services/context/context_test.go | 10 +++++++ templates/base/head_script.tmpl | 25 +---------------- web_src/js/bootstrap.ts | 15 ++++++----- web_src/js/iife.ts | 2 +- web_src/js/modules/errors.ts | 10 +++++-- 8 files changed, 72 insertions(+), 50 deletions(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index f071353bfad..ae7f3636a6d 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -96,9 +96,6 @@ func newFuncMapWebPage() template.FuncMap { "AllowedReactions": func() []string { return setting.UI.Reactions }, - "CustomEmojis": func() map[string]string { - return setting.UI.CustomEmojisMap - }, "MetaAuthor": func() string { return setting.UI.Meta.Author }, @@ -114,16 +111,6 @@ func newFuncMapWebPage() template.FuncMap { "DisableWebhooks": func() bool { return setting.DisableWebhooks }, - "NotificationSettings": func() map[string]any { - return 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), - } - }, - "MermaidMaxSourceCharacters": func() int { - return setting.MermaidMaxSourceCharacters - }, // ----------------------------------------------------------------- // render diff --git a/services/context/context.go b/services/context/context.go index f13d51c55f1..99e40ea72e6 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -46,8 +46,11 @@ type Context struct { TemplateContext TemplateContext - Render Render - PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` + Render Render + + // PageData is used by JavaScript modules, it is `window.config.pageData`. + // Deprecated: it was introduced for refactoring some legacy JS code, it should not be used in new code anymore. + PageData map[string]any Cache cache.StringCache Flash *middleware.Flash diff --git a/services/context/context_template.go b/services/context/context_template.go index b4775171eb9..2d990b44add 100644 --- a/services/context/context_template.go +++ b/services/context/context_template.go @@ -13,11 +13,11 @@ import ( "time" user_model "gitea.dev/models/user" - "gitea.dev/modules/htmlutil" "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" ) @@ -148,5 +148,41 @@ func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML { if csp == "" { return "" } - return htmlutil.HTMLFormat(``, csp) + return template.HTML(``) +} + +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"), + }, + } } diff --git a/services/context/context_test.go b/services/context/context_test.go index f6e34619761..8e56007347c 100644 --- a/services/context/context_test.go +++ b/services/context/context_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" "gitea.dev/modules/reqctx" @@ -64,3 +65,12 @@ func TestAppFullLink(t *testing.T) { 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"))) } + +func TestHeadMetaContentSecurityPolicy(t *testing.T) { + tmplCtx := NewTemplateContext(reqctx.NewRequestContextForTest(t), nil) + nonce := tmplCtx.CspScriptNonce() + assert.Equal(t, ``, string(tmplCtx.HeadMetaContentSecurityPolicy())) + assert.False(t, strings.ContainsAny(WebContentSecurityPolicy(nonce), `"<>&`)) + defer test.MockVariableValue(&setting.Security.ContentSecurityPolicyGeneral, "unset")() + assert.Empty(t, tmplCtx.HeadMetaContentSecurityPolicy()) +} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 7a918d8bdb1..d1605e97e72 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -6,29 +6,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* before our JS code gets loaded, use arrays to store errors, then the arrays will be switched to our error handler later */}} window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);}); window.addEventListener('unhandledrejection', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);}); - window.config = { - appUrl: '{{ctx.AppFullLink "/"}}', - appSubUrl: '{{AppSubUrl}}', - assetUrlPrefix: '{{AssetUrlPrefix}}', - runModeIsProd: {{.RunModeIsProd}}, - customEmojis: {{CustomEmojis}}, - pageData: {{.PageData}}, - notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}} - enableTimeTracking: {{EnableTimetracking}}, - mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, - sharedWorkerUri: '{{AssetURI "web_src/js/user-events.sharedworker.ts"}}', - {{/* 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 */}} - i18n: { - error_occurred: {{ctx.Locale.Tr "error.occurred"}}, - remove_label_str: {{ctx.Locale.Tr "remove_label_str"}}, - modal_confirm: {{ctx.Locale.Tr "modal.confirm"}}, - modal_cancel: {{ctx.Locale.Tr "modal.cancel"}}, - more_items: {{ctx.Locale.Tr "more_items"}}, - copy_success: {{ctx.Locale.Tr "copy_success"}}, - copy_error: {{ctx.Locale.Tr "copy_error"}}, - }, - }; - {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} - window.config.pageData = window.config.pageData || {}; + {{ctx.ScriptImport "web_src/js/iife.ts"}} diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index f88f4900637..191279685db 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -1,15 +1,18 @@ -// DO NOT IMPORT window.config HERE! -// to make sure the error handler always works, we should never import `window.config`, because -// some user's custom template breaks it. import {showGlobalErrorMessage, processWindowErrorEvent} from './modules/errors.ts'; +// window.config is initialized here +try { + window.config = JSON.parse(document.querySelector('#global-window-config')!.textContent); + // in case some pages don't render the pageData, we make sure it is an object to prevent null access + window.config.pageData ??= {}; +} catch { + showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`); +} + // A module should not be imported twice, otherwise there will be bugs when a module has its internal states. // A real example is "generateElemId" in "utils/dom.ts", if it is imported twice in different module scopes, // It will generate duplicate IDs (ps: don't try to use "random" to fix, it is just a real example to show the importance of "do not import a module twice") if (!window._globalHandlerErrors?._inited) { - if (!window.config) { - showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`); - } // we added an event handler for window error at the very beginning of