Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions services/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 38 additions & 2 deletions services/context/context_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -148,5 +148,41 @@ func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
if csp == "" {
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"),
},
}
}
10 changes: 10 additions & 0 deletions services/context/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"gitea.dev/modules/reqctx"
Expand Down Expand Up @@ -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, `<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())
}
25 changes: 1 addition & 24 deletions templates/base/head_script.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {};
</script>
<script type="application/json" id="global-window-config">{{ctx.WindowConfig}}</script>
{{ctx.ScriptImport "web_src/js/iife.ts"}}
15 changes: 9 additions & 6 deletions web_src/js/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -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 <script> of page head the
// 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.
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/iife.ts
Original file line number Diff line number Diff line change
@@ -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

// 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';

// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml)
Expand Down
10 changes: 8 additions & 2 deletions web_src/js/modules/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import {html} from '../utils/html.ts';
import isNetworkError from 'is-network-error';
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;
}

Comment thread
silverwind marked this conversation as resolved.
/** Extract a message string from an unknown caught value. */
export function errorMessage(err: unknown): string {
return (err as Error)?.message || String(err);
Expand Down Expand Up @@ -50,7 +56,7 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error', d
const extensionRe = /(chrome|moz|safari(-web)?)-extension:\/\//;
export function isGiteaError(filename: string, stack: string): boolean {
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;
return !stack || stack.includes(assetBaseUrl);
}
Expand All @@ -64,7 +70,7 @@ export function processWindowErrorEvent({error, reason, message, type, filename,
// - https://github.com/go-gitea/gitea/issues/20240
if (!err) {
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
Expand Down
Loading