Compare commits
1
Commits
4abc8ec202
..
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
522e016a29 |
@@ -29,7 +29,7 @@ func newTestServer138(t *testing.T) *httptest.ResponseRecorder {
|
||||
globalSettingsFn = ss.get
|
||||
t.Cleanup(func() { globalSettingsFn = nil })
|
||||
a := &apiServer{store: st, cfg: cfg, ui: ui, settings: ss, adminKey: "test-admin-key"}
|
||||
req := httptest.NewRequest("GET", "/public", nil)
|
||||
req := httptest.NewRequest("GET", "/history", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
a.routes().ServeHTTP(rec, req)
|
||||
return rec
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestMineCreateListDelete(t *testing.T) {
|
||||
a := &apiServer{store: st, cfg: cfg, ui: ui, settings: ss, adminKey: "test-admin-key"}
|
||||
h := a.routes()
|
||||
|
||||
alice := viewerCookieFor(t, h, "/public")
|
||||
alice := viewerCookieFor(t, h, "/history")
|
||||
if alice == "" {
|
||||
t.Fatal("no viewer cookie issued")
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func TestMineCreateListDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
// a different browser's cookie does NOT see it
|
||||
bob := viewerCookieFor(t, h, "/public")
|
||||
bob := viewerCookieFor(t, h, "/history")
|
||||
rec = doReq(t, h, "GET", "/api/mine", bob, "")
|
||||
json.Unmarshal(rec.Body.Bytes(), &list)
|
||||
if list.Total != 0 {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package api
|
||||
|
||||
// #256: renamed page routes; old URLs redirect.
|
||||
import (
|
||||
"palette/internal/store"
|
||||
"palette/internal/web"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenamedPageRoutes(t *testing.T) {
|
||||
globalLimiter = newLimiter() // fresh rate-limit buckets
|
||||
st, err := store.OpenStore(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ui, err := web.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := Config{MaxTextBytes: 5 * 1024 * 1024}
|
||||
ss := NewTestSettingsStore(t, cfg)
|
||||
globalSettingsFn = ss.get
|
||||
t.Cleanup(func() { globalSettingsFn = nil })
|
||||
a := &apiServer{store: st, cfg: cfg, ui: ui, settings: ss, adminKey: "test-admin-key"}
|
||||
h := a.routes()
|
||||
|
||||
// new routes render pages
|
||||
for _, path := range []string{"/public", "/saved"} {
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET %s: %d, want 200", path, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// old routes redirect
|
||||
for _, tc := range [][2]string{{"/history", "/public"}, {"/mine", "/saved"}, {"/", "/public"}} {
|
||||
req := httptest.NewRequest("GET", tc[0], nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusMovedPermanently && rec.Code != http.StatusFound {
|
||||
t.Fatalf("GET %s: %d, want redirect", tc[0], rec.Code)
|
||||
}
|
||||
if loc := rec.Header().Get("Location"); loc != tc[1] {
|
||||
t.Fatalf("GET %s redirects to %s, want %s", tc[0], loc, tc[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,14 +118,11 @@ func (a *apiServer) routes() http.Handler {
|
||||
r.Get("/raw/{id}", a.handleRaw)
|
||||
|
||||
// web pages
|
||||
r.Get("/", http.RedirectHandler("/public", http.StatusFound).ServeHTTP)
|
||||
r.Get("/", http.RedirectHandler("/history", http.StatusFound).ServeHTTP)
|
||||
r.Get("/new", a.ui.Handlers().HandleNewPage)
|
||||
r.Get("/public", a.ui.Handlers().HandleHistoryPage)
|
||||
r.Get("/saved", a.ui.Handlers().HandleMinePage)
|
||||
r.Get("/history", a.ui.Handlers().HandleHistoryPage)
|
||||
r.Get("/settings", a.ui.Handlers().HandleSettingsPage)
|
||||
// #256: old URLs redirect to the renamed pages
|
||||
r.Get("/history", http.RedirectHandler("/public", http.StatusMovedPermanently).ServeHTTP)
|
||||
r.Get("/mine", http.RedirectHandler("/saved", http.StatusMovedPermanently).ServeHTTP)
|
||||
r.Get("/mine", a.ui.Handlers().HandleMinePage)
|
||||
r.Handle("/static/*", a.ui.StaticHandler())
|
||||
r.Get("/unlock/{id}", a.handlePasteView)
|
||||
r.Post("/unlock/{id}", a.handlePasteView)
|
||||
|
||||
@@ -26,10 +26,7 @@ type Attachment struct {
|
||||
SizeHuman string `json:"-"` // template-only: human-readable size
|
||||
}
|
||||
|
||||
// MaxFilenameLen caps stored attachment filenames (bytes) to bound DB
|
||||
// rows and Content-Disposition echoes. 128 keeps names readable while
|
||||
// stopping filename-bloat abuse; longer names truncate.
|
||||
const MaxFilenameLen = 128
|
||||
const MaxFilenameLen = 255
|
||||
|
||||
// ErrFileTooLarge is returned when an attachment exceeds the per-file cap.
|
||||
var ErrFileTooLarge = errors.New("file too large")
|
||||
|
||||
@@ -105,9 +105,4 @@ func TestSanitizeFilename(t *testing.T) {
|
||||
if got := SanitizeFilename(long); len(got) != MaxFilenameLen {
|
||||
t.Errorf("long name len = %d want %d", len(got), MaxFilenameLen)
|
||||
}
|
||||
// issue #248: a 250-char multipart filename must truncate to the cap
|
||||
repro := strings.Repeat("b", 246) + ".txt"
|
||||
if got := SanitizeFilename(repro); len(got) != MaxFilenameLen {
|
||||
t.Errorf("repro name len = %d want %d", len(got), MaxFilenameLen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ var reservedSlugs = map[string]bool{
|
||||
"api": true, "raw": true, "can": true, "cans": true, "public": true,
|
||||
"history": true, "static": true, "assets": true, "favicon.ico": true,
|
||||
"new": true, "login": true, "logout": true, "admin": true, "settings": true,
|
||||
"mine": true, "saved": true, "unlock": true, "guess": true, "f": true,
|
||||
"mine": true, "unlock": true, "guess": true, "f": true,
|
||||
}
|
||||
|
||||
var ErrInvalidSlug = errors.New("custom slug must be 1-64 chars: letters, digits, dash, underscore; must start with letter or digit")
|
||||
|
||||
@@ -197,7 +197,7 @@ function finishCreate(data) {
|
||||
try {
|
||||
navigator.clipboard.writeText(url);
|
||||
copyBtn.classList.add('ok'); // in-place success feedback (#53)
|
||||
copyBtn.textContent = '✓';
|
||||
copyBtn.textContent = 'Success!';
|
||||
setTimeout(() => { copyBtn.classList.remove('ok'); copyBtn.textContent = '⧉'; }, 2000);
|
||||
} catch(e) { toast('Copy failed', 'error'); }
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ function copyFeedback(btn) {
|
||||
if (!btn) return;
|
||||
if (!btn.dataset.label) btn.dataset.label = btn.textContent; // remember the original label (Copy/Link)
|
||||
btn.classList.add('ok');
|
||||
btn.textContent = '✓';
|
||||
btn.textContent = 'Success!';
|
||||
clearTimeout(btn._okh);
|
||||
btn._okh = setTimeout(() => { btn.classList.remove('ok'); btn.textContent = btn.dataset.label; }, 2000);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ function redeem() {
|
||||
try { tok = sessionStorage.getItem('deletion_token_' + PASTE_ID) || ''; } catch(e) {}
|
||||
if (!tok) { alert('deletion token not available in this browser'); return; }
|
||||
fetch('/api/pastes/' + PASTE_ID + '/redeem', {method: 'DELETE', headers: {'Authorization': 'Bearer ' + tok}})
|
||||
.then(r => { if (r.ok) location.href = '/public'; else alert('delete failed'); });
|
||||
.then(r => { if (r.ok) location.href = '/history'; else alert('delete failed'); });
|
||||
}
|
||||
|
||||
// wiring (moved from inline handlers for CSP #139)
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
{{define "topbar"}}
|
||||
<div class="topbar">
|
||||
<a class="logo" href="/public">Palette <em>/ {{ version }}</em></a>
|
||||
<a class="logo" href="/history">Palette <em>/ {{ version }}</em></a>
|
||||
<nav>
|
||||
<a href="/new" {{if eq .Page "new"}}class="on"{{end}}>New</a>
|
||||
<a href="/public" {{if eq .Page "public"}}class="on"{{end}}>Public</a>
|
||||
<a href="/saved" {{if eq .Page "saved"}}class="on"{{end}}>Saved</a>
|
||||
<a href="/history" {{if eq .Page "history"}}class="on"{{end}}>Public</a>
|
||||
<a href="/mine" {{if eq .Page "mine"}}class="on"{{end}}>Saved</a>
|
||||
<a href="https://git.archfox.org/poslop/palette" target="_blank" rel="noopener">Git<svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if not .Attachment}}
|
||||
{{if not .AttachmentImage}}
|
||||
<div class="float">
|
||||
<div class="code" id="code"><div class="gutter" id="gutter">{{.Gutter}}</div><div class="codebody" id="codebody">{{.ContentHTML}}</div></div>
|
||||
</div>
|
||||
|
||||
+4
-4
@@ -433,9 +433,9 @@ func (h *Handlers) HandleNewPage(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPage(w, "new.html", map[string]any{"Page": "new"})
|
||||
}
|
||||
|
||||
// HandleHistoryPage serves /public.
|
||||
// HandleHistoryPage serves /history.
|
||||
func (h *Handlers) HandleHistoryPage(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPage(w, "history.html", map[string]any{"Page": "public"})
|
||||
h.renderPage(w, "history.html", map[string]any{"Page": "history"})
|
||||
}
|
||||
|
||||
// HandleSettingsPage serves /settings.
|
||||
@@ -452,9 +452,9 @@ func (h *Handlers) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPage(w, "settings.html", map[string]any{"Page": "settings", "Themes": themes})
|
||||
}
|
||||
|
||||
// HandleMinePage serves /saved.
|
||||
// HandleMinePage serves /mine.
|
||||
func (h *Handlers) HandleMinePage(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPage(w, "mine.html", map[string]any{"Page": "saved"})
|
||||
h.renderPage(w, "mine.html", map[string]any{"Page": "mine"})
|
||||
}
|
||||
|
||||
// HandleAdminPage serves /admin.
|
||||
|
||||
Reference in New Issue
Block a user