TF-3691 Support HTML escaping in search snippets
Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
@@ -592,6 +592,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.3"
|
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:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:html_unescape/html_unescape.dart';
|
||||||
|
|
||||||
import 'js_interop_stub.dart' if (dart.library.html) 'dart:js_interop';
|
import 'js_interop_stub.dart' if (dart.library.html) 'dart:js_interop';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ import 'package:universal_html/html.dart' as html;
|
|||||||
|
|
||||||
class HtmlUtils {
|
class HtmlUtils {
|
||||||
static final random = Random();
|
static final random = Random();
|
||||||
|
static final htmlUnescape = HtmlUnescape();
|
||||||
|
|
||||||
static const lineHeight100Percent = (
|
static const lineHeight100Percent = (
|
||||||
script: '''
|
script: '''
|
||||||
@@ -530,4 +533,20 @@ class HtmlUtils {
|
|||||||
logError('AppUtils::setWindowBrowserTitle:Exception = $e');
|
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', '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,6 +585,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.3"
|
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:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ dependencies:
|
|||||||
|
|
||||||
pull_to_refresh: 2.0.0
|
pull_to_refresh: 2.0.0
|
||||||
|
|
||||||
|
html_unescape: 2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart';
|
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';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -31,4 +32,112 @@ void main() {
|
|||||||
expect(result, null);
|
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 = '<div>Hello & Welcome</div>';
|
||||||
|
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 = '<script>alert("Hello");</script>&@@';
|
||||||
|
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'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:core/data/constants/constant.dart';
|
|||||||
import 'package:core/domain/extensions/datetime_extension.dart';
|
import 'package:core/domain/extensions/datetime_extension.dart';
|
||||||
import 'package:core/presentation/extensions/color_extension.dart';
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
import 'package:core/utils/app_logger.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:http_parser/http_parser.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.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
|
String? _sanitizeSearchSnippet(String? searchSnippet) {
|
||||||
?.replaceAll('\r', '')
|
if (searchSnippet == null) return null;
|
||||||
.replaceAll('\n', '')
|
return HtmlUtils.unescapeHtml(HtmlUtils.removeWhitespace(searchSnippet));
|
||||||
.replaceAll('\t', '');
|
}
|
||||||
|
|
||||||
String? get sanitizedSearchSnippetSubject => _sanitizeSearchSnippet(searchSnippetSubject);
|
String? get sanitizedSearchSnippetSubject => _sanitizeSearchSnippet(searchSnippetSubject);
|
||||||
String? get sanitizedSearchSnippetPreview => _sanitizeSearchSnippet(searchSnippetPreview);
|
String? get sanitizedSearchSnippetPreview => _sanitizeSearchSnippet(searchSnippetPreview);
|
||||||
}
|
}
|
||||||
@@ -592,6 +592,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.3"
|
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:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1241,6 +1241,14 @@ packages:
|
|||||||
url: "https://github.com/linagora/html-editor-enhanced.git"
|
url: "https://github.com/linagora/html-editor-enhanced.git"
|
||||||
source: git
|
source: git
|
||||||
version: "3.1.6"
|
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:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user