Fix disabled vanity field
This commit is contained in:
@@ -1627,6 +1627,65 @@ func TestUploadPageKeepsTheOneOffTokenField(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The server reads the multipart body as a stream and stops at the file part,
|
||||||
|
// so anything the upload depends on has to be in the markup ahead of it. The
|
||||||
|
// form is laid out to look otherwise, which is exactly why this is pinned: a
|
||||||
|
// tidy-up that moves the drop zone back up in the markup would silently strip
|
||||||
|
// the expiry, the custom name and the one-off token from every upload.
|
||||||
|
func TestUploadFormSendsTheFileLast(t *testing.T) {
|
||||||
|
h := newHarness(t, nil)
|
||||||
|
resp := h.get(t, "/", "")
|
||||||
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
page := string(raw)
|
||||||
|
|
||||||
|
file := strings.Index(page, `name="file"`)
|
||||||
|
if file < 0 {
|
||||||
|
t.Fatal("the upload form has no file field")
|
||||||
|
}
|
||||||
|
for _, field := range []string{"expiry", "vanity", "token"} {
|
||||||
|
at := strings.Index(page, `name="`+field+`"`)
|
||||||
|
if at < 0 {
|
||||||
|
t.Errorf("the upload form has no %s field", field)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if at > file {
|
||||||
|
t.Errorf("the %s field follows the file part, where the server can no longer read it", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anonymous visitors may type a custom name: the token that permits it can be
|
||||||
|
// supplied in the same form, for this upload only. The rule itself is the
|
||||||
|
// server's to enforce, not the markup's.
|
||||||
|
func TestCustomNameFieldIsAlwaysUsable(t *testing.T) {
|
||||||
|
h := newHarness(t, nil)
|
||||||
|
resp := h.get(t, "/", "")
|
||||||
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
form := string(raw)
|
||||||
|
if i := strings.Index(form, `name="vanity"`); i < 0 {
|
||||||
|
t.Fatal("the upload form has no custom name field")
|
||||||
|
} else if j := strings.Index(form[i:], ">"); strings.Contains(form[i:i+j], "disabled") {
|
||||||
|
t.Error("the custom name field is disabled, so a one-off token cannot be used with it")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled in the page, still refused on the wire without a token.
|
||||||
|
res := h.formUpload(t, map[string]string{"vanity": "anonymous-pick"}, "f.txt", "hello")
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusForbidden {
|
||||||
|
t.Errorf("anonymous vanity upload = %s, want 403", res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And accepted when the form carries a token that allows it.
|
||||||
|
res2 := h.formUpload(t, map[string]string{"vanity": "chosen-name", "token": h.token}, "f.txt", "hello")
|
||||||
|
defer res2.Body.Close()
|
||||||
|
if res2.StatusCode != http.StatusCreated {
|
||||||
|
t.Errorf("one-off token vanity upload = %s, want 201", res2.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A chosen passphrase has to work everywhere a generated token does: at the
|
// A chosen passphrase has to work everywhere a generated token does: at the
|
||||||
// login form, on an upload, and as a session.
|
// login form, on an upload, and as a session.
|
||||||
func TestChosenPassphraseWorksEndToEnd(t *testing.T) {
|
func TestChosenPassphraseWorksEndToEnd(t *testing.T) {
|
||||||
|
|||||||
+3
-2
@@ -108,8 +108,9 @@
|
|||||||
submit.disabled = true;
|
submit.disabled = true;
|
||||||
progress.hidden = false;
|
progress.hidden = false;
|
||||||
|
|
||||||
// FormData follows DOM order, so the token, expiry and vanity fields all
|
// FormData follows markup order, not painted order, so the options and the
|
||||||
// precede the file part, which is exactly what the server requires.
|
// token precede the file part exactly as the server requires. The drop
|
||||||
|
// zone only looks like it comes first; see the order rules in style.css.
|
||||||
var data = new FormData(form);
|
var data = new FormData(form);
|
||||||
var started = Date.now();
|
var started = Date.now();
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,14 @@ input[type=text], input[type=password] {
|
|||||||
.row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
.row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||||
.row > .field { flex: 1 1 12rem; }
|
.row > .field { flex: 1 1 12rem; }
|
||||||
|
|
||||||
|
/* The file input has to be the last part in the submitted body, so it sits at
|
||||||
|
the bottom of the form's markup and is lifted back to the top here. Only the
|
||||||
|
painting order moves; the submission order, which the server depends on, is
|
||||||
|
the markup's. */
|
||||||
|
#upload { display: flex; flex-direction: column; }
|
||||||
|
#upload > .drop { order: -1; }
|
||||||
|
#upload > button { align-self: flex-start; }
|
||||||
|
|
||||||
.drop {
|
.drop {
|
||||||
border: 2px dashed var(--line);
|
border: 2px dashed var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<form id="upload" class="card" method="post" action="{{.Base}}api/upload"
|
<form id="upload" class="card" method="post" action="{{.Base}}api/upload"
|
||||||
enctype="multipart/form-data" data-max-size="{{.MaxSizeBytes}}">
|
enctype="multipart/form-data" data-max-size="{{.MaxSizeBytes}}">
|
||||||
|
|
||||||
<div class="drop" id="drop">
|
<!-- Field order is load-bearing: the server streams this body rather than
|
||||||
<input type="file" name="file" id="file" required>
|
spooling it, so every option and the token have to arrive before the
|
||||||
<p class="hint" id="drop-hint">Choose a file, or drop one here.</p>
|
file part does. The drop zone is lifted back to the top of the card with
|
||||||
</div>
|
CSS order, which changes what is seen without changing what is sent. -->
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Custom name <em>({{if .AllowVanity}}optional{{else}}needs a token{{end}})</em></span>
|
<span>Custom name <em>({{if .AllowVanity}}optional{{else}}needs a token{{end}})</em></span>
|
||||||
<input type="text" name="vanity" id="vanity" placeholder="auto" autocomplete="off"
|
<input type="text" name="vanity" id="vanity" placeholder="auto" autocomplete="off"
|
||||||
pattern="[A-Za-z0-9][A-Za-z0-9._-]{1,63}"{{if not .AllowVanity}} disabled{{end}}>
|
pattern="[A-Za-z0-9][A-Za-z0-9._-]{1,63}">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,6 +35,11 @@
|
|||||||
</label>
|
</label>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<div class="drop" id="drop">
|
||||||
|
<input type="file" name="file" id="file" required>
|
||||||
|
<p class="hint" id="drop-hint">Choose a file, or drop one here.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="progress" id="progress" hidden>
|
<div class="progress" id="progress" hidden>
|
||||||
<div class="bar"><div class="fill" id="bar-fill"></div></div>
|
<div class="bar"><div class="fill" id="bar-fill"></div></div>
|
||||||
<p class="hint" id="progress-text"></p>
|
<p class="hint" id="progress-text"></p>
|
||||||
|
|||||||
Reference in New Issue
Block a user