FIX: display tables in posts history diff (#6032)

This commit is contained in:
OsamaSayegh
2018-07-12 07:13:52 +03:00
committed by Sam
parent 1ed4f0ee8a
commit f13a7226db
4 changed files with 92 additions and 18 deletions

View File

@ -2,9 +2,8 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
import { categoryBadgeHTML } from "discourse/helpers/category-link"; import { categoryBadgeHTML } from "discourse/helpers/category-link";
import computed from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators";
import { propertyGreaterThan, propertyLessThan } from "discourse/lib/computed"; import { propertyGreaterThan, propertyLessThan } from "discourse/lib/computed";
import { on } from "ember-addons/ember-computed-decorators"; import { on, observes } from "ember-addons/ember-computed-decorators";
import { default as WhiteLister } from "pretty-text/white-lister"; import { sanitizeAsync } from "discourse/lib/text";
import { sanitize } from "pretty-text/sanitizer";
function customTagArray(fieldName) { function customTagArray(fieldName) {
return function() { return function() {
@ -246,15 +245,23 @@ export default Ember.Controller.extend(ModalFunctionality, {
return this.get("model.title_changes." + viewMode); return this.get("model.title_changes." + viewMode);
}, },
@computed("viewMode", "model.body_changes") @observes("viewMode", "model.body_changes")
bodyDiff(viewMode) { bodyDiffChanged() {
const viewMode = this.get("viewMode");
const html = this.get(`model.body_changes.${viewMode}`); const html = this.get(`model.body_changes.${viewMode}`);
if (viewMode === "side_by_side_markdown") { if (viewMode === "side_by_side_markdown") {
return html; this.set("bodyDiff", html);
} else { } else {
const whiteLister = new WhiteLister({ features: { editHistory: true } }); const opts = {
whiteLister.whiteListFeature("editHistory", { custom: () => true }); features: { editHistory: true },
return sanitize(html, whiteLister); whiteListed: {
editHistory: { custom: (tag, attr) => attr === "class" }
}
};
return sanitizeAsync(html, opts).then(result =>
this.set("bodyDiff", result)
);
} }
}, },

View File

@ -25,25 +25,39 @@ function getOpts(opts) {
// Use this to easily create a pretty text instance with proper options // Use this to easily create a pretty text instance with proper options
export function cook(text, options) { export function cook(text, options) {
return new Handlebars.SafeString(new PrettyText(getOpts(options)).cook(text)); return new Handlebars.SafeString(createPrettyText(options).cook(text));
} }
// everything should eventually move to async API and this should be renamed // everything should eventually move to async API and this should be renamed
// cook // cook
export function cookAsync(text, options) { export function cookAsync(text, options) {
if (Discourse.MarkdownItURL) { return loadMarkdownIt().then(() => cook(text, options));
return loadScript(Discourse.MarkdownItURL)
.then(() => cook(text, options))
.catch(e => Ember.Logger.error(e));
} else {
return Ember.RSVP.Promise.resolve(cook(text));
}
} }
export function sanitize(text, options) { export function sanitize(text, options) {
return textSanitize(text, new WhiteLister(options)); return textSanitize(text, new WhiteLister(options));
} }
export function sanitizeAsync(text, options) {
return new loadMarkdownIt().then(() => {
return createPrettyText(options).sanitize(text);
});
}
function loadMarkdownIt() {
if (Discourse.MarkdownItURL) {
return loadScript(Discourse.MarkdownItURL).catch(e =>
Ember.Logger.error(e)
);
} else {
return Ember.RSVP.Promise.resolve();
}
}
function createPrettyText(options) {
return new PrettyText(getOpts(options));
}
function emojiOptions() { function emojiOptions() {
const siteSettings = Discourse.__container__.lookup("site-settings:main"); const siteSettings = Discourse.__container__.lookup("site-settings:main");
if (!siteSettings.enable_emoji) { if (!siteSettings.enable_emoji) {

View File

@ -210,6 +210,10 @@ export function setup(opts, siteSettings, state) {
} }
}); });
Object.entries(state.whiteListed || {}).forEach(entry => {
whiteListed.push(entry);
});
optionCallbacks.forEach(([, callback]) => { optionCallbacks.forEach(([, callback]) => {
callback(opts, siteSettings, state); callback(opts, siteSettings, state);
}); });

View File

@ -22,10 +22,59 @@ QUnit.test("displayEdit", function(assert) {
); );
HistoryController.set("model.current_revision", 2); HistoryController.set("model.current_revision", 2);
assert.equal( assert.equal(
HistoryController.get("displayEdit"), HistoryController.get("displayEdit"),
false, false,
"it should only display the edit button on the latest revision" "it should only display the edit button on the latest revision"
); );
const html = `<div class="revision-content">
<p><img src="/uploads/default/original/1X/6b963ffc13cb0c053bbb90c92e99d4fe71b286ef.jpg" alt="" class="diff-del"><img/src=x onerror=alert(document.domain)>" width="276" height="183"></p>
</div>
<table background="javascript:alert(\"HACKEDXSS\")">
<thead>
<tr>
<th>Column</th>
<th style="text-align:left">Test</th>
</tr>
</thead>
<tbody>
<tr>
<td background="javascript:alert('HACKEDXSS')">Osama</td>
<td style="text-align:right">Testing</td>
</tr>
</tbody>
</table>`;
const expectedOutput = `<div class="revision-content">
<p><img src="/uploads/default/original/1X/6b963ffc13cb0c053bbb90c92e99d4fe71b286ef.jpg" alt class="diff-del">" width="276" height="183"&gt;</p>
</div>
<table>
<thead>
<tr>
<th>Column</th>
<th style="text-align:left">Test</th>
</tr>
</thead>
<tbody>
<tr>
<td>Osama</td>
<td style="text-align:right">Testing</td>
</tr>
</tbody>
</table>`;
HistoryController.setProperties({
viewMode: "side_by_side",
model: {
body_changes: {
side_by_side: html
}
}
});
HistoryController.bodyDiffChanged().then(() => {
const output = HistoryController.get("bodyDiff");
assert.equal(output, expectedOutput, "it keeps safe HTML");
});
}); });