mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-26 10:30:46 +00:00
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-4 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-2 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-3 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-5 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-6 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-7 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-8 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-9 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-10 (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
* fix: split html sanitization from plaintext handling Add a dedicated SanitizeHTML helper for HTML-rendered values so entity-encoded markup is decoded before bluemonday sanitization. Use the new helper for the login welcome message and artist biographies while preserving SanitizeText semantics for lyrics and other plaintext callers. Add regression coverage for both helpers and the serveIndex welcomeMessage path. * docs: add SanitizeText and SanitizeHTML godoc Signed-off-by: Deluan <deluan@navidrome.org> * fix: preserve plain text in artist biographies Revert artist biography storage to SanitizeText so entity-encoded plain text remains decoded for Subsonic consumers. This avoids double-escaping values like R&B in XML responses while keeping the new welcomeMessage HTML sanitization in place, and adds a regression test covering the biography storage behavior. --------- Signed-off-by: Deluan <deluan@navidrome.org>
188 lines
6.4 KiB
Go
188 lines
6.4 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/mime"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/utils/slice"
|
|
"github.com/navidrome/navidrome/utils/str"
|
|
)
|
|
|
|
func Index(ds model.DataStore, fs fs.FS) http.HandlerFunc {
|
|
return serveIndex(ds, fs, nil)
|
|
}
|
|
|
|
func IndexWithShare(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.HandlerFunc {
|
|
return serveIndex(ds, fs, shareInfo)
|
|
}
|
|
|
|
// Injects the config in the `index.html` template
|
|
func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
c, err := ds.User(r.Context()).CountAll()
|
|
firstTime := c == 0 && err == nil
|
|
|
|
t, err := getIndexTemplate(r, fs)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
appConfig := map[string]any{
|
|
"version": consts.Version,
|
|
"firstTime": firstTime,
|
|
"variousArtistsId": consts.VariousArtistsID,
|
|
"baseURL": str.SanitizeText(strings.TrimSuffix(conf.Server.BasePath, "/")),
|
|
"loginBackgroundURL": str.SanitizeText(conf.Server.UILoginBackgroundURL),
|
|
"welcomeMessage": str.SanitizeHTML(conf.Server.UIWelcomeMessage),
|
|
"maxSidebarPlaylists": conf.Server.MaxSidebarPlaylists,
|
|
"enableTranscodingConfig": conf.Server.EnableTranscodingConfig,
|
|
"enableDownloads": conf.Server.EnableDownloads,
|
|
"enableFavourites": conf.Server.EnableFavourites,
|
|
"enableStarRating": conf.Server.EnableStarRating,
|
|
"defaultTheme": conf.Server.DefaultTheme,
|
|
"defaultLanguage": conf.Server.DefaultLanguage,
|
|
"defaultUIVolume": conf.Server.DefaultUIVolume,
|
|
"uiSearchDebounceMs": conf.Server.UISearchDebounceMs,
|
|
"uiCoverArtSize": conf.Server.UICoverArtSize,
|
|
"enableCoverAnimation": conf.Server.EnableCoverAnimation,
|
|
"enableNowPlaying": conf.Server.EnableNowPlaying,
|
|
"gaTrackingId": conf.Server.GATrackingID,
|
|
"losslessFormats": strings.ToUpper(strings.Join(mime.LosslessFormats, ",")),
|
|
"devActivityPanel": conf.Server.DevActivityPanel,
|
|
"enableUserEditing": conf.Server.EnableUserEditing,
|
|
"enableArtworkUpload": conf.Server.EnableArtworkUpload,
|
|
"enableSharing": conf.Server.EnableSharing,
|
|
"shareURL": conf.Server.ShareURL,
|
|
"defaultDownloadableShare": conf.Server.DefaultDownloadableShare,
|
|
"devSidebarPlaylists": conf.Server.DevSidebarPlaylists,
|
|
"lastFMEnabled": conf.Server.LastFM.Enabled,
|
|
"devShowArtistPage": conf.Server.DevShowArtistPage,
|
|
"devUIShowConfig": conf.Server.DevUIShowConfig,
|
|
"devNewEventStream": conf.Server.DevNewEventStream,
|
|
"listenBrainzEnabled": conf.Server.ListenBrainz.Enabled,
|
|
"enableExternalServices": conf.Server.EnableExternalServices,
|
|
"enableReplayGain": conf.Server.EnableReplayGain,
|
|
"defaultDownsamplingFormat": conf.Server.DefaultDownsamplingFormat,
|
|
"separator": string(os.PathSeparator),
|
|
"enableInspect": conf.Server.Inspect.Enabled,
|
|
"pluginsEnabled": conf.Server.Plugins.Enabled,
|
|
"extAuthLogoutURL": conf.Server.ExtAuth.LogoutURL,
|
|
}
|
|
if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") {
|
|
appConfig["loginBackgroundURL"] = path.Join(conf.Server.BasePath, conf.Server.UILoginBackgroundURL)
|
|
}
|
|
auth := handleLoginFromHeaders(ds, r)
|
|
if auth != nil {
|
|
appConfig["auth"] = auth
|
|
}
|
|
appConfigJson, err := json.Marshal(appConfig)
|
|
if err != nil {
|
|
log.Error(r, "Error converting config to JSON", "config", appConfig, err)
|
|
} else {
|
|
log.Trace(r, "Injecting config in index.html", "config", string(appConfigJson))
|
|
}
|
|
|
|
log.Debug("UI configuration", "appConfig", appConfig)
|
|
version := consts.Version
|
|
if version != "dev" {
|
|
version = "v" + version
|
|
}
|
|
data := map[string]any{
|
|
"AppConfig": string(appConfigJson),
|
|
"Version": version,
|
|
}
|
|
addShareData(r, data, shareInfo)
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
err = t.Execute(w, data)
|
|
if err != nil {
|
|
log.Error(r, "Could not execute `index.html` template", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getIndexTemplate(r *http.Request, fs fs.FS) (*template.Template, error) {
|
|
t := template.New("initial state")
|
|
indexHtml, err := fs.Open("index.html")
|
|
if err != nil {
|
|
log.Error(r, "Could not find `index.html` template", err)
|
|
return nil, err
|
|
}
|
|
indexStr, err := io.ReadAll(indexHtml)
|
|
if err != nil {
|
|
log.Error(r, "Could not read from `index.html`", err)
|
|
return nil, err
|
|
}
|
|
t, err = t.Parse(string(indexStr))
|
|
if err != nil {
|
|
log.Error(r, "Error parsing `index.html`", err)
|
|
return nil, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
type shareData struct {
|
|
ID string `json:"id"`
|
|
Description string `json:"description"`
|
|
Downloadable bool `json:"downloadable"`
|
|
Tracks []shareTrack `json:"tracks"`
|
|
}
|
|
|
|
type shareTrack struct {
|
|
ID string `json:"id,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Artist string `json:"artist,omitempty"`
|
|
Album string `json:"album,omitempty"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
Duration float32 `json:"duration,omitempty"`
|
|
}
|
|
|
|
func addShareData(r *http.Request, data map[string]any, shareInfo *model.Share) {
|
|
ctx := r.Context()
|
|
if shareInfo == nil || shareInfo.ID == "" {
|
|
return
|
|
}
|
|
sd := shareData{
|
|
ID: shareInfo.ID,
|
|
Description: shareInfo.Description,
|
|
Downloadable: shareInfo.Downloadable,
|
|
}
|
|
sd.Tracks = slice.Map(shareInfo.Tracks, func(mf model.MediaFile) shareTrack {
|
|
return shareTrack{
|
|
ID: mf.ID,
|
|
Title: mf.Title,
|
|
Artist: mf.Artist,
|
|
Album: mf.Album,
|
|
Duration: mf.Duration,
|
|
UpdatedAt: mf.UpdatedAt,
|
|
}
|
|
})
|
|
|
|
shareInfoJson, err := json.Marshal(sd)
|
|
if err != nil {
|
|
log.Error(ctx, "Error converting shareInfo to JSON", "config", shareInfo, err)
|
|
} else {
|
|
log.Trace(ctx, "Injecting shareInfo in index.html", "config", string(shareInfoJson))
|
|
}
|
|
|
|
if shareInfo.Description != "" {
|
|
data["ShareDescription"] = shareInfo.Description
|
|
} else {
|
|
data["ShareDescription"] = shareInfo.Contents
|
|
}
|
|
data["ShareURL"] = shareInfo.URL
|
|
data["ShareImageURL"] = shareInfo.ImageURL
|
|
data["ShareInfo"] = string(shareInfoJson)
|
|
}
|