2
2
// for details. All rights reserved. Use of this source code is governed by a
3
3
// BSD-style license that can be found in the LICENSE file.
4
4
5
- import 'package:html/parser.dart' show parse;
5
+ import 'package:html/dom.dart' as dom;
6
+ import 'package:html/parser.dart' show parseFragment;
7
+
6
8
import 'package:markdown/markdown.dart' as md;
7
9
8
10
abstract class DocumentationRenderer {
9
11
DocumentationRenderResult render (
10
12
List <md.Node > nodes, {
11
13
required bool processFullDocs,
14
+ required bool sanitizeHtml,
12
15
});
13
16
}
14
17
@@ -19,16 +22,16 @@ class DocumentationRendererHtml implements DocumentationRenderer {
19
22
DocumentationRenderResult render (
20
23
List <md.Node > nodes, {
21
24
required bool processFullDocs,
25
+ required bool sanitizeHtml,
22
26
}) {
23
27
if (nodes.isEmpty) {
24
28
return DocumentationRenderResult .empty;
25
29
}
30
+
26
31
var rawHtml = md.HtmlRenderer ().render (nodes);
27
- var asHtmlDocument = parse (rawHtml);
28
- for (var s in asHtmlDocument.querySelectorAll ('script' )) {
29
- s.remove ();
30
- }
31
- for (var pre in asHtmlDocument.querySelectorAll ('pre' )) {
32
+ var asHtmlFragment = parseFragment (rawHtml);
33
+
34
+ for (var pre in asHtmlFragment.querySelectorAll ('pre' )) {
32
35
if (pre.children.length > 1 && pre.children.first.localName != 'code' ) {
33
36
continue ;
34
37
}
@@ -43,15 +46,21 @@ class DocumentationRendererHtml implements DocumentationRenderer {
43
46
// Assume the user intended Dart if there are no other classes present.
44
47
if (! specifiesLanguage) pre.classes.add ('language-dart' );
45
48
}
49
+
50
+ if (sanitizeHtml) {
51
+ _sanitize (asHtmlFragment);
52
+ }
53
+
46
54
var asHtml = '' ;
47
55
48
56
if (processFullDocs) {
49
57
// `trim` fixes an issue with line ending differences between Mac and
50
58
// Windows.
51
- asHtml = (asHtmlDocument.body ? .innerHtml ?? '' ) .trim ();
59
+ asHtml = asHtmlFragment.outerHtml .trim ();
52
60
}
53
- var children = asHtmlDocument.body? .children ?? [];
54
- var asOneLiner = children.isEmpty ? '' : children.first.innerHtml;
61
+ var asOneLiner = asHtmlFragment.children.isEmpty
62
+ ? ''
63
+ : asHtmlFragment.children.first.innerHtml;
55
64
56
65
return DocumentationRenderResult (asHtml: asHtml, asOneLiner: asOneLiner);
57
66
}
@@ -60,9 +69,259 @@ class DocumentationRendererHtml implements DocumentationRenderer {
60
69
class DocumentationRenderResult {
61
70
static const empty = DocumentationRenderResult (asHtml: '' , asOneLiner: '' );
62
71
63
- final String /*?*/ asHtml;
72
+ final String asHtml;
64
73
final String asOneLiner;
65
74
66
75
const DocumentationRenderResult (
67
76
{required this .asHtml, required this .asOneLiner});
68
77
}
78
+
79
+ bool _allowClassName (String className) =>
80
+ className == 'deprecated' || className.startsWith ('language-' );
81
+
82
+ Iterable <String > _addLinkRel (String uri) {
83
+ final u = Uri .tryParse (uri);
84
+ if (u != null && u.host.isNotEmpty) {
85
+ // TODO(jonasfj): Consider allowing non-ugc links for trusted sites.
86
+ return ['ugc' ];
87
+ }
88
+ return [];
89
+ }
90
+
91
+ void _sanitize (dom.Node node) {
92
+ if (node is dom.Element ) {
93
+ final tagName = node.localName! .toUpperCase ();
94
+ if (! _allowedElements.contains (tagName)) {
95
+ node.remove ();
96
+ return ;
97
+ }
98
+ node.attributes.removeWhere ((k, v) {
99
+ final attrName = k.toString ();
100
+ if (attrName == 'class' ) {
101
+ node.classes.removeWhere ((cn) => ! _allowClassName (cn));
102
+ return node.classes.isEmpty;
103
+ }
104
+ return ! _isAttributeAllowed (tagName, attrName, v);
105
+ });
106
+ if (tagName == 'A' ) {
107
+ final href = node.attributes['href' ];
108
+ if (href != null ) {
109
+ final rels = _addLinkRel (href);
110
+ if (rels.isNotEmpty) {
111
+ node.attributes['rel' ] = rels.join (' ' );
112
+ }
113
+ }
114
+ }
115
+ }
116
+ if (node.hasChildNodes ()) {
117
+ // doing it in reverse order, because we could otherwise skip one, when a
118
+ // node is removed...
119
+ for (var i = node.nodes.length - 1 ; i >= 0 ; i-- ) {
120
+ _sanitize (node.nodes[i]);
121
+ }
122
+ }
123
+ }
124
+
125
+ bool _isAttributeAllowed (String tagName, String attrName, String value) {
126
+ if (_alwaysAllowedAttributes.contains (attrName)) return true ;
127
+
128
+ // Special validators for special attributes on special tags (href/src/cite)
129
+ final attributeValidators = _elementAttributeValidators[tagName];
130
+ if (attributeValidators == null ) {
131
+ return false ;
132
+ }
133
+
134
+ final validator = attributeValidators[attrName];
135
+ if (validator == null ) {
136
+ return false ;
137
+ }
138
+
139
+ return validator (value);
140
+ }
141
+
142
+ // Inspired by the set of HTML tags allowed in GFM.
143
+ final _allowedElements = < String > {
144
+ 'H1' ,
145
+ 'H2' ,
146
+ 'H3' ,
147
+ 'H4' ,
148
+ 'H5' ,
149
+ 'H6' ,
150
+ 'H7' ,
151
+ 'H8' ,
152
+ 'BR' ,
153
+ 'B' ,
154
+ 'I' ,
155
+ 'STRONG' ,
156
+ 'EM' ,
157
+ 'A' ,
158
+ 'PRE' ,
159
+ 'CODE' ,
160
+ 'IMG' ,
161
+ 'TT' ,
162
+ 'DIV' ,
163
+ 'INS' ,
164
+ 'DEL' ,
165
+ 'SUP' ,
166
+ 'SUB' ,
167
+ 'P' ,
168
+ 'OL' ,
169
+ 'UL' ,
170
+ 'TABLE' ,
171
+ 'THEAD' ,
172
+ 'TBODY' ,
173
+ 'TFOOT' ,
174
+ 'BLOCKQUOTE' ,
175
+ 'DL' ,
176
+ 'DT' ,
177
+ 'DD' ,
178
+ 'KBD' ,
179
+ 'Q' ,
180
+ 'SAMP' ,
181
+ 'VAR' ,
182
+ 'HR' ,
183
+ 'RUBY' ,
184
+ 'RT' ,
185
+ 'RP' ,
186
+ 'LI' ,
187
+ 'TR' ,
188
+ 'TD' ,
189
+ 'TH' ,
190
+ 'S' ,
191
+ 'STRIKE' ,
192
+ 'SUMMARY' ,
193
+ 'DETAILS' ,
194
+ 'CAPTION' ,
195
+ 'FIGURE' ,
196
+ 'FIGCAPTION' ,
197
+ 'ABBR' ,
198
+ 'BDO' ,
199
+ 'CITE' ,
200
+ 'DFN' ,
201
+ 'MARK' ,
202
+ 'SMALL' ,
203
+ 'SPAN' ,
204
+ 'TIME' ,
205
+ 'WBR' ,
206
+ };
207
+
208
+ // Inspired by the set of HTML attributes allowed in GFM.
209
+ final _alwaysAllowedAttributes = < String > {
210
+ 'abbr' ,
211
+ 'accept' ,
212
+ 'accept-charset' ,
213
+ 'accesskey' ,
214
+ 'action' ,
215
+ 'align' ,
216
+ 'alt' ,
217
+ 'aria-describedby' ,
218
+ 'aria-hidden' ,
219
+ 'aria-label' ,
220
+ 'aria-labelledby' ,
221
+ 'axis' ,
222
+ 'border' ,
223
+ 'cellpadding' ,
224
+ 'cellspacing' ,
225
+ 'char' ,
226
+ 'charoff' ,
227
+ 'charset' ,
228
+ 'checked' ,
229
+ 'clear' ,
230
+ 'cols' ,
231
+ 'colspan' ,
232
+ 'color' ,
233
+ 'compact' ,
234
+ 'coords' ,
235
+ 'datetime' ,
236
+ 'dir' ,
237
+ 'disabled' ,
238
+ 'enctype' ,
239
+ 'for' ,
240
+ 'frame' ,
241
+ 'headers' ,
242
+ 'height' ,
243
+ 'hreflang' ,
244
+ 'hspace' ,
245
+ 'ismap' ,
246
+ 'label' ,
247
+ 'lang' ,
248
+ 'maxlength' ,
249
+ 'media' ,
250
+ 'method' ,
251
+ 'multiple' ,
252
+ 'name' ,
253
+ 'nohref' ,
254
+ 'noshade' ,
255
+ 'nowrap' ,
256
+ 'open' ,
257
+ 'prompt' ,
258
+ 'readonly' ,
259
+ 'rel' ,
260
+ 'rev' ,
261
+ 'rows' ,
262
+ 'rowspan' ,
263
+ 'rules' ,
264
+ 'scope' ,
265
+ 'selected' ,
266
+ 'shape' ,
267
+ 'size' ,
268
+ 'span' ,
269
+ 'start' ,
270
+ 'summary' ,
271
+ 'tabindex' ,
272
+ 'target' ,
273
+ 'title' ,
274
+ 'type' ,
275
+ 'usemap' ,
276
+ 'valign' ,
277
+ 'value' ,
278
+ 'vspace' ,
279
+ 'width' ,
280
+ 'itemprop' ,
281
+ };
282
+
283
+ bool _alwaysAllowed (String _) => true ;
284
+
285
+ bool _validLink (String url) {
286
+ try {
287
+ final uri = Uri .parse (url);
288
+ return uri.isScheme ('https' ) ||
289
+ uri.isScheme ('http' ) ||
290
+ uri.isScheme ('mailto' ) ||
291
+ ! uri.hasScheme;
292
+ } on FormatException {
293
+ return false ;
294
+ }
295
+ }
296
+
297
+ bool _validUrl (String url) {
298
+ try {
299
+ final uri = Uri .parse (url);
300
+ return uri.isScheme ('https' ) || uri.isScheme ('http' ) || ! uri.hasScheme;
301
+ } on FormatException {
302
+ return false ;
303
+ }
304
+ }
305
+
306
+ final _citeAttributeValidator = < String , bool Function (String )> {
307
+ 'cite' : _validUrl,
308
+ };
309
+
310
+ final _elementAttributeValidators =
311
+ < String , Map <String , bool Function (String )>> {
312
+ 'A' : {
313
+ 'href' : _validLink,
314
+ },
315
+ 'IMG' : {
316
+ 'src' : _validUrl,
317
+ 'longdesc' : _validUrl,
318
+ },
319
+ 'DIV' : {
320
+ 'itemscope' : _alwaysAllowed,
321
+ 'itemtype' : _alwaysAllowed,
322
+ },
323
+ 'BLOCKQUOTE' : _citeAttributeValidator,
324
+ 'DEL' : _citeAttributeValidator,
325
+ 'INS' : _citeAttributeValidator,
326
+ 'Q' : _citeAttributeValidator,
327
+ };
0 commit comments