do less rewriting for composer quote to prevent breaking pills
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
8bb08b1b75
commit
19e5dc5799
2 changed files with 113 additions and 101 deletions
213
src/HtmlUtils.js
213
src/HtmlUtils.js
|
@ -141,6 +141,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 = {
|
const sanitizeHtmlParams = {
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
'font', // custom to matrix for IRC-style font coloring
|
'font', // custom to matrix for IRC-style font coloring
|
||||||
|
@ -164,102 +257,14 @@ const sanitizeHtmlParams = {
|
||||||
allowedSchemes: PERMITTED_URL_SCHEMES,
|
allowedSchemes: PERMITTED_URL_SCHEMES,
|
||||||
|
|
||||||
allowProtocolRelative: false,
|
allowProtocolRelative: false,
|
||||||
|
transformTags,
|
||||||
|
};
|
||||||
|
|
||||||
transformTags: { // custom to matrix
|
// this is the same as the above except with less rewriting
|
||||||
// add blank targets to all hyperlinks except vector URLs
|
const composerSanitizeHtmlParams = Object.assign({}, sanitizeHtmlParams);
|
||||||
'a': function(tagName, attribs) {
|
composerSanitizeHtmlParams.transformTags = {
|
||||||
if (attribs.href) {
|
'code': transformTags['code'],
|
||||||
attribs.target = '_blank'; // by default
|
'*': transformTags['*'],
|
||||||
|
|
||||||
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 };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class BaseHighlighter {
|
class BaseHighlighter {
|
||||||
|
@ -385,6 +390,7 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
|
* 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.returnString: return an HTML string rather than JSX elements
|
||||||
* opts.emojiOne: optional param to do emojiOne (default true)
|
* 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={}) {
|
export function bodyToHtml(content, highlights, opts={}) {
|
||||||
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||||
|
@ -392,6 +398,11 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
|
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
|
||||||
let bodyHasEmoji = false;
|
let bodyHasEmoji = false;
|
||||||
|
|
||||||
|
let sanitizeParams = sanitizeHtmlParams;
|
||||||
|
if (opts.forComposerQuote) {
|
||||||
|
sanitizeParams = composerSanitizeHtmlParams;
|
||||||
|
}
|
||||||
|
|
||||||
let strippedBody;
|
let strippedBody;
|
||||||
let safeBody;
|
let safeBody;
|
||||||
let isDisplayedWithHtml;
|
let isDisplayedWithHtml;
|
||||||
|
@ -403,10 +414,10 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
if (highlights && highlights.length > 0) {
|
if (highlights && highlights.length > 0) {
|
||||||
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||||
const safeHighlights = highlights.map(function(highlight) {
|
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.
|
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
|
||||||
sanitizeHtmlParams.textFilter = function(safeText) {
|
sanitizeParams.textFilter = function(safeText) {
|
||||||
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -422,13 +433,13 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
||||||
if (isHtmlMessage) {
|
if (isHtmlMessage) {
|
||||||
isDisplayedWithHtml = true;
|
isDisplayedWithHtml = true;
|
||||||
safeBody = sanitizeHtml(formattedBody, sanitizeHtmlParams);
|
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
|
||||||
} else {
|
} else {
|
||||||
// ... or if there are emoji, which we insert as HTML alongside the
|
// ... or if there are emoji, which we insert as HTML alongside the
|
||||||
// escaped plaintext body.
|
// escaped plaintext body.
|
||||||
if (bodyHasEmoji) {
|
if (bodyHasEmoji) {
|
||||||
isDisplayedWithHtml = true;
|
isDisplayedWithHtml = true;
|
||||||
safeBody = sanitizeHtml(escape(strippedBody), sanitizeHtmlParams);
|
safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,7 +450,7 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
safeBody = unicodeToImage(safeBody);
|
safeBody = unicodeToImage(safeBody);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
delete sanitizeHtmlParams.textFilter;
|
delete sanitizeParams.textFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.returnString) {
|
if (opts.returnString) {
|
||||||
|
|
|
@ -373,6 +373,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
break;
|
break;
|
||||||
case 'quote': {
|
case 'quote': {
|
||||||
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
|
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
|
||||||
|
forComposerQuote: true,
|
||||||
returnString: true,
|
returnString: true,
|
||||||
emojiOne: false,
|
emojiOne: false,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue