[BUG] Remove HtmlMessagePurifier when not used
This commit is contained in:
@@ -62,9 +62,6 @@ 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';
|
|
||||||
|
|
||||||
// Local
|
// Local
|
||||||
export 'data/local/config/database_config.dart';
|
export 'data/local/config/database_config.dart';
|
||||||
export 'data/local/config/email_address_table.dart';
|
export 'data/local/config/email_address_table.dart';
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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()
|
|
||||||
.addBorderLefForBlockQuote();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
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"'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -73,7 +73,6 @@ class ComposerBindings extends Bindings {
|
|||||||
Get.find<AppToast>(),
|
Get.find<AppToast>(),
|
||||||
Get.find<Uuid>(),
|
Get.find<Uuid>(),
|
||||||
Get.find<TextEditingController>(),
|
Get.find<TextEditingController>(),
|
||||||
Get.find<HtmlMessagePurifier>(),
|
|
||||||
Get.find<LocalFilePickerInteractor>(),
|
Get.find<LocalFilePickerInteractor>(),
|
||||||
Get.find<UploadMultipleAttachmentInteractor>()));
|
Get.find<UploadMultipleAttachmentInteractor>()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class ComposerController extends BaseController {
|
|||||||
final AppToast _appToast;
|
final AppToast _appToast;
|
||||||
final Uuid _uuid;
|
final Uuid _uuid;
|
||||||
final TextEditingController subjectEmailInputController;
|
final TextEditingController subjectEmailInputController;
|
||||||
final HtmlMessagePurifier _htmlMessagePurifier;
|
|
||||||
final LocalFilePickerInteractor _localFilePickerInteractor;
|
final LocalFilePickerInteractor _localFilePickerInteractor;
|
||||||
final UploadMultipleAttachmentInteractor _uploadMultipleAttachmentInteractor;
|
final UploadMultipleAttachmentInteractor _uploadMultipleAttachmentInteractor;
|
||||||
|
|
||||||
@@ -73,7 +72,6 @@ class ComposerController extends BaseController {
|
|||||||
this._appToast,
|
this._appToast,
|
||||||
this._uuid,
|
this._uuid,
|
||||||
this.subjectEmailInputController,
|
this.subjectEmailInputController,
|
||||||
this._htmlMessagePurifier,
|
|
||||||
this._localFilePickerInteractor,
|
this._localFilePickerInteractor,
|
||||||
this._uploadMultipleAttachmentInteractor,
|
this._uploadMultipleAttachmentInteractor,
|
||||||
);
|
);
|
||||||
@@ -138,7 +136,7 @@ class ComposerController extends BaseController {
|
|||||||
if (composerArguments.value != null
|
if (composerArguments.value != null
|
||||||
&& composerArguments.value!.emailActionType != EmailActionType.compose
|
&& composerArguments.value!.emailActionType != EmailActionType.compose
|
||||||
&& Get.context != null) {
|
&& Get.context != null) {
|
||||||
final contentEmailQuoted = _getBodyEmailQuotedAsHtml(Get.context!, _htmlMessagePurifier);
|
final contentEmailQuoted = _getBodyEmailQuotedAsHtml(Get.context!);
|
||||||
return contentEmailQuoted;
|
return contentEmailQuoted;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
@@ -210,7 +208,7 @@ class ComposerController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getBodyEmailQuotedAsHtml(BuildContext context, HtmlMessagePurifier htmlMessagePurifier) {
|
String _getBodyEmailQuotedAsHtml(BuildContext context) {
|
||||||
final headerEmailQuoted = getHeaderEmailQuoted(Localizations.localeOf(context).toLanguageTag());
|
final headerEmailQuoted = getHeaderEmailQuoted(Localizations.localeOf(context).toLanguageTag());
|
||||||
|
|
||||||
final headerEmailQuotedAsHtml = headerEmailQuoted != null
|
final headerEmailQuotedAsHtml = headerEmailQuoted != null
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class EmailView extends GetView {
|
|||||||
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) {
|
||||||
@@ -279,6 +278,7 @@ class EmailView extends GetView {
|
|||||||
switch(emailController.emailContent.value!.type) {
|
switch(emailController.emailContent.value!.type) {
|
||||||
case EmailContentType.textHtml:
|
case EmailContentType.textHtml:
|
||||||
return HtmlContentViewer(
|
return HtmlContentViewer(
|
||||||
|
minHeight: 400,
|
||||||
contentHtml: emailController.emailContent.value!.content,
|
contentHtml: emailController.emailContent.value!.content,
|
||||||
onLoadStart: (webController, uri) => emailController.dispatchState(Right(WebViewLoadingState())),
|
onLoadStart: (webController, uri) => emailController.dispatchState(Right(WebViewLoadingState())),
|
||||||
onLoadStop: (webController, uri) => emailController.dispatchState(Right(WebViewLoadCompletedState())),
|
onLoadStop: (webController, uri) => emailController.dispatchState(Right(WebViewLoadCompletedState())),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class CoreBindings extends Bindings {
|
|||||||
_bindingAppImagePaths();
|
_bindingAppImagePaths();
|
||||||
_bindingResponsiveManager();
|
_bindingResponsiveManager();
|
||||||
_bindingKeyboardManager();
|
_bindingKeyboardManager();
|
||||||
_bindingValidator();
|
_bindingTransformer();
|
||||||
_bindingToast();
|
_bindingToast();
|
||||||
_bindingDeviceManager();
|
_bindingDeviceManager();
|
||||||
}
|
}
|
||||||
@@ -34,8 +34,7 @@ class CoreBindings extends Bindings {
|
|||||||
Get.put(KeyboardUtils());
|
Get.put(KeyboardUtils());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _bindingValidator() {
|
void _bindingTransformer() {
|
||||||
Get.put(HtmlMessagePurifier());
|
|
||||||
Get.put(HtmlAnalyzer());
|
Get.put(HtmlAnalyzer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user