From 3d369bb014d7dff01677c72a71ff5b46d6c5f925 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 2 Mar 2022 19:00:26 +0800
Subject: [PATCH 01/11] Show messages for users if the ROOT_URL is wrong

---
 templates/base/footer.tmpl           |  2 +-
 templates/base/head.tmpl             |  1 +
 web_src/js/features/common-global.js | 27 ++++++++++++++++++++++++++-
 web_src/js/index.js                  |  3 +++
 4 files changed, 31 insertions(+), 2 deletions(-)

diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl
index 1aabfa2f5c644..9bf16f8aa5b55 100644
--- a/templates/base/footer.tmpl
+++ b/templates/base/footer.tmpl
@@ -22,7 +22,7 @@
 		<script src='https://hcaptcha.com/1/api.js' async></script>
 	{{end}}
 {{end}}
-	<script src="{{AssetUrlPrefix}}/js/index.js?v={{MD5 AppVer}}"></script>
+	<script src="{{AssetUrlPrefix}}/js/index.js?v={{MD5 AppVer}}" onerror="alert('Failed to load asset files from ' + this.src + ', please make sure the asset files can be accessed and the ROOT_URL setting in app.ini is correct.')"></script>
 {{template "custom/footer" .}}
 </body>
 </html>
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 32e206a95d58e..8c2f10051a4ad 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -19,6 +19,7 @@
 		<!-- /* eslint-disable */ -->
 		window.config = {
 			appVer: '{{AppVer}}',
+			appUrl: '{{AppUrl}}',
 			appSubUrl: '{{AppSubUrl}}',
 			assetUrlPrefix: '{{AssetUrlPrefix}}',
 			runModeIsProd: {{.RunModeIsProd}},
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index a9baf9be0c683..eab213a395396 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -3,8 +3,9 @@ import 'jquery.are-you-sure';
 import {mqBinarySearch} from '../utils.js';
 import createDropzone from './dropzone.js';
 import {initCompColorPicker} from './comp/ColorPicker.js';
+import {htmlEscape} from 'escape-goat';
 
-const {csrfToken} = window.config;
+const {appUrl, csrfToken} = window.config;
 
 export function initGlobalFormDirtyLeaveConfirm() {
   // Warn users that try to leave a page after entering data into a form.
@@ -343,3 +344,27 @@ export function initGlobalButtons() {
     });
   });
 }
