[BUG] Remove HtmlMessagePurifier when not used

This commit is contained in:
dab246
2021-10-14 09:08:47 +07:00
committed by Dat H. Pham
parent d3bdd9c3e9
commit 8346e030c8
8 changed files with 5 additions and 409 deletions
-3
View File
@@ -62,9 +62,6 @@ export 'presentation/state/success.dart';
export 'presentation/state/failure.dart';
export 'presentation/state/app_state.dart';
// Validator
export 'presentation/validator/html_message_purifier.dart';
// Local
export 'data/local/config/database_config.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);
}
}
-80
View File
@@ -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<Uuid>(),
Get.find<TextEditingController>(),
Get.find<HtmlMessagePurifier>(),
Get.find<LocalFilePickerInteractor>(),
Get.find<UploadMultipleAttachmentInteractor>()));
}
@@ -52,7 +52,6 @@ class ComposerController extends BaseController {
final AppToast _appToast;
final Uuid _uuid;
final TextEditingController subjectEmailInputController;
final HtmlMessagePurifier _htmlMessagePurifier;
final LocalFilePickerInteractor _localFilePickerInteractor;
final UploadMultipleAttachmentInteractor _uploadMultipleAttachmentInteractor;
@@ -73,7 +72,6 @@ class ComposerController extends BaseController {
this._appToast,
this._uuid,
this.subjectEmailInputController,
this._htmlMessagePurifier,
this._localFilePickerInteractor,
this._uploadMultipleAttachmentInteractor,
);
@@ -138,7 +136,7 @@ class ComposerController extends BaseController {
if (composerArguments.value != null
&& composerArguments.value!.emailActionType != EmailActionType.compose
&& Get.context != null) {
final contentEmailQuoted = _getBodyEmailQuotedAsHtml(Get.context!, _htmlMessagePurifier);
final contentEmailQuoted = _getBodyEmailQuotedAsHtml(Get.context!);
return contentEmailQuoted;
}
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 headerEmailQuotedAsHtml = headerEmailQuoted != null
@@ -23,7 +23,6 @@ class EmailView extends GetView {
final emailController = Get.find<EmailController>();
final responsiveUtils = Get.find<ResponsiveUtils>();
final imagePaths = Get.find<ImagePaths>();
final htmlMessagePurifier = Get.find<HtmlMessagePurifier>();
@override
Widget build(BuildContext context) {
@@ -279,6 +278,7 @@ class EmailView extends GetView {
switch(emailController.emailContent.value!.type) {
case EmailContentType.textHtml:
return HtmlContentViewer(
minHeight: 400,
contentHtml: emailController.emailContent.value!.content,
onLoadStart: (webController, uri) => emailController.dispatchState(Right(WebViewLoadingState())),
onLoadStop: (webController, uri) => emailController.dispatchState(Right(WebViewLoadCompletedState())),
+2 -3
View File
@@ -13,7 +13,7 @@ class CoreBindings extends Bindings {
_bindingAppImagePaths();
_bindingResponsiveManager();
_bindingKeyboardManager();
_bindingValidator();
_bindingTransformer();
_bindingToast();
_bindingDeviceManager();
}
@@ -34,8 +34,7 @@ class CoreBindings extends Bindings {
Get.put(KeyboardUtils());
}
void _bindingValidator() {
Get.put(HtmlMessagePurifier());
void _bindingTransformer() {
Get.put(HtmlAnalyzer());
}