mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-30 08:25:53 +08:00
fix(composer): pasting into composer sanitizes and preserves whitespace
Summary: Fixes T1132 Added tests, and an html sanitizer. Test Plan: See contenteditable-component-spec.cjsx edgehill --test Reviewers: bengotow Reviewed By: bengotow Subscribers: mg Maniphest Tasks: T1132 Differential Revision: https://review.inboxapp.com/D1492
This commit is contained in:
parent
958eceec56
commit
ecb4e49a7a
3 changed files with 138 additions and 38 deletions
|
@ -670,53 +670,57 @@ class ContenteditableComponent extends React.Component
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
####### CLEAN PASTE #########
|
####### CLEAN PASTE #########
|
||||||
|
|
||||||
_onPaste: (evt) =>
|
_onPaste: (evt) =>
|
||||||
html = evt.clipboardData.getData("text/html") ? ""
|
inputText = evt.clipboardData.getData("text/html") ? ""
|
||||||
if html.length is 0
|
type = "text/html"
|
||||||
text = evt.clipboardData.getData("text/plain") ? ""
|
if inputText.length is 0
|
||||||
if text.length > 0
|
inputText = evt.clipboardData.getData("text/plain") ? ""
|
||||||
evt.preventDefault()
|
type = "text/plain"
|
||||||
cleanHtml = text
|
|
||||||
else
|
|
||||||
else
|
|
||||||
evt.preventDefault()
|
|
||||||
cleanHtml = @_sanitizeHtml(html)
|
|
||||||
|
|
||||||
document.execCommand("insertHTML", false, cleanHtml)
|
if inputText.length > 0
|
||||||
|
cleanHtml = @_sanitizeInput(inputText, type)
|
||||||
|
document.execCommand("insertHTML", false, cleanHtml)
|
||||||
|
|
||||||
|
evt.preventDefault()
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# This is used primarily when pasting text in
|
# This is used primarily when pasting text in
|
||||||
_sanitizeHtml: (html) =>
|
_sanitizeInput: (inputText="", type="text/html") =>
|
||||||
cleanHTML = sanitizeHtml html.replace(/[\n\r]/g, "<br/>"),
|
if type is "text/plain"
|
||||||
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike']
|
inputText = Utils.encodeHTMLEntities(inputText)
|
||||||
allowedAttributes:
|
inputText = inputText.replace(/[\r\n]|[03];/g, "<br/>").
|
||||||
a: ['href', 'name']
|
replace(/\s\s/g, " ")
|
||||||
img: ['src', 'alt']
|
else
|
||||||
transformTags:
|
inputText = sanitizeHtml inputText.replace(/[\n\r]/g, "<br/>"),
|
||||||
h1: "p"
|
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike']
|
||||||
h2: "p"
|
allowedAttributes:
|
||||||
h3: "p"
|
a: ['href', 'name']
|
||||||
h4: "p"
|
img: ['src', 'alt']
|
||||||
h5: "p"
|
transformTags:
|
||||||
h6: "p"
|
h1: "p"
|
||||||
div: "p"
|
h2: "p"
|
||||||
pre: "p"
|
h3: "p"
|
||||||
blockquote: "p"
|
h4: "p"
|
||||||
table: "p"
|
h5: "p"
|
||||||
|
h6: "p"
|
||||||
|
div: "p"
|
||||||
|
pre: "p"
|
||||||
|
blockquote: "p"
|
||||||
|
table: "p"
|
||||||
|
|
||||||
# We sanitized everything and convert all whitespace-inducing elements
|
# We sanitized everything and convert all whitespace-inducing
|
||||||
# into <p> tags. We want to de-wrap <p> tags and replace with two line
|
# elements into <p> tags. We want to de-wrap <p> tags and replace
|
||||||
# breaks instead.
|
# with two line breaks instead.
|
||||||
cleanHTML = cleanHTML.replace(/<p[\s\S]*?>/gim, "").replace(/<\/p>/gi, "<br/>")
|
inputText = inputText.replace(/<p[\s\S]*?>/gim, "").
|
||||||
|
replace(/<\/p>/gi, "<br/>")
|
||||||
|
|
||||||
# We never want more then 2 line breaks in a row.
|
# We never want more then 2 line breaks in a row.
|
||||||
# https://regex101.com/r/gF6bF4/4
|
# https://regex101.com/r/gF6bF4/4
|
||||||
cleanHTML = cleanHTML.replace(/(<br\s*\/?>\s*){3,}/g, "<br/><br/>")
|
inputText = inputText.replace(/(<br\s*\/?>\s*){3,}/g, "<br/><br/>")
|
||||||
|
|
||||||
return cleanHTML
|
return inputText
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -744,4 +748,4 @@ class ContenteditableComponent extends React.Component
|
||||||
else return (innerHTML + @props.html.substr(quoteStart))
|
else return (innerHTML + @props.html.substr(quoteStart))
|
||||||
|
|
||||||
|
|
||||||
module.exports = ContenteditableComponent
|
module.exports = ContenteditableComponent
|
||||||
|
|
|
@ -43,3 +43,77 @@ describe "ContenteditableComponent", ->
|
||||||
expect(@onChange.callCount).toBe(1)
|
expect(@onChange.callCount).toBe(1)
|
||||||
@performEdit(@changedHtmlWithoutQuote)
|
@performEdit(@changedHtmlWithoutQuote)
|
||||||
expect(@onChange.callCount).toBe(2)
|
expect(@onChange.callCount).toBe(2)
|
||||||
|
|
||||||
|
describe "pasting behavior", ->
|
||||||
|
tests = [
|
||||||
|
{
|
||||||
|
in: ""
|
||||||
|
sanitizedAsHTML: ""
|
||||||
|
sanitizedAsPlain: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "Hello World"
|
||||||
|
sanitizedAsHTML: "Hello World"
|
||||||
|
sanitizedAsPlain: "Hello World"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: " Hello World"
|
||||||
|
# Should collapse to 1 space when rendered
|
||||||
|
sanitizedAsHTML: " Hello World"
|
||||||
|
# Preserving 2 spaces
|
||||||
|
sanitizedAsPlain: " Hello World"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: " Hello World"
|
||||||
|
sanitizedAsHTML: " Hello World"
|
||||||
|
# Preserving 3 spaces
|
||||||
|
sanitizedAsPlain: " Hello World"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: " Hello World"
|
||||||
|
sanitizedAsHTML: " Hello World"
|
||||||
|
# Preserving 4 spaces
|
||||||
|
sanitizedAsPlain: " Hello World"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "Hello\nWorld"
|
||||||
|
sanitizedAsHTML: "Hello<br />World"
|
||||||
|
# Convert newline to br
|
||||||
|
sanitizedAsPlain: "Hello<br/>World"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "Hello\rWorld"
|
||||||
|
sanitizedAsHTML: "Hello<br />World"
|
||||||
|
# Convert carriage return to br
|
||||||
|
sanitizedAsPlain: "Hello<br/>World"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "Hello\n\n\nWorld"
|
||||||
|
# Never have more than 2 br's in a row
|
||||||
|
sanitizedAsHTML: "Hello<br/><br/>World"
|
||||||
|
# Convert multiple newlines to same number of brs
|
||||||
|
sanitizedAsPlain: "Hello<br/><br/><br/>World"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "<style>Yo</style> Foo Bar <div>Baz</div>"
|
||||||
|
# Strip bad tags
|
||||||
|
sanitizedAsHTML: " Foo Bar Baz<br/>"
|
||||||
|
# HTML encode tags for literal display
|
||||||
|
sanitizedAsPlain: "<style>Yo</style> Foo Bar <div>Baz</div>"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
in: "<script>Bah</script> Yo < script>Boo! < / script >"
|
||||||
|
# Strip non white-list tags and encode malformed ones.
|
||||||
|
sanitizedAsHTML: " Yo < script>Boo! < / script >"
|
||||||
|
# HTML encode tags for literal display
|
||||||
|
sanitizedAsPlain: "<script>Bah</script> Yo < script>Boo! < / script >"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
it "sanitizes plain text properly", ->
|
||||||
|
for test in tests
|
||||||
|
expect(@component._sanitizeInput(test.in, "text/plain")).toBe test.sanitizedAsPlain
|
||||||
|
|
||||||
|
it "sanitizes html text properly", ->
|
||||||
|
for test in tests
|
||||||
|
expect(@component._sanitizeInput(test.in, "text/html")).toBe test.sanitizedAsHTML
|
||||||
|
|
|
@ -44,6 +44,28 @@ Utils =
|
||||||
set[item] = true for item in arr
|
set[item] = true for item in arr
|
||||||
return set
|
return set
|
||||||
|
|
||||||
|
# Escapes potentially dangerous html characters
|
||||||
|
# This code is lifted from Angular.js
|
||||||
|
# See their specs here:
|
||||||
|
# https://github.com/angular/angular.js/blob/master/test/ngSanitize/sanitizeSpec.js
|
||||||
|
# And the original source here: https://github.com/angular/angular.js/blob/master/src/ngSanitize/sanitize.js#L451
|
||||||
|
encodeHTMLEntities: (value) ->
|
||||||
|
SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g
|
||||||
|
pairFix = (value) ->
|
||||||
|
hi = value.charCodeAt(0)
|
||||||
|
low = value.charCodeAt(1)
|
||||||
|
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'
|
||||||
|
|
||||||
|
# Match everything outside of normal chars and " (quote character)
|
||||||
|
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g
|
||||||
|
alphaFix = (value) -> '&#' + value.charCodeAt(0) + ';'
|
||||||
|
|
||||||
|
value.replace(/&/g, '&').
|
||||||
|
replace(SURROGATE_PAIR_REGEXP, pairFix).
|
||||||
|
replace(NON_ALPHANUMERIC_REGEXP, alphaFix).
|
||||||
|
replace(/</g, '<').
|
||||||
|
replace(/>/g, '>')
|
||||||
|
|
||||||
modelClassMap: ->
|
modelClassMap: ->
|
||||||
return Utils._modelClassMap if Utils._modelClassMap
|
return Utils._modelClassMap if Utils._modelClassMap
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue