mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
feat(api): ✨ Allow more HTML tags in Markdown
This commit is contained in:
parent
4ce5dfeae3
commit
b979daa39a
|
|
@ -1,7 +1,7 @@
|
||||||
import { applyConfig, auth, handleZodError, qs } from "@api";
|
import { applyConfig, auth, handleZodError, qs } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { sanitizeHtml, sanitizedHtmlStrip } from "@sanitization";
|
import { sanitizedHtmlStrip } from "@sanitization";
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
|
|
@ -224,17 +224,25 @@ export default (app: Hono) =>
|
||||||
self.source.fields = [];
|
self.source.fields = [];
|
||||||
for (const field of fields_attributes) {
|
for (const field of fields_attributes) {
|
||||||
// Can be Markdown or plaintext, also has emojis
|
// Can be Markdown or plaintext, also has emojis
|
||||||
const parsedName = await contentToHtml({
|
const parsedName = await contentToHtml(
|
||||||
|
{
|
||||||
"text/markdown": {
|
"text/markdown": {
|
||||||
content: field.name,
|
content: field.name,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const parsedValue = await contentToHtml({
|
const parsedValue = await contentToHtml(
|
||||||
|
{
|
||||||
"text/markdown": {
|
"text/markdown": {
|
||||||
content: field.value,
|
content: field.value,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
// Parse emojis
|
// Parse emojis
|
||||||
const nameEmojis = await parseEmojis(parsedName);
|
const nameEmojis = await parseEmojis(parsedName);
|
||||||
|
|
|
||||||
|
|
@ -394,5 +394,37 @@ describe(meta.route, () => {
|
||||||
"uwu <script>alert('Hello, world!');</script>",
|
"uwu <script>alert('Hello, world!');</script>",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should rewrite all image and video src to go through proxy", async () => {
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(new URL(meta.route, config.http.base_url), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
status: "<img src='https://example.com/image.jpg'> <video src='https://example.com/video.mp4'> Test!",
|
||||||
|
federate: "false",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("content-type")).toBe(
|
||||||
|
"application/json",
|
||||||
|
);
|
||||||
|
|
||||||
|
const object = (await response.json()) as APIStatus;
|
||||||
|
// Proxy url is base_url/media/proxy/<base64url encoded url>
|
||||||
|
expect(object.content).toBe(
|
||||||
|
`<p><img src="${config.http.base_url}/media/proxy/${Buffer.from(
|
||||||
|
"https://example.com/image.jpg",
|
||||||
|
).toString("base64url")}"> <video src="${
|
||||||
|
config.http.base_url
|
||||||
|
}/media/proxy/${Buffer.from(
|
||||||
|
"https://example.com/video.mp4",
|
||||||
|
).toString("base64url")}"> Test!</p>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,29 @@ export const sanitizedHtmlStrip = (html: string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sanitizeHtmlInline = async (
|
||||||
|
html: string,
|
||||||
|
extraConfig?: IFilterXSSOptions,
|
||||||
|
) => {
|
||||||
|
return sanitizeHtml(html, {
|
||||||
|
whiteList: {
|
||||||
|
a: ["href", "title", "target", "rel", "class"],
|
||||||
|
p: ["class"],
|
||||||
|
b: ["class"],
|
||||||
|
i: ["class"],
|
||||||
|
em: ["class"],
|
||||||
|
strong: ["class"],
|
||||||
|
del: ["class"],
|
||||||
|
u: ["class"],
|
||||||
|
font: ["color", "size", "face", "class"],
|
||||||
|
strike: ["class"],
|
||||||
|
mark: ["class"],
|
||||||
|
small: ["class"],
|
||||||
|
},
|
||||||
|
...extraConfig,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const sanitizeHtml = async (
|
export const sanitizeHtml = async (
|
||||||
html: string,
|
html: string,
|
||||||
extraConfig?: IFilterXSSOptions,
|
extraConfig?: IFilterXSSOptions,
|
||||||
|
|
@ -28,6 +51,34 @@ export const sanitizeHtml = async (
|
||||||
ol: ["class"],
|
ol: ["class"],
|
||||||
li: ["class"],
|
li: ["class"],
|
||||||
blockquote: ["class"],
|
blockquote: ["class"],
|
||||||
|
h1: ["class"],
|
||||||
|
h2: ["class"],
|
||||||
|
h3: ["class"],
|
||||||
|
h4: ["class"],
|
||||||
|
h5: ["class"],
|
||||||
|
h6: ["class"],
|
||||||
|
img: ["src", "alt", "title", "class"],
|
||||||
|
font: ["color", "size", "face", "class"],
|
||||||
|
table: ["class"],
|
||||||
|
tr: ["class"],
|
||||||
|
td: ["class"],
|
||||||
|
th: ["class"],
|
||||||
|
tbody: ["class"],
|
||||||
|
thead: ["class"],
|
||||||
|
tfoot: ["class"],
|
||||||
|
hr: ["class"],
|
||||||
|
strike: ["class"],
|
||||||
|
figcaption: ["class"],
|
||||||
|
figure: ["class"],
|
||||||
|
mark: ["class"],
|
||||||
|
summary: ["class"],
|
||||||
|
details: ["class"],
|
||||||
|
caption: ["class"],
|
||||||
|
small: ["class"],
|
||||||
|
video: ["class", "src", "controls"],
|
||||||
|
audio: ["class", "src", "controls"],
|
||||||
|
source: ["src", "type"],
|
||||||
|
track: ["src", "label", "kind"],
|
||||||
},
|
},
|
||||||
stripIgnoreTag: false,
|
stripIgnoreTag: false,
|
||||||
escapeHtml: (unsafeHtml) =>
|
escapeHtml: (unsafeHtml) =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue