2015-11-27 16:02:32 +01:00
|
|
|
/*
|
2016-01-07 05:06:39 +01:00
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2015-11-27 16:02:32 +01:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var React = require('react');
|
2016-02-10 21:25:32 +01:00
|
|
|
var ReactDOMServer = require('react-dom/server')
|
2015-11-27 16:02:32 +01:00
|
|
|
var sanitizeHtml = require('sanitize-html');
|
|
|
|
var highlight = require('highlight.js');
|
|
|
|
|
|
|
|
var sanitizeHtmlParams = {
|
|
|
|
allowedTags: [
|
2016-02-09 16:07:39 +01:00
|
|
|
'font', // custom to matrix for IRC-style font coloring
|
2015-11-28 13:44:10 +01:00
|
|
|
'del', // for markdown
|
2016-02-09 16:07:39 +01:00
|
|
|
// deliberately no h1/h2 to stop people shouting.
|
2015-11-27 16:02:32 +01:00
|
|
|
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
|
|
|
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
|
|
|
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
|
|
|
|
],
|
|
|
|
allowedAttributes: {
|
|
|
|
// custom ones first:
|
|
|
|
font: [ 'color' ], // custom to matrix
|
|
|
|
a: [ 'href', 'name', 'target' ], // remote target: custom to matrix
|
|
|
|
// We don't currently allow img itself by default, but this
|
|
|
|
// would make sense if we did
|
|
|
|
img: [ 'src' ],
|
|
|
|
},
|
|
|
|
// Lots of these won't come up by default because we don't allow them
|
|
|
|
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
|
|
|
|
// URL schemes we permit
|
|
|
|
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
|
|
|
allowedSchemesByTag: {},
|
|
|
|
|
|
|
|
transformTags: { // custom to matrix
|
|
|
|
// add blank targets to all hyperlinks
|
|
|
|
'a': sanitizeHtml.simpleTransform('a', { target: '_blank'} )
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2015-12-24 00:50:35 +01:00
|
|
|
class Highlighter {
|
|
|
|
constructor(html, highlightClass, onHighlightClick) {
|
|
|
|
this.html = html;
|
|
|
|
this.highlightClass = highlightClass;
|
|
|
|
this.onHighlightClick = onHighlightClick;
|
|
|
|
this._key = 0;
|
|
|
|
}
|
|
|
|
|
2016-02-10 21:25:32 +01:00
|
|
|
applyHighlights(safeSnippet, safeHighlights) {
|
2015-11-29 04:22:01 +01:00
|
|
|
var lastOffset = 0;
|
|
|
|
var offset;
|
|
|
|
var nodes = [];
|
|
|
|
|
2016-02-10 21:25:32 +01:00
|
|
|
var safeHighlight = safeHighlights[0];
|
2015-12-28 04:14:50 +01:00
|
|
|
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
2015-11-29 04:22:01 +01:00
|
|
|
// handle preamble
|
|
|
|
if (offset > lastOffset) {
|
2015-12-24 00:50:35 +01:00
|
|
|
var subSnippet = safeSnippet.substring(lastOffset, offset);
|
2016-02-10 21:25:32 +01:00
|
|
|
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
|
2015-11-29 04:22:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// do highlight
|
2015-12-24 00:50:35 +01:00
|
|
|
nodes.push(this._createSpan(safeHighlight, true));
|
2015-11-29 04:22:01 +01:00
|
|
|
|
|
|
|
lastOffset = offset + safeHighlight.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
// handle postamble
|
|
|
|
if (lastOffset != safeSnippet.length) {
|
2015-12-24 00:50:35 +01:00
|
|
|
var subSnippet = safeSnippet.substring(lastOffset, undefined);
|
2016-02-10 21:25:32 +01:00
|
|
|
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
|
2015-11-29 14:00:58 +01:00
|
|
|
}
|
|
|
|
return nodes;
|
2015-12-24 00:50:35 +01:00
|
|
|
}
|
2015-11-29 14:00:58 +01:00
|
|
|
|
2016-02-10 21:25:32 +01:00
|
|
|
_applySubHighlights(safeSnippet, safeHighlights) {
|
|
|
|
if (safeHighlights[1]) {
|
2015-11-29 14:00:58 +01:00
|
|
|
// recurse into this range to check for the next set of highlight matches
|
2016-02-10 21:25:32 +01:00
|
|
|
return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
|
2015-11-29 14:00:58 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// no more highlights to be found, just return the unhighlighted string
|
2015-12-24 00:50:35 +01:00
|
|
|
return [this._createSpan(safeSnippet, false)];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* create a <span> node to hold the given content
|
|
|
|
*
|
|
|
|
* spanBody: content of the span. If html, must have been sanitised
|
|
|
|
* highlight: true to highlight as a search match
|
|
|
|
*/
|
|
|
|
_createSpan(spanBody, highlight) {
|
|
|
|
var spanProps = {
|
|
|
|
key: this._key++,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (highlight) {
|
|
|
|
spanProps.onClick = this.onHighlightClick;
|
|
|
|
spanProps.className = this.highlightClass;
|
2015-11-29 04:22:01 +01:00
|
|
|
}
|
|
|
|
|
2015-12-24 00:50:35 +01:00
|
|
|
if (this.html) {
|
|
|
|
return (<span {...spanProps} dangerouslySetInnerHTML={{ __html: spanBody }} />);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return (<span {...spanProps}>{ spanBody }</span>);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-11-27 16:02:32 +01:00
|
|
|
|
|
|
|
|
2015-12-24 00:50:35 +01:00
|
|
|
module.exports = {
|
|
|
|
/* turn a matrix event body into html
|
|
|
|
*
|
|
|
|
* content: 'content' of the MatrixEvent
|
|
|
|
*
|
2016-02-10 21:25:32 +01:00
|
|
|
* highlights: optional list of words to highlight, ordered by longest word first
|
2015-12-24 00:50:35 +01:00
|
|
|
*
|
|
|
|
* opts.onHighlightClick: optional callback function to be called when a
|
|
|
|
* highlighted word is clicked
|
|
|
|
*/
|
|
|
|
bodyToHtml: function(content, highlights, opts) {
|
|
|
|
opts = opts || {};
|
|
|
|
|
|
|
|
var isHtml = (content.format === "org.matrix.custom.html");
|
|
|
|
|
2016-02-10 21:25:32 +01:00
|
|
|
var safeBody, body;
|
2015-12-24 00:50:35 +01:00
|
|
|
if (isHtml) {
|
2016-02-10 21:25:32 +01:00
|
|
|
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
|
|
|
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
|
|
|
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
|
|
|
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
|
|
|
if (highlights && highlights.length > 0) {
|
|
|
|
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
|
2016-02-11 00:45:07 +01:00
|
|
|
var safeHighlights = highlights.map(function(highlight) {
|
|
|
|
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
|
|
|
});
|
2016-02-10 21:25:32 +01:00
|
|
|
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
|
|
|
sanitizeHtmlParams.textFilter = function(safeText) {
|
2016-02-11 00:45:07 +01:00
|
|
|
return highlighter.applyHighlights(safeText, safeHighlights).map(function(span) {
|
2016-02-10 21:25:32 +01:00
|
|
|
// XXX: rather clunky conversion from the react nodes returned by applyHighlights
|
|
|
|
// (which need to be nodes for the non-html highlighting case), to convert them
|
|
|
|
// back into raw HTML given that's what sanitize-html works in terms of.
|
|
|
|
return ReactDOMServer.renderToString(span);
|
|
|
|
}).join('');
|
|
|
|
};
|
|
|
|
}
|
2015-12-24 00:50:35 +01:00
|
|
|
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
2016-02-10 21:25:32 +01:00
|
|
|
delete sanitizeHtmlParams.textFilter;
|
|
|
|
return <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
2015-12-24 00:50:35 +01:00
|
|
|
} else {
|
|
|
|
safeBody = content.body;
|
2016-02-10 21:25:32 +01:00
|
|
|
if (highlights && highlights.length > 0) {
|
|
|
|
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
|
|
|
|
return highlighter.applyHighlights(safeBody, highlights);
|
2015-11-27 16:02:32 +01:00
|
|
|
}
|
|
|
|
else {
|
2016-02-10 21:25:32 +01:00
|
|
|
return safeBody;
|
2015-11-27 16:02:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
highlightDom: function(element) {
|
|
|
|
var blocks = element.getElementsByTagName("code");
|
|
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
|
|
highlight.highlightBlock(blocks[i]);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
}
|
|
|
|
|