| 1 | diff --git a/internal/webui/browse.html b/internal/webui/browse.html |
| 2 | index 7de76f6..c760f71 100644 |
| 3 | --- a/internal/webui/browse.html |
| 4 | +++ b/internal/webui/browse.html |
| 5 | @@ -18,7 +18,7 @@ |
| 6 | {{else if .Feed}}<table id="rows"> |
| 7 | <thead><tr><th>hash</th><th>type</th><th>uploaded</th></tr></thead> |
| 8 | <tbody> |
| 9 | -{{range .Feed}}{{$f := asMap .}}<tr><td><a href="/file?sha={{str (index $f "sha256")}}">{{shortID (index $f "sha256")}}</a></td><td>{{str (index $f "type")}} <small>{{str (index $f "size")}} B</small></td><td>{{when (index $f "uploaded")}}</td></tr> |
| 10 | +{{range .Feed}}{{$f := asMap .}}<tr><td><a href="/file?sha={{urlquery (str (index $f "sha256"))}}">{{shortID (index $f "sha256")}}</a></td><td>{{str (index $f "type")}} <small>{{str (index $f "size")}} B</small></td><td>{{when (index $f "uploaded")}}</td></tr> |
| 11 | {{end}}</tbody> |
| 12 | </table> |
| 13 | {{else}}<p>No files were reported.</p> |
| 14 | @@ -27,23 +27,23 @@ |
| 15 | {{end}} |
| 16 | |
| 17 | {{define "file"}}{{if .Error}}<section><h1>File</h1>{{template "alert" .Error}}</section> |
| 18 | -{{else}}{{$file := asMap .Event}}{{$hash := or (.Query.Get "hash") (.Query.Get "sha")}}<section> |
| 19 | +{{else}}{{$file := asMap .Event}}{{$hash := or (.Query.Get "hash") (.Query.Get "sha")}}<section id="file-view"> |
| 20 | <h1>File</h1> |
| 21 | <table id="facts"> |
| 22 | <tr><th>sha256</th><td><code>{{str (index $file "sha256")}}</code></td></tr> |
| 23 | <tr><th>type</th><td>{{str (index $file "type")}}</td></tr> |
| 24 | <tr><th>size</th><td>{{str (index $file "size")}} bytes</td></tr> |
| 25 | {{with index $file "blossom_uri"}}<tr><th>blossom</th><td><code>{{.}}</code></td></tr>{{end}} |
| 26 | -<tr><th>download</th><td><a href="/files/raw?hash={{str (index $file "sha256")}}">raw</a></td></tr> |
| 27 | +<tr><th>download</th><td><a fx-ignore href="/files/raw?hash={{urlquery (str (index $file "sha256"))}}">raw</a></td></tr> |
| 28 | </table> |
| 29 | -</section> |
| 30 | -{{$type := str (index $file "type")}}{{$raw := printf "/files/raw?hash=%s" (str (index $file "sha256"))}}{{if hasPrefix $type "image/"}}<section id="media"><img src="{{$raw}}" alt=""></section> |
| 31 | -{{else if hasPrefix $type "video/"}}<section id="media"><video controls src="{{$raw}}"></video></section> |
| 32 | -{{else if hasPrefix $type "audio/"}}<section id="media"><audio controls src="{{$raw}}"></audio></section> |
| 33 | -{{else if eq $type "application/pdf"}}<section id="media"><a href="{{$raw}}">Open the PDF</a></section> |
| 34 | -{{else if index $file "binary"}}<section><p>This is a binary file. Download it to view its contents.</p></section> |
| 35 | -{{else}}{{sourceHTML .Event}} |
| 36 | -{{end}}{{if .Actor}}<section><details><summary>Manage file</summary><rpc-form method="deleteblob"><input type="hidden" name="param" value="{{$hash}}"><button>Delete file</button></rpc-form></details></section>{{end}} |
| 37 | +{{$type := str (index $file "type")}}{{$raw := printf "/files/raw?hash=%s" (urlquery (str (index $file "sha256")))}}{{if hasPrefix $type "image/"}}<div id="media"><img src="{{$raw}}" alt=""></div> |
| 38 | +{{else if hasPrefix $type "video/"}}<div id="media"><video controls src="{{$raw}}"></video></div> |
| 39 | +{{else if hasPrefix $type "audio/"}}<div id="media"><audio controls src="{{$raw}}"></audio></div> |
| 40 | +{{else if eq $type "application/pdf"}}<div id="media"><a fx-ignore href="{{$raw}}">Open the PDF</a></div> |
| 41 | +{{else if index $file "binary"}}<p id="media">This is a binary file. Download it to view its contents.</p> |
| 42 | +{{else if index $file "content"}}{{sourceHTML .Event}} |
| 43 | +{{else}}<p id="media">This file is empty.</p> |
| 44 | +{{end}}{{if .Actor}}<details id="manage-file"><summary>Manage file</summary><rpc-form method="deleteblob"><input type="hidden" name="param" value="{{$hash}}"><button>Delete file</button></rpc-form></details>{{end}}</section> |
| 45 | {{end}}{{end}} |
| 46 | |
| 47 | {{define "sites"}}<section> |
| 48 | diff --git a/internal/webui/browse_test.go b/internal/webui/browse_test.go |
| 49 | index 6c76094..56a1a50 100644 |
| 50 | --- a/internal/webui/browse_test.go |
| 51 | +++ b/internal/webui/browse_test.go |
| 52 | @@ -104,6 +104,32 @@ func TestBrowseRendersTypedGitRelayPage(t *testing.T) { |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | +func TestFileViewerEscapesHashAndExplainsEmptyFiles(t *testing.T) { |
| 57 | + b := &emptyFileBackend{browseBackend: &browseBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}} |
| 58 | + app, err := New(b, Options{}) |
| 59 | + if err != nil { |
| 60 | + t.Fatal(err) |
| 61 | + } |
| 62 | + recorder := httptest.NewRecorder() |
| 63 | + app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/file?hash=a%26b", nil)) |
| 64 | + body := recorder.Body.String() |
| 65 | + if recorder.Code != http.StatusOK || !strings.Contains(body, `fx-ignore href="/files/raw?hash=a%26b"`) { |
| 66 | + t.Fatalf("file hash was not URL encoded: status=%d body=%s", recorder.Code, body) |
| 67 | + } |
| 68 | + if !strings.Contains(body, "This file is empty.") { |
| 69 | + t.Fatalf("empty file did not get an explanatory preview: %s", body) |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +type emptyFileBackend struct{ *browseBackend } |
| 74 | + |
| 75 | +func (b *emptyFileBackend) Query(ctx context.Context, method string, params []json.RawMessage, actor string) (any, error) { |
| 76 | + if method == "browsefile" { |
| 77 | + return map[string]any{"sha256": "a&b", "type": "text/plain", "content": ""}, nil |
| 78 | + } |
| 79 | + return b.browseBackend.Query(ctx, method, params, actor) |
| 80 | +} |
| 81 | + |
| 82 | type typedBrowseBackend struct { |
| 83 | *browseBackend |
| 84 | page gitrelay.BrowsePage |
| 85 | diff --git a/internal/webui/public_pages.go b/internal/webui/public_pages.go |
| 86 | index 7d42160..804d484 100644 |
| 87 | --- a/internal/webui/public_pages.go |
| 88 | +++ b/internal/webui/public_pages.go |
| 89 | @@ -111,7 +111,7 @@ func (a *App) siteRows(feed []any) []any { |
| 90 | } else if scheme == "wss" { |
| 91 | scheme = "https" |
| 92 | } |
| 93 | - rows = append(rows, map[string]any{"id": e.ID, "kind": e.Kind, "author": e.PubKey, "label": label, "paths": len(sites.SitePaths(e)), "created_at": e.CreatedAt, "url": scheme + "://" + label + "." + base.Hostname()}) |
| 94 | + rows = append(rows, map[string]any{"id": e.ID, "kind": e.Kind, "author": e.PubKey, "label": label, "paths": len(sites.SitePaths(e)), "created_at": e.CreatedAt, "url": scheme + "://" + label + "." + base.Host}) |
| 95 | } |
| 96 | return rows |
| 97 | } |
| 98 | diff --git a/internal/webui/repo.html b/internal/webui/repo.html |
| 99 | index 7a36ef7..9aad9e7 100644 |
| 100 | --- a/internal/webui/repo.html |
| 101 | +++ b/internal/webui/repo.html |
| 102 | @@ -4,7 +4,7 @@ |
| 103 | </section> |
| 104 | {{else if or (eq $view "tree") (eq $view "file")}}<table id="split"><tbody><tr><td id="tree"><header><a href="{{repoURL .Query "tree" ""}}">{{str (index $repo "identifier")}}/</a>{{range $index, $crumb := repoBreadcrumbs .Query}}{{if $index}}{{if $crumb.URL}}<a href="{{$crumb.URL}}">{{$crumb.Label}}</a>{{else}}<b>{{$crumb.Label}}</b>{{end}}/{{end}}{{end}}</header> |
| 105 | {{template "repoTree" .}}</td> |
| 106 | -<td id="preview">{{if eq $view "file"}}<header>{{.Query.Get "path"}} <a href="/repo/raw?owner={{urlquery (.Query.Get "owner")}}&repo={{urlquery (.Query.Get "repo")}}&ref={{urlquery (.Query.Get "ref")}}&path={{urlquery (.Query.Get "path")}}">raw</a></header> |
| 107 | +<td id="preview">{{if eq $view "file"}}<header>{{.Query.Get "path"}} <a fx-ignore href="/repo/raw?owner={{urlquery (.Query.Get "owner")}}&repo={{urlquery (.Query.Get "repo")}}&ref={{urlquery (.Query.Get "ref")}}&path={{urlquery (.Query.Get "path")}}">raw</a></header> |
| 108 | {{if index $repo "binary"}}<p>This is a binary file. Download it to view its contents.</p> |
| 109 | {{else if index $repo "content"}}{{sourceHTML .Event}} |
| 110 | {{else}}<p>This file is empty.</p> |
| 111 | diff --git a/internal/webui/shell_test.go b/internal/webui/shell_test.go |
| 112 | index edfc743..1f66d71 100644 |
| 113 | --- a/internal/webui/shell_test.go |
| 114 | +++ b/internal/webui/shell_test.go |
| 115 | @@ -81,6 +81,23 @@ func TestSitesPageListsHostedSites(t *testing.T) { |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | +func TestSitesPagePreservesRelayPortInHostedURL(t *testing.T) { |
| 120 | + backend := &portSiteBackend{siteBackend: &siteBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}} |
| 121 | + app, err := New(backend, Options{}) |
| 122 | + if err != nil { |
| 123 | + t.Fatal(err) |
| 124 | + } |
| 125 | + recorder := httptest.NewRecorder() |
| 126 | + app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/sites", nil)) |
| 127 | + if !strings.Contains(recorder.Body.String(), ".relay.example:8787") { |
| 128 | + t.Fatalf("hosted URL lost relay port: %s", recorder.Body.String()) |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +type portSiteBackend struct{ *siteBackend } |
| 133 | + |
| 134 | +func (*portSiteBackend) URL() string { return "http://relay.example:8787" } |
| 135 | + |
| 136 | func TestFeedFiltersNarrowTheQuery(t *testing.T) { |
| 137 | filter := map[string]any{"kinds": []int{1, 30023}, "limit": 12} |
| 138 | applyFeedFilters(filter, url.Values{"kinds": {"7"}, "author": {strings.Repeat("b", 64)}, "since": {"2026-09-01"}}) |
| 139 | diff --git a/internal/webui/style.css b/internal/webui/style.css |
| 140 | index c039a03..c913a8d 100644 |
| 141 | --- a/internal/webui/style.css |
| 142 | +++ b/internal/webui/style.css |
| 143 | @@ -82,6 +82,7 @@ scroll-box th,json-view th,scroll-box td code,scroll-box td nostr-key,scroll-box |
| 144 | dl{margin:.5rem 0}dt{font-weight:600}dd{margin:0 0 .35rem 1rem} |
| 145 | #facts{border-collapse:collapse;font:13px var(--mono);width:auto} |
| 146 | #facts th{text-align:left;font-weight:normal;color:var(--muted);width:9em;padding:3px 0;border:0}#facts td{padding:3px 0;border:0} |
| 147 | +#facts td{overflow-wrap:anywhere} |
| 148 | |
| 149 | /* Identity: the relay entry page. */ |
| 150 | #identity{max-width:720px}#identity>header{border-bottom:1px solid var(--line);padding-bottom:12px;margin-bottom:12px} |
| 151 | diff --git a/internal/webui/webui.go b/internal/webui/webui.go |
| 152 | index 21c74f4..6ab88ec 100644 |
| 153 | --- a/internal/webui/webui.go |
| 154 | +++ b/internal/webui/webui.go |
| 155 | @@ -445,6 +445,7 @@ func (a *App) eventPage(writer http.ResponseWriter, request *http.Request) { |
| 156 | return |
| 157 | } |
| 158 | data := PageData{Event: result, View: identifier, Tab: "event"} |
| 159 | + data.Title = "Event " + shortID(identifier) + " | " + a.backend.Slug() |
| 160 | if address { |
| 161 | data.Feed = browseRows(result) |
| 162 | } |
| 163 | @@ -535,6 +536,7 @@ func (a *App) browse(writer http.ResponseWriter, request *http.Request) { |
| 164 | pageQuery.Set("view", "tree") |
| 165 | } |
| 166 | data := PageData{Tab: browseTab(path), Feed: browseRows(result), Event: result, Query: pageQuery} |
| 167 | + data.Title = browseTitle(path, pageQuery, result, a.backend.Slug()) |
| 168 | if method == "browserepo" && pageQuery.Get("view") == "home" { |
| 169 | data.Readme = a.readme(request.Context(), actor, pageQuery) |
| 170 | } |
| 171 | @@ -542,6 +544,24 @@ func (a *App) browse(writer http.ResponseWriter, request *http.Request) { |
| 172 | a.render(writer, request, data) |
| 173 | } |
| 174 | |
| 175 | +func browseTitle(path string, query url.Values, result any, slug string) string { |
| 176 | + label := map[string]string{"/repos": "Repositories", "/files": "Files", "/file": "File"}[path] |
| 177 | + if path == "/repo" { |
| 178 | + label = query.Get("repo") |
| 179 | + if label == "" { |
| 180 | + label = plainString(valueMap(result)["identifier"]) |
| 181 | + } |
| 182 | + if label == "" { |
| 183 | + label = "Repository" |
| 184 | + } |
| 185 | + label += " | " + repoView(query) |
| 186 | + } |
| 187 | + if label == "" { |
| 188 | + label = "Browse" |
| 189 | + } |
| 190 | + return label + " | " + slug |
| 191 | +} |
| 192 | + |
| 193 | // readme renders README.md from the requested ref for the repository entry |
| 194 | // page. A missing or binary README simply yields no content. |
| 195 | func (a *App) readme(ctx context.Context, actor string, query url.Values) template.HTML { |