Skip to content

Commit

Permalink
Merge branch 'main' into farmish
Browse files Browse the repository at this point in the history
  • Loading branch information
mattslack committed May 24, 2024
2 parents 847b1db + 0c79bcb commit 6835477
Show file tree
Hide file tree
Showing 14 changed files with 127 additions and 27 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "trix",
"version": "2.0.10",
"version": "2.1.1",
"description": "A rich text editor for everyday writing",
"main": "dist/trix.umd.min.js",
"module": "dist/trix.esm.min.js",
Expand Down
4 changes: 4 additions & 0 deletions src/inspector/templates/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ window.JST["trix/inspector/templates/document"] = function() {
Attributes: ${JSON.stringify(block.attributes)}
</div>
<div class="htmlAttributes">
HTML Attributes: ${JSON.stringify(block.htmlAttributes)}
</div>
<div class="text">
<div class="title">
Text: ${text.id}, Pieces: ${pieces.length}, Length: ${text.getLength()}
Expand Down
30 changes: 30 additions & 0 deletions src/test/system/pasting_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,36 @@ testGroup("Pasting", { template: "editor_empty" }, () => {
delete window.unsanitized
})

test("paste unsafe html with noscript", async () => {
window.unsanitized = []
const pasteData = {
"text/plain": "x",
"text/html": `\
<div><noscript><div class="123</noscript>456<img src=1 onerror=window.unsanitized.push(1)//"></div></noscript></div>
`
}

await pasteContent(pasteData)
await delay(20)
assert.deepEqual(window.unsanitized, [])
delete window.unsanitized
})

test("paste data-trix-attachment unsafe html", async () => {
window.unsanitized = []
const pasteData = {
"text/plain": "x",
"text/html": `\
copy<div data-trix-attachment="{&quot;contentType&quot;:&quot;text/html&quot;,&quot;content&quot;:&quot;&lt;img src=1 onerror=window.unsanitized.push(1)&gt;HELLO123&quot;}"></div>me
`,
}

await pasteContent(pasteData)
await delay(20)
assert.deepEqual(window.unsanitized, [])
delete window.unsanitized
})

test("prefers plain text when html lacks formatting", async () => {
const pasteData = {
"text/html": "<meta charset='utf-8'>a\nb",
Expand Down
10 changes: 8 additions & 2 deletions src/test/test_helpers/fixtures/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ const { css } = config

const createDocument = function (...parts) {
const blocks = parts.map((part) => {
const [ string, textAttributes, blockAttributes ] = Array.from(part)
const [ string, textAttributes, blockAttributes, htmlAttributes = {} ] = Array.from(part)
const text = Text.textForStringWithAttributes(string, textAttributes)
return new Block(text, blockAttributes)
return new Block(text, blockAttributes, htmlAttributes)
})

return new Document(blocks)
Expand Down Expand Up @@ -192,6 +192,12 @@ export const fixtures = {
html: `<pre>${blockComment}12\n3</pre>`,
},

"code with custom language": {
document: createDocument([ "puts \"Hello world!\"", {}, [ "code" ], { "language": "ruby" } ]),
html: `<pre language="ruby">${blockComment}puts "Hello world!"</pre>`,
serializedHTML: "<pre language=\"ruby\">puts \"Hello world!\"</pre>"
},

"multiple blocks with block comments in their text": {
document: createDocument([ `a${blockComment}b`, {}, [ "quote" ] ], [ `${blockComment}c`, {}, [ "code" ] ]),
html: `<blockquote>${blockComment}a&lt;!--block--&gt;b</blockquote><pre>${blockComment}&lt;!--block--&gt;c</pre>`,
Expand Down
2 changes: 1 addition & 1 deletion src/test/unit/html_parser_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const cursorTargetLeft = createCursorTarget("left").outerHTML
const cursorTargetRight = createCursorTarget("right").outerHTML

testGroup("HTMLParser", () => {
eachFixture((name, { html, serializedHTML, document }) => {
eachFixture((name, { html, document }) => {
test(name, () => {
const parsedDocument = HTMLParser.parse(html).getDocument()
assert.documentHTMLEqual(parsedDocument.copyUsingObjectsFromDocument(document), html)
Expand Down
1 change: 1 addition & 0 deletions src/trix/config/block_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const attributes = {
code: {
tagName: "pre",
terminal: true,
htmlAttributes: [ "language" ],
text: {
plaintext: true,
},
Expand Down
18 changes: 13 additions & 5 deletions src/trix/models/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import {
arraysAreEqual,
getBlockConfig,
getListAttributeNames,
objectsAreEqual,
spliceArray,
} from "trix/core/helpers"

export default class Block extends TrixObject {
static fromJSON(blockJSON) {
const text = Text.fromJSON(blockJSON.text)
return new this(text, blockJSON.attributes)
return new this(text, blockJSON.attributes, blockJSON.htmlAttributes)
}

constructor(text, attributes) {
constructor(text, attributes, htmlAttributes) {
super(...arguments)
this.text = applyBlockBreakToText(text || new Text())
this.attributes = attributes || []
this.htmlAttributes = htmlAttributes || {}
}

isEmpty() {
Expand All @@ -27,19 +29,19 @@ export default class Block extends TrixObject {
isEqualTo(block) {
if (super.isEqualTo(block)) return true

return this.text.isEqualTo(block?.text) && arraysAreEqual(this.attributes, block?.attributes)
return this.text.isEqualTo(block?.text) && arraysAreEqual(this.attributes, block?.attributes) && objectsAreEqual(this.htmlAttributes, block?.htmlAttributes)
}

copyWithText(text) {
return new Block(text, this.attributes)
return new Block(text, this.attributes, this.htmlAttributes)
}

copyWithoutText() {
return this.copyWithText(null)
}

copyWithAttributes(attributes) {
return new Block(this.text, attributes)
return new Block(this.text, attributes, this.htmlAttributes)
}

copyWithoutAttributes() {
Expand All @@ -60,6 +62,11 @@ export default class Block extends TrixObject {
return this.copyWithAttributes(attributes)
}

addHTMLAttribute(attribute, value) {
const htmlAttributes = Object.assign({}, this.htmlAttributes, { [attribute]: value })
return new Block(this.text, this.attributes, htmlAttributes)
}

removeAttribute(attribute) {
const { listAttribute } = getBlockConfig(attribute)
const attributes = removeLastValue(removeLastValue(this.attributes, attribute), listAttribute)
Expand Down Expand Up @@ -173,6 +180,7 @@ export default class Block extends TrixObject {
return {
text: this.text,
attributes: this.attributes,
htmlAttributes: this.htmlAttributes,
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/trix/models/composition.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,16 @@ export default class Composition extends BasicObject {
}
}

setHTMLAtributeAtPosition(position, attributeName, value) {
const block = this.document.getBlockAtPosition(position)
const allowedHTMLAttributes = getBlockConfig(block.getLastAttribute())?.htmlAttributes

if (block && allowedHTMLAttributes?.includes(attributeName)) {
const newDocument = this.document.setHTMLAttributeAtPosition(position, attributeName, value)
this.setDocument(newDocument)
}
}

setTextAttribute(attributeName, value) {
const selectedRange = this.getSelectedRange()
if (!selectedRange) return
Expand Down
6 changes: 6 additions & 0 deletions src/trix/models/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ export default class Document extends TrixObject {
return this.removeAttributeAtRange(attribute, range)
}

setHTMLAttributeAtPosition(position, name, value) {
const block = this.getBlockAtPosition(position)
const updatedBlock = block.addHTMLAttribute(name, value)
return this.replaceBlock(block, updatedBlock)
}

insertBlockBreakAtRange(range) {
let blocks
range = normalizeRange(range)
Expand Down
5 changes: 5 additions & 0 deletions src/trix/models/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ export default class Editor {
return this.composition.removeCurrentAttribute(name)
}

// HTML attributes
setHTMLAtributeAtPosition(position, name, value) {
this.composition.setHTMLAtributeAtPosition(position, name, value)
}

// Nesting level

canDecreaseNestingLevel() {
Expand Down
36 changes: 29 additions & 7 deletions src/trix/models/html_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,20 @@ const pieceForAttachment = (attachment, attributes = {}) => {
return { attachment, attributes, type }
}

const blockForAttributes = (attributes = {}) => {
const blockForAttributes = (attributes = {}, htmlAttributes = {}) => {
const text = []
return { text, attributes }
return { text, attributes, htmlAttributes }
}

const parseTrixDataAttribute = (element, name) => {
try {
return JSON.parse(element.getAttribute(`data-trix-${name}`))
const data = JSON.parse(element.getAttribute(`data-trix-${name}`))

if (data.contentType === "text/html" && data.content) {
data.content = HTMLSanitizer.sanitize(data.content).getHTML()
}

return data
} catch (error) {
return {}
}
Expand Down Expand Up @@ -133,8 +139,9 @@ export default class HTMLParser extends BasicObject {
return this.appendStringWithAttributes("\n")
} else if (element === this.containerElement || this.isBlockElement(element)) {
const attributes = this.getBlockAttributes(element)
const htmlAttributes = this.getBlockHTMLAttributes(element)
if (!arraysAreEqual(attributes, this.currentBlock?.attributes)) {
this.currentBlock = this.appendBlockForAttributesWithElement(attributes, element)
this.currentBlock = this.appendBlockForAttributesWithElement(attributes, element, htmlAttributes)
this.currentBlockElement = element
}
}
Expand All @@ -147,9 +154,10 @@ export default class HTMLParser extends BasicObject {
if (elementIsBlockElement && !this.isBlockElement(element.firstChild)) {
if (!this.isInsignificantTextNode(element.firstChild) || !this.isBlockElement(element.firstElementChild)) {
const attributes = this.getBlockAttributes(element)
const htmlAttributes = this.getBlockHTMLAttributes(element)
if (element.firstChild) {
if (!(currentBlockContainsElement && arraysAreEqual(attributes, this.currentBlock.attributes))) {
this.currentBlock = this.appendBlockForAttributesWithElement(attributes, element)
this.currentBlock = this.appendBlockForAttributesWithElement(attributes, element, htmlAttributes)
this.currentBlockElement = element
} else {
return this.appendStringWithAttributes("\n")
Expand Down Expand Up @@ -233,9 +241,9 @@ export default class HTMLParser extends BasicObject {

// Document construction

appendBlockForAttributesWithElement(attributes, element) {
appendBlockForAttributesWithElement(attributes, element, htmlAttributes = {}) {
this.blockElements.push(element)
const block = blockForAttributes(attributes)
const block = blockForAttributes(attributes, htmlAttributes)
this.blocks.push(block)
return block
}
Expand Down Expand Up @@ -350,6 +358,20 @@ export default class HTMLParser extends BasicObject {
return attributes.reverse()
}

getBlockHTMLAttributes(element) {
const attributes = {}
const blockConfig = Object.values(config.blockAttributes).find(settings => settings.tagName === tagName(element))
const allowedAttributes = blockConfig?.htmlAttributes || []

allowedAttributes.forEach((attribute) => {
if (element.hasAttribute(attribute)) {
attributes[attribute] = element.getAttribute(attribute)
}
})

return attributes
}

findBlockElementAncestors(element) {
const ancestors = []
while (element && element !== this.containerElement) {
Expand Down
4 changes: 2 additions & 2 deletions src/trix/models/html_sanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import BasicObject from "trix/core/basic_object"

import { nodeIsAttachmentElement, removeNode, tagName, walkTree } from "trix/core/helpers"

const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height class".split(" ")
const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height language class".split(" ")
const DEFAULT_FORBIDDEN_PROTOCOLS = "javascript:".split(" ")
const DEFAULT_FORBIDDEN_ELEMENTS = "script iframe form".split(" ")
const DEFAULT_FORBIDDEN_ELEMENTS = "script iframe form noscript".split(" ")

export default class HTMLSanitizer extends BasicObject {
static sanitize(html, options) {
Expand Down
14 changes: 11 additions & 3 deletions src/trix/views/block_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,27 @@ export default class BlockView extends ObjectView {
}

createContainerElement(depth) {
let attributes, className
const attributes = {}
let className
const attributeName = this.attributes[depth]

const { tagName } = getBlockConfig(attributeName)
const { tagName, htmlAttributes = [] } = getBlockConfig(attributeName)

if (depth === 0 && this.block.isRTL()) {
attributes = { dir: "rtl" }
Object.assign(attributes, { dir: "rtl" })
}

if (attributeName === "attachmentGallery") {
const size = this.block.getBlockBreakPosition()
className = `${css.attachmentGallery} ${css.attachmentGallery}--${size}`
}

Object.entries(this.block.htmlAttributes).forEach(([ name, value ]) => {
if (htmlAttributes.includes(name)) {
attributes[name] = value
}
})

return makeElement({ tagName, className, attributes })
}

Expand Down
12 changes: 6 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3348,9 +3348,9 @@ flatted@^3.2.7:
integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==

follow-redirects@^1.0.0:
version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==

forever-agent@~0.6.1:
version "0.6.1"
Expand Down Expand Up @@ -6288,9 +6288,9 @@ tar-stream@^2.1.4, tar-stream@^2.2.0:
readable-stream "^3.1.1"

tar@^6.0.2, tar@^6.1.2:
version "6.2.0"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73"
integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==
version "6.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
Expand Down

0 comments on commit 6835477

Please sign in to comment.