diff --git a/app/components/Preview.jsx b/app/components/Preview.jsx
index 4c440c8e..933a6df4 100644
--- a/app/components/Preview.jsx
+++ b/app/components/Preview.jsx
@@ -5,7 +5,6 @@ import PreviewLoader from './loaders/Preview';
import { Templates } from './TemplateForm';
import grayMatter from 'gray-matter';
import isEqual from 'lodash.isequal';
-import sanitizeHtml from 'sanitize-html';
const { array, func, number, object, string } = PropTypes;
@@ -70,8 +69,8 @@ export default class Preview extends Component {
componentWillMount() {
this.props.previewLoader().then((deps) => {
- this.markdownIt = deps.markdownIt({
- html: true,
+ this.markdownIt = deps.markdownIt('commonmark', {
+ html: false,
linkify: true,
typographer: true,
highlight: (str, lang) => {
@@ -84,7 +83,19 @@ export default class Preview extends Component {
}
return ''; // use external default escaping
+ },
+ modifyToken: function (token, env) {
+ switch (token.type) {
+ case 'link_open':
+ token.attrObj.rel = 'noreferrer noopener';
+ break;
+ }
}
+ })
+ .enable('linkify');
+
+ deps.markdownItPlugins.forEach((plugin) => {
+ this.markdownIt.use(plugin);
});
this.emojione = deps.emojione;
@@ -129,19 +140,9 @@ export default class Preview extends Component {
*/
getChunks(raw, env) {
// Parse the whole markdown document and get tokens
- let tokens = this.markdownIt.parse(raw, env);
-
- // Sanitize html chunks to avoid browser DOM manipulation
- // that could possibly crash the app (because of React)
- tokens = tokens.map((token) => {
- if (token.type === 'html_block') {
- token.content = sanitizeHtml(token.content);
- }
-
- return token;
- });
-
+ const tokens = this.markdownIt.parse(raw, env);
const chunks = [];
+
let start = 0;
let stop = 0;
diff --git a/app/components/__tests__/Preview-test.js b/app/components/__tests__/Preview-test.js
index b00a7a12..e6197b30 100644
--- a/app/components/__tests__/Preview-test.js
+++ b/app/components/__tests__/Preview-test.js
@@ -2,6 +2,8 @@ import React from 'react';
import { mount, shallow, render } from 'enzyme';
import { expect } from 'chai';
import mdit from 'markdown-it';
+import mditfa from 'markdown-it-fontawesome';
+import mditmt from 'markdown-it-modify-token';
import emojione from 'emojione';
import hljs from 'highlight.js';
@@ -20,6 +22,10 @@ describe('', () => {
return Promise.resolve({
markdownIt: mdit,
+ markdownItPlugins: [
+ mditfa,
+ mditmt,
+ ],
hljs: hljs,
emojione: emojione
});
@@ -229,76 +235,6 @@ describe('', () => {
}, 5);
});
-
- it('handles html block chunks', (done) => {
- let chunks;
- const wrapper = shallow(
-
- );
-
- let html = [
- '
',
- '
sub-section
',
- '
lorem ipsum
',
- '
'
- ];
-
- setTimeout(() => {
- const preview = wrapper.instance();
-
- // raw html block
- chunks = preview.getChunks(html.join('\n'), {});
- expect(chunks).to.have.lengthOf(1);
- expect(chunks[0]).to.have.lengthOf(1);
- expect(chunks[0][0]).to.have.property('type', 'html_block');
-
- // Insert an empty row
- html.splice(2, 0, '\n');
- chunks = preview.getChunks(html.join('\n'), {});
- expect(chunks).to.have.lengthOf(2);
- expect(chunks[0]).to.have.lengthOf(1);
- expect(chunks[0][0]).to.have.property('type', 'html_block');
- expect(chunks[1][0]).to.have.property('type', 'html_block');
-
- done();
- }, 5);
- });
-
- it('sanitizes incomplete html blocks', (done) => {
- let chunks;
- const wrapper = shallow(
-
- );
-
- setTimeout(() => {
- const preview = wrapper.instance();
-
- chunks = preview.getChunks('', {});
- expect(chunks).to.have.lengthOf(1);
- expect(chunks[0]).to.have.lengthOf(1);
- expect(chunks[0][0]).to.have.property('type', 'html_block');
- expect(chunks[0][0]).to.have.property('content', '
');
-
- chunks = preview.getChunks('
', {});
- expect(chunks).to.have.lengthOf(1);
- expect(chunks[0]).to.have.lengthOf(1);
- expect(chunks[0][0]).to.have.property('type', 'html_block');
- expect(chunks[0][0]).to.have.property('content', '');
-
- done();
- }, 5);
- });
-
it('removes front-matter YAML header from preview', (done) => {
const wrapper = mount(
', () => {
done();
}, 5);
});
+
+ it('should not display iframes (#122)', (done) => {
+ const content = '';
+ const wrapper = mount(
+
+ );
+
+ setTimeout(() => {
+ expect(wrapper.html()).not.to.contain(content);
+
+ done();
+ }, 5);
+ });
+
+ it('should not render bad input tag (#122)', (done) => {
+ const content = '>';
+ const wrapper = mount(
+
+ );
+
+ setTimeout(() => {
+ expect(wrapper.html()).not.to.contain('input onfocus="alert(1)" autofocus=""');
+
+ done();
+ }, 5);
+ });
+
+ it('should not render bad HTML tag (#122)', (done) => {
+ const content = '<
>';
+ const wrapper = mount(
+
+ );
+
+ setTimeout(() => {
+ expect(wrapper.html()).not.to.contain('img onerror="alert(1)" src="x/"');
+
+ done();
+ }, 5);
+ });
+
+ it('supports FontAwesome', (done) => {
+ const wrapper = mount(
+
+ );
+
+ setTimeout(() => {
+ expect(wrapper.html()).to.contain('');
+
+ done();
+ }, 5);
+ });
+
+ it('should display links with rel="noopener"', (done) => {
+ const wrapper = mount(
+
+ );
+
+ setTimeout(() => {
+ expect(wrapper.html()).to.contain('foo');
+
+ done();
+ }, 5);
+ });
});
diff --git a/app/components/loaders/Preview.jsx b/app/components/loaders/Preview.jsx
index 1cfab8ad..1a4cc865 100644
--- a/app/components/loaders/Preview.jsx
+++ b/app/components/loaders/Preview.jsx
@@ -8,6 +8,10 @@ export default () => {
resolve({
hljs: require('highlight.js'),
markdownIt: require('markdown-it'),
+ markdownItPlugins: [
+ require('markdown-it-fontawesome'),
+ require('markdown-it-modify-token'),
+ ],
emojione: require('emojione')
});
});
diff --git a/app/scss/components/_preview.scss b/app/scss/components/_preview.scss
index 91ad528a..cb353bcf 100644
--- a/app/scss/components/_preview.scss
+++ b/app/scss/components/_preview.scss
@@ -79,4 +79,8 @@
vertical-align: middle;
line-height: 1.4rem;
}
+
+ .fa {
+ margin-right: 0.5rem;
+ }
}
diff --git a/package.json b/package.json
index f431c85f..7f1af50f 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,8 @@
"lodash.debounce": "^4.0.3",
"lodash.isequal": "^4.1.1",
"markdown-it": "^6.0.0",
+ "markdown-it-fontawesome": "^0.2.0",
+ "markdown-it-modify-token": "^1.0.2",
"mocha": "^2.4.5",
"mocha-circleci-reporter": "0.0.1",
"node-sass": "^3.4.2",
@@ -68,7 +70,6 @@
"react-addons-test-utils": "^0.14.7",
"react-dom": "^0.14.7",
"react-loader": "^2.1.0",
- "sanitize-html": "^1.11.3",
"sass-loader": "^3.1.2",
"sinon": "^1.17.3",
"sjcl": "^1.0.3",