feat: escapes html in guestbook, and adds a missing label

This commit is contained in:
Lewis Wynne 2026-03-26 22:01:17 +00:00
parent e431533a39
commit 331d843f68
4 changed files with 34 additions and 7 deletions

View file

@ -1,3 +1,12 @@
export function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
export function formatDate(date: Date): string { export function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0'); const m = String(date.getMonth() + 1).padStart(2, '0');

View file

@ -2,7 +2,7 @@
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import { getApprovedEntries, type GuestbookEntry } from '../lib/db'; import { getApprovedEntries, type GuestbookEntry } from '../lib/db';
import { formatDate, formatListItem } from '../lib/format'; import { formatDate, formatListItem, escapeHtml } from '../lib/format';
import { organizePostsByCategory, getSlug, enrichPostsWithDates } from '../lib/md'; import { organizePostsByCategory, getSlug, enrichPostsWithDates } from '../lib/md';
import { getTxtFiles } from '../lib/txt'; import { getTxtFiles } from '../lib/txt';
import { DEFAULT_CATEGORY, SECTIONS, SUBDOMAINS } from '../lib/consts'; import { DEFAULT_CATEGORY, SECTIONS, SUBDOMAINS } from '../lib/consts';
@ -64,13 +64,18 @@ const urls = [
<section data-section={SECTIONS.guestbook}> <section data-section={SECTIONS.guestbook}>
<pre class="guestbook-entries" set:html={guestbookEntries.map((e, i) => { <pre class="guestbook-entries" set:html={guestbookEntries.map((e, i) => {
const prefix = i === 0 ? labelPrefix(SECTIONS.guestbook, `?just=${SECTIONS.guestbook}`) : blankPrefix; const prefix = i === 0 ? labelPrefix(SECTIONS.guestbook, `?just=${SECTIONS.guestbook}`) : blankPrefix;
const nameHtml = e.url ? `<a href="${e.url}"><b>${e.name}</b></a>` : `<b>${e.name}</b>`; const safeName = escapeHtml(e.name);
return `<span class="guestbook-entry" style="padding-left: ${labelWidth + 12}ch; text-indent: -${labelWidth + 12}ch;"><span class="list-meta">${prefix}<span class="muted">${formatDate(e.createdAt)}</span> </span>${nameHtml} ${e.message.replace(/\n/g, ' ')}</span>`; const safeMessage = escapeHtml(e.message.replace(/\n/g, ' '));
const nameHtml = e.url ? `<a href="${escapeHtml(e.url)}"><b>${safeName}</b></a>` : `<b>${safeName}</b>`;
return `<span class="guestbook-entry" style="padding-left: ${labelWidth + 12}ch; text-indent: -${labelWidth + 12}ch;"><span class="list-meta">${prefix}<span class="muted">${formatDate(e.createdAt)}</span> </span>${nameHtml} ${safeMessage}</span>`;
}).join('')} /> }).join('')} />
<form id="guestbook-form" class="guestbook-form" style={`margin-left: ${labelWidth + 12}ch`}> <form id="guestbook-form" class="guestbook-form" style={`margin-left: ${labelWidth + 12}ch`}>
<input type="text" name="name" placeholder="name" required maxlength="100" /><br /> <label class="sr-only" for="gb-name">name</label>
<input type="text" name="message" placeholder="message" required maxlength="500" /><br /> <input id="gb-name" type="text" name="name" placeholder="name" required maxlength="100" /><br />
<input type="url" name="url" placeholder="url (optional)" maxlength="200" /><br /> <label class="sr-only" for="gb-message">message</label>
<input id="gb-message" type="text" name="message" placeholder="message" required maxlength="500" /><br />
<label class="sr-only" for="gb-url">url</label>
<input id="gb-url" type="url" name="url" placeholder="url (optional)" maxlength="200" /><br />
<button type="submit">sign</button> <button type="submit">sign</button>
<span id="guestbook-status"></span> <span id="guestbook-status"></span>
</form> </form>

View file

@ -31,7 +31,8 @@ export function initGuestbookForm() {
} else if (res.status === 429) { } else if (res.status === 429) {
status.textContent = ' too many requests, try later.'; status.textContent = ' too many requests, try later.';
} else { } else {
status.textContent = ' error'; const body = await res.json().catch(() => null);
status.textContent = body?.error ? ` ${body.error}` : ' error';
} }
} catch { } catch {
status.textContent = ' failed'; status.textContent = ' failed';

View file

@ -125,3 +125,15 @@ html[data-compact] .guestbook-entry {
html[data-compact] .guestbook-form { html[data-compact] .guestbook-form {
margin-left: 0 !important; margin-left: 0 !important;
} }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}