mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 22:43:33 +08:00
FIX: Don't double sanitize values, allow blockquotes with leading text
This commit is contained in:
@ -7,7 +7,8 @@
|
|||||||
var parser = window.BetterMarkdown,
|
var parser = window.BetterMarkdown,
|
||||||
MD = parser.Markdown,
|
MD = parser.Markdown,
|
||||||
dialect = MD.dialects.Discourse = MD.subclassDialect( MD.dialects.Gruber ),
|
dialect = MD.dialects.Discourse = MD.subclassDialect( MD.dialects.Gruber ),
|
||||||
initialized = false;
|
initialized = false,
|
||||||
|
emitters = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Initialize our dialects for processing.
|
Initialize our dialects for processing.
|
||||||
@ -21,6 +22,51 @@ function initializeDialects() {
|
|||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Process the text nodes in the JsonML tree, calling any emitters that have
|
||||||
|
been added.
|
||||||
|
|
||||||
|
@method processTextNodes
|
||||||
|
@param {Array} node the JsonML tree
|
||||||
|
@param {Object} event the parse node event data
|
||||||
|
**/
|
||||||
|
function processTextNodes(node, event) {
|
||||||
|
if (node.length < 2) { return; }
|
||||||
|
|
||||||
|
if (node[0] === '__RAW') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var skipSanitize = [];
|
||||||
|
for (var j=1; j<node.length; j++) {
|
||||||
|
var textContent = node[j];
|
||||||
|
if (typeof textContent === "string") {
|
||||||
|
|
||||||
|
if (dialect.options.sanitize && !skipSanitize[textContent]) {
|
||||||
|
textContent = Discourse.Markdown.sanitize(textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = textContent;
|
||||||
|
emitters.forEach(function (e) {
|
||||||
|
result = e(result, event)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
if (result instanceof Array) {
|
||||||
|
result.forEach(function (r) { skipSanitize[r] = true; });
|
||||||
|
node.splice.apply(node, [j, 1].concat(result));
|
||||||
|
} else {
|
||||||
|
node[j] = result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node[j] = textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Parse a JSON ML tree, using registered handlers to adjust it if necessary.
|
Parse a JSON ML tree, using registered handlers to adjust it if necessary.
|
||||||
|
|
||||||
@ -33,8 +79,9 @@ function initializeDialects() {
|
|||||||
function parseTree(tree, path, insideCounts) {
|
function parseTree(tree, path, insideCounts) {
|
||||||
|
|
||||||
if (tree instanceof Array) {
|
if (tree instanceof Array) {
|
||||||
Discourse.Dialect.trigger('parseNode', {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}});
|
var event = {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}};
|
||||||
|
Discourse.Dialect.trigger('parseNode', event);
|
||||||
|
processTextNodes(tree, event);
|
||||||
|
|
||||||
path = path || [];
|
path = path || [];
|
||||||
insideCounts = insideCounts || {};
|
insideCounts = insideCounts || {};
|
||||||
@ -100,7 +147,9 @@ Discourse.Dialect = {
|
|||||||
if (!initialized) { initializeDialects(); }
|
if (!initialized) { initializeDialects(); }
|
||||||
dialect.options = opts;
|
dialect.options = opts;
|
||||||
var tree = parser.toHTMLTree(text, 'Discourse');
|
var tree = parser.toHTMLTree(text, 'Discourse');
|
||||||
return parser.renderJsonML(parseTree(tree));
|
html = parser.renderJsonML(parseTree(tree));
|
||||||
|
|
||||||
|
return html;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -363,36 +412,7 @@ Discourse.Dialect = {
|
|||||||
@param {Function} emitter The function to call with the text. It returns JsonML to modify the tree.
|
@param {Function} emitter The function to call with the text. It returns JsonML to modify the tree.
|
||||||
**/
|
**/
|
||||||
postProcessText: function(emitter) {
|
postProcessText: function(emitter) {
|
||||||
Discourse.Dialect.on("parseNode", function(event) {
|
emitters.push(emitter);
|
||||||
var node = event.node;
|
|
||||||
|
|
||||||
if (node.length < 2) { return; }
|
|
||||||
|
|
||||||
if (node[0] === '__RAW') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var j=1; j<node.length; j++) {
|
|
||||||
var textContent = node[j];
|
|
||||||
if (typeof textContent === "string") {
|
|
||||||
|
|
||||||
if (dialect.options.sanitize) {
|
|
||||||
textContent = Discourse.Markdown.sanitize(textContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = emitter(textContent, event);
|
|
||||||
if (result) {
|
|
||||||
if (result instanceof Array) {
|
|
||||||
node.splice.apply(node, [j, 1].concat(result));
|
|
||||||
} else {
|
|
||||||
node[j] = result;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
node[j] = textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,14 +4,38 @@
|
|||||||
var blockTags = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div',
|
var blockTags = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div',
|
||||||
'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
|
'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
|
||||||
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'noscript', 'ol', 'output',
|
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'noscript', 'ol', 'output',
|
||||||
'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video'];
|
'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video'],
|
||||||
|
|
||||||
|
splitAtLast = function(tag, block, next, first) {
|
||||||
|
var endTag = "</" + tag + ">",
|
||||||
|
endTagIndex = first ? block.indexOf(endTag) : block.lastIndexOf(endTag);
|
||||||
|
|
||||||
|
if (endTagIndex !== -1) {
|
||||||
|
endTagIndex += endTag.length;
|
||||||
|
|
||||||
|
var leading = block.substr(0, endTagIndex),
|
||||||
|
trailing = block.substr(endTagIndex).replace(/^\s+/, '');
|
||||||
|
|
||||||
|
if (trailing.length) {
|
||||||
|
next.unshift(trailing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ leading ];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Discourse.Dialect.registerBlock('html', function(block, next) {
|
Discourse.Dialect.registerBlock('html', function(block, next) {
|
||||||
|
|
||||||
var m = /^<([^>]+)\>/.exec(block);
|
// Fix manual blockquote paragraphing even though it's not strictly correct
|
||||||
|
var split = splitAtLast('blockquote', block, next, true);
|
||||||
|
if (split) { return split; }
|
||||||
|
|
||||||
|
var m = /^<([^>]+)\>/m.exec(block);
|
||||||
if (m && m[1]) {
|
if (m && m[1]) {
|
||||||
var tag = m[1].split(/\s/);
|
var tag = m[1].split(/\s/);
|
||||||
if (tag && tag[0] && blockTags.indexOf(tag[0]) !== -1) {
|
if (tag && tag[0] && blockTags.indexOf(tag[0]) !== -1) {
|
||||||
|
split = splitAtLast(tag[0], block, next);
|
||||||
|
if (split) { return split; }
|
||||||
return [ block.toString() ];
|
return [ block.toString() ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,11 @@ Discourse.Dialect.postProcessText(function (text, event) {
|
|||||||
if (linebreaks || (insideCounts.pre > 0)) { return; }
|
if (linebreaks || (insideCounts.pre > 0)) { return; }
|
||||||
|
|
||||||
if (text === "\n") {
|
if (text === "\n") {
|
||||||
// If the tage is just a new line, replace it with a `<br>`
|
// If the tag is just a new line, replace it with a `<br>`
|
||||||
return [['br']];
|
return [['br']];
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
||||||
// If the text node contains new lines, perhaps with text between them, insert the
|
// If the text node contains new lines, perhaps with text between them, insert the
|
||||||
// `<br>` tags.
|
// `<br>` tags.
|
||||||
var split = text.split(/\n+/);
|
var split = text.split(/\n+/);
|
||||||
@ -25,6 +26,7 @@ Discourse.Dialect.postProcessText(function (text, event) {
|
|||||||
if (i !== split.length-1) { replacement.push(['br']); }
|
if (i !== split.length-1) { replacement.push(['br']); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return replacement;
|
return replacement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,14 @@ test("Line Breaks", function() {
|
|||||||
cooked("[] first choice\n[] second choice",
|
cooked("[] first choice\n[] second choice",
|
||||||
"<p>[] first choice<br/>[] second choice</p>",
|
"<p>[] first choice<br/>[] second choice</p>",
|
||||||
"it handles new lines correctly with [] options");
|
"it handles new lines correctly with [] options");
|
||||||
|
|
||||||
|
cooked("<blockquote>evil</blockquote>\ntrout",
|
||||||
|
"<blockquote>evil</blockquote>\n\n<p>trout</p>",
|
||||||
|
"it doesn't insert <br> after blockquotes");
|
||||||
|
|
||||||
|
cooked("leading<blockquote>evil</blockquote>\ntrout",
|
||||||
|
"leading<blockquote>evil</blockquote>\n\n<p>trout</p>",
|
||||||
|
"it doesn't insert <br> after blockquotes with leading text");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Paragraphs for HTML", function() {
|
test("Paragraphs for HTML", function() {
|
||||||
@ -311,6 +319,7 @@ test("sanitize", function() {
|
|||||||
"<p><a href=\"http://disneyland.disney.go.com/\">disney</a> <a href=\"http://reddit.com\">reddit</a></p>",
|
"<p><a href=\"http://disneyland.disney.go.com/\">disney</a> <a href=\"http://reddit.com\">reddit</a></p>",
|
||||||
"we can embed proper links");
|
"we can embed proper links");
|
||||||
|
|
||||||
|
cooked("<blockquote>a\n</blockquote>\n", "<blockquote>a\n\n<br/>\n\n</blockquote>", "it does not double sanitize");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("URLs in BBCode tags", function() {
|
test("URLs in BBCode tags", function() {
|
||||||
|
Reference in New Issue
Block a user