Browse pages carry the repository or file in the title, file hashes are URL encoded in links, raw downloads bypass in-page navigation, empty files say so, and hosted site URLs keep the relay port.
- commit
- 411567228bc1d930b30bcd3cfc4d95ef1a49aa1c
- parent
- 789c96dc188d999ff4e67486387d97e9df2e6299
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 22 | +++++++++++----------- |
| M | internal/ | 26 | ++++++++++++++++++++++++++ |
| M | internal/ | 2 | +- |
| M | internal/ | 2 | +- |
| M | internal/ | 17 | +++++++++++++++++ |
| M | internal/ | 1 | + |
| M | internal/ | 20 | ++++++++++++++++++++ |
@@ -18,7 +18,7 @@
1818 {{else if .Feed}}<table id="rows">1919 <thead><tr><th>hash</th><th>type</th><th>uploaded</th></tr></thead>2020 <tbody>21-{{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>21+{{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>2222 {{end}}</tbody>2323 </table>2424 {{else}}<p>No files were reported.</p>@@ -27,23 +27,23 @@
2727 {{end}}2828 2929 {{define "file"}}{{if .Error}}<section><h1>File</h1>{{template "alert" .Error}}</section>30-{{else}}{{$file := asMap .Event}}{{$hash := or (.Query.Get "hash") (.Query.Get "sha")}}<section>30+{{else}}{{$file := asMap .Event}}{{$hash := or (.Query.Get "hash") (.Query.Get "sha")}}<section id="file-view">3131 <h1>File</h1>3232 <table id="facts">3333 <tr><th>sha256</th><td><code>{{str (index $file "sha256")}}</code></td></tr>3434 <tr><th>type</th><td>{{str (index $file "type")}}</td></tr>3535 <tr><th>size</th><td>{{str (index $file "size")}} bytes</td></tr>3636 {{with index $file "blossom_uri"}}<tr><th>blossom</th><td><code>{{.}}</code></td></tr>{{end}}37-<tr><th>download</th><td><a href="/files/raw?hash={{str (index $file "sha256")}}">raw</a></td></tr>37+<tr><th>download</th><td><a fx-ignore href="/files/raw?hash={{urlquery (str (index $file "sha256"))}}">raw</a></td></tr>3838 </table>39-</section>40-{{$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>41-{{else if hasPrefix $type "video/"}}<section id="media"><video controls src="{{$raw}}"></video></section>42-{{else if hasPrefix $type "audio/"}}<section id="media"><audio controls src="{{$raw}}"></audio></section>43-{{else if eq $type "application/pdf"}}<section id="media"><a href="{{$raw}}">Open the PDF</a></section>44-{{else if index $file "binary"}}<section><p>This is a binary file. Download it to view its contents.</p></section>45-{{else}}{{sourceHTML .Event}}46-{{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}}39+{{$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>40+{{else if hasPrefix $type "video/"}}<div id="media"><video controls src="{{$raw}}"></video></div>41+{{else if hasPrefix $type "audio/"}}<div id="media"><audio controls src="{{$raw}}"></audio></div>42+{{else if eq $type "application/pdf"}}<div id="media"><a fx-ignore href="{{$raw}}">Open the PDF</a></div>43+{{else if index $file "binary"}}<p id="media">This is a binary file. Download it to view its contents.</p>44+{{else if index $file "content"}}{{sourceHTML .Event}}45+{{else}}<p id="media">This file is empty.</p>46+{{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>4747 {{end}}{{end}}4848 4949 {{define "sites"}}<section>@@ -104,6 +104,32 @@ func TestBrowseRendersTypedGitRelayPage(t *testing.T) {
104104 }105105 }106106 107+func TestFileViewerEscapesHashAndExplainsEmptyFiles(t *testing.T) {108+ b := &emptyFileBackend{browseBackend: &browseBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}}109+ app, err := New(b, Options{})110+ if err != nil {111+ t.Fatal(err)112+ }113+ recorder := httptest.NewRecorder()114+ app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/file?hash=a%26b", nil))115+ body := recorder.Body.String()116+ if recorder.Code != http.StatusOK || !strings.Contains(body, `fx-ignore href="/files/raw?hash=a%26b"`) {117+ t.Fatalf("file hash was not URL encoded: status=%d body=%s", recorder.Code, body)118+ }119+ if !strings.Contains(body, "This file is empty.") {120+ t.Fatalf("empty file did not get an explanatory preview: %s", body)121+ }122+}123+124+type emptyFileBackend struct{ *browseBackend }125+126+func (b *emptyFileBackend) Query(ctx context.Context, method string, params []json.RawMessage, actor string) (any, error) {127+ if method == "browsefile" {128+ return map[string]any{"sha256": "a&b", "type": "text/plain", "content": ""}, nil129+ }130+ return b.browseBackend.Query(ctx, method, params, actor)131+}132+107133 type typedBrowseBackend struct {108134 *browseBackend109135 page gitrelay.BrowsePage@@ -111,7 +111,7 @@ func (a *App) siteRows(feed []any) []any {
111111 } else if scheme == "wss" {112112 scheme = "https"113113 }114- 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()})114+ 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})115115 }116116 return rows117117 }@@ -4,7 +4,7 @@
44 </section>55 {{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>66 {{template "repoTree" .}}</td>7-<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>7+<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>88 {{if index $repo "binary"}}<p>This is a binary file. Download it to view its contents.</p>99 {{else if index $repo "content"}}{{sourceHTML .Event}}1010 {{else}}<p>This file is empty.</p>@@ -81,6 +81,23 @@ func TestSitesPageListsHostedSites(t *testing.T) {
8181 }8282 }8383 84+func TestSitesPagePreservesRelayPortInHostedURL(t *testing.T) {85+ backend := &portSiteBackend{siteBackend: &siteBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}}86+ app, err := New(backend, Options{})87+ if err != nil {88+ t.Fatal(err)89+ }90+ recorder := httptest.NewRecorder()91+ app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/sites", nil))92+ if !strings.Contains(recorder.Body.String(), ".relay.example:8787") {93+ t.Fatalf("hosted URL lost relay port: %s", recorder.Body.String())94+ }95+}96+97+type portSiteBackend struct{ *siteBackend }98+99+func (*portSiteBackend) URL() string { return "http://relay.example:8787" }100+84101 func TestFeedFiltersNarrowTheQuery(t *testing.T) {85102 filter := map[string]any{"kinds": []int{1, 30023}, "limit": 12}86103 applyFeedFilters(filter, url.Values{"kinds": {"7"}, "author": {strings.Repeat("b", 64)}, "since": {"2026-09-01"}})@@ -82,6 +82,7 @@ scroll-box th,json-view th,scroll-box td code,scroll-box td nostr-key,scroll-box
8282 dl{margin:.5rem 0}dt{font-weight:600}dd{margin:0 0 .35rem 1rem}8383 #facts{border-collapse:collapse;font:13px var(--mono);width:auto}8484 #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}85+#facts td{overflow-wrap:anywhere}8586 8687 /* Identity: the relay entry page. */8788 #identity{max-width:720px}#identity>header{border-bottom:1px solid var(--line);padding-bottom:12px;margin-bottom:12px}@@ -445,6 +445,7 @@ func (a *App) eventPage(writer http.ResponseWriter, request *http.Request) {
445445 return446446 }447447 data := PageData{Event: result, View: identifier, Tab: "event"}448+ data.Title = "Event " + shortID(identifier) + " | " + a.backend.Slug()448449 if address {449450 data.Feed = browseRows(result)450451 }@@ -535,6 +536,7 @@ func (a *App) browse(writer http.ResponseWriter, request *http.Request) {
535536 pageQuery.Set("view", "tree")536537 }537538 data := PageData{Tab: browseTab(path), Feed: browseRows(result), Event: result, Query: pageQuery}539+ data.Title = browseTitle(path, pageQuery, result, a.backend.Slug())538540 if method == "browserepo" && pageQuery.Get("view") == "home" {539541 data.Readme = a.readme(request.Context(), actor, pageQuery)540542 }@@ -542,6 +544,24 @@ func (a *App) browse(writer http.ResponseWriter, request *http.Request) {
542544 a.render(writer, request, data)543545 }544546 547+func browseTitle(path string, query url.Values, result any, slug string) string {548+ label := map[string]string{"/repos": "Repositories", "/files": "Files", "/file": "File"}[path]549+ if path == "/repo" {550+ label = query.Get("repo")551+ if label == "" {552+ label = plainString(valueMap(result)["identifier"])553+ }554+ if label == "" {555+ label = "Repository"556+ }557+ label += " | " + repoView(query)558+ }559+ if label == "" {560+ label = "Browse"561+ }562+ return label + " | " + slug563+}564+545565 // readme renders README.md from the requested ref for the repository entry546566 // page. A missing or binary README simply yields no content.547567 func (a *App) readme(ctx context.Context, actor string, query url.Values) template.HTML {