-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
html_escape.dart
279 lines (256 loc) · 9.62 KB
/
html_escape.dart
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of dart.convert;
/// A `String` converter that converts characters to HTML entities.
///
/// This is intended to sanitize text before inserting the text into an HTML
/// document. Characters that are meaningful in HTML are converted to
/// HTML entities (like `&` for `&`).
///
/// The general converter escapes all characters that are meaningful in HTML
/// attributes or normal element context. Elements with special content types
/// (like CSS or JavaScript) may need a more specialized escaping that
/// understands that content type.
///
/// If the context where the text will be inserted is known in more detail,
/// it's possible to omit escaping some characters (like quotes when not
/// inside an attribute value).
///
/// The escaped text should only be used inside quoted HTML attributes values
/// or as text content of a normal element. Using the escaped text inside a
/// tag, but not inside a quoted attribute value, is still dangerous.
const HtmlEscape htmlEscape = HtmlEscape();
/// HTML escape modes.
///
/// Allows specifying a mode for HTML escaping that depends on the context
/// where the escaped result is going to be used.
/// The relevant contexts are:
///
/// * as text content of an HTML element.
/// * as value of a (single- or double-) quoted attribute value.
///
/// All modes require escaping of `&` (ampersand) characters, and may
/// enable escaping of more characters.
///
/// Custom escape modes can be created using the [HtmlEscapeMode.new]
/// constructor.
///
/// Example:
/// ```dart
/// const htmlEscapeMode = HtmlEscapeMode(
/// name: 'custom',
/// escapeLtGt: true,
/// escapeQuot: false,
/// escapeApos: false,
/// escapeSlash: false,
/// );
///
/// const HtmlEscape htmlEscape = HtmlEscape(htmlEscapeMode);
/// String unescaped = 'Text & subject';
/// String escaped = htmlEscape.convert(unescaped);
/// print(escaped); // Text & subject
///
/// unescaped = '10 > 1 and 1 < 10';
/// escaped = htmlEscape.convert(unescaped);
/// print(escaped); // 10 > 1 and 1 < 10
///
/// unescaped = "Single-quoted: 'text'";
/// escaped = htmlEscape.convert(unescaped);
/// print(escaped); // Single-quoted: 'text'
///
/// unescaped = 'Double-quoted: "text"';
/// escaped = htmlEscape.convert(unescaped);
/// print(escaped); // Double-quoted: "text"
///
/// unescaped = 'Path: /system/';
/// escaped = htmlEscape.convert(unescaped);
/// print(escaped); // Path: /system/
/// ```
final class HtmlEscapeMode {
final String _name;
/// Whether to escape '<' and '>'.
final bool escapeLtGt;
/// Whether to escape '"' (quote).
final bool escapeQuot;
/// Whether to escape "'" (apostrophe).
final bool escapeApos;
/// Whether to escape "/" (forward slash, solidus).
///
/// Escaping a slash is recommended to avoid cross-site scripting attacks by
/// [the Open Web Application Security Project](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content)
final bool escapeSlash;
/// Default escaping mode, which escapes all characters.
///
/// The result of such an escaping is usable both in element content and
/// in any attribute value.
///
/// The escaping only works for elements with normal HTML content,
/// and not, for example, for script or style element content,
/// which require escapes matching their particular content syntax.
static const HtmlEscapeMode unknown =
HtmlEscapeMode._('unknown', true, true, true, true);
/// Escaping mode for text going into double-quoted HTML attribute values.
///
/// The result should not be used as the content of an unquoted
/// or single-quoted attribute value.
///
/// Escapes double quotes (`"`) but not single quotes (`'`),
/// and escapes `<` and `>` characters because they are not allowed
/// in strict XHTML attributes
static const HtmlEscapeMode attribute =
HtmlEscapeMode._('attribute', true, true, false, false);
/// Escaping mode for text going into single-quoted HTML attribute values.
///
/// The result should not be used as the content of an unquoted
/// or double-quoted attribute value.
///
/// Escapes single quotes (`'`) but not double quotes (`"`),
/// and escapes `<` and `>` characters because they are not allowed
/// in strict XHTML attributes.
static const HtmlEscapeMode sqAttribute =
HtmlEscapeMode._('attribute', true, false, true, false);
/// Escaping mode for text going into HTML element content.
///
/// The escaping only works for elements with normal HTML content,
/// and not, for example, for script or style element content,
/// which require escapes matching their particular content syntax.
///
/// Escapes `<` and `>` characters.
static const HtmlEscapeMode element =
HtmlEscapeMode._('element', true, false, false, false);
const HtmlEscapeMode._(this._name, this.escapeLtGt, this.escapeQuot,
this.escapeApos, this.escapeSlash);
/// Create a custom escaping mode.
///
/// All modes escape `&`.
/// The mode can further be set to escape `<` and `>` ([escapeLtGt]),
/// `"` ([escapeQuot]), `'` ([escapeApos]), and/or `/` ([escapeSlash]).
const HtmlEscapeMode(
{String name = "custom",
this.escapeLtGt = false,
this.escapeQuot = false,
this.escapeApos = false,
this.escapeSlash = false})
: _name = name;
String toString() => _name;
}
/// Converter which escapes characters with special meaning in HTML.
///
/// The converter finds characters that are significant in the HTML source and
/// replaces them with corresponding HTML entities.
///
/// The characters that need escaping in HTML are:
///
/// * `&` (ampersand) always needs to be escaped.
/// * `<` (less than) and `>` (greater than) when inside an element.
/// * `"` (quote) when inside a double-quoted attribute value.
/// * `'` (apostrophe) when inside a single-quoted attribute value.
/// Apostrophe is escaped as `'` instead of `'` since
/// not all browsers understand `'`.
/// * `/` (slash) is recommended to be escaped because it may be used
/// to terminate an element in some HTML dialects.
///
/// Escaping `>` (greater than) isn't necessary, but the result is often
/// found to be easier to read if greater-than is also escaped whenever
/// less-than is.
///
/// Example:
/// ```dart
/// const HtmlEscape htmlEscape = HtmlEscape();
/// String unescaped = 'Text & subject';
/// String escaped = htmlEscape.convert(unescaped);
/// print(escaped); // Text & subject
///
/// unescaped = '10 > 1 and 1 < 10';
/// escaped = htmlEscape.convert(unescaped);
/// print(escaped); // 10 > 1 and 1 < 10
///
/// unescaped = "Single-quoted: 'text'";
/// escaped = htmlEscape.convert(unescaped);
/// print(escaped); // Single-quoted: 'text'
///
/// unescaped = 'Double-quoted: "text"';
/// escaped = htmlEscape.convert(unescaped);
/// print(escaped); // Double-quoted: "text"
///
/// unescaped = 'Path: /system/';
/// escaped = htmlEscape.convert(unescaped);
/// print(escaped); // Path: /system/
/// ```
final class HtmlEscape extends Converter<String, String> {
/// The [HtmlEscapeMode] used by the converter.
final HtmlEscapeMode mode;
/// Create converter that escapes HTML characters.
///
/// If [mode] is provided as either [HtmlEscapeMode.attribute] or
/// [HtmlEscapeMode.element], only the corresponding subset of HTML
/// characters is escaped.
/// The default is to escape all HTML characters.
const HtmlEscape([this.mode = HtmlEscapeMode.unknown]);
String convert(String text) {
var val = _convert(text, 0, text.length);
return val == null ? text : val;
}
/// Converts the substring of text from start to end.
///
/// Returns `null` if no changes were necessary, otherwise returns
/// the converted string.
String? _convert(String text, int start, int end) {
StringBuffer? result;
for (var i = start; i < end; i++) {
var ch = text[i];
String? replacement;
switch (ch) {
case '&':
replacement = '&';
break;
case '"':
if (mode.escapeQuot) replacement = '"';
break;
case "'":
if (mode.escapeApos) replacement = ''';
break;
case '<':
if (mode.escapeLtGt) replacement = '<';
break;
case '>':
if (mode.escapeLtGt) replacement = '>';
break;
case '/':
if (mode.escapeSlash) replacement = '/';
break;
}
if (replacement != null) {
result ??= StringBuffer();
if (i > start) result.write(text.substring(start, i));
result.write(replacement);
start = i + 1;
}
}
if (result == null) return null;
if (end > start) result.write(text.substring(start, end));
return result.toString();
}
StringConversionSink startChunkedConversion(Sink<String> sink) {
return _HtmlEscapeSink(this,
sink is StringConversionSink ? sink : StringConversionSink.from(sink));
}
}
class _HtmlEscapeSink extends StringConversionSink {
final HtmlEscape _escape;
final StringConversionSink _sink;
_HtmlEscapeSink(this._escape, this._sink);
void addSlice(String chunk, int start, int end, bool isLast) {
var val = _escape._convert(chunk, start, end);
if (val == null) {
_sink.addSlice(chunk, start, end, isLast);
} else {
_sink.add(val);
if (isLast) _sink.close();
}
}
void close() {
_sink.close();
}
}