Fix: Protokoll-relative URLs, PDF-Fragment-Merge, Startup-Token-Cache, Test-Nil-Deref

- URL-Normalisierung überspringt jetzt //protocol-relative URLs
- PDF-Viewer-Parameter werden mit bestehenden Fragments gemerged statt blind angehängt
- /api/startup-token setzt Cache-Control: no-store (Server + Client)
- Tote Goroutine mit ignoriertem net.Listen-Error aus TestAssetsServed entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-23 15:21:26 +01:00
parent 2534dbbe05
commit 6bc4d3d2f8
3 changed files with 24 additions and 18 deletions

View file

@ -349,8 +349,11 @@ func (a *App) fetchPlaylist(ctx context.Context) {
}
for i := range pr.Items {
if strings.HasPrefix(pr.Items[i].Src, "/") {
pr.Items[i].Src = a.Config.ServerBaseURL + pr.Items[i].Src
src := pr.Items[i].Src
// Nur echte relative Pfade prefixen (einzelnes /), nicht protokoll-relative
// URLs (//cdn.example.com/...) und keine absoluten URLs (http://, https://).
if strings.HasPrefix(src, "/") && !strings.HasPrefix(src, "//") {
pr.Items[i].Src = a.Config.ServerBaseURL + src
}
}

View file

@ -108,6 +108,7 @@ func (s *Server) handleNowPlaying(w http.ResponseWriter, _ *http.Request) {
// wurde und lädt die Seite automatisch neu.
func (s *Server) handleStartupToken(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(map[string]string{"token": s.startupToken}) //nolint:errcheck
}
@ -435,7 +436,23 @@ const playerHTML = `<!DOCTYPE html>
} else {
// type === 'web', 'pdf' oder unbekannt → iframe
if (type === 'pdf') {
frame.src = item.src + '#toolbar=0&navpanes=0&scrollbar=0&view=Fit&page=1';
frame.src = (function pdfUrl(src) {
var defaults = {toolbar: '0', navpanes: '0', scrollbar: '0', view: 'Fit', page: '1'};
var hashIdx = src.indexOf('#');
var base = hashIdx >= 0 ? src.substring(0, hashIdx) : src;
var existing = hashIdx >= 0 ? src.substring(hashIdx + 1) : '';
var params = {};
existing.split('&').forEach(function(p) {
var kv = p.split('=');
if (kv[0]) params[kv[0]] = kv[1] || '';
});
for (var k in defaults) {
if (!(k in params)) params[k] = defaults[k];
}
var parts = [];
for (var k in params) parts.push(k + '=' + params[k]);
return base + '#' + parts.join('&');
})(item.src);
} else {
if (frame.src !== item.src) { frame.src = item.src; }
}
@ -603,7 +620,7 @@ const playerHTML = `<!DOCTYPE html>
var knownStartupToken = null;
function pollStartupToken() {
fetch('/api/startup-token')
fetch('/api/startup-token', {cache: 'no-store'})
.then(function(r) { return r.json(); })
.then(function(d) {
if (!d || !d.token) return;

View file

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"io/fs"
"net"
"net/http"
"net/http/httptest"
"strings"
@ -98,17 +97,6 @@ func TestHandleSysInfoReturnsItems(t *testing.T) {
}
func TestAssetsServed(t *testing.T) {
s := New("127.0.0.1:0", func() NowPlaying { return NowPlaying{} })
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ready := make(chan string, 1)
go func() {
ln, _ := net.Listen("tcp", "127.0.0.1:0")
ready <- ln.Addr().String()
ln.Close()
}()
// Use httptest recorder to test asset handler directly via the embed FS.
sub, err := fs.Sub(assetsFS, "assets")
if err != nil {
@ -127,8 +115,6 @@ func TestAssetsServed(t *testing.T) {
t.Errorf("GET /assets/%s Content-Type = %q, want image/png", name, ct)
}
}
_ = s
_ = ctx
}
func TestServerRunAndStop(t *testing.T) {