Skip to content

Commit 3205e00

Browse files
authored
Standardize HTML sanitizing when preview email (#3223)
1 parent 124c4ce commit 3205e00

17 files changed

+330
-30
lines changed

contact/pubspec.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,15 @@ packages:
992992
url: "https://pub.dev"
993993
source: hosted
994994
version: "3.2.1"
995+
sanitize_html:
996+
dependency: transitive
997+
description:
998+
path: sanitize_html
999+
ref: support_mail
1000+
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987
1001+
url: "https://github.com/linagora/dart-neats.git"
1002+
source: git
1003+
version: "2.1.0"
9951004
shelf:
9961005
dependency: transitive
9971006
description:

core/lib/presentation/utils/html_transformer/message_content_transformer.dart

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ class MessageContentTransformer {
2323
Map<String, String>? mapUrlDownloadCID
2424
}) async {
2525
await Future.wait([
26-
if (_configuration.domTransformers.isNotEmpty)
27-
..._configuration.domTransformers.map((domTransformer) async =>
28-
domTransformer.process(
29-
document: document,
30-
dioClient: _dioClient,
31-
mapUrlDownloadCID: mapUrlDownloadCID,
32-
)
26+
..._configuration.domTransformers.map((domTransformer) async =>
27+
domTransformer.process(
28+
document: document,
29+
dioClient: _dioClient,
30+
mapUrlDownloadCID: mapUrlDownloadCID,
31+
)
3332
)
3433
]);
3534
}
@@ -38,24 +37,32 @@ class MessageContentTransformer {
3837
required String message,
3938
Map<String, String>? mapUrlDownloadCID
4039
}) async {
41-
final document = parse(message);
42-
await _transformDocument(
43-
document: document,
44-
mapUrlDownloadCID: mapUrlDownloadCID,
45-
);
40+
final newMessage = _configuration.textTransformers.isNotEmpty
41+
? _transformMessage(message)
42+
: message;
43+
44+
final document = parse(newMessage);
45+
46+
if (_configuration.domTransformers.isNotEmpty) {
47+
await _transformDocument(
48+
document: document,
49+
mapUrlDownloadCID: mapUrlDownloadCID,
50+
);
51+
}
52+
4653
return document;
4754
}
4855

4956
String _transformMessage(String message) {
50-
if (_configuration.textTransformers.isNotEmpty) {
51-
for (var transformer in _configuration.textTransformers) {
52-
message = transformer.process(message, _htmlEscape);
53-
}
57+
for (var transformer in _configuration.textTransformers) {
58+
message = transformer.process(message, _htmlEscape);
5459
}
5560
return message;
5661
}
5762

5863
String toMessage(String message) {
59-
return _transformMessage(message);
64+
return _configuration.textTransformers.isNotEmpty
65+
? _transformMessage(message)
66+
: message;
6067
}
6168
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'package:sanitize_html/sanitize_html.dart';
2+
3+
class SanitizeHtml {
4+
String process({
5+
required String inputHtml,
6+
List<String>? allowAttributes,
7+
List<String>? allowTags,
8+
}) {
9+
final outputHtml = sanitizeHtml(
10+
inputHtml,
11+
allowAttributes: allowAttributes,
12+
allowTags: allowTags,
13+
);
14+
return outputHtml;
15+
}
16+
}

core/lib/presentation/utils/html_transformer/sanitize_url.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'package:core/utils/app_logger.dart';
21
import 'package:get/get.dart';
32

43
class SanitizeUrl {
@@ -24,7 +23,6 @@ class SanitizeUrl {
2423
} else {
2524
originalUrl = '';
2625
}
27-
log('SanitizeUrl::process:originalUrl = $originalUrl');
2826
return originalUrl;
2927
}
3028
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'dart:convert';
2+
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
3+
import 'package:core/presentation/utils/html_transformer/sanitize_html.dart';
4+
5+
class StandardizeHtmlSanitizingTransformers extends TextTransformer {
6+
7+
static const List<String> mailAllowedHtmlAttributes = [
8+
'style',
9+
'public-asset-id',
10+
'data-filename',
11+
'bgcolor',
12+
'id',
13+
'class',
14+
];
15+
16+
static const List<String> mailAllowedHtmlTags = [
17+
'font',
18+
'u',
19+
'center',
20+
'style',
21+
'body',
22+
];
23+
24+
const StandardizeHtmlSanitizingTransformers();
25+
26+
@override
27+
String process(String text, HtmlEscape htmlEscape) =>
28+
SanitizeHtml().process(
29+
inputHtml: text,
30+
allowAttributes: mailAllowedHtmlAttributes,
31+
allowTags: mailAllowedHtmlTags,
32+
);
33+
}

core/lib/presentation/utils/html_transformer/transform_configuration.dart

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link
1515
import 'package:core/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart';
1616
import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart';
1717
import 'package:core/presentation/utils/html_transformer/dom/signature_transformers.dart';
18-
import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart';
18+
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
1919
import 'package:core/utils/platform_info.dart';
2020

2121
/// Contains the configuration for all transformations.
@@ -37,7 +37,9 @@ class TransformConfiguration {
3737

3838
factory TransformConfiguration.fromDomTransformers(List<DomTransformer> domTransformers) => TransformConfiguration(domTransformers, []);
3939

40-
factory TransformConfiguration.empty() => const TransformConfiguration([], []);
40+
factory TransformConfiguration.fromTextTransformers(
41+
List<TextTransformer> textTransformers
42+
) => TransformConfiguration([], textTransformers);
4143

4244
factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.fromDomTransformers([
4345
if (PlatformInfo.isWeb)
@@ -46,10 +48,15 @@ class TransformConfiguration {
4648
const RemoveCollapsedSignatureButtonTransformer(),
4749
]);
4850

49-
factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.fromDomTransformers([const ImageTransformer()]);
50-
factory TransformConfiguration.forEditDraftsEmail() => TransformConfiguration.fromDomTransformers([
51-
...TransformConfiguration.forDraftsEmail().domTransformers,
52-
const HideDraftSignatureTransformer()]);
51+
factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create(
52+
customDomTransformers: [const ImageTransformer()]
53+
);
54+
factory TransformConfiguration.forEditDraftsEmail() => TransformConfiguration.create(
55+
customDomTransformers: [
56+
...TransformConfiguration.forDraftsEmail().domTransformers,
57+
const HideDraftSignatureTransformer()
58+
]
59+
);
5360

5461
factory TransformConfiguration.forPreviewEmailOnWeb() => TransformConfiguration.create(
5562
customDomTransformers: [
@@ -65,7 +72,9 @@ class TransformConfiguration {
6572

6673
factory TransformConfiguration.forPreviewEmail() => TransformConfiguration.standardConfiguration;
6774

68-
factory TransformConfiguration.forRestoreEmail() => TransformConfiguration.fromDomTransformers([const ImageTransformer()]);
75+
factory TransformConfiguration.forRestoreEmail() => TransformConfiguration.create(
76+
customDomTransformers: [const ImageTransformer()]
77+
);
6978

7079
factory TransformConfiguration.forPrintEmail() => TransformConfiguration.fromDomTransformers([
7180
if (PlatformInfo.isWeb)
@@ -115,6 +124,6 @@ class TransformConfiguration {
115124
];
116125

117126
static const List<TextTransformer> standardTextTransformers = [
118-
SanitizeAutolinkHtmlTransformers()
127+
StandardizeHtmlSanitizingTransformers(),
119128
];
120129
}

core/pubspec.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,15 @@ packages:
945945
url: "https://pub.dev"
946946
source: hosted
947947
version: "3.0.1"
948+
sanitize_html:
949+
dependency: "direct main"
950+
description:
951+
path: sanitize_html
952+
ref: support_mail
953+
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987
954+
url: "https://github.com/linagora/dart-neats.git"
955+
source: git
956+
version: "2.1.0"
948957
shelf:
949958
dependency: transitive
950959
description:

core/pubspec.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ dependencies:
3434
url: https://github.com/dab246/languagetool_textfield.git
3535
ref: twake-supported
3636

37+
# Sanitize_html is restricting Tags and Attributes. So some of our own tags and attributes (signature, public asset,...) will be lost when sanitizing html.
38+
# TODO: We will change it when the PR in upstream repository will be merged
39+
# https://github.com/google/dart-neats/pull/259
40+
sanitize_html:
41+
git:
42+
url: https://github.com/linagora/dart-neats.git
43+
ref: support_mail
44+
path: sanitize_html
45+
3746
### Dependencies from pub.dev ###
3847
cupertino_icons: 1.0.6
3948

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'dart:convert';
4+
5+
void main() {
6+
group('StandardizeHtmlSanitizingTransformers::test', () {
7+
const transformer = StandardizeHtmlSanitizingTransformers();
8+
const htmlEscape = HtmlEscape();
9+
10+
test('SHOULD remove all `on*` attributes tag', () {
11+
const listOnEventAttributes = [
12+
'mousedown',
13+
'mouseenter',
14+
'mouseleave',
15+
'mousemove',
16+
'mouseover',
17+
'mouseout',
18+
'mouseup',
19+
'load',
20+
'unload',
21+
'loadstart',
22+
'loadeddata',
23+
'loadedmetadata',
24+
'playing',
25+
'show',
26+
'error',
27+
'message',
28+
'focus',
29+
'focusin',
30+
'focusout',
31+
'keydown',
32+
'keydpress',
33+
'keydup',
34+
'input',
35+
'ended',
36+
'drag',
37+
'drop',
38+
'dragstart',
39+
'dragover',
40+
'dragleave',
41+
'dragend',
42+
'dragenter',
43+
'beforeunload',
44+
'beforeprint',
45+
'afterprint',
46+
'blur',
47+
'click',
48+
'change',
49+
'contextmenu',
50+
'cut',
51+
'copy',
52+
'dblclick',
53+
'abort',
54+
'durationchange',
55+
'progress',
56+
'resize',
57+
'reset',
58+
'scroll',
59+
'seeked',
60+
'select',
61+
'submit',
62+
'toggle',
63+
'volumechange',
64+
'touchstart',
65+
'touchmove',
66+
'touchend',
67+
'touchcancel'
68+
];
69+
70+
for (var i = 0; i < listOnEventAttributes.length; i++) {
71+
final inputHtml = '<img src="1" href="1" on${listOnEventAttributes[i]}="javascript:alert(1)">';
72+
final result = transformer.process(inputHtml, htmlEscape);
73+
74+
expect(result, equals('<img src="1">'));
75+
}
76+
});
77+
78+
test('SHOULD remove all `on*` attributes for any tags', () {
79+
const listOnEventAttributes = [
80+
'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover',
81+
'mouseout', 'mouseup', 'load', 'unload', 'loadstart', 'loadeddata',
82+
'loadedmetadata', 'playing', 'show', 'error', 'message', 'focus',
83+
'focusin', 'focusout', 'keydown', 'keypress', 'keyup', 'input', 'ended',
84+
'drag', 'drop', 'dragstart', 'dragover', 'dragleave', 'dragend', 'dragenter',
85+
'beforeunload', 'beforeprint', 'afterprint', 'blur', 'click', 'change',
86+
'contextmenu', 'cut', 'copy', 'dblclick', 'abort', 'durationchange',
87+
'progress', 'resize', 'reset', 'scroll', 'seeked', 'select', 'submit',
88+
'toggle', 'volumechange', 'touchstart', 'touchmove', 'touchend', 'touchcancel'
89+
];
90+
91+
const listHTMLTags = [
92+
'div', 'span', 'p', 'a', 'u', 'i', 'table'
93+
];
94+
95+
for (var tag in listHTMLTags) {
96+
for (var event in listOnEventAttributes) {
97+
final inputHtml = '<$tag on$event="javascript:alert(1)"></$tag>';
98+
final result = transformer.process(inputHtml, htmlEscape);
99+
100+
expect(result, equals('<$tag></$tag>'));
101+
}
102+
}
103+
});
104+
105+
test('SHOULD remove attributes of IMG tag WHEN they are invalid', () {
106+
const inputHtml = '<img src="1" href="1" onerror="javascript:alert(1)">';
107+
final result = transformer.process(inputHtml, htmlEscape);
108+
109+
expect(result, equals('<img src="1">'));
110+
});
111+
112+
test('SHOULD remove all SCRIPTS tags', () {
113+
const inputHtml = '<script>alert("This is an alert message!");</script>';
114+
final result = transformer.process(inputHtml, htmlEscape).trim();
115+
116+
expect(result, equals(''));
117+
});
118+
119+
test('SHOULD remove all IFRAME tags', () {
120+
const inputHtml = '<iframe style="xg-p:absolute;top:0;left:0;width:100%;height:100%" onmouseover="prompt(1)">';
121+
final result = transformer.process(inputHtml, htmlEscape);
122+
123+
expect(result, equals(''));
124+
});
125+
126+
test('SHOULD remove href attribute of A tag WHEN it is invalid', () {
127+
const inputHtml = '<a href="javascript:alert(1)" id="id1">test</a>';
128+
final result = transformer.process(inputHtml, htmlEscape);
129+
130+
expect(result, equals('<a id="id1">test</a>'));
131+
});
132+
133+
test('SHOULD persist value src attribute of IMG tag WHEN it is base64 string', () {
134+
const inputHtml = '<img src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">';
135+
final result = transformer.process(inputHtml, htmlEscape);
136+
137+
expect(result, equals('<img src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">'));
138+
});
139+
140+
test('SHOULD persist value src attribute of IMG tag WHEN it is CID string', () {
141+
const inputHtml = '<img src="cid:email123">';
142+
final result = transformer.process(inputHtml, htmlEscape);
143+
144+
expect(result, equals('<img src="cid:email123">'));
145+
});
146+
});
147+
}

0 commit comments

Comments
 (0)