Skip to content

Commit

Permalink
support CSP for inline styles
Browse files Browse the repository at this point in the history
  • Loading branch information
alexanderPolosatov committed Feb 27, 2024
1 parent 0800d94 commit 610d687
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 1 deletion.
23 changes: 23 additions & 0 deletions core/quill.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import Composition from './composition';

const debug = logger('quill');

const STYLE_ATTRIBUTE_KEY = 'style-data-key';

const globalRegistry = new Parchment.Registry();
Parchment.ParentBlot.uiClass = 'ql-ui';

Expand Down Expand Up @@ -467,6 +469,27 @@ Quill.events = Emitter.events;
Quill.sources = Emitter.sources;
// eslint-disable-next-line no-undef
Quill.version = typeof QUILL_VERSION === 'undefined' ? 'dev' : QUILL_VERSION;
Quill.LIST_STYLE_KEY = 'mso-list-data';

Quill.replaceStyleAttribute = (html) => {
const tagAttrsRegex = /(?:(<[a-z0-9]+\s*))([\s\S]*?)(>|\/>)/gi;

return html.replace(tagAttrsRegex, (allTagAttrs, tagStart, tagAttrs, tagEnd) => {
const contentWithoutStyle = tagAttrs.replace(/style(\s?)+=/gi, `${STYLE_ATTRIBUTE_KEY}=`);

return tagStart + contentWithoutStyle + tagEnd;
});
};

Quill.restoreStyleAttribute = (element) => {
element.querySelectorAll(`[${STYLE_ATTRIBUTE_KEY}]`).forEach((currentElement) => {
const attrValue = currentElement.getAttribute(STYLE_ATTRIBUTE_KEY);

currentElement.style = attrValue;
currentElement.setAttribute(Quill.LIST_STYLE_KEY, attrValue);
currentElement.removeAttribute(STYLE_ATTRIBUTE_KEY);
});
};

