Compare commits
2
Commits
main
...
c3f466565d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3f466565d | ||
|
|
1bfefe079e |
@@ -451,6 +451,14 @@ func (a *apiServer) handleRaw(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "not found", 404)
|
http.Error(w, "not found", 404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// #221: an attachment paste has no text content; raw view must serve the
|
||||||
|
// file itself, not empty text. Redirect to the /f/ serving route, which
|
||||||
|
// applies the same sniffed-mime + disposition safety rules.
|
||||||
|
if att, err := a.store.GetAttachmentForPaste(row.ID); err == nil && att != nil {
|
||||||
|
a.store.IncrementViews(row.ID, "", 0) // raw views always count (#49/#95)
|
||||||
|
http.Redirect(w, r, "/f/"+att.ID+"/"+att.Filename, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
// #34: content_type is attacker-controlled via the create API. Serving it
|
// #34: content_type is attacker-controlled via the create API. Serving it
|
||||||
// verbatim let a paste be stored with text/html (or image/svg+xml) and
|
// verbatim let a paste be stored with text/html (or image/svg+xml) and
|
||||||
// render as active content on this origin when fetched from /raw —
|
// render as active content on this origin when fetched from /raw —
|
||||||
|
|||||||
@@ -809,7 +809,7 @@ button[type="submit"]:focus-visible,
|
|||||||
font-size: 22px; padding: 0 4px; border-radius: var(--radius-sm);
|
font-size: 22px; padding: 0 4px; border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
.file-chip .file-chip-remove:hover { color: var(--danger, #c0392b); }
|
.file-chip .file-chip-remove:hover { color: var(--danger, #c0392b); }
|
||||||
.attachment-bar { display: flex; flex-direction: column; gap: 10px; }
|
.attachment-bar { display: flex; flex-direction: column; gap: 10px; padding: 12px; }
|
||||||
.attachment-chip {
|
.attachment-chip {
|
||||||
display: inline-flex; align-items: center; gap: 12px; align-self: flex-start;
|
display: inline-flex; align-items: center; gap: 12px; align-self: flex-start;
|
||||||
border: 1px solid var(--border); border-radius: var(--radius);
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
@@ -818,7 +818,15 @@ button[type="submit"]:focus-visible,
|
|||||||
}
|
}
|
||||||
.attachment-chip:hover { border-color: var(--accent); }
|
.attachment-chip:hover { border-color: var(--accent); }
|
||||||
.attachment-chip .attachment-size { color: var(--muted-fg); font-size: 19px; }
|
.attachment-chip .attachment-size { color: var(--muted-fg); font-size: 19px; }
|
||||||
.attachment-preview img { max-width: 480px; max-height: 360px; border-radius: var(--radius); border: 1px solid var(--border); }
|
/* #221: image pastes scale to fit the viewer box (no text box below), and
|
||||||
|
the link pill sits above the image without drifting out of place. */
|
||||||
|
.attachment-bar.is-image { align-items: flex-start; }
|
||||||
|
.attachment-bar.is-image .attachment-chip { max-width: 100%; }
|
||||||
|
.attachment-preview { max-width: 100%; }
|
||||||
|
.attachment-preview img {
|
||||||
|
display: block; max-width: 100%; max-height: 70vh; width: auto; height: auto;
|
||||||
|
object-fit: contain; border-radius: var(--radius); border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
/* #139: CSP-safe replacements for inline style attributes (style-src 'self') */
|
/* #139: CSP-safe replacements for inline style attributes (style-src 'self') */
|
||||||
.hidden { display: none; }
|
.hidden { display: none; }
|
||||||
|
|||||||
@@ -38,20 +38,22 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{if .Attachment}}
|
{{if .Attachment}}
|
||||||
<div class="float">
|
<div class="float">
|
||||||
<div class="attachment-bar">
|
<div class="attachment-bar{{if .IsImagePaste}} is-image{{end}}">
|
||||||
<a class="attachment-chip" href="/f/{{.Attachment.ID}}/{{.Attachment.Filename}}" data-mime="{{.Attachment.Mime}}">
|
<a class="attachment-chip" href="/f/{{.Attachment.ID}}/{{.Attachment.Filename}}" data-mime="{{.Attachment.Mime}}">
|
||||||
<span class="attachment-name">{{.Attachment.Filename}}</span>
|
<span class="attachment-name">{{.Attachment.Filename}}</span>
|
||||||
<span class="attachment-size">{{.Attachment.SizeHuman}}</span>
|
<span class="attachment-size">{{.Attachment.SizeHuman}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{if or (eq .Attachment.Mime "image/png") (eq .Attachment.Mime "image/jpeg") (eq .Attachment.Mime "image/gif") (eq .Attachment.Mime "image/webp")}}
|
{{if .IsImagePaste}}
|
||||||
<div class="attachment-preview"><img src="/f/{{.Attachment.ID}}/{{.Attachment.Filename}}" alt="{{.Attachment.Filename}}"></div>
|
<div class="attachment-preview"><img src="/f/{{.Attachment.ID}}/{{.Attachment.Filename}}" alt="{{.Attachment.Filename}}"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if not .IsImagePaste}}
|
||||||
<div class="float">
|
<div class="float">
|
||||||
<div class="code" id="code"><div class="gutter" id="gutter">{{.Gutter}}</div><div class="codebody" id="codebody">{{.ContentHTML}}</div></div>
|
<div class="code" id="code"><div class="gutter" id="gutter">{{.Gutter}}</div><div class="codebody" id="codebody">{{.ContentHTML}}</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="raw-content" value="{{.ContentAttr}}">
|
<input type="hidden" id="raw-content" value="{{.ContentAttr}}">
|
||||||
<script src="/static/paste.js" defer data-paste-id="{{.ID}}"></script>
|
<script src="/static/paste.js" defer data-paste-id="{{.ID}}"></script>
|
||||||
|
|||||||
+11
-3
@@ -245,28 +245,36 @@ func (h *Handlers) renderPaste(w http.ResponseWriter, row *store.PasteRow, justC
|
|||||||
if lang == "" {
|
if lang == "" {
|
||||||
lang = "text"
|
lang = "text"
|
||||||
}
|
}
|
||||||
summary := fmt.Sprintf("%s · %s · %d views · %s", lang, humanSize(len(row.Content)), row.ViewCount, agoString(row.CreatedAt))
|
|
||||||
// #38: one optional file attachment per paste; nil when none.
|
// #38: one optional file attachment per paste; nil when none.
|
||||||
attachment, err := h.Store.GetAttachmentForPaste(row.ID)
|
attachment, err := h.Store.GetAttachmentForPaste(row.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "db error", 500)
|
http.Error(w, "db error", 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// #221: for attachment pastes the stored text content is empty (the file
|
||||||
|
// replaced it), so size must come from the attachment blob, not
|
||||||
|
// len(row.Content), or the view details show a wrong size.
|
||||||
|
sizeBytes := len(row.Content)
|
||||||
|
if attachment != nil {
|
||||||
|
sizeBytes = int(attachment.Size)
|
||||||
|
}
|
||||||
|
summary := fmt.Sprintf("%s · %s · %d views · %s", lang, humanSize(sizeBytes), row.ViewCount, agoString(row.CreatedAt))
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"Page": "paste",
|
"Page": "paste",
|
||||||
"ID": row.ID,
|
"ID": row.ID,
|
||||||
"Title": row.Title.String,
|
"Title": row.Title.String,
|
||||||
"Language": row.Language.String,
|
"Language": row.Language.String,
|
||||||
"StatsSummary": summary,
|
"StatsSummary": summary,
|
||||||
"SizeHuman": humanSize(len(row.Content)),
|
"SizeHuman": humanSize(sizeBytes),
|
||||||
"HasPassword": row.PasswordHash.Valid,
|
"HasPassword": row.PasswordHash.Valid,
|
||||||
"BurnAfterRead": row.BurnAfterRead,
|
"BurnAfterRead": row.BurnAfterRead,
|
||||||
"CustomSlug": row.CustomSlug.String,
|
"CustomSlug": row.CustomSlug.String,
|
||||||
|
"IsImagePaste": attachment != nil && strings.HasPrefix(attachment.Mime, "image/"),
|
||||||
"ContentHTML": template.HTML(langpkg.HighlightCode(row.Content, row.Language.String)), // safe: HighlightCode escapes all non-span text
|
"ContentHTML": template.HTML(langpkg.HighlightCode(row.Content, row.Language.String)), // safe: HighlightCode escapes all non-span text
|
||||||
"ContentAttr": row.Content,
|
"ContentAttr": row.Content,
|
||||||
"Gutter": strings.TrimSuffix(gutter, "\n"),
|
"Gutter": strings.TrimSuffix(gutter, "\n"),
|
||||||
"LineCount": lines,
|
"LineCount": lines,
|
||||||
"SizeBytes": len(row.Content),
|
"SizeBytes": sizeBytes,
|
||||||
"CreatedAgo": agoString(row.CreatedAt),
|
"CreatedAgo": agoString(row.CreatedAt),
|
||||||
"CreatedAtUnix": row.CreatedAt,
|
"CreatedAtUnix": row.CreatedAt,
|
||||||
"ViewCount": row.ViewCount,
|
"ViewCount": row.ViewCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user