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",