+
+/**
+ * Too many users set their ROOT_URL to wrong value, and it causes a lot of problems:
+ *   * Cross-origin API request without correct cookie
+ *   * Incorrect href in <a>
+ *   * ...
+ * So we check whether current URL starts with AppUrl(ROOT_URL).
+ * If they don't match, show a warning to users.
+ */
+export function checkAppUrl() {
+  const curUrl = window.location.href;
+  if (curUrl.startsWith(appUrl)) {
+    return;
+  }
+  const $pageContent = $('.page-content');
+  if (!$pageContent.length) {
+    return;
+  }
+  const $tip = $(`<div class="ui container negative message center aligned">
+    Your ROOT_URL in app.ini is ${htmlEscape(appUrl)} but you are visiting ${htmlEscape(curUrl)}<br />
+    You should set ROOT_URL correctly, otherwise the web may not work correctly.
+  </div>`);
+  $($pageContent[0]).prepend($tip);
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index b7eba5e6649a4..64c485d283219 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -39,6 +39,7 @@ import {
 } from './features/repo-issue.js';
 import {initRepoEllipsisButton, initRepoCommitLastCommitLoader} from './features/repo-commit.js';
 import {
+  checkAppUrl,
   initFootLanguageMenu,
   initGlobalButtonClickOnEnter,
   initGlobalButtons,
@@ -169,4 +170,6 @@ $(document).ready(() => {
   initUserAuthWebAuthn();
   initUserAuthWebAuthnRegister();
   initUserSettings();
+
+  checkAppUrl();
 });

From 94e6862d0519a19a60f437e9d95668425cf5f40f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 16:45:58 +0800
Subject: [PATCH 02/11] add global error handler

---
 templates/base/head.tmpl             |  1 +
 web_src/js/features/common-global.js | 33 ++++++++++++++++++++--------
 web_src/js/index.js                  |  3 ++-
 3 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 8c2f10051a4ad..4b294a6a2c37c 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -17,6 +17,7 @@
 {{end}}
 	<script>
 		<!-- /* eslint-disable */ -->
+		window.addEventListener('error', function(e) {window._globalHandlerErrors = window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
 		window.config = {
 			appVer: '{{AppVer}}',
 			appUrl: '{{AppUrl}}',
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index eab213a395396..64143934aecd1 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -345,6 +345,26 @@ export function initGlobalButtons() {
   });
 }
 
+function showErrorMessageHtml(msgHtml) {
+  const $pageContent = $('.page-content');
+  if (!$pageContent.length) {
+    return;
+  }
+  const $tip = $(`<div class="ui container negative message center aligned">${msgHtml}</div>`);
+  $($pageContent[0]).prepend($tip);
+}
+
+function processWindowErrorEvent(e) {
+  showErrorMessageHtml(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
+}
+
+export function initGlobalErrorHandler() {
+  for (const e of window._globalHandlerErrors || []) {
+    processWindowErrorEvent(e);
+  }
+  window._globalHandlerErrors = {'push': (e) => processWindowErrorEvent(e)};
+}
+
 /**
  * Too many users set their ROOT_URL to wrong value, and it causes a lot of problems:
  *   * Cross-origin API request without correct cookie
@@ -358,13 +378,8 @@ export function checkAppUrl() {
   if (curUrl.startsWith(appUrl)) {
     return;
   }
-  const $pageContent = $('.page-content');
-  if (!$pageContent.length) {
-    return;
-  }
-  const $tip = $(`<div class="ui container negative message center aligned">
-    Your ROOT_URL in app.ini is ${htmlEscape(appUrl)} but you are visiting ${htmlEscape(curUrl)}<br />
-    You should set ROOT_URL correctly, otherwise the web may not work correctly.
-  </div>`);
-  $($pageContent[0]).prepend($tip);
+  showErrorMessageHtml(`
+Your ROOT_URL in app.ini is ${htmlEscape(appUrl)} but you are visiting ${htmlEscape(curUrl)}<br />
+You should set ROOT_URL correctly, otherwise the web may not work correctly.
+`);
 }
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 64c485d283219..60613c8b2231a 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -45,7 +45,7 @@ import {
   initGlobalButtons,
   initGlobalCommon,
   initGlobalDropzone,
-  initGlobalEnterQuickSubmit,
+  initGlobalEnterQuickSubmit, initGlobalErrorHandler,
   initGlobalFormDirtyLeaveConfirm,
   initGlobalLinkActions,
   initHeadNavbarContentToggle,
@@ -85,6 +85,7 @@ $.fn.checkbox.settings.enableEnterKey = false;
 initVueEnv();
 
 $(document).ready(() => {
+  initGlobalErrorHandler();
   initGlobalCommon();
 
   initGlobalButtonClickOnEnter();

From 223900ef35578d56b392a9a013f864debd686e14 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 17:04:18 +0800
Subject: [PATCH 03/11] add comments

---
 templates/base/head.tmpl             | 2 +-
 web_src/js/features/common-global.js | 7 +++++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index b7d4917e44b1c..8101f9aa6b754 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -21,7 +21,7 @@
 {{end}}
 	<script>
 		<!-- /* eslint-disable */ -->
-		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.config = {
 			appVer: '{{AppVer}}',
 			appUrl: '{{AppUrl}}',
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 64143934aecd1..09bf2d3fb11d6 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -354,14 +354,21 @@ function showErrorMessageHtml(msgHtml) {
   $($pageContent[0]).prepend($tip);
 }
 
+/**
+ * @param {ErrorEvent} e
+ */
 function processWindowErrorEvent(e) {
   showErrorMessageHtml(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
 }
 
 export function initGlobalErrorHandler() {
+  // we added an event handler for window error at the very beginning of head.tmpl
+  // 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
   for (const e of window._globalHandlerErrors || []) {
     processWindowErrorEvent(e);
   }
+  // then, change _globalHandlerErrors to an object with push method, to process further error events directly
   window._globalHandlerErrors = {'push': (e) => processWindowErrorEvent(e)};
 }
 

From 7228a3a86b84cd12324d45295365a50628f879a9 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 17:15:26 +0800
Subject: [PATCH 04/11] use specialized CSS class "js-global-error", end users
 still have a chance to hide error messages by customized CSS styles.

---
 web_src/js/features/common-global.js | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 09bf2d3fb11d6..6acaf1214706f 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -345,12 +345,13 @@ export function initGlobalButtons() {
   });
 }
 
-function showErrorMessageHtml(msgHtml) {
+function showGlobalErrorMessageHtml(msgHtml) {
   const $pageContent = $('.page-content');
   if (!$pageContent.length) {
     return;
   }
-  const $tip = $(`<div class="ui container negative message center aligned">${msgHtml}</div>`);
+  // here we use a specialized CSS class "js-global-error", then end users still have a chance to hide these error messages by customized CSS styles.
+  const $tip = $(`<div class="ui container negative message center aligned js-global-error">${msgHtml}</div>`);
   $($pageContent[0]).prepend($tip);
 }
 
@@ -358,7 +359,7 @@ function showErrorMessageHtml(msgHtml) {
  * @param {ErrorEvent} e
  */
 function processWindowErrorEvent(e) {
-  showErrorMessageHtml(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
+  showGlobalErrorMessageHtml(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
 }
 
 export function initGlobalErrorHandler() {
@@ -385,7 +386,7 @@ export function checkAppUrl() {
   if (curUrl.startsWith(appUrl)) {
     return;
   }
-  showErrorMessageHtml(`
+  showGlobalErrorMessageHtml(`
 Your ROOT_URL in app.ini is ${htmlEscape(appUrl)} but you are visiting ${htmlEscape(curUrl)}<br />
 You should set ROOT_URL correctly, otherwise the web may not work correctly.
 `);

From 8f42b72d7a835b744e43f296169994182a02f905 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 18:00:16 +0800
Subject: [PATCH 05/11] make sure error handler can always be executed

---
 web_src/js/bootstrap.js              | 40 ++++++++++++++++++++++++++++
 web_src/js/features/common-global.js | 29 +-------------------
 web_src/js/index.js                  |  7 +++--
 web_src/js/publicpath.js             |  6 -----
 4 files changed, 44 insertions(+), 38 deletions(-)
 create mode 100644 web_src/js/bootstrap.js
 delete mode 100644 web_src/js/publicpath.js

diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
new file mode 100644
index 0000000000000..b8db8b8290015
--- /dev/null
+++ b/web_src/js/bootstrap.js
@@ -0,0 +1,40 @@
+import {joinPaths} from './utils.js';
+
+// 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.
+
+// This sets up the URL prefix used in webpack's chunk loading.
+// This file must be imported before any lazy-loading is being attempted.
+__webpack_public_path__ = joinPaths(window?.config?.assetUrlPrefix ?? '/', '/');
+
+export function showGlobalErrorMessageHtml(msgHtml) {
+  const pageContent = document.querySelector('.page-content');
+  if (!pageContent) return;
+  const el = document.createElement('div');
+  el.innerHTML = `<div class="ui container negative message center aligned js-global-error">${msgHtml}</div>`;
+  pageContent.prepend(el.childNodes[0]);
+}
+
+/**
+ * @param {ErrorEvent} e
+ */
+function processWindowErrorEvent(e) {
+  showGlobalErrorMessageHtml(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
+}
+
+function initGlobalErrorHandler() {
+  if (!window.config) {
+    showGlobalErrorMessageHtml(`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 head.tmpl
+  // 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
+  for (const e of window._globalHandlerErrors || []) {
+    processWindowErrorEvent(e);
+  }
+  // then, change _globalHandlerErrors to an object with push method, to process further error events directly
+  window._globalHandlerErrors = {'push': (e) => processWindowErrorEvent(e)};
+}
+
+initGlobalErrorHandler();
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 6acaf1214706f..240bd78685149 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -4,6 +4,7 @@ import {mqBinarySearch} from '../utils.js';
 import createDropzone from './dropzone.js';
 import {initCompColorPicker} from './comp/ColorPicker.js';
 import {htmlEscape} from 'escape-goat';
+import {showGlobalErrorMessageHtml} from '../bootstrap.js';
 
 const {appUrl, csrfToken} = window.config;
 
@@ -345,34 +346,6 @@ export function initGlobalButtons() {
   });
 }
 
-function showGlobalErrorMessageHtml(msgHtml) {
-  const $pageContent = $('.page-content');
-  if (!$pageContent.length) {
-    return;
-  }
-  // here we use a specialized CSS class "js-global-error", then end users still have a chance to hide these error messages by customized CSS styles.
-  const $tip = $(`<div class="ui container negative message center aligned js-global-error">${msgHtml}</div>`);
-  $($pageContent[0]).prepend($tip);
-}
-
-/**
- * @param {ErrorEvent} e
- */
-function processWindowErrorEvent(e) {
-  showGlobalErrorMessageHtml(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
-}
-
-export function initGlobalErrorHandler() {
-  // we added an event handler for window error at the very beginning of head.tmpl
-  // 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
-  for (const e of window._globalHandlerErrors || []) {
-    processWindowErrorEvent(e);
-  }
-  // then, change _globalHandlerErrors to an object with push method, to process further error events directly
-  window._globalHandlerErrors = {'push': (e) => processWindowErrorEvent(e)};
-}
-
 /**
  * Too many users set their ROOT_URL to wrong value, and it causes a lot of problems:
  *   * Cross-origin API request without correct cookie
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 60613c8b2231a..18b949e4e62f7 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -1,4 +1,5 @@
-import './publicpath.js';
+// bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors
+import './bootstrap.js';
 
 import $ from 'jquery';
 import {initVueEnv} from './components/VueComponentLoader.js';
@@ -45,7 +46,7 @@ import {
   initGlobalButtons,
   initGlobalCommon,
   initGlobalDropzone,
-  initGlobalEnterQuickSubmit, initGlobalErrorHandler,
+  initGlobalEnterQuickSubmit,
   initGlobalFormDirtyLeaveConfirm,
   initGlobalLinkActions,
   initHeadNavbarContentToggle,
@@ -83,9 +84,7 @@ $.fn.tab.settings.silent = true;
 $.fn.checkbox.settings.enableEnterKey = false;
 
 initVueEnv();
-
 $(document).ready(() => {
-  initGlobalErrorHandler();
   initGlobalCommon();
 
   initGlobalButtonClickOnEnter();
diff --git a/web_src/js/publicpath.js b/web_src/js/publicpath.js
deleted file mode 100644
index 44448a8447c49..0000000000000
--- a/web_src/js/publicpath.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// This sets up the URL prefix used in webpack's chunk loading.
-// This file must be imported before any lazy-loading is being attempted.
-import {joinPaths} from './utils.js';
-const {assetUrlPrefix} = window.config;
-
-__webpack_public_path__ = joinPaths(assetUrlPrefix, '/');

From dd95b554cdeca21801355709bcdd2b03da2f4980 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 18:07:56 +0800
Subject: [PATCH 06/11] Some people like to modify the `head.tmpl`, so we
 separate the script part, then it's much safer.

---
 templates/base/head.tmpl        | 47 +++------------------------------
 templates/base/head_script.tmpl | 44 ++++++++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 44 deletions(-)
 create mode 100644 templates/base/head_script.tmpl

diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 8101f9aa6b754..35157e9b954b4 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -19,53 +19,12 @@
 	<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
 	<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
 {{end}}
-	<script>
-		<!-- /* eslint-disable */ -->
-		window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
-		window.config = {
-			appVer: '{{AppVer}}',
-			appUrl: '{{AppUrl}}',
-			appSubUrl: '{{AppSubUrl}}',
-			assetUrlPrefix: '{{AssetUrlPrefix}}',
-			runModeIsProd: {{.RunModeIsProd}},
-			customEmojis: {{CustomEmojis}},
-			useServiceWorker: {{UseServiceWorker}},
-			csrfToken: '{{.CsrfToken}}',
-			pageData: {{.PageData}},
-			requireTribute: {{.RequireTribute}},
-			notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}}
-			enableTimeTracking: {{EnableTimetracking}},
-			{{if .RequireTribute}}
-			tributeValues: Array.from(new Map([
-				{{ range .Participants }}
-				['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
-				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}],
-				{{ end }}
-				{{ range .Assignees }}
-				['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
-				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}],
-				{{ end }}
-				{{ range .MentionableTeams }}
-					['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}',
-					name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvatar}}'}],
-				{{ end }}
-			]).values()),
-			{{end}}
-			mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
-			{{/* 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: {
-				copy_success: '{{.i18n.Tr "copy_success"}}',
-				copy_error: '{{.i18n.Tr "copy_error"}}',
-				error_occurred: '{{.i18n.Tr "error.occurred"}}',
-				network_error: '{{.i18n.Tr "error.network_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>
 	<link rel="icon" href="{{AssetUrlPrefix}}/img/logo.svg" type="image/svg+xml">
 	<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
 	<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/index.css?v={{MD5 AppVer}}">
+
+	{{template "base/head_script" .}}
+
 	<noscript>
 		<style>
 			.dropdown:hover > .menu { display: block; }
diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl
new file mode 100644
index 0000000000000..4e4f3ea319c6b
--- /dev/null
+++ b/templates/base/head_script.tmpl
@@ -0,0 +1,44 @@
+<script>
+	<!-- /* eslint-disable */ -->
+	window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
+	window.config = {
+		appVer: '{{AppVer}}',
+		appUrl: '{{AppUrl}}',
+		appSubUrl: '{{AppSubUrl}}',
+		assetUrlPrefix: '{{AssetUrlPrefix}}',
+		runModeIsProd: {{.RunModeIsProd}},
+		customEmojis: {{CustomEmojis}},
+		useServiceWorker: {{UseServiceWorker}},
+		csrfToken: '{{.CsrfToken}}',
+		pageData: {{.PageData}},
+		requireTribute: {{.RequireTribute}},
+		notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}}
+		enableTimeTracking: {{EnableTimetracking}},
+      {{if .RequireTribute}}
+		tributeValues: Array.from(new Map([
+        {{ range .Participants }}
+			['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
+				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}],
+        {{ end }}
+        {{ range .Assignees }}
+			['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
+				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}],
+        {{ end }}
+        {{ range .MentionableTeams }}
+			['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}',
+				name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvatar}}'}],
+        {{ end }}
+		]).values()),
+      {{end}}
+		mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
+      {{/* 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: {
+			copy_success: '{{.i18n.Tr "copy_success"}}',
+			copy_error: '{{.i18n.Tr "copy_error"}}',
+			error_occurred: '{{.i18n.Tr "error.occurred"}}',
+			network_error: '{{.i18n.Tr "error.network_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>

From 1dfbe4db72b73471b349b829a0354e65f83b5ea2 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 18:11:59 +0800
Subject: [PATCH 07/11] fix lint

---
 templates/base/head_script.tmpl | 27 +++++++++++++--------------
 1 file changed, 13 insertions(+), 14 deletions(-)

diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl
index 4e4f3ea319c6b..c853f8e60586a 100644
--- a/templates/base/head_script.tmpl
+++ b/templates/base/head_script.tmpl
@@ -3,7 +3,6 @@
 	window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
 	window.config = {
 		appVer: '{{AppVer}}',
-		appUrl: '{{AppUrl}}',
 		appSubUrl: '{{AppSubUrl}}',
 		assetUrlPrefix: '{{AssetUrlPrefix}}',
 		runModeIsProd: {{.RunModeIsProd}},
@@ -14,24 +13,24 @@
 		requireTribute: {{.RequireTribute}},
 		notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}}
 		enableTimeTracking: {{EnableTimetracking}},
-      {{if .RequireTribute}}
+		{{if .RequireTribute}}
 		tributeValues: Array.from(new Map([
-        {{ range .Participants }}
+			{{ range .Participants }}
 			['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
-				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}],
-        {{ end }}
-        {{ range .Assignees }}
+			name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}],
+			{{ end }}
+			{{ range .Assignees }}
 			['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
-				name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}],
-        {{ end }}
-        {{ range .MentionableTeams }}
-			['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}',
+			name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink}}'}],
+			{{ end }}
+			{{ range .MentionableTeams }}
+				['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}',
 				name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvatar}}'}],
-        {{ end }}
+			{{ end }}
 		]).values()),
-      {{end}}
+		{{end}}
 		mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
-      {{/* 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 */}}
+		{{/* 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: {
 			copy_success: '{{.i18n.Tr "copy_success"}}',
 			copy_error: '{{.i18n.Tr "copy_error"}}',
@@ -39,6 +38,6 @@
 			network_error: '{{.i18n.Tr "error.network_error"}}',
 		},
 	};
-  {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
+	{{/* 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>

From 48eaaee41c22ef135d94821f7b4d57afdd488047 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 18:16:55 +0800
Subject: [PATCH 08/11] Add ==== DO NOT EDIT ==== warning to head_script.tmpl

---
 templates/base/head_script.tmpl | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl
index c853f8e60586a..459b1151e5c92 100644
--- a/templates/base/head_script.tmpl
+++ b/templates/base/head_script.tmpl
@@ -1,3 +1,8 @@
+{{/*
+==== DO NOT EDIT ====
+If you are customizing Gitea, please do not change this file.
+If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
+*/}}
 <script>
 	<!-- /* eslint-disable */ -->
 	window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});

From e1236a9b7a7c71770837788f330a27a2f5356d2c Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 18:20:02 +0800
Subject: [PATCH 09/11] fix comment

---
 web_src/js/bootstrap.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
index b8db8b8290015..d0182d36706f5 100644
--- a/web_src/js/bootstrap.js
+++ b/web_src/js/bootstrap.js
@@ -27,7 +27,7 @@ function initGlobalErrorHandler() {
     showGlobalErrorMessageHtml(`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 head.tmpl
+  // 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
   for (const e of window._globalHandlerErrors || []) {

From 93446c6f90324d414173c93bb304423dd71571d5 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 18:52:41 +0800
Subject: [PATCH 10/11] add appUrl to window.config again

---
 templates/base/head_script.tmpl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl
index 459b1151e5c92..e6a8060a16f3e 100644
--- a/templates/base/head_script.tmpl
+++ b/templates/base/head_script.tmpl
@@ -8,6 +8,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
 	window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
 	window.config = {
 		appVer: '{{AppVer}}',
+		appUrl: '{{AppUrl}}',
 		appSubUrl: '{{AppSubUrl}}',
 		assetUrlPrefix: '{{AssetUrlPrefix}}',
 		runModeIsProd: {{.RunModeIsProd}},

From ce5a9b7238d1fe6e456c7c65dae9d3b1a42c8c1b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 29 Mar 2022 19:05:31 +0800
Subject: [PATCH 11/11] No necessary to support html msg, so just use pre-line
 to render new line.

---
 web_src/js/bootstrap.js              | 9 +++++----
 web_src/js/features/common-global.js | 9 +++------
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
index d0182d36706f5..cf13b2a559d30 100644
--- a/web_src/js/bootstrap.js
+++ b/web_src/js/bootstrap.js
@@ -7,11 +7,12 @@ import {joinPaths} from './utils.js';
 // This file must be imported before any lazy-loading is being attempted.
 __webpack_public_path__ = joinPaths(window?.config?.assetUrlPrefix ?? '/', '/');
 
-export function showGlobalErrorMessageHtml(msgHtml) {
+export function showGlobalErrorMessage(msg) {
   const pageContent = document.querySelector('.page-content');
   if (!pageContent) return;
   const el = document.createElement('div');
-  el.innerHTML = `<div class="ui container negative message center aligned js-global-error">${msgHtml}</div>`;
+  el.innerHTML = `<div class="ui container negative message center aligned js-global-error" style="white-space: pre-line;"></div>`;
+  el.childNodes[0].textContent = msg;
   pageContent.prepend(el.childNodes[0]);
 }
 
@@ -19,12 +20,12 @@ export function showGlobalErrorMessageHtml(msgHtml) {
  * @param {ErrorEvent} e
  */
 function processWindowErrorEvent(e) {
-  showGlobalErrorMessageHtml(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
+  showGlobalErrorMessage(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`);
 }
 
 function initGlobalErrorHandler() {
   if (!window.config) {
-    showGlobalErrorMessageHtml(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
+    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
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 240bd78685149..60af4d0d67e0e 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -3,8 +3,7 @@ import 'jquery.are-you-sure';
 import {mqBinarySearch} from '../utils.js';
 import createDropzone from './dropzone.js';
 import {initCompColorPicker} from './comp/ColorPicker.js';
-import {htmlEscape} from 'escape-goat';
-import {showGlobalErrorMessageHtml} from '../bootstrap.js';
+import {showGlobalErrorMessage} from '../bootstrap.js';
 
 const {appUrl, csrfToken} = window.config;
 
@@ -359,8 +358,6 @@ export function checkAppUrl() {
   if (curUrl.startsWith(appUrl)) {
     return;
   }
-  showGlobalErrorMessageHtml(`
-Your ROOT_URL in app.ini is ${htmlEscape(appUrl)} but you are visiting ${htmlEscape(curUrl)}<br />
-You should set ROOT_URL correctly, otherwise the web may not work correctly.
-`);
+  showGlobalErrorMessage(`Your ROOT_URL in app.ini is ${appUrl} but you are visiting ${curUrl}
+You should set ROOT_URL correctly, otherwise the web may not work correctly.`);
 }