mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-27 00:04:32 +00:00
fix: prevent custom branding flash on initial load
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -209,12 +210,64 @@ func (s *FrontendServer) injectSettings(settingsJSON []byte) []byte {
|
||||
headClose := []byte("</head>")
|
||||
result := bytes.Replace(s.baseHTML, headClose, append(script, headClose...), 1)
|
||||
|
||||
// Replace <title> with custom site name so the browser tab shows it immediately
|
||||
// Apply custom branding before the browser paints the static defaults.
|
||||
result = injectSiteTitle(result, settingsJSON)
|
||||
result = injectSiteFavicon(result, settingsJSON)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// injectSiteFavicon replaces the static favicon with a configured, browser-safe image URL.
|
||||
func injectSiteFavicon(html, settingsJSON []byte) []byte {
|
||||
var cfg struct {
|
||||
SiteLogo string `json:"site_logo"`
|
||||
}
|
||||
if err := json.Unmarshal(settingsJSON, &cfg); err != nil {
|
||||
return html
|
||||
}
|
||||
|
||||
logoURL := safeImageURL(cfg.SiteLogo)
|
||||
if logoURL == "" {
|
||||
return html
|
||||
}
|
||||
|
||||
linkStart := bytes.Index(html, []byte(`<link rel="icon"`))
|
||||
if linkStart == -1 {
|
||||
return html
|
||||
}
|
||||
linkEndOffset := bytes.IndexByte(html[linkStart:], '>')
|
||||
if linkEndOffset == -1 {
|
||||
return html
|
||||
}
|
||||
linkEnd := linkStart + linkEndOffset + 1
|
||||
replacement := []byte(`<link rel="icon" href="` + htmlpkg.EscapeString(logoURL) + `" />`)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.Write(html[:linkStart])
|
||||
buf.Write(replacement)
|
||||
buf.Write(html[linkEnd:])
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func safeImageURL(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "/") && !strings.HasPrefix(trimmed, "//") {
|
||||
return trimmed
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(trimmed), "data:image/") {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
|
||||
return ""
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// injectSiteTitle replaces the static <title> in HTML with the configured site name.
|
||||
// This ensures the browser tab shows the correct title before JS executes.
|
||||
func injectSiteTitle(html, settingsJSON []byte) []byte {
|
||||
|
||||
@@ -114,6 +114,41 @@ func TestInjectSiteTitle(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestInjectSiteFavicon(t *testing.T) {
|
||||
t.Run("replaces_favicon_with_site_logo", func(t *testing.T) {
|
||||
html := []byte(`<html><head><link rel="icon" type="image/png" href="/logo.png" /></head></html>`)
|
||||
settingsJSON := []byte(`{"site_logo":"https://example.com/custom-logo.png"}`)
|
||||
|
||||
result := injectSiteFavicon(html, settingsJSON)
|
||||
|
||||
assert.Contains(t, string(result), `<link rel="icon" href="https://example.com/custom-logo.png" />`)
|
||||
assert.NotContains(t, string(result), `/logo.png`)
|
||||
})
|
||||
|
||||
t.Run("supports_relative_and_data_image_urls", func(t *testing.T) {
|
||||
html := []byte(`<link rel="icon" href="/logo.png" />`)
|
||||
|
||||
assert.Contains(t, string(injectSiteFavicon(html, []byte(`{"site_logo":"/uploads/logo.svg"}`))), `/uploads/logo.svg`)
|
||||
assert.Contains(t, string(injectSiteFavicon(html, []byte(`{"site_logo":"data:image/png;base64,abc"}`))), `data:image/png;base64,abc`)
|
||||
})
|
||||
|
||||
t.Run("rejects_unsafe_logo_urls", func(t *testing.T) {
|
||||
html := []byte(`<link rel="icon" href="/logo.png" />`)
|
||||
|
||||
result := injectSiteFavicon(html, []byte(`{"site_logo":"javascript:alert(1)"}`))
|
||||
|
||||
assert.Equal(t, string(html), string(result))
|
||||
})
|
||||
|
||||
t.Run("escapes_logo_url_for_html", func(t *testing.T) {
|
||||
html := []byte(`<link rel="icon" href="/logo.png" />`)
|
||||
|
||||
result := injectSiteFavicon(html, []byte(`{"site_logo":"https://example.com/logo.png?a=1&b=2"}`))
|
||||
|
||||
assert.Contains(t, string(result), `a=1&b=2`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceNoncePlaceholder(t *testing.T) {
|
||||
t.Run("replaces_single_placeholder", func(t *testing.T) {
|
||||
html := []byte(`<script nonce="__CSP_NONCE_VALUE__">console.log('test');</script>`)
|
||||
|
||||
Reference in New Issue
Block a user