diff --git a/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart b/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart new file mode 100644 index 000000000..a1be6ece6 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:html/dom.dart'; + +/// Transforms the HTML DOM. +abstract class DomTransformer { + + const DomTransformer(); + + /// Uses the `DOM` [document] and specified [message] to transform the `document`. + /// + /// All changes will be visible to subsequent transformers. + void process(Document document, String message, TransformConfiguration configuration); + + /// Adds a HEAD element if necessary + void ensureDocumentHeadIsAvailable(Document document) { + if (document.head == null) { + document.children.insert(0, Element.html('
')); + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/base/text_transformer.dart b/core/lib/presentation/utils/html_transformer/base/text_transformer.dart new file mode 100644 index 000000000..ac374b85e --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/base/text_transformer.dart @@ -0,0 +1,9 @@ + +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; + +/// Transforms plain text messages. +abstract class TextTransformer { + const TextTransformer(); + + String transform(String text, String message, TransformConfiguration configuration); +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/blockquote_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/blockquote_transformers.dart new file mode 100644 index 000000000..7263afa02 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/blockquote_transformers.dart @@ -0,0 +1,24 @@ + +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:html/dom.dart'; + +class SetStyleBlockQuoteTransformer extends DomTransformer { + + const SetStyleBlockQuoteTransformer(); + + @override + void process(Document document, String message, TransformConfiguration configuration) { + final quoteElements = document.getElementsByTagName('blockquote'); + for (final quote in quoteElements) { + final style = quote.attributes['style']; + if (style == null) { + quote.attributes['style'] = ''' + margin: 0px 8px 0px 8px; + padding: 8px 16px 8px 16px; + border-left:5px solid #eee; + '''; + } + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart new file mode 100644 index 000000000..94d4197f3 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart @@ -0,0 +1,34 @@ + +import 'package:html/dom.dart'; +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; + +class ImageTransformer extends DomTransformer { + + const ImageTransformer(); + + @override + void process(Document document, String message, TransformConfiguration configuration) { + final imageElements = document.getElementsByTagName('img'); + for (final imageElement in imageElements) { + final src = imageElement.attributes['src']; + if (src != null) { + if (src.startsWith('http')) { + if (configuration.blockExternalImages) { + imageElement.attributes.remove('src'); + } else if (src.startsWith('http:')) { + // always at least enforce HTTPS images: + final url = src.substring('http:'.length); + imageElement.attributes['src'] = 'https:$url'; + } + } + } + final style = imageElement.attributes['style']; + if (style == null) { + imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;'; + } else { + imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;' + style; + } + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/link_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/link_transformers.dart new file mode 100644 index 000000000..c74c906d8 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/link_transformers.dart @@ -0,0 +1,17 @@ + +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:html/dom.dart'; + +class EnsureRelationNoReferrerTransformer extends DomTransformer { + + const EnsureRelationNoReferrerTransformer(); + + @override + void process(Document document, String message, TransformConfiguration configuration) { + final linkElements = document.getElementsByTagName('a'); + for (final linkElement in linkElements) { + linkElement.attributes['rel'] = 'noopener noreferrer'; + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/meta_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/meta_transformers.dart new file mode 100644 index 000000000..be2b8a61e --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/meta_transformers.dart @@ -0,0 +1,43 @@ + +import 'package:html/dom.dart'; +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; + +class ViewPortTransformer extends DomTransformer { + + static final Element _viewPortMetaElement = Element.html( + ''); + static final Element _contentTypeMetaElement = Element.html( + ''); + + const ViewPortTransformer(); + + @override + void process(Document document, String message, TransformConfiguration configuration) { + final metaElements = document.getElementsByTagName('meta'); + var viewportNeedsToBeAdded = true; + var contentTypeNeedsToBeAdded = true; + for (final metaElement in metaElements) { + if (metaElement.attributes['name'] == 'viewport') { + viewportNeedsToBeAdded = false; + metaElement.attributes['content'] = 'width=device-width, initial-scale=1.0'; + } else if (metaElement.attributes['charset'] != null) { + metaElement.attributes['charset'] = 'utf-8'; + } else { + final httpEquiv = metaElement.attributes['http-equiv']; + if (httpEquiv != null && httpEquiv.toLowerCase() == 'content-type') { + contentTypeNeedsToBeAdded = false; + metaElement.attributes['content'] = 'text/html; charset=utf-8'; + } + } + } + if (contentTypeNeedsToBeAdded) { + ensureDocumentHeadIsAvailable(document); + document.head!.append(_contentTypeMetaElement); + } + if (viewportNeedsToBeAdded) { + ensureDocumentHeadIsAvailable(document); + document.head!.append(_viewPortMetaElement); + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/script_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/script_transformers.dart new file mode 100644 index 000000000..cd6d595d6 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/script_transformers.dart @@ -0,0 +1,17 @@ + +import 'package:html/dom.dart'; +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; + +class RemoveScriptTransformer extends DomTransformer { + + const RemoveScriptTransformer(); + + @override + void process(Document document, String message, TransformConfiguration configuration) { + final scriptElements = document.getElementsByTagName('script'); + for (final scriptElement in scriptElements) { + scriptElement.remove(); + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/table_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/table_transformers.dart new file mode 100644 index 000000000..7c3055e74 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/table_transformers.dart @@ -0,0 +1,36 @@ + +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:html/dom.dart'; + +class SetStyleTableTransformer extends DomTransformer { + + const SetStyleTableTransformer(); + + @override + void process(Document document, String message, TransformConfiguration configuration) { + final tableElements = document.getElementsByTagName('table'); + for (final table in tableElements) { + final style = table.attributes['style']; + if (style == null) { + table.attributes['style'] = 'width: 100%;max-width: 100%;border:1px solid #f0f0f0;border-collapse: collapse;border-spacing: 2px;'; + } + } + + final tdElements = document.getElementsByTagName('td'); + for (final tdTag in tdElements) { + final style = tdTag.attributes['style']; + if (style == null) { + tdTag.attributes['style'] = 'padding: 13px;margin: 0px;border:1px solid #f0f0f0;'; + } + } + + final thElements = document.getElementsByTagName('th'); + for (final thTag in thElements) { + final style = thTag.attributes['style']; + if (style == null) { + thTag.attributes['style'] = 'padding: 13px;margin: 0px;border:1px solid #f0f0f0;'; + } + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/html_transform.dart b/core/lib/presentation/utils/html_transformer/html_transform.dart new file mode 100644 index 000000000..5dc89206b --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/html_transform.dart @@ -0,0 +1,42 @@ +import 'package:core/presentation/utils/html_transformer/message_content_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:html/dom.dart'; + +class HtmlTransform { + + final String _message; + + HtmlTransform(this._message); + + /// Transforms this message to HTML code. + /// + /// Set [blockExternalImages] to `true` in case external images should be blocked. + /// Optionally specify the [transformConfiguration] to control all aspects of the transformation - in that case other parameters are ignored. + String transformToHtml({ + bool? blockExternalImages, + TransformConfiguration? transformConfiguration, + }) { + final document = transformToDocument( + blockExternalImages: blockExternalImages, + transformConfiguration: transformConfiguration, + ); + return document.outerHtml; + } + + /// Transforms this message to Document. + /// + /// Set [blockExternalImages] to `true` in case external images should be blocked. + /// Optionally specify the [transformConfiguration] to control all aspects of the transformation - in that case other parameters are ignored. + Document transformToDocument({ + bool? blockExternalImages, + int? maxImageWidth, + TransformConfiguration? transformConfiguration, + }) { + transformConfiguration ??= TransformConfiguration.create( + blockExternalImages: blockExternalImages, + maxImageWidth: maxImageWidth, + ); + final transformer = MessageContentTransformer(transformConfiguration); + return transformer.toDocument(_message); + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/message_content_transformer.dart b/core/lib/presentation/utils/html_transformer/message_content_transformer.dart new file mode 100644 index 000000000..4dcca68cb --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/message_content_transformer.dart @@ -0,0 +1,28 @@ +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:html/dom.dart'; +import 'package:html/parser.dart' show parse; + +/// Transforms messages +class MessageContentTransformer { + /// The configuration used for the transformation + final TransformConfiguration configuration; + + MessageContentTransformer(this.configuration); + + void transformDocument(Document document, String message) { + for (final domTransformer in configuration.domTransformers) { + domTransformer.process(document, message, configuration); + } + } + + Document toDocument(String message) { + var html = message; + final document = parse(html); + transformDocument(document, message); + return document; + } + + String toHtml(String message) { + return toDocument(message).outerHtml; + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/text/convert_tags_text_transformer.dart b/core/lib/presentation/utils/html_transformer/text/convert_tags_text_transformer.dart new file mode 100644 index 000000000..a235b9c12 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/text/convert_tags_text_transformer.dart @@ -0,0 +1,13 @@ + +import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; + +class ConvertTagsTextTransformer implements TextTransformer { + + const ConvertTagsTextTransformer(); + + @override + String transform(String text, String message, TransformConfiguration configuration) { + return text.replaceAll('<', '<').replaceAll('>', '>'); + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/text/linebreak_text_transformer.dart b/core/lib/presentation/utils/html_transformer/text/linebreak_text_transformer.dart new file mode 100644 index 000000000..98abf9a6b --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/text/linebreak_text_transformer.dart @@ -0,0 +1,15 @@ + +import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; + +class LineBreakTextTransformer extends TextTransformer { + + const LineBreakTextTransformer(); + + @override + String transform(String text, String message, TransformConfiguration configuration) { + text = text.replaceAll('\r\n', '