diff --git a/contact/pubspec.lock b/contact/pubspec.lock index a9c43212f..4632d56ff 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -592,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.3" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: transitive description: diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart index 30c533f54..6ac8ab997 100644 --- a/core/lib/utils/html/html_utils.dart +++ b/core/lib/utils/html/html_utils.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:math'; +import 'package:html_unescape/html_unescape.dart'; + import 'js_interop_stub.dart' if (dart.library.html) 'dart:js_interop'; import 'dart:typed_data'; @@ -13,6 +15,7 @@ import 'package:universal_html/html.dart' as html; class HtmlUtils { static final random = Random(); + static final htmlUnescape = HtmlUnescape(); static const lineHeight100Percent = ( script: ''' @@ -530,4 +533,20 @@ class HtmlUtils { logError('AppUtils::setWindowBrowserTitle:Exception = $e'); } } + + static String unescapeHtml(String input) { + try { + return htmlUnescape.convert(input); + } catch (e) { + logError('HtmlUtils::unescapeHtml:Exception = $e'); + return input; + } + } + + static String removeWhitespace(String input) { + return input + .replaceAll('\r', '') + .replaceAll('\n', '') + .replaceAll('\t', ''); + } } diff --git a/core/pubspec.lock b/core/pubspec.lock index 853dfb85a..64532506e 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -585,6 +585,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.3" + html_unescape: + dependency: "direct main" + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: "direct main" description: diff --git a/core/pubspec.yaml b/core/pubspec.yaml index a0fa2c7e6..871f458b6 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -104,6 +104,8 @@ dependencies: pull_to_refresh: 2.0.0 + html_unescape: 2.0.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/core/test/utils/html_utils_test.dart b/core/test/utils/html_utils_test.dart index c1228052a..bfb172dfc 100644 --- a/core/test/utils/html_utils_test.dart +++ b/core/test/utils/html_utils_test.dart @@ -1,4 +1,5 @@ import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart'; +import 'package:core/utils/html/html_utils.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -31,4 +32,112 @@ void main() { expect(result, null); }); }); + + group('HtmlUtils.unescapeHtml', () { + test('should return original string when no HTML entities present', () { + const input = 'This is a normal string'; + expect(HtmlUtils.unescapeHtml(input), equals(input)); + }); + + test('should unescape basic HTML entities', () { + const input = '<div>Hello & Welcome</div>'; + const expected = '
Hello & Welcome
'; + expect(HtmlUtils.unescapeHtml(input), equals(expected)); + }); + + test('should unescape numeric decimal HTML entities', () { + const input = 'ABC'; + const expected = 'ABC'; + expect(HtmlUtils.unescapeHtml(input), equals(expected)); + }); + + test('should unescape numeric hexadecimal HTML entities', () { + const input = 'ABC'; + const expected = 'ABC'; + expect(HtmlUtils.unescapeHtml(input), equals(expected)); + }); + + test('should unescape special character entities', () { + const input = '"Hello' © ® ™'; + const expected = '"Hello\' © ® ™'; + expect(HtmlUtils.unescapeHtml(input), equals(expected)); + }); + + test('should handle empty string', () { + const input = ''; + expect(HtmlUtils.unescapeHtml(input), isEmpty); + }); + + test('should handle string with only HTML entity', () { + const input = '&'; + const expected = '&'; + expect(HtmlUtils.unescapeHtml(input), equals(expected)); + }); + + test('should leave invalid HTML entities unchanged', () { + const input = 'This & is not an entity &invalid; &123'; + const expected = 'This & is not an entity &invalid; &123'; + expect(HtmlUtils.unescapeHtml(input), equals(expected)); + }); + + test('should handle Unicode characters', () { + const input = '<こんにちは>'; // <こんにちは> + const expected = '<こんにちは>'; + expect(HtmlUtils.unescapeHtml(input), equals(expected)); + }); + + test('should handle complex mixed entities', () { + const input = '<script>alert("Hello");</script>&@@'; + const expected = '&@@'; + expect(HtmlUtils.unescapeHtml(input), equals(expected)); + }); + }); + + group('HtmlUtils.removeWhitespace', () { + test('should remove carriage returns (\\r)', () { + expect(HtmlUtils.removeWhitespace('Hello\rWorld'), equals('HelloWorld')); + expect(HtmlUtils.removeWhitespace('\r\r\r'), equals('')); + expect(HtmlUtils.removeWhitespace('a\rb\rc'), equals('abc')); + }); + + test('should remove newlines (\\n)', () { + expect(HtmlUtils.removeWhitespace('Hello\nWorld'), equals('HelloWorld')); + expect(HtmlUtils.removeWhitespace('\n\n\n'), equals('')); + expect(HtmlUtils.removeWhitespace('a\nb\nc'), equals('abc')); + }); + + test('should remove tabs (\\t)', () { + expect(HtmlUtils.removeWhitespace('Hello\tWorld'), equals('HelloWorld')); + expect(HtmlUtils.removeWhitespace('\t\t\t'), equals('')); + expect(HtmlUtils.removeWhitespace('a\tb\tc'), equals('abc')); + }); + + test('should remove all whitespace characters when combined', () { + expect(HtmlUtils.removeWhitespace('Hello\r\n\tWorld'), equals('HelloWorld')); + expect(HtmlUtils.removeWhitespace('a\rb\nc\td'), equals('abcd')); + expect(HtmlUtils.removeWhitespace('\r\n\t\r\n\t'), equals('')); + }); + + test('should return empty string for empty input', () { + expect(HtmlUtils.removeWhitespace(''), equals('')); + }); + + test('should leave string unchanged when no whitespace present', () { + expect(HtmlUtils.removeWhitespace('HelloWorld'), equals('HelloWorld')); + expect(HtmlUtils.removeWhitespace('12345'), equals('12345')); + expect(HtmlUtils.removeWhitespace('!@#\$%^'), equals('!@#\$%^')); + }); + + test('should preserve spaces if not configured to remove them', () { + expect(HtmlUtils.removeWhitespace('Hello World'), equals('Hello World')); + expect(HtmlUtils.removeWhitespace('a b c'), equals('a b c')); + }); + + test('should handle mixed content with various whitespace', () { + expect( + HtmlUtils.removeWhitespace('Text\rwith\nnew\tlines\r\nand\ttabs'), + equals('Textwithnewlinesandtabs'), + ); + }); + }); } \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 39c34be71..503e40304 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -4,6 +4,7 @@ import 'package:core/data/constants/constant.dart'; import 'package:core/domain/extensions/datetime_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/html/html_utils.dart'; import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -185,10 +186,11 @@ extension PresentationEmailExtension on PresentationEmail { ); } - String? _sanitizeSearchSnippet(String? searchSnippet) => searchSnippet - ?.replaceAll('\r', '') - .replaceAll('\n', '') - .replaceAll('\t', ''); + String? _sanitizeSearchSnippet(String? searchSnippet) { + if (searchSnippet == null) return null; + return HtmlUtils.unescapeHtml(HtmlUtils.removeWhitespace(searchSnippet)); + } + String? get sanitizedSearchSnippetSubject => _sanitizeSearchSnippet(searchSnippetSubject); String? get sanitizedSearchSnippetPreview => _sanitizeSearchSnippet(searchSnippetPreview); } \ No newline at end of file diff --git a/model/pubspec.lock b/model/pubspec.lock index 9b01cba24..ca9e853b2 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -592,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.3" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index 5ac9fbbd5..425db15fc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1241,6 +1241,14 @@ packages: url: "https://github.com/linagora/html-editor-enhanced.git" source: git version: "3.1.6" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: transitive description: