diff --git a/modules/setting/service.go b/modules/setting/service.go
index 5717225578..7e43ebc5d4 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -288,6 +288,10 @@ func loadServiceFrom(rootCfg ConfigProvider) {
}
}
Service.ValidSiteURLSchemes = schemes
+
+ // A pattern from ValidSiteURLSchemes must be valid for use in HTML validation
+ _ = regexp.MustCompile(`^(?:` + ValidSiteURLPattern() + `)$`)
+
Service.UsernameCooldownPeriod = sec.Key("USERNAME_COOLDOWN_PERIOD").MustInt64(0)
// Only set a default if USERNAME_COOLDOWN_PERIOD's feature is active.
@@ -302,6 +306,26 @@ func loadServiceFrom(rootCfg ConfigProvider) {
loadOpenIDSetting(rootCfg)
}
+// Returns a regex pattern string based on the current value of
+// `Service.ValidSiteURLSchemes`.
+//
+// This pattern string is meant to be used as the value of an element's
+// `pattern` attribute. As such, this function assumes that the pattern will be
+// implicitly wrapped with `^(?:` and `)$`, such that the match is required
+// against the entire input value, i.e., `^(?:)$`.
+//
+// See [MDN] for more details.
+//
+// [MDN]: https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/input#pattern
+func ValidSiteURLPattern() string {
+ // While technically possible, returning a compiled Regexp from this seems heavy-handed.
+ // We could store a compiled Regexp instead of generating on the fly, but that complicates testing!
+ // Much easier to simply edit `Service.ValidSiteURLSchemes for a given test and proceed.
+ // We run the compiler against the initial config value anyway, so this string always works in production.
+ schemes := strings.Join(Service.ValidSiteURLSchemes, "|")
+ return `(` + schemes + `)://.+`
+}
+
func loadOpenIDSetting(rootCfg ConfigProvider) {
sec := rootCfg.Section("openid")
Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)
diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json
index 39f98ef7a9..03a7ab28a4 100644
--- a/options/locale_next/locale_en-US.json
+++ b/options/locale_next/locale_en-US.json
@@ -937,6 +937,7 @@
"markup.filepreview.lines": "Lines %[1]d to %[2]d in %[3]s",
"markup.filepreview.truncated": "Preview has been truncated",
"form.RunnerName": "Name",
+ "form.website.valid_url_schemes": "Allowed URL schemes include: %s",
"graphs.recent_commits.title": "Number of commits in the past year",
"graphs.code_frequency.title": "Code frequency over the history of {0}",
"meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it."
diff --git a/services/context/context.go b/services/context/context.go
index d5c4f6fd2a..650b8e33e8 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -146,6 +146,8 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
ctx.Data["Link"] = ctx.Link
+ ctx.Data["ValidSiteURLSchemes"] = setting.Service.ValidSiteURLSchemes
+ ctx.Data["ValidSiteURLPattern"] = setting.ValidSiteURLPattern()
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
ctx.PageData = map[string]any{}
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index f18317e694..fe4c0bcaa7 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -97,7 +97,8 @@