Quill.imports = {
delta: Delta,
Expand Down
6 changes: 5 additions & 1 deletion modules/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,13 @@ class Clipboard extends Module {
}

applyMatchers(html, keepLastNewLine, formats = {}) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const safeHtml = Quill.replaceStyleAttribute(html);
const doc = new DOMParser().parseFromString(safeHtml, 'text/html');
const container = doc.body;
const nodeMatches = new WeakMap();

Quill.restoreStyleAttribute(doc);

const [elementMatchers, textMatchers] = this.prepareMatching(
container,
nodeMatches,
Expand Down
80 changes: 80 additions & 0 deletions test/unit/core/quill.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Range } from '../../../core/selection';
import TableMain from '../../../modules/table';
import Embed from '../../../blots/embed';

const STYLE_ATTRIBUTE_KEY = 'style-data-key';

describe('Quill', function () {
it('imports', function () {
Object.keys(Quill.imports).forEach(function (path) {
Expand Down Expand Up @@ -959,4 +961,82 @@ describe('Quill', function () {
this.container.style.visibility = '';
});
});

describe('check replaceStyleAttribute', function () {
const testCases = [{
testName: 'simple style attribute should be replaced',
inputMarkup: '<p style="text-align: right;">content</p>',
expectedMarkup: `<p ${STYLE_ATTRIBUTE_KEY}="text-align: right;">content</p>`,
}, {
testName: 'uppercase style attribute should be replaced',
inputMarkup: '<p STYLE="text-align: right;">content</p>',
expectedMarkup: `<p ${STYLE_ATTRIBUTE_KEY}="text-align: right;">content</p>`,
}, {
testName: 'style attribute with one space after attribute should be replaced',
inputMarkup: '<p style ="text-align: right;">content</p>',
expectedMarkup: `<p ${STYLE_ATTRIBUTE_KEY}="text-align: right;">content</p>`,
}, {
testName: 'style attribute with two spaces after attribute should be replaced',
inputMarkup: '<p style ="text-align: right;">content</p>',
expectedMarkup: `<p ${STYLE_ATTRIBUTE_KEY}="text-align: right;">content</p>`,
}, {
testName: 'several style attributes should be replaced',
inputMarkup: '<p style="text-align: right;" style="border: solid;">content</p>',
expectedMarkup: `<p ${STYLE_ATTRIBUTE_KEY}="text-align: right;" ${STYLE_ATTRIBUTE_KEY}="border: solid;">content</p>`,
}, {
testName: 'style inside tag attribute should not be replaced',
inputMarkup: '<p>style="text-align: right;"</p>',
expectedMarkup: '<p>style="text-align: right;"</p>',
}];

testCases.forEach(({ testName, inputMarkup, expectedMarkup }) => {
it(testName, function () {
const processedMarkup = Quill.replaceStyleAttribute(inputMarkup);

expect(processedMarkup).toEqual(expectedMarkup);
});
});
});

describe('check restoreStyleAttribute', function () {
it('STYLE_ATTRIBUTE_KEY should be replaced', function () {
const container = document.createElement('p');
const pElement = document.createElement('p');

pElement.setAttribute(STYLE_ATTRIBUTE_KEY, 'text-align: right;');

container.appendChild(pElement);

Quill.restoreStyleAttribute(container);

expect(pElement.style.textAlign).toEqual('right');
expect(pElement.hasAttribute(STYLE_ATTRIBUTE_KEY)).toEqual(false);
});

it('STYLE_ATTRIBUTE_KEY located in content should be stay', function () {
const container = document.createElement('p');
const pElement = document.createElement('p');

pElement.textContent = `${STYLE_ATTRIBUTE_KEY}="text-align: right;"`;

container.appendChild(pElement);

Quill.restoreStyleAttribute(container);

expect(pElement.textContent).toEqual(`${STYLE_ATTRIBUTE_KEY}="text-align: right;"`);
});

it('LIST_STYLE_KEY should be exist on element after call restoreStyleAttribute', function () {
const container = document.createElement('p');
const pElement = document.createElement('p');

pElement.setAttribute(STYLE_ATTRIBUTE_KEY, 'text-align: right;');

container.appendChild(pElement);

Quill.restoreStyleAttribute(container);

expect(pElement.hasAttribute(Quill.LIST_STYLE_KEY)).toEqual(true);
});
});
});
63 changes: 63 additions & 0 deletions test/unit/modules/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,69 @@ describe('Clipboard', function () {
}, 2);
});

it('content with inline style should be rendered', function (done) {
this.quill.setSelection(0, 0);
const captureData = {
...this.clipboardEvent,
clipboardData: {
...this.clipboardData,
getData: (type) => {
return type === 'text/html' ? '<p style="text-align: right;">123</p>' : '123';
},
},
};

this.quill.clipboard.onCapturePaste(captureData);

setTimeout(() => {
expect(this.quill.root).toEqualHTML(
'<p class="ql-align-right">123</p><h1>0123</h1><p>5<em>67</em>8</p>',
);
done();
}, 2);
});

describe('check restoreStyleAttribute and replaceStyleAttribute methods', function () {
beforeEach(function () {
this.sourceReplaceStyleAttribute = Quill.replaceStyleAttribute;
this.sourceRestoreStyleAttribute = Quill.restoreStyleAttribute;
this.replaceStyleAttributeCallCount = 0;
this.restoreStyleAttributeCallCount = 0;
Quill.replaceStyleAttribute = () => {
this.replaceStyleAttributeCallCount += 1;
};
Quill.restoreStyleAttribute = () => {
this.restoreStyleAttributeCallCount += 1;
};
});

afterEach(function () {
Quill.replaceStyleAttribute = this.sourceReplaceStyleAttribute;
Quill.restoreStyleAttribute = this.sourceRestoreStyleAttribute;
});

it('restoreStyleAttribute and replaceStyleAttribute should be called', function (done) {
this.quill.setSelection(0, 0);
const captureData = {
...this.clipboardEvent,
clipboardData: {
...this.clipboardData,
getData: (type) => {
return type === 'text/html' ? '<p style="text-align: right;">123</p>' : '123';
},
},
};

this.quill.clipboard.onCapturePaste(captureData);

setTimeout(() => {
expect(this.replaceStyleAttributeCallCount).toEqual(1);
expect(this.restoreStyleAttributeCallCount).toEqual(1);
done();
}, 2);
});
});

// Copying from Word includes both html and files
it('pastes html data if present with file', function (done) {
const upload = spyOn(this.quill.uploader, 'upload');
Expand Down

0 comments on commit 610d687

Please sign in to comment.