mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 22:13:26 +09:00
enhance: move window.config to JSON, improve CSP format (#39236)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -96,9 +96,6 @@ func newFuncMapWebPage() template.FuncMap {
|
|||||||
"AllowedReactions": func() []string {
|
"AllowedReactions": func() []string {
|
||||||
return setting.UI.Reactions
|
return setting.UI.Reactions
|
||||||
},
|
},
|
||||||
"CustomEmojis": func() map[string]string {
|
|
||||||
return setting.UI.CustomEmojisMap
|
|
||||||
},
|
|
||||||
"MetaAuthor": func() string {
|
"MetaAuthor": func() string {
|
||||||
return setting.UI.Meta.Author
|
return setting.UI.Meta.Author
|
||||||
},
|
},
|
||||||
@@ -114,16 +111,6 @@ func newFuncMapWebPage() template.FuncMap {
|
|||||||
"DisableWebhooks": func() bool {
|
"DisableWebhooks": func() bool {
|
||||||
return setting.DisableWebhooks
|
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
|
// render
|
||||||
|
|||||||
@@ -46,8 +46,11 @@ type Context struct {
|
|||||||
|
|
||||||
TemplateContext TemplateContext
|
TemplateContext TemplateContext
|
||||||
|
|
||||||
Render Render
|
Render Render
|
||||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
|
||||||
|
// 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
|
Cache cache.StringCache
|
||||||
Flash *middleware.Flash
|
Flash *middleware.Flash
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/htmlutil"
|
|
||||||
"gitea.dev/modules/httplib"
|
"gitea.dev/modules/httplib"
|
||||||
"gitea.dev/modules/public"
|
"gitea.dev/modules/public"
|
||||||
"gitea.dev/modules/reqctx"
|
"gitea.dev/modules/reqctx"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/translation"
|
||||||
"gitea.dev/modules/web/middleware"
|
"gitea.dev/modules/web/middleware"
|
||||||
"gitea.dev/services/webtheme"
|
"gitea.dev/services/webtheme"
|
||||||
)
|
)
|
||||||
@@ -148,5 +148,41 @@ func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
|
|||||||
if csp == "" {
|
if csp == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return htmlutil.HTMLFormat(`<meta http-equiv="Content-Security-Policy" content="%s">`, csp)
|
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"),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.dev/modules/reqctx"
|
"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")))
|
||||||
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, `<meta http-equiv="Content-Security-Policy" content="default-src * data: blob:;script-src * 'nonce-`+nonce+`';style-src * 'unsafe-inline';">`, string(tmplCtx.HeadMetaContentSecurityPolicy()))
|
||||||
|
assert.False(t, strings.ContainsAny(WebContentSecurityPolicy(nonce), `"<>&`))
|
||||||
|
defer test.MockVariableValue(&setting.Security.ContentSecurityPolicyGeneral, "unset")()
|
||||||
|
assert.Empty(t, tmplCtx.HeadMetaContentSecurityPolicy())
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 */}}
|
{{/* 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('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.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 || {};
|
|
||||||
</script>
|
</script>
|
||||||
|
<script type="application/json" id="global-window-config">{{ctx.WindowConfig}}</script>
|
||||||
{{ctx.ScriptImport "web_src/js/iife.ts"}}
|
{{ctx.ScriptImport "web_src/js/iife.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';
|
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 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,
|
// 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")
|
// 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._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 <script> of page head the
|
// we added an event handler for window error at the very beginning of <script> of page head the
|
||||||
// handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before
|
// handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before
|
||||||
// this init then in this init, we can collect all error events and show them.
|
// this init then in this init, we can collect all error events and show them.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// This file is the entry point for the code which should block the page rendering, it is compiled by our "iife" vite plugin
|
// This file is the entry point for the code which should block the page rendering, it is compiled by our "iife" vite plugin
|
||||||
|
|
||||||
// bootstrap module must be the first one to be imported, it handles global errors
|
// bootstrap module must be the first one to be imported, it handles global config and errors
|
||||||
import './bootstrap.ts';
|
import './bootstrap.ts';
|
||||||
|
|
||||||
// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml)
|
// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml)
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import {html} from '../utils/html.ts';
|
|||||||
import isNetworkError from 'is-network-error';
|
import isNetworkError from 'is-network-error';
|
||||||
import type {Intent} from '../types.ts';
|
import type {Intent} from '../types.ts';
|
||||||
|
|
||||||
|
// The code in this module might be executed before window.config is initialized,
|
||||||
|
// Don't access window.config directly.
|
||||||
|
function windowConfig(): typeof window.config | undefined {
|
||||||
|
return window.config;
|
||||||
|
}
|
||||||
|
|
||||||
/** Extract a message string from an unknown caught value. */
|
/** Extract a message string from an unknown caught value. */
|
||||||
export function errorMessage(err: unknown): string {
|
export function errorMessage(err: unknown): string {
|
||||||
return (err as Error)?.message || String(err);
|
return (err as Error)?.message || String(err);
|
||||||
@@ -50,7 +56,7 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error', d
|
|||||||
const extensionRe = /(chrome|moz|safari(-web)?)-extension:\/\//;
|
const extensionRe = /(chrome|moz|safari(-web)?)-extension:\/\//;
|
||||||
export function isGiteaError(filename: string, stack: string): boolean {
|
export function isGiteaError(filename: string, stack: string): boolean {
|
||||||
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
|
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
|
||||||
const assetBaseUrl = new URL(`${window.config.assetUrlPrefix}/`, window.location.origin).href;
|
const assetBaseUrl = new URL(`${windowConfig()?.assetUrlPrefix}/`, window.location.origin).href;
|
||||||
if (filename && !filename.startsWith(assetBaseUrl) && !filename.startsWith(window.location.origin)) return false;
|
if (filename && !filename.startsWith(assetBaseUrl) && !filename.startsWith(window.location.origin)) return false;
|
||||||
return !stack || stack.includes(assetBaseUrl);
|
return !stack || stack.includes(assetBaseUrl);
|
||||||
}
|
}
|
||||||
@@ -64,7 +70,7 @@ export function processWindowErrorEvent({error, reason, message, type, filename,
|
|||||||
// - https://github.com/go-gitea/gitea/issues/20240
|
// - https://github.com/go-gitea/gitea/issues/20240
|
||||||
if (!err) {
|
if (!err) {
|
||||||
if (message) console.error(new Error(message));
|
if (message) console.error(new Error(message));
|
||||||
if (window.config.runModeIsProd) return;
|
if (windowConfig()?.runModeIsProd) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show network errors, happens on ref-issue when clicking on the
|
// Don't show network errors, happens on ref-issue when clicking on the
|
||||||
|
|||||||
Reference in New Issue
Block a user