- MarkdownView: add rehype-sanitize between rehype-raw and rehype-katex to strip scripts/event-handlers/javascript: URLs from user-authored markdown (was stored XSS, also affected the public /pg/* route); keep className on code/span/div so KaTeX and syntax highlighting still work. Add rehype-sanitize ^6.0.0 to deps and lockfile. - MarkdownContent / StandaloneMarkdownPage: parse markdown content via parseMarkdownContent() instead of an unguarded JSON.parse, so a single corrupt/legacy record no longer white-screens the whole page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
4.2 KiB
JavaScript
118 lines
4.2 KiB
JavaScript
import React from "react";
|
|
import ReactMarkdown from "react-markdown";
|
|
import remarkMath from "remark-math";
|
|
import rehypeKatex from "rehype-katex";
|
|
import rehypeRaw from "rehype-raw";
|
|
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
import remarkGfm from "remark-gfm";
|
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
import "katex/dist/katex.min.css";
|
|
import "./MarkdownView.css";
|
|
import {useLinks} from "../../utils/queries/markdown-queries";
|
|
|
|
const Translate = ({variable, value}) => {
|
|
if (variable.type.base_type === "markdown" || variable.type.base_type === "string" || variable.type.base_type === "enum") {
|
|
return value;
|
|
}
|
|
if(variable.type.base_type === "list"){
|
|
if (!variable.type.extend_type)
|
|
return [];
|
|
|
|
return (value || []).map((item, index) => Translate({
|
|
variable: {name: index, type: variable.type.extend_type},
|
|
value: item,
|
|
})).map((item) => variable.type.definition.iter_layout.replaceAll('<item/>', item)).join("");
|
|
}
|
|
if(variable.type.base_type === "template"){
|
|
return ParseTemplate({
|
|
template: variable.type.definition.template,
|
|
variables: value
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
const ParseTemplate = ({template, variables}) => {
|
|
if(!template || !Array.isArray(template.parameters)) return '';
|
|
const vars = variables || {};
|
|
let res = template.layout ?? '';
|
|
for (const parameter of template.parameters) {
|
|
res = res.replaceAll(`<${parameter.name}/>`, Translate({
|
|
variable: parameter,
|
|
value: vars[parameter.name]
|
|
}));
|
|
}
|
|
return res;
|
|
};
|
|
|
|
|
|
// Markdown content is authored by users and rendered for everyone
|
|
// (including the unauthenticated /pg/* route), so raw HTML must be
|
|
// sanitized to prevent stored XSS. className is kept on code/span/div so
|
|
// syntax highlighting and KaTeX (which runs after sanitize) still work;
|
|
// scripts, event handlers and javascript: URLs are stripped.
|
|
const sanitizeSchema = {
|
|
...defaultSchema,
|
|
attributes: {
|
|
...defaultSchema.attributes,
|
|
code: [...(defaultSchema.attributes?.code || []), ["className"]],
|
|
span: [...(defaultSchema.attributes?.span || []), ["className"]],
|
|
div: [...(defaultSchema.attributes?.div || []), ["className"]],
|
|
},
|
|
};
|
|
|
|
const MarkdownView = ({ content, template, height="auto" }) => {
|
|
const {data: links, isLoading} = useLinks();
|
|
|
|
if (isLoading)
|
|
return (<p>Loading...</p>);
|
|
const linkDefinitions = "\n<!-- Definitions -->\n" + links.join("\n");
|
|
const _template = template || {
|
|
parameters: [
|
|
{
|
|
name: "markdown",
|
|
type: {
|
|
base_type: "markdown",
|
|
definition: {}
|
|
}
|
|
}
|
|
],
|
|
layout: "<markdown/>",
|
|
title: "default"
|
|
};
|
|
|
|
return (
|
|
<div className="markdown-preview" style={{height}}>
|
|
<ReactMarkdown
|
|
children={ParseTemplate({
|
|
template: _template,
|
|
variables: content
|
|
}) + "\n" + linkDefinitions}
|
|
remarkPlugins={[remarkMath, remarkGfm]}
|
|
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema], rehypeKatex]}
|
|
components={{
|
|
code({ node, inline, className, children, ...props }) {
|
|
const match = /language-(\w+)/.exec(className || "");
|
|
return !inline && match ? (
|
|
<SyntaxHighlighter
|
|
style={okaidia}
|
|
language={match[1]}
|
|
PreTag="div"
|
|
{...props}
|
|
>
|
|
{String(children).replace(/\n$/, "")}
|
|
</SyntaxHighlighter>
|
|
) : (
|
|
<code className={className} {...props}>
|
|
{children}
|
|
</code>
|
|
);
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MarkdownView; |