diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
index 09ce1187d50..b6a2bd0acb3 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -112,6 +112,33 @@ export function charactersToImageNode(alt, useSvg, ...unicode) {
/>;
}
+export function processHtmlForSending(html: string): string {
+ const contentDiv = document.createElement('div');
+ contentDiv.innerHTML = html;
+
+ if (contentDiv.children.length === 0) {
+ return contentDiv.innerHTML;
+ }
+
+ let contentHTML = "";
+ for (let i=0; i < contentDiv.children.length; i++) {
+ const element = contentDiv.children[i];
+ if (element.tagName.toLowerCase() === 'p') {
+ contentHTML += element.innerHTML;
+ // Don't add a
for the last
+ if (i !== contentDiv.children.length - 1) {
+ contentHTML += '
';
+ }
+ } else {
+ const temp = document.createElement('div');
+ temp.appendChild(element.cloneNode(true));
+ contentHTML += temp.innerHTML;
+ }
+ }
+
+ return contentHTML;
+}
+
/*
* Given an untrusted HTML string, return a React node with an sanitized version
* of that HTML.
@@ -141,6 +168,99 @@ export function isUrlPermitted(inputUrl) {
}
}
+const transformTags = { // custom to matrix
+ // add blank targets to all hyperlinks except vector URLs
+ 'a': function(tagName, attribs) {
+ if (attribs.href) {
+ attribs.target = '_blank'; // by default
+
+ let m;
+ // FIXME: horrible duplication with linkify-matrix
+ m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
+ if (m) {
+ attribs.href = m[1];
+ delete attribs.target;
+ } else {
+ m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
+ if (m) {
+ const entity = m[1];
+ switch (entity[0]) {
+ case '@':
+ attribs.href = '#/user/' + entity;
+ break;
+ case '+':
+ attribs.href = '#/group/' + entity;
+ break;
+ case '#':
+ case '!':
+ attribs.href = '#/room/' + entity;
+ break;
+ }
+ delete attribs.target;
+ }
+ }
+ }
+ attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
+ return { tagName, attribs };
+ },
+ 'img': function(tagName, attribs) {
+ // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
+ // because transformTags is used _before_ we filter by allowedSchemesByTag and
+ // we don't want to allow images with `https?` `src`s.
+ if (!attribs.src || !attribs.src.startsWith('mxc://')) {
+ return { tagName, attribs: {}};
+ }
+ attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
+ attribs.src,
+ attribs.width || 800,
+ attribs.height || 600,
+ );
+ return { tagName, attribs };
+ },
+ 'code': function(tagName, attribs) {
+ if (typeof attribs.class !== 'undefined') {
+ // Filter out all classes other than ones starting with language- for syntax highlighting.
+ const classes = attribs.class.split(/\s+/).filter(function(cl) {
+ return cl.startsWith('language-');
+ });
+ attribs.class = classes.join(' ');
+ }
+ return { tagName, attribs };
+ },
+ '*': function(tagName, attribs) {
+ // Delete any style previously assigned, style is an allowedTag for font and span
+ // because attributes are stripped after transforming
+ delete attribs.style;
+
+ // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
+ // equivalents
+ const customCSSMapper = {
+ 'data-mx-color': 'color',
+ 'data-mx-bg-color': 'background-color',
+ // $customAttributeKey: $cssAttributeKey
+ };
+
+ let style = "";
+ Object.keys(customCSSMapper).forEach((customAttributeKey) => {
+ const cssAttributeKey = customCSSMapper[customAttributeKey];
+ const customAttributeValue = attribs[customAttributeKey];
+ if (customAttributeValue &&
+ typeof customAttributeValue === 'string' &&
+ COLOR_REGEX.test(customAttributeValue)
+ ) {
+ style += cssAttributeKey + ":" + customAttributeValue + ";";
+ delete attribs[customAttributeKey];
+ }
+ });
+
+ if (style) {
+ attribs.style = style;
+ }
+
+ return { tagName, attribs };
+ },
+};
+
const sanitizeHtmlParams = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
@@ -164,102 +284,14 @@ const sanitizeHtmlParams = {
allowedSchemes: PERMITTED_URL_SCHEMES,
allowProtocolRelative: false,
+ transformTags,
+};
- transformTags: { // custom to matrix
- // add blank targets to all hyperlinks except vector URLs
- 'a': function(tagName, attribs) {
- if (attribs.href) {
- attribs.target = '_blank'; // by default
-
- let m;
- // FIXME: horrible duplication with linkify-matrix
- m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
- if (m) {
- attribs.href = m[1];
- delete attribs.target;
- } else {
- m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
- if (m) {
- const entity = m[1];
- switch (entity[0]) {
- case '@':
- attribs.href = '#/user/' + entity;
- break;
- case '+':
- attribs.href = '#/group/' + entity;
- break;
- case '#':
- case '!':
- attribs.href = '#/room/' + entity;
- break;
- }
- delete attribs.target;
- }
- }
- }
- attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
- return { tagName: tagName, attribs: attribs };
- },
- 'img': function(tagName, attribs) {
- // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
- // because transformTags is used _before_ we filter by allowedSchemesByTag and
- // we don't want to allow images with `https?` `src`s.
- if (!attribs.src || !attribs.src.startsWith('mxc://')) {
- return { tagName, attribs: {}};
- }
- attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
- attribs.src,
- attribs.width || 800,
- attribs.height || 600,
- );
- return { tagName: tagName, attribs: attribs };
- },
- 'code': function(tagName, attribs) {
- if (typeof attribs.class !== 'undefined') {
- // Filter out all classes other than ones starting with language- for syntax highlighting.
- const classes = attribs.class.split(/\s+/).filter(function(cl) {
- return cl.startsWith('language-');
- });
- attribs.class = classes.join(' ');
- }
- return {
- tagName: tagName,
- attribs: attribs,
- };
- },
- '*': function(tagName, attribs) {
- // Delete any style previously assigned, style is an allowedTag for font and span
- // because attributes are stripped after transforming
- delete attribs.style;
-
- // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
- // equivalents
- const customCSSMapper = {
- 'data-mx-color': 'color',
- 'data-mx-bg-color': 'background-color',
- // $customAttributeKey: $cssAttributeKey
- };
-
- let style = "";
- Object.keys(customCSSMapper).forEach((customAttributeKey) => {
- const cssAttributeKey = customCSSMapper[customAttributeKey];
- const customAttributeValue = attribs[customAttributeKey];
- if (customAttributeValue &&
- typeof customAttributeValue === 'string' &&
- COLOR_REGEX.test(customAttributeValue)
- ) {
- style += cssAttributeKey + ":" + customAttributeValue + ";";
- delete attribs[customAttributeKey];
- }
- });
-
- if (style) {
- attribs.style = style;
- }
-
- return { tagName: tagName, attribs: attribs };
- },
- },
+// this is the same as the above except with less rewriting
+const composerSanitizeHtmlParams = Object.assign({}, sanitizeHtmlParams);
+composerSanitizeHtmlParams.transformTags = {
+ 'code': transformTags['code'],
+ '*': transformTags['*'],
};
class BaseHighlighter {
@@ -385,6 +417,7 @@ class TextHighlighter extends BaseHighlighter {
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
* opts.returnString: return an HTML string rather than JSX elements
* opts.emojiOne: optional param to do emojiOne (default true)
+ * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
*/
export function bodyToHtml(content, highlights, opts={}) {
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
@@ -392,6 +425,11 @@ export function bodyToHtml(content, highlights, opts={}) {
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
let bodyHasEmoji = false;
+ let sanitizeParams = sanitizeHtmlParams;
+ if (opts.forComposerQuote) {
+ sanitizeParams = composerSanitizeHtmlParams;
+ }
+
let strippedBody;
let safeBody;
let isDisplayedWithHtml;
@@ -403,10 +441,10 @@ export function bodyToHtml(content, highlights, opts={}) {
if (highlights && highlights.length > 0) {
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
const safeHighlights = highlights.map(function(highlight) {
- return sanitizeHtml(highlight, sanitizeHtmlParams);
+ return sanitizeHtml(highlight, sanitizeParams);
});
- // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
- sanitizeHtmlParams.textFilter = function(safeText) {
+ // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
+ sanitizeParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join('');
};
}
@@ -422,13 +460,13 @@ export function bodyToHtml(content, highlights, opts={}) {
// Only generate safeBody if the message was sent as org.matrix.custom.html
if (isHtmlMessage) {
isDisplayedWithHtml = true;
- safeBody = sanitizeHtml(formattedBody, sanitizeHtmlParams);
+ safeBody = sanitizeHtml(formattedBody, sanitizeParams);
} else {
// ... or if there are emoji, which we insert as HTML alongside the
// escaped plaintext body.
if (bodyHasEmoji) {
isDisplayedWithHtml = true;
- safeBody = sanitizeHtml(escape(strippedBody), sanitizeHtmlParams);
+ safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams);
}
}
@@ -439,7 +477,7 @@ export function bodyToHtml(content, highlights, opts={}) {
safeBody = unicodeToImage(safeBody);
}
} finally {
- delete sanitizeHtmlParams.textFilter;
+ delete sanitizeParams.textFilter;
}
if (opts.returnString) {
diff --git a/src/Markdown.js b/src/Markdown.js
index dc0d5962fdd..acfea52100c 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -111,7 +111,7 @@ export default class Markdown {
// you can nest them.
//
// Let's try sending with