This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / markup / sanitize.ts
4.2 kB 158 lines
1import sanitizeHtml from "sanitize-html"; 2import { isRepoRelative, mediaSrcset, mediaUrl, treeUrl } from "./paths"; 3import type { MarkupContext } from "./paths"; 4 5const HEADINGS = ["h1", "h2", "h3", "h4", "h5", "h6"]; 6 7// mirrors appview/pages/markup/sanitizer, which is bluemonday's UGC policy plus 8// the elements our own extensions emit. markdown renders with raw html enabled, 9// so anything hand written in a readme lands here too 10const ALLOWED_TAGS = [ 11 ...HEADINGS, 12 "p", 13 "br", 14 "hr", 15 "div", 16 "span", 17 "section", 18 "blockquote", 19 "pre", 20 "code", 21 "kbd", 22 "samp", 23 "var", 24 "tt", 25 "b", 26 "strong", 27 "i", 28 "em", 29 "u", 30 "s", 31 "strike", 32 "del", 33 "ins", 34 "sub", 35 "sup", 36 "small", 37 "mark", 38 "a", 39 "img", 40 "picture", 41 "source", 42 "video", 43 "ul", 44 "ol", 45 "li", 46 "dl", 47 "dt", 48 "dd", 49 "table", 50 "thead", 51 "tbody", 52 "tfoot", 53 "tr", 54 "th", 55 "td", 56 "caption", 57 "colgroup", 58 "col", 59 "details", 60 "summary", 61 "figure", 62 "figcaption", 63 "abbr", 64 "bdo", 65 "cite", 66 "dfn", 67 "q", 68 "ruby", 69 "rt", 70 "rp", 71 "time", 72 "wbr", 73 "center", 74 "input", 75 "label" 76]; 77 78// bluemonday's standard attributes 79const GLOBAL_ATTRIBUTES = ["id", "title", "dir", "lang", "align"]; 80 81const ALLOWED_ATTRIBUTES: sanitizeHtml.IOptions["allowedAttributes"] = { 82 "*": GLOBAL_ATTRIBUTES, 83 a: ["href", "name", "rel", "aria-hidden"], 84 img: ["src", "srcset", "alt", "width", "height", "loading"], 85 source: ["src", "srcset", "type", "media"], 86 video: ["src", "poster", "controls", "autoplay", "loop", "muted", "width", "height"], 87 // the tasklist plugin renders disabled checkboxes tied to their labels 88 input: ["type", "checked", "disabled"], 89 label: ["for"], 90 th: ["colspan", "rowspan", "scope"], 91 td: ["colspan", "rowspan"], 92 col: ["span", "width"], 93 colgroup: ["span"], 94 ol: ["start", "type", "reversed"], 95 details: ["open"], 96 time: ["datetime"], 97 abbr: ["title"] 98}; 99 100// classes are allowlisted per tag, so a readme cannot reach the app's own styles 101const ALLOWED_CLASSES: sanitizeHtml.IOptions["allowedClasses"] = { 102 a: ["anchor", "mention", "footnote-ref", "footnote-backref", "footnote-anchor"], 103 sup: ["footnote-ref"], 104 hr: ["footnotes-sep"], 105 section: ["footnotes"], 106 ol: ["footnotes-list", "task-list-container"], 107 ul: ["task-list-container"], 108 li: ["footnote-item", "task-list-item"], 109 input: ["task-list-item-checkbox"], 110 label: ["task-list-item-label"], 111 div: ["markdown-alert", "markdown-alert-*"], 112 p: ["markdown-alert-title"], 113 code: ["language-*"] 114}; 115 116const externalRel = (href: string): string | undefined => 117 isRepoRelative(href) || href.startsWith("#") ? undefined : "nofollow noopener noreferrer"; 118 119const optionsFor = (ctx: MarkupContext): sanitizeHtml.IOptions => ({ 120 allowedTags: ALLOWED_TAGS, 121 allowedAttributes: ALLOWED_ATTRIBUTES, 122 allowedClasses: ALLOWED_CLASSES, 123 allowedSchemes: ["http", "https", "mailto"], 124 allowedSchemesAppliedToAttributes: ["href", "src", "srcset", "poster"], 125 // resolving urls here rather than in a renderer rule catches the ones 126 // written as raw html too 127 transformTags: { 128 a: (tagName, attribs) => { 129 const href = attribs.href ?? ""; 130 // a mention already points at a profile 131 const rewritten = 132 isRepoRelative(href) && attribs.class !== "mention" ? treeUrl(href, ctx) : href; 133 const rel = externalRel(rewritten); 134 return { tagName, attribs: { ...attribs, href: rewritten, ...(rel ? { rel } : {}) } }; 135 }, 136 img: (tagName, attribs) => ({ tagName, attribs: resolveMedia(attribs, ctx) }), 137 source: (tagName, attribs) => ({ tagName, attribs: resolveMedia(attribs, ctx) }), 138 video: (tagName, attribs) => ({ tagName, attribs: resolveMedia(attribs, ctx) }) 139 }, 140 // the tasklist checkboxes are the only inputs we render 141 exclusiveFilter: (frame) => frame.tag === "input" && frame.attribs.type !== "checkbox" 142}); 143 144const resolveMedia = ( 145 attribs: Record<string, string>, 146 ctx: MarkupContext 147): Record<string, string> => { 148 const resolved = { ...attribs }; 149 for (const key of ["src", "poster"]) { 150 const value = resolved[key]; 151 if (value) resolved[key] = mediaUrl(value, ctx); 152 } 153 if (resolved.srcset) resolved.srcset = mediaSrcset(resolved.srcset, ctx); 154 return resolved; 155}; 156 157export const sanitizeMarkup = (html: string, ctx: MarkupContext): string => 158 sanitizeHtml(html, optionsFor(ctx));