diff --git a/contact/pubspec.lock b/contact/pubspec.lock index ce6c986509..09e768f070 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -992,6 +992,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + sanitize_html: + dependency: transitive + description: + path: sanitize_html + ref: support_mail + resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987 + url: "https://github.com/linagora/dart-neats.git" + source: git + version: "2.1.0" shelf: dependency: transitive description: diff --git a/core/lib/presentation/utils/html_transformer/message_content_transformer.dart b/core/lib/presentation/utils/html_transformer/message_content_transformer.dart index 034855b738..a42a5024f7 100644 --- a/core/lib/presentation/utils/html_transformer/message_content_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/message_content_transformer.dart @@ -23,13 +23,12 @@ class MessageContentTransformer { Map? mapUrlDownloadCID }) async { await Future.wait([ - if (_configuration.domTransformers.isNotEmpty) - ..._configuration.domTransformers.map((domTransformer) async => - domTransformer.process( - document: document, - dioClient: _dioClient, - mapUrlDownloadCID: mapUrlDownloadCID, - ) + ..._configuration.domTransformers.map((domTransformer) async => + domTransformer.process( + document: document, + dioClient: _dioClient, + mapUrlDownloadCID: mapUrlDownloadCID, + ) ) ]); } @@ -38,24 +37,32 @@ class MessageContentTransformer { required String message, Map? mapUrlDownloadCID }) async { - final document = parse(message); - await _transformDocument( - document: document, - mapUrlDownloadCID: mapUrlDownloadCID, - ); + final newMessage = _configuration.textTransformers.isNotEmpty + ? _transformMessage(message) + : message; + + final document = parse(newMessage); + + if (_configuration.domTransformers.isNotEmpty) { + await _transformDocument( + document: document, + mapUrlDownloadCID: mapUrlDownloadCID, + ); + } + return document; } String _transformMessage(String message) { - if (_configuration.textTransformers.isNotEmpty) { - for (var transformer in _configuration.textTransformers) { - message = transformer.process(message, _htmlEscape); - } + for (var transformer in _configuration.textTransformers) { + message = transformer.process(message, _htmlEscape); } return message; } String toMessage(String message) { - return _transformMessage(message); + return _configuration.textTransformers.isNotEmpty + ? _transformMessage(message) + : message; } } \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/sanitize_html.dart b/core/lib/presentation/utils/html_transformer/sanitize_html.dart new file mode 100644 index 0000000000..0d3e5ed673 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/sanitize_html.dart @@ -0,0 +1,16 @@ +import 'package:sanitize_html/sanitize_html.dart'; + +class SanitizeHtml { + String process({ + required String inputHtml, + List? allowAttributes, + List? allowTags, + }) { + final outputHtml = sanitizeHtml( + inputHtml, + allowAttributes: allowAttributes, + allowTags: allowTags, + ); + return outputHtml; + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/sanitize_url.dart b/core/lib/presentation/utils/html_transformer/sanitize_url.dart index 3c835dc5ee..fc773655b7 100644 --- a/core/lib/presentation/utils/html_transformer/sanitize_url.dart +++ b/core/lib/presentation/utils/html_transformer/sanitize_url.dart @@ -1,4 +1,3 @@ -import 'package:core/utils/app_logger.dart'; import 'package:get/get.dart'; class SanitizeUrl { @@ -24,7 +23,6 @@ class SanitizeUrl { } else { originalUrl = ''; } - log('SanitizeUrl::process:originalUrl = $originalUrl'); return originalUrl; } } \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart b/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart new file mode 100644 index 0000000000..474b14dac5 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; +import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/sanitize_html.dart'; + +class StandardizeHtmlSanitizingTransformers extends TextTransformer { + + static const List mailAllowedHtmlAttributes = [ + 'style', + 'public-asset-id', + 'data-filename', + 'bgcolor', + 'id', + 'class', + ]; + + static const List mailAllowedHtmlTags = [ + 'font', + 'u', + 'center', + 'style', + 'body', + ]; + + const StandardizeHtmlSanitizingTransformers(); + + @override + String process(String text, HtmlEscape htmlEscape) => + SanitizeHtml().process( + inputHtml: text, + allowAttributes: mailAllowedHtmlAttributes, + allowTags: mailAllowedHtmlTags, + ); +} diff --git a/core/lib/presentation/utils/html_transformer/transform_configuration.dart b/core/lib/presentation/utils/html_transformer/transform_configuration.dart index 4b277d40c1..36cf86077a 100644 --- a/core/lib/presentation/utils/html_transformer/transform_configuration.dart +++ b/core/lib/presentation/utils/html_transformer/transform_configuration.dart @@ -15,7 +15,7 @@ import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link import 'package:core/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/signature_transformers.dart'; -import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart'; import 'package:core/utils/platform_info.dart'; /// Contains the configuration for all transformations. @@ -37,7 +37,9 @@ class TransformConfiguration { factory TransformConfiguration.fromDomTransformers(List domTransformers) => TransformConfiguration(domTransformers, []); - factory TransformConfiguration.empty() => const TransformConfiguration([], []); + factory TransformConfiguration.fromTextTransformers( + List textTransformers + ) => TransformConfiguration([], textTransformers); factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.fromDomTransformers([ if (PlatformInfo.isWeb) @@ -46,10 +48,15 @@ class TransformConfiguration { const RemoveCollapsedSignatureButtonTransformer(), ]); - factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.fromDomTransformers([const ImageTransformer()]); - factory TransformConfiguration.forEditDraftsEmail() => TransformConfiguration.fromDomTransformers([ - ...TransformConfiguration.forDraftsEmail().domTransformers, - const HideDraftSignatureTransformer()]); + factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create( + customDomTransformers: [const ImageTransformer()] + ); + factory TransformConfiguration.forEditDraftsEmail() => TransformConfiguration.create( + customDomTransformers: [ + ...TransformConfiguration.forDraftsEmail().domTransformers, + const HideDraftSignatureTransformer() + ] + ); factory TransformConfiguration.forPreviewEmailOnWeb() => TransformConfiguration.create( customDomTransformers: [ @@ -65,7 +72,9 @@ class TransformConfiguration { factory TransformConfiguration.forPreviewEmail() => TransformConfiguration.standardConfiguration; - factory TransformConfiguration.forRestoreEmail() => TransformConfiguration.fromDomTransformers([const ImageTransformer()]); + factory TransformConfiguration.forRestoreEmail() => TransformConfiguration.create( + customDomTransformers: [const ImageTransformer()] + ); factory TransformConfiguration.forPrintEmail() => TransformConfiguration.fromDomTransformers([ if (PlatformInfo.isWeb) @@ -115,6 +124,6 @@ class TransformConfiguration { ]; static const List standardTextTransformers = [ - SanitizeAutolinkHtmlTransformers() + StandardizeHtmlSanitizingTransformers(), ]; } \ No newline at end of file diff --git a/core/pubspec.lock b/core/pubspec.lock index b520f72981..e18fb8b14d 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -945,6 +945,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + sanitize_html: + dependency: "direct main" + description: + path: sanitize_html + ref: support_mail + resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987 + url: "https://github.com/linagora/dart-neats.git" + source: git + version: "2.1.0" shelf: dependency: transitive description: diff --git a/core/pubspec.yaml b/core/pubspec.yaml index b376cd7b9f..2a37478ca8 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -34,6 +34,15 @@ dependencies: url: https://github.com/dab246/languagetool_textfield.git ref: twake-supported + # Sanitize_html is restricting Tags and Attributes. So some of our own tags and attributes (signature, public asset,...) will be lost when sanitizing html. + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/google/dart-neats/pull/259 + sanitize_html: + git: + url: https://github.com/linagora/dart-neats.git + ref: support_mail + path: sanitize_html + ### Dependencies from pub.dev ### cupertino_icons: 1.0.6 diff --git a/core/test/utils/standardize_html_sanitizing_transformers_test.dart b/core/test/utils/standardize_html_sanitizing_transformers_test.dart new file mode 100644 index 0000000000..2249cac95e --- /dev/null +++ b/core/test/utils/standardize_html_sanitizing_transformers_test.dart @@ -0,0 +1,147 @@ +import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'dart:convert'; + +void main() { + group('StandardizeHtmlSanitizingTransformers::test', () { + const transformer = StandardizeHtmlSanitizingTransformers(); + const htmlEscape = HtmlEscape(); + + test('SHOULD remove all `on*` attributes tag', () { + const listOnEventAttributes = [ + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseover', + 'mouseout', + 'mouseup', + 'load', + 'unload', + 'loadstart', + 'loadeddata', + 'loadedmetadata', + 'playing', + 'show', + 'error', + 'message', + 'focus', + 'focusin', + 'focusout', + 'keydown', + 'keydpress', + 'keydup', + 'input', + 'ended', + 'drag', + 'drop', + 'dragstart', + 'dragover', + 'dragleave', + 'dragend', + 'dragenter', + 'beforeunload', + 'beforeprint', + 'afterprint', + 'blur', + 'click', + 'change', + 'contextmenu', + 'cut', + 'copy', + 'dblclick', + 'abort', + 'durationchange', + 'progress', + 'resize', + 'reset', + 'scroll', + 'seeked', + 'select', + 'submit', + 'toggle', + 'volumechange', + 'touchstart', + 'touchmove', + 'touchend', + 'touchcancel' + ]; + + for (var i = 0; i < listOnEventAttributes.length; i++) { + final inputHtml = ''; + final result = transformer.process(inputHtml, htmlEscape); + + expect(result, equals('')); + } + }); + + test('SHOULD remove all `on*` attributes for any tags', () { + const listOnEventAttributes = [ + 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover', + 'mouseout', 'mouseup', 'load', 'unload', 'loadstart', 'loadeddata', + 'loadedmetadata', 'playing', 'show', 'error', 'message', 'focus', + 'focusin', 'focusout', 'keydown', 'keypress', 'keyup', 'input', 'ended', + 'drag', 'drop', 'dragstart', 'dragover', 'dragleave', 'dragend', 'dragenter', + 'beforeunload', 'beforeprint', 'afterprint', 'blur', 'click', 'change', + 'contextmenu', 'cut', 'copy', 'dblclick', 'abort', 'durationchange', + 'progress', 'resize', 'reset', 'scroll', 'seeked', 'select', 'submit', + 'toggle', 'volumechange', 'touchstart', 'touchmove', 'touchend', 'touchcancel' + ]; + + const listHTMLTags = [ + 'div', 'span', 'p', 'a', 'u', 'i', 'table' + ]; + + for (var tag in listHTMLTags) { + for (var event in listOnEventAttributes) { + final inputHtml = '<$tag on$event="javascript:alert(1)">'; + final result = transformer.process(inputHtml, htmlEscape); + + expect(result, equals('<$tag>')); + } + } + }); + + test('SHOULD remove attributes of IMG tag WHEN they are invalid', () { + const inputHtml = ''; + final result = transformer.process(inputHtml, htmlEscape); + + expect(result, equals('')); + }); + + test('SHOULD remove all SCRIPTS tags', () { + const inputHtml = ''; + final result = transformer.process(inputHtml, htmlEscape).trim(); + + expect(result, equals('')); + }); + + test('SHOULD remove all IFRAME tags', () { + const inputHtml = '