File size: 2,957 Bytes
cb43fbd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import DOMPurify from 'isomorphic-dompurify';

/**
 * HTML sanitisation for TREK.
 *
 * TREK currently has no rich-text editor and no user-provided HTML reaches
 * the database, so this module exists only to guard the handful of client
 * sites that interpolate user-controlled strings into a markup template
 * (today: the Journey suggestion banner). It is also the future home for
 * sanitisation if TipTap / Markdown ever ships.
 *
 * Why isomorphic-dompurify: works unchanged in browser (DOMPurify) and Node
 * (DOMPurify + jsdom). Tree-shakes correctly so the client bundle does not
 * pull jsdom.
 */

// Inline-only tags. Block-level markup (paragraphs, lists, headings) is not
// expected in the surfaces we render today, so we keep the allow-list minimal
// and rely on `sanitizeRichTextHtml` when a richer surface needs full prose.
const INLINE_TAGS = [
  'b',
  'strong',
  'i',
  'em',
  'u',
  's',
  'del',
  'ins',
  'mark',
  'code',
  'sub',
  'sup',
  'br',
  'span',
] as const;

const FULL_TAGS = [
  ...INLINE_TAGS,
  'p',
  'div',
  'ul',
  'ol',
  'li',
  'h1',
  'h2',
  'h3',
  'h4',
  'h5',
  'h6',
  'blockquote',
  'pre',
  'hr',
  'a',
] as const;

const SAFE_ATTRIBUTES = ['href', 'rel', 'target'] as const;

/**
 * Escapes the five HTML metacharacters so a raw string can be safely
 * interpolated into an HTML template. Use this BEFORE substitution when a
 * user-controlled value lands inside a markup-shaped translation string.
 *
 * This is *not* a substitute for `sanitizeInlineHtml`: escape input, then
 * sanitise the resulting template — both layers run together in `tHtml`.
 */
export function escapeHtml(value: string): string {
  return value
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

/**
 * Strict inline sanitiser. Use for short, mostly-text strings that may include
 * basic emphasis (`<strong>`, `<em>`, …) — e.g. the Journey suggestion banner
 * where a translated template embeds a user-controlled trip title.
 *
 * Drops every tag outside the inline allow-list, strips all attributes, and
 * blocks every URL scheme except http/https/mailto/tel via DOMPurify's
 * built-in URL allow-list.
 */
export function sanitizeInlineHtml(html: string): string {
  if (!html) return '';
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: [...INLINE_TAGS],
    ALLOWED_ATTR: [],
    KEEP_CONTENT: true,
    ALLOW_DATA_ATTR: false,
  });
}

/**
 * Permissive rich-text sanitiser. Use when a surface legitimately renders a
 * prose document (lists, paragraphs, links). Keeps the same tag families as
 * the inline sanitiser plus block-level markup and anchors with safe attrs.
 */
export function sanitizeRichTextHtml(html: string): string {
  if (!html) return '';
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: [...FULL_TAGS],
    ALLOWED_ATTR: [...SAFE_ATTRIBUTES],
    ALLOW_DATA_ATTR: false,
  });
}