Sanitize Html message content
This commit is contained in:
@@ -6,6 +6,7 @@ export 'presentation/extensions/url_extension.dart';
|
|||||||
export 'presentation/extensions/capitalize_extension.dart';
|
export 'presentation/extensions/capitalize_extension.dart';
|
||||||
export 'presentation/extensions/list_extensions.dart';
|
export 'presentation/extensions/list_extensions.dart';
|
||||||
export 'domain/extensions/datetime_extension.dart';
|
export 'domain/extensions/datetime_extension.dart';
|
||||||
|
export 'presentation/extensions/html_extension.dart';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export 'presentation/utils/theme_utils.dart';
|
export 'presentation/utils/theme_utils.dart';
|
||||||
@@ -44,3 +45,6 @@ export 'data/network/dio_client.dart';
|
|||||||
export 'presentation/state/success.dart';
|
export 'presentation/state/success.dart';
|
||||||
export 'presentation/state/failure.dart';
|
export 'presentation/state/failure.dart';
|
||||||
export 'presentation/state/app_state.dart';
|
export 'presentation/state/app_state.dart';
|
||||||
|
|
||||||
|
// Validator
|
||||||
|
export 'presentation/validator/html_message_purifier.dart';
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
extension HtmlExtension on String {
|
||||||
|
|
||||||
|
String removeFontSizeZeroPixel() => replaceAll(RegExp('font-size:0px;'), '');
|
||||||
|
|
||||||
|
String removeHeightZeroPixel() => replaceAll(RegExp('height:0px;'), '');
|
||||||
|
|
||||||
|
String removeWidthZeroPixel() => replaceAll(RegExp('width:0px;'), '');
|
||||||
|
|
||||||
|
String removeMaxWidthZeroPixel() => replaceAll(RegExp('max-width:0px;'), '');
|
||||||
|
|
||||||
|
String removeMaxHeightZeroPixel() => replaceAll(RegExp('max-height:0px;'), '');
|
||||||
|
|
||||||
|
String changeStyleBackground() => replaceAll(RegExp('background:'), 'background-color:');
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'sane_html_validator.dart' show SaneHtmlValidator;
|
||||||
|
import 'package:core/core.dart';
|
||||||
|
|
||||||
|
class HtmlMessagePurifier {
|
||||||
|
String purifyHtmlMessage(
|
||||||
|
String html,
|
||||||
|
{
|
||||||
|
Set<String>? allowElementIds,
|
||||||
|
Set<String>? allowClassNames,
|
||||||
|
Set<String>? allowAttributes
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return sanitizeHtml(
|
||||||
|
html,
|
||||||
|
allowElementId: (elementId) => allowElementIds != null ? allowElementIds.contains(elementId) : false,
|
||||||
|
allowClassName: (className) => allowClassNames != null ? allowClassNames.contains(className) : false,
|
||||||
|
allowAttributes: (attribute) => allowAttributes != null ? allowAttributes.contains(attribute) : false)
|
||||||
|
.changeStyleBackground()
|
||||||
|
.removeFontSizeZeroPixel()
|
||||||
|
.removeMaxHeightZeroPixel()
|
||||||
|
.removeMaxWidthZeroPixel()
|
||||||
|
.removeHeightZeroPixel()
|
||||||
|
.removeWidthZeroPixel();
|
||||||
|
}
|
||||||
|
|
||||||
|
String sanitizeHtml(
|
||||||
|
String htmlString,
|
||||||
|
{
|
||||||
|
bool Function(String)? allowElementId,
|
||||||
|
bool Function(String)? allowClassName,
|
||||||
|
bool Function(String)? allowAttributes,
|
||||||
|
Iterable<String>? Function(String)? addLinkRel
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return SaneHtmlValidator(
|
||||||
|
allowElementId: allowElementId,
|
||||||
|
allowClassName: allowClassName,
|
||||||
|
allowAttributes: allowAttributes,
|
||||||
|
addLinkRel: addLinkRel,
|
||||||
|
).sanitize(htmlString);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:html/parser.dart' as html_parser;
|
||||||
|
|
||||||
|
final _allowedElements = <String>{
|
||||||
|
'H1',
|
||||||
|
'H2',
|
||||||
|
'H3',
|
||||||
|
'H4',
|
||||||
|
'H5',
|
||||||
|
'H6',
|
||||||
|
'H7',
|
||||||
|
'H8',
|
||||||
|
'BR',
|
||||||
|
'B',
|
||||||
|
'I',
|
||||||
|
'STRONG',
|
||||||
|
'EM',
|
||||||
|
'A',
|
||||||
|
'PRE',
|
||||||
|
'CODE',
|
||||||
|
'IMG',
|
||||||
|
'TT',
|
||||||
|
'DIV',
|
||||||
|
'INS',
|
||||||
|
'DEL',
|
||||||
|
'SUP',
|
||||||
|
'SUB',
|
||||||
|
'P',
|
||||||
|
'OL',
|
||||||
|
'UL',
|
||||||
|
'TABLE',
|
||||||
|
'THEAD',
|
||||||
|
'TBODY',
|
||||||
|
'TFOOT',
|
||||||
|
'BLOCKQUOTE',
|
||||||
|
'DL',
|
||||||
|
'DT',
|
||||||
|
'DD',
|
||||||
|
'KBD',
|
||||||
|
'Q',
|
||||||
|
'SAMP',
|
||||||
|
'VAR',
|
||||||
|
'HR',
|
||||||
|
'RUBY',
|
||||||
|
'RT',
|
||||||
|
'RP',
|
||||||
|
'LI',
|
||||||
|
'TR',
|
||||||
|
'TD',
|
||||||
|
'TH',
|
||||||
|
'S',
|
||||||
|
'STRIKE',
|
||||||
|
'SUMMARY',
|
||||||
|
'DETAILS',
|
||||||
|
'CAPTION',
|
||||||
|
'FIGURE',
|
||||||
|
'FIGCAPTION',
|
||||||
|
'ABBR',
|
||||||
|
'BDO',
|
||||||
|
'CITE',
|
||||||
|
'DFN',
|
||||||
|
'MARK',
|
||||||
|
'SMALL',
|
||||||
|
'SPAN',
|
||||||
|
'TIME',
|
||||||
|
'WBR',
|
||||||
|
};
|
||||||
|
|
||||||
|
final _alwaysAllowedAttributes = <String>{
|
||||||
|
'abbr',
|
||||||
|
'accept',
|
||||||
|
'accept-charset',
|
||||||
|
'accesskey',
|
||||||
|
'action',
|
||||||
|
'align',
|
||||||
|
'alt',
|
||||||
|
'aria-describedby',
|
||||||
|
'aria-hidden',
|
||||||
|
'aria-label',
|
||||||
|
'aria-labelledby',
|
||||||
|
'axis',
|
||||||
|
'border',
|
||||||
|
'cellpadding',
|
||||||
|
'cellspacing',
|
||||||
|
'char',
|
||||||
|
'charoff',
|
||||||
|
'charset',
|
||||||
|
'checked',
|
||||||
|
'clear',
|
||||||
|
'cols',
|
||||||
|
'colspan',
|
||||||
|
'color',
|
||||||
|
'compact',
|
||||||
|
'coords',
|
||||||
|
'datetime',
|
||||||
|
'dir',
|
||||||
|
'disabled',
|
||||||
|
'enctype',
|
||||||
|
'for',
|
||||||
|
'frame',
|
||||||
|
'headers',
|
||||||
|
'height',
|
||||||
|
'hreflang',
|
||||||
|
'hspace',
|
||||||
|
'ismap',
|
||||||
|
'label',
|
||||||
|
'lang',
|
||||||
|
'maxlength',
|
||||||
|
'media',
|
||||||
|
'method',
|
||||||
|
'multiple',
|
||||||
|
'name',
|
||||||
|
'nohref',
|
||||||
|
'noshade',
|
||||||
|
'nowrap',
|
||||||
|
'open',
|
||||||
|
'prompt',
|
||||||
|
'readonly',
|
||||||
|
'rel',
|
||||||
|
'rev',
|
||||||
|
'rows',
|
||||||
|
'rowspan',
|
||||||
|
'rules',
|
||||||
|
'scope',
|
||||||
|
'selected',
|
||||||
|
'shape',
|
||||||
|
'size',
|
||||||
|
'span',
|
||||||
|
'start',
|
||||||
|
'summary',
|
||||||
|
'tabindex',
|
||||||
|
'target',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'usemap',
|
||||||
|
'valign',
|
||||||
|
'value',
|
||||||
|
'vspace',
|
||||||
|
'width',
|
||||||
|
'itemprop',
|
||||||
|
};
|
||||||
|
|
||||||
|
bool _alwaysAllowed(String _) => true;
|
||||||
|
|
||||||
|
bool _validLink(String url) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
return uri.isScheme('https') ||
|
||||||
|
uri.isScheme('http') ||
|
||||||
|
uri.isScheme('mailto') ||
|
||||||
|
!uri.hasScheme;
|
||||||
|
} on FormatException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _validUrl(String url) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
return uri.isScheme('https') || uri.isScheme('http') || !uri.hasScheme;
|
||||||
|
} on FormatException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _citeAttributeValidator = <String, bool Function(String)>{
|
||||||
|
'cite': _validUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
final _elementAttributeValidators =
|
||||||
|
<String, Map<String, bool Function(String)>>{
|
||||||
|
'A': {
|
||||||
|
'href': _validLink,
|
||||||
|
},
|
||||||
|
'IMG': {
|
||||||
|
'src': _alwaysAllowed,
|
||||||
|
'longdesc': _validUrl,
|
||||||
|
},
|
||||||
|
'DIV': {
|
||||||
|
'itemscope': _alwaysAllowed,
|
||||||
|
'itemtype': _alwaysAllowed,
|
||||||
|
},
|
||||||
|
'BLOCKQUOTE': _citeAttributeValidator,
|
||||||
|
'DEL': _citeAttributeValidator,
|
||||||
|
'INS': _citeAttributeValidator,
|
||||||
|
'Q': _citeAttributeValidator,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An implementation of [html.NodeValidator] that only allows sane HTML tags
|
||||||
|
/// and attributes protecting against XSS.
|
||||||
|
///
|
||||||
|
/// Modeled after the [rules employed by Github][1] when sanitizing GFM (Github
|
||||||
|
/// Flavored Markdown). Notably this excludes CSS styles and other tags that
|
||||||
|
/// easily interferes with the rest of the page.
|
||||||
|
///
|
||||||
|
/// [1]: https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/sanitization_filter.rb
|
||||||
|
class SaneHtmlValidator {
|
||||||
|
final bool Function(String)? allowElementId;
|
||||||
|
final bool Function(String)? allowClassName;
|
||||||
|
final bool Function(String)? allowAttributes;
|
||||||
|
final Iterable<String>? Function(String)? addLinkRel;
|
||||||
|
|
||||||
|
SaneHtmlValidator({
|
||||||
|
required this.allowElementId,
|
||||||
|
required this.allowClassName,
|
||||||
|
required this.allowAttributes,
|
||||||
|
required this.addLinkRel,
|
||||||
|
});
|
||||||
|
|
||||||
|
String sanitize(String htmlString) {
|
||||||
|
final root = html_parser.parseFragment(htmlString);
|
||||||
|
_sanitize(root);
|
||||||
|
return root.outerHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sanitize(Node node) {
|
||||||
|
if (node is Element) {
|
||||||
|
final tagName = node.localName!.toUpperCase();
|
||||||
|
if (!_allowedElements.contains(tagName)) {
|
||||||
|
node.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.attributes.removeWhere((k, v) {
|
||||||
|
final attrName = k.toString();
|
||||||
|
if (attrName == 'id') {
|
||||||
|
return allowElementId == null || !allowElementId!(v);
|
||||||
|
}
|
||||||
|
if (attrName == 'class') {
|
||||||
|
if (allowClassName == null) return true;
|
||||||
|
node.classes.removeWhere((cn) => !allowClassName!(cn));
|
||||||
|
return node.classes.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !_isAttributeAllowed(tagName, attrName, v);
|
||||||
|
});
|
||||||
|
if (tagName == 'A') {
|
||||||
|
final href = node.attributes['href'];
|
||||||
|
if (href != null && addLinkRel != null) {
|
||||||
|
final rels = addLinkRel!(href);
|
||||||
|
if (rels != null && rels.isNotEmpty) {
|
||||||
|
node.attributes['rel'] = rels.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.hasChildNodes()) {
|
||||||
|
// doing it in reverse order, because we could otherwise skip one, when a
|
||||||
|
// node is removed...
|
||||||
|
for (var i = node.nodes.length - 1; i >= 0; i--) {
|
||||||
|
_sanitize(node.nodes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isAttributeAllowed(String tagName, String attrName, String value) {
|
||||||
|
if (_alwaysAllowedAttributes.contains(attrName)) return true;
|
||||||
|
|
||||||
|
if (allowAttributes != null && allowAttributes!(attrName)) return true;
|
||||||
|
|
||||||
|
// Special validators for special attributes on special tags (href/src/cite)
|
||||||
|
final attributeValidators = _elementAttributeValidators[tagName];
|
||||||
|
if (attributeValidators == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final validator = attributeValidators[attrName];
|
||||||
|
if (validator == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validator(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ dependencies:
|
|||||||
|
|
||||||
built_collection: 5.1.0
|
built_collection: 5.1.0
|
||||||
|
|
||||||
|
html: 0.15.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:core/presentation/validator/sane_html_validator.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('sane_html_validator test', () {
|
||||||
|
|
||||||
|
test('sanitize should return clean html not contain style attribute', () async {
|
||||||
|
|
||||||
|
final saneHtmlValidator = SaneHtmlValidator(
|
||||||
|
allowAttributes: null,
|
||||||
|
allowClassName: null,
|
||||||
|
allowElementId: null,
|
||||||
|
addLinkRel: null
|
||||||
|
);
|
||||||
|
|
||||||
|
final cleanHtml = saneHtmlValidator.sanitize('<a style="background-color: #FF0000" class="className" id="idElement" href="javascript:alert()">Hello</a>');
|
||||||
|
|
||||||
|
expect(cleanHtml, isNot(contains('style')));
|
||||||
|
expect(cleanHtml, isNot(contains('className')));
|
||||||
|
expect(cleanHtml, isNot(contains('idElement')));
|
||||||
|
expect(cleanHtml, isNot(contains('href')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitize should return clean html contain style attribute', () async {
|
||||||
|
|
||||||
|
final saneHtmlValidator = SaneHtmlValidator(
|
||||||
|
allowAttributes: (attribute) => attribute == 'style',
|
||||||
|
allowClassName: null,
|
||||||
|
allowElementId: null,
|
||||||
|
addLinkRel: null
|
||||||
|
);
|
||||||
|
|
||||||
|
final cleanHtml = saneHtmlValidator.sanitize('<a style="background-color: #FF0000">Hello</a>');
|
||||||
|
|
||||||
|
expect(cleanHtml, contains('style'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitize should return clean html contain test class name', () async {
|
||||||
|
|
||||||
|
final saneHtmlValidator = SaneHtmlValidator(
|
||||||
|
allowAttributes: null,
|
||||||
|
allowClassName: (className) => className == 'test',
|
||||||
|
allowElementId: null,
|
||||||
|
addLinkRel: null
|
||||||
|
);
|
||||||
|
|
||||||
|
final cleanHtml = saneHtmlValidator.sanitize('<span class="test">Hello</span');
|
||||||
|
|
||||||
|
expect(cleanHtml, contains('test'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitize should return clean html contain test element id', () async {
|
||||||
|
|
||||||
|
final saneHtmlValidator = SaneHtmlValidator(
|
||||||
|
allowAttributes: null,
|
||||||
|
allowClassName: null,
|
||||||
|
allowElementId: (elementId) => elementId == 'test',
|
||||||
|
addLinkRel: null
|
||||||
|
);
|
||||||
|
|
||||||
|
final cleanHtml = saneHtmlValidator.sanitize('<span id="test">Hello</span');
|
||||||
|
|
||||||
|
expect(cleanHtml, contains('test'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitize should return clean html contain rel="nofollow" in tag', () async {
|
||||||
|
|
||||||
|
final saneHtmlValidator = SaneHtmlValidator(
|
||||||
|
allowAttributes: null,
|
||||||
|
allowClassName: null,
|
||||||
|
allowElementId: null,
|
||||||
|
addLinkRel: (href) => href == 'http://example.com/test.jpg' ? ['nofollow'] : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final cleanHtml = saneHtmlValidator.sanitize('<a href="http://example.com/test.jpg">Hello</a>');
|
||||||
|
|
||||||
|
expect(cleanHtml, contains('rel="nofollow"'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ class EmailView extends GetWidget {
|
|||||||
final emailController = Get.find<EmailController>();
|
final emailController = Get.find<EmailController>();
|
||||||
final responsiveUtils = Get.find<ResponsiveUtils>();
|
final responsiveUtils = Get.find<ResponsiveUtils>();
|
||||||
final imagePaths = Get.find<ImagePaths>();
|
final imagePaths = Get.find<ImagePaths>();
|
||||||
|
final htmlMessagePurifier = Get.find<HtmlMessagePurifier>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -192,8 +193,8 @@ class EmailView extends GetWidget {
|
|||||||
itemCount: messageContents.length,
|
itemCount: messageContents.length,
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) =>
|
||||||
MessageContentTileBuilder(
|
MessageContentTileBuilder(
|
||||||
|
htmlMessagePurifier,
|
||||||
messageContents[index],
|
messageContents[index],
|
||||||
imagePaths,
|
|
||||||
attachmentsInline,
|
attachmentsInline,
|
||||||
emailController.mailboxDashBoardController.sessionCurrent,
|
emailController.mailboxDashBoardController.sessionCurrent,
|
||||||
emailController.mailboxDashBoardController.accountId.value)
|
emailController.mailboxDashBoardController.accountId.value)
|
||||||
|
|||||||
@@ -2,27 +2,24 @@
|
|||||||
import 'package:core/core.dart';
|
import 'package:core/core.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||||
import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
|
||||||
import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart';
|
import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/model/message_content.dart';
|
import 'package:tmail_ui_user/features/email/presentation/model/message_content.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart';
|
import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart';
|
||||||
import 'package:sanitize_html/sanitize_html.dart' show sanitizeHtml;
|
|
||||||
|
|
||||||
class MessageContentTileBuilder {
|
class MessageContentTileBuilder {
|
||||||
|
|
||||||
final MessageContent _messageContent;
|
final MessageContent _messageContent;
|
||||||
final ImagePaths _imagePaths;
|
|
||||||
final List<AttachmentFile> _attachmentInlines;
|
final List<AttachmentFile> _attachmentInlines;
|
||||||
final Session? _session;
|
final Session? _session;
|
||||||
final AccountId? _accountId;
|
final AccountId? _accountId;
|
||||||
|
final HtmlMessagePurifier _htmlMessagePurifier;
|
||||||
|
|
||||||
MessageContentTileBuilder(
|
MessageContentTileBuilder(
|
||||||
|
this._htmlMessagePurifier,
|
||||||
this._messageContent,
|
this._messageContent,
|
||||||
this._imagePaths,
|
|
||||||
this._attachmentInlines,
|
this._attachmentInlines,
|
||||||
this._session,
|
this._session,
|
||||||
this._accountId,
|
this._accountId,
|
||||||
@@ -39,10 +36,9 @@ class MessageContentTileBuilder {
|
|||||||
child: _messageContent.textFormat == TextFormat.PLAIN
|
child: _messageContent.textFormat == TextFormat.PLAIN
|
||||||
? Text('${_messageContent.content}', style: TextStyle(fontSize: 12, color: AppColor.mailboxTextColor))
|
? Text('${_messageContent.content}', style: TextStyle(fontSize: 12, color: AppColor.mailboxTextColor))
|
||||||
: HtmlWidget(
|
: HtmlWidget(
|
||||||
cleanHtmlMessage(_messageContent.content),
|
getHtmlMessageText(),
|
||||||
textStyle: TextStyle(fontSize: 12, color: AppColor.mailboxTextColor),
|
textStyle: TextStyle(fontSize: 12, color: AppColor.mailboxTextColor),
|
||||||
buildAsync: true,
|
buildAsync: true,
|
||||||
factoryBuilder: () => _PopupPhotoViewWidgetFactory(_imagePaths),
|
|
||||||
buildAsyncBuilder: (_, snapshot) => snapshot.data ?? SizedBox(
|
buildAsyncBuilder: (_, snapshot) => snapshot.data ?? SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
width: 30,
|
width: 30,
|
||||||
@@ -57,47 +53,11 @@ class MessageContentTileBuilder {
|
|||||||
final message = (_attachmentInlines.isNotEmpty && _session != null && _accountId != null && _messageContent.hasImageInlineWithCid())
|
final message = (_attachmentInlines.isNotEmpty && _session != null && _accountId != null && _messageContent.hasImageInlineWithCid())
|
||||||
? '${_messageContent.getContentHasInlineAttachment(_session!, _accountId!, _attachmentInlines)}'
|
? '${_messageContent.getContentHasInlineAttachment(_session!, _accountId!, _attachmentInlines)}'
|
||||||
: '${_messageContent.content}';
|
: '${_messageContent.content}';
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
String cleanHtmlMessage(String message) => hasTagTable(getHtmlMessageText()) ? sanitizeHtml(getHtmlMessageText()) : message;
|
final trustAsHtml = _htmlMessagePurifier.purifyHtmlMessage(
|
||||||
|
message,
|
||||||
|
allowAttributes: {'style', 'input', 'form'});
|
||||||
|
|
||||||
bool hasTagTable(String html) => html.toLowerCase().contains('</table>');
|
return trustAsHtml;
|
||||||
}
|
|
||||||
|
|
||||||
class _PopupPhotoViewWidgetFactory extends WidgetFactory {
|
|
||||||
|
|
||||||
final ImagePaths _imagePaths;
|
|
||||||
|
|
||||||
_PopupPhotoViewWidgetFactory(this._imagePaths);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? buildImageWidget(BuildMetadata meta, {String? semanticLabel, required String url}) {
|
|
||||||
final built = super.buildImageWidget(meta, semanticLabel: semanticLabel, url: url);
|
|
||||||
|
|
||||||
if (built is Image) {
|
|
||||||
return Builder(
|
|
||||||
builder: (context) => GestureDetector(
|
|
||||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (_) => Scaffold(
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
PhotoView(
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: url),
|
|
||||||
imageProvider: built.image),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: 16, top: 35),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
icon: SvgPicture.asset(_imagePaths.icCloseMailbox, width: 30, height: 30, fit: BoxFit.fill)))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
))),
|
|
||||||
child: Hero(tag: url, child: built),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return built;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ class CoreBindings extends Bindings {
|
|||||||
_bindingAppImagePaths();
|
_bindingAppImagePaths();
|
||||||
_bindingResponsiveManager();
|
_bindingResponsiveManager();
|
||||||
_bindingKeyboardManager();
|
_bindingKeyboardManager();
|
||||||
|
_bindingValiadtor();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _bindingAppImagePaths() {
|
void _bindingAppImagePaths() {
|
||||||
@@ -27,4 +28,8 @@ class CoreBindings extends Bindings {
|
|||||||
void _bindingKeyboardManager() {
|
void _bindingKeyboardManager() {
|
||||||
Get.put(KeyboardUtils());
|
Get.put(KeyboardUtils());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _bindingValiadtor() {
|
||||||
|
Get.put(HtmlMessagePurifier());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -89,11 +89,6 @@ dependencies:
|
|||||||
# flutter_widget_from_html
|
# flutter_widget_from_html
|
||||||
flutter_widget_from_html: 0.6.2
|
flutter_widget_from_html: 0.6.2
|
||||||
|
|
||||||
# photo_view
|
|
||||||
photo_view: 0.12.0
|
|
||||||
|
|
||||||
sanitize_html: 2.0.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user