TF-3267 Implement HTML attachment preview
This commit is contained in:
@@ -13,4 +13,5 @@ class Constant {
|
||||
static const mailtoScheme = 'mailto';
|
||||
static const attachmentScheme = 'attachment';
|
||||
static const emlPreviewerScheme = 'eml-previewer';
|
||||
static const htmlExtension = '.html';
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
class UnsupportedCharsetException implements Exception {
|
||||
const UnsupportedCharsetException();
|
||||
}
|
||||
|
||||
class NullCharsetException implements Exception {
|
||||
const NullCharsetException();
|
||||
}
|
||||
@@ -47,4 +47,9 @@ extension MediaTypeExtension on MediaType {
|
||||
fileName?.endsWith(Constant.pdfExtension) == true);
|
||||
|
||||
bool get isEMLFile => mimeType == Constant.emlMimeType;
|
||||
|
||||
bool isHTMLFile({required String? fileName}) =>
|
||||
mimeType == Constant.textHtmlMimeType ||
|
||||
(mimeType == Constant.octetStreamMimeType &&
|
||||
fileName?.endsWith(Constant.htmlExtension) == true);
|
||||
}
|
||||
|
||||
+3
@@ -20,6 +20,9 @@ class StandardizeHtmlSanitizingTransformers extends TextTransformer {
|
||||
'style',
|
||||
'body',
|
||||
'section',
|
||||
'nav',
|
||||
'main',
|
||||
'footer',
|
||||
];
|
||||
|
||||
const StandardizeHtmlSanitizingTransformers();
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
typedef OnClickHyperLinkAction = Function(Uri?);
|
||||
typedef OnMailtoClicked = void Function(Uri? uri);
|
||||
|
||||
class HtmlContentViewerOnWeb extends StatefulWidget {
|
||||
|
||||
@@ -21,13 +22,15 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
|
||||
final TextDirection? direction;
|
||||
|
||||
/// Handler for mailto: links
|
||||
final Function(Uri?)? mailtoDelegate;
|
||||
final OnMailtoClicked? mailtoDelegate;
|
||||
|
||||
final OnClickHyperLinkAction? onClickHyperLinkAction;
|
||||
|
||||
// if widthContent is bigger than width of htmlContent, set this to true let widget able to resize to width of htmlContent
|
||||
// if widthContent is bigger than width of htmlContent, set this to true let widget able to resize to width of htmlContent
|
||||
final bool allowResizeToDocumentSize;
|
||||
|
||||
|
||||
final bool keepWidthWhileLoading;
|
||||
|
||||
const HtmlContentViewerOnWeb({
|
||||
Key? key,
|
||||
required this.contentHtml,
|
||||
@@ -37,6 +40,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
|
||||
this.mailtoDelegate,
|
||||
this.direction,
|
||||
this.onClickHyperLinkAction,
|
||||
this.keepWidthWhileLoading = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -99,7 +103,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
|
||||
}
|
||||
}
|
||||
|
||||
if (data['type'] != null && data['type'].contains('toDart: htmlWidth')) {
|
||||
if (data['type'] != null && data['type'].contains('toDart: htmlWidth') && !widget.keepWidthWhileLoading) {
|
||||
final docWidth = data['width'] ?? _actualWidth;
|
||||
if (docWidth != null && mounted) {
|
||||
if (docWidth > _minWidth && widget.allowResizeToDocumentSize) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'app_logger.dart';
|
||||
|
||||
import 'package:core/domain/exceptions/string_exception.dart';
|
||||
|
||||
class StringConvert {
|
||||
static const String separatorPattern = r'[ ,;]+';
|
||||
|
||||
@@ -38,4 +41,24 @@ class StringConvert {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static String decodeFromBytes(
|
||||
Uint8List bytes, {
|
||||
required String? charset,
|
||||
bool isHtml = false,
|
||||
}) {
|
||||
if (isHtml) {
|
||||
return utf8.decode(bytes);
|
||||
} else if (charset == null) {
|
||||
throw const NullCharsetException();
|
||||
} else if (charset.toLowerCase().contains('utf-8')) {
|
||||
return utf8.decode(bytes);
|
||||
} else if (charset.toLowerCase().contains('latin')) {
|
||||
return latin1.decode(bytes);
|
||||
} else if (charset.toLowerCase().contains('ascii')) {
|
||||
return ascii.decode(bytes);
|
||||
} else {
|
||||
throw const UnsupportedCharsetException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,5 +143,35 @@ void main() {
|
||||
|
||||
expect(result, equals('<img src="cid:email123">'));
|
||||
});
|
||||
|
||||
test(
|
||||
'SHOULD persist nav tag and remove href attribute of A tag '
|
||||
'WHEN href is invalid',
|
||||
() {
|
||||
const inputHtml = '<nav href="javascript:alert(1)"></nav>';
|
||||
final result = transformer.process(inputHtml, htmlEscape);
|
||||
|
||||
expect(result, equals('<nav></nav>'));
|
||||
});
|
||||
|
||||
test(
|
||||
'SHOULD persist main tag and remove href attribute of A tag '
|
||||
'WHEN href is invalid',
|
||||
() {
|
||||
const inputHtml = '<main href="javascript:alert(1)"></main>';
|
||||
final result = transformer.process(inputHtml, htmlEscape);
|
||||
|
||||
expect(result, equals('<main></main>'));
|
||||
});
|
||||
|
||||
test(
|
||||
'SHOULD persist footer tag and remove href attribute of A tag '
|
||||
'WHEN href is invalid',
|
||||
() {
|
||||
const inputHtml = '<footer href="javascript:alert(1)"></footer>';
|
||||
final result = transformer.process(inputHtml, htmlEscape);
|
||||
|
||||
expect(result, equals('<footer></footer>'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:core/domain/exceptions/string_exception.dart';
|
||||
import 'package:core/utils/string_convert.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -159,4 +160,91 @@ void main() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('string convert test:', () {
|
||||
const testText = 'Hello';
|
||||
test(
|
||||
'should use utf8 decoder '
|
||||
'when isHtml is true',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = utf8.encode(testText);
|
||||
|
||||
// act
|
||||
final result = StringConvert.decodeFromBytes(bytes, charset: null, isHtml: true);
|
||||
|
||||
// assert
|
||||
expect(result, testText);
|
||||
});
|
||||
|
||||
test(
|
||||
'should use utf8 decoder '
|
||||
'when charset contains utf-8',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = utf8.encode(testText);
|
||||
|
||||
// act
|
||||
final result = StringConvert.decodeFromBytes(bytes, charset: 'utf-8');
|
||||
|
||||
// assert
|
||||
expect(result, testText);
|
||||
});
|
||||
|
||||
test(
|
||||
'should use latin1 decoder '
|
||||
'when charset contains latin-1',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = latin1.encode(testText);
|
||||
|
||||
// act
|
||||
final result = StringConvert.decodeFromBytes(bytes, charset: 'latin-1');
|
||||
|
||||
// assert
|
||||
expect(result, testText);
|
||||
});
|
||||
|
||||
test(
|
||||
'should use ascii decoder '
|
||||
'when charset contains ascii',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = ascii.encode(testText);
|
||||
|
||||
// act
|
||||
final result = StringConvert.decodeFromBytes(bytes, charset: 'ascii');
|
||||
|
||||
// assert
|
||||
expect(result, testText);
|
||||
});
|
||||
|
||||
test(
|
||||
'should throw NullCharsetException '
|
||||
'when charset is null',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = utf8.encode(testText);
|
||||
|
||||
// assert
|
||||
expect(
|
||||
() => StringConvert.decodeFromBytes(bytes, charset: null),
|
||||
throwsA(isA<NullCharsetException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'should throw UnsupportedCharsetException '
|
||||
'when charset is unsupported',
|
||||
() {
|
||||
// arrange
|
||||
final bytes = utf8.encode(testText);
|
||||
|
||||
// assert
|
||||
expect(
|
||||
() => StringConvert.decodeFromBytes(bytes, charset: 'unsupported'),
|
||||
throwsA(isA<UnsupportedCharsetException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user