Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit df83343

Browse files
authored
Merge pull request #1104 from ckeditor/t/1088
Fix: `DataController#insertContent()` and `DataController#deleteContent()` should strip disallowed attributes from text nodes. Closes #1088.
2 parents 712ccfc + e592d2d commit df83343

File tree

8 files changed

+332
-31
lines changed

8 files changed

+332
-31
lines changed

src/controller/deletecontent.js

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ export default function deleteContent( selection, batch, options = {} ) {
6868
// want to override that behavior anyway.
6969
if ( !options.leaveUnmerged ) {
7070
mergeBranches( batch, startPos, endPos );
71+
72+
// We need to check and strip disallowed attributes in all nested nodes because after merge
73+
// some attributes could end up in a path where are disallowed.
74+
//
75+
// e.g. bold is disallowed for <H1>
76+
// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
77+
removeDisallowedAttributes( startPos.parent.getChildren(), startPos, batch );
7178
}
7279

7380
selection.setCollapsedAt( startPos );
@@ -208,9 +215,41 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) {
208215
return false;
209216
}
210217

211-
if ( !schema.check( { name: 'paragraph', inside: limitElement.name } ) ) {
212-
return false;
213-
}
218+
return schema.check( { name: 'paragraph', inside: limitElement.name } );
219+
}
214220

215-
return true;
221+
// Gets a name under which we should check this node in the schema.
222+
//
223+
// @param {module:engine/model/node~Node} node The node.
224+
// @returns {String} node name.
225+
function getNodeSchemaName( node ) {
226+
return node.is( 'text' ) ? '$text' : node.name;
227+
}
228+
229+
// Creates AttributeDeltas that removes attributes that are disallowed by schema on given node and its children.
230+
//
231+
// @param {Array<module:engine/model/node~Node>} nodes Nodes that will be filtered.
232+
// @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked.
233+
// @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
234+
function removeDisallowedAttributes( nodes, inside, batch ) {
235+
const schema = batch.document.schema;
236+
237+
for ( const node of nodes ) {
238+
const name = getNodeSchemaName( node );
239+
240+
// When node with attributes is not allowed in current position.
241+
if ( !schema.check( { name, inside, attributes: Array.from( node.getAttributeKeys() ) } ) ) {
242+
// Let's remove attributes one by one.
243+
// This should be improved to check all combination of attributes.
244+
for ( const attribute of node.getAttributeKeys() ) {
245+
if ( !schema.check( { name, inside, attributes: attribute } ) ) {
246+
batch.removeAttribute( node, attribute );
247+
}
248+
}
249+
}
250+
251+
if ( node.is( 'element' ) ) {
252+
removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), batch );
253+
}
254+
}
216255
}

src/controller/insertcontent.js

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,17 @@ class Insertion {
222222
* @param {Object} context
223223
*/
224224
_handleDisallowedNode( node, context ) {
225-
// Try inserting its children (strip the parent).
225+
// If the node is an element, try inserting its children (strip the parent).
226226
if ( node.is( 'element' ) ) {
227227
this.handleNodes( node.getChildren(), context );
228228
}
229-
// Try autoparagraphing.
229+
// If the node is a text and bare text is allowed in current position it means that the node
230+
// contains disallowed attributes and we have to remove them.
231+
else if ( this.schema.check( { name: '$text', inside: this.position } ) ) {
232+
removeDisallowedAttributes( [ node ], this.position, this.schema );
233+
this._handleNode( node, context );
234+
}
235+
// If text is not allowed, try autoparagraphing.
230236
else {
231237
this._tryAutoparagraphing( node, context );
232238
}
@@ -237,7 +243,7 @@ class Insertion {
237243
*/
238244
_insert( node ) {
239245
/* istanbul ignore if */
240-
if ( !this._checkIsAllowed( node, [ this.position.parent ] ) ) {
246+
if ( !this._checkIsAllowed( node, this.position ) ) {
241247
// Algorithm's correctness check. We should never end up here but it's good to know that we did.
242248
// Note that it would often be a silent issue if we insert node in a place where it's not allowed.
243249
log.error(
@@ -256,7 +262,7 @@ class Insertion {
256262
livePos.detach();
257263

258264
// The last inserted object should be selected because we can't put a collapsed selection after it.
259-
if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: [ this.position.parent ] } ) ) {
265+
if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: this.position } ) ) {
260266
this.nodeToSelect = node;
261267
} else {
262268
this.nodeToSelect = null;
@@ -282,6 +288,11 @@ class Insertion {
282288

283289
this.batch.merge( mergePosLeft );
284290

291+
// We need to check and strip disallowed attributes in all nested nodes because after merge
292+
// some attributes could end up in a path where are disallowed.
293+
const parent = position.nodeBefore;
294+
removeDisallowedAttributes( parent.getChildren(), Position.createAt( parent ), this.schema, this.batch );
295+
285296
this.position = Position.createFromPosition( position );
286297
position.detach();
287298
}
@@ -305,12 +316,22 @@ class Insertion {
305316

306317
this.batch.merge( mergePosRight );
307318

319+
// We need to check and strip disallowed attributes in all nested nodes because after merge
320+
// some attributes could end up in a place where are disallowed.
321+
removeDisallowedAttributes( position.parent.getChildren(), position, this.schema, this.batch );
322+
308323
this.position = Position.createFromPosition( position );
309324
position.detach();
310325
}
311326

312327
mergePosLeft.detach();
313328
mergePosRight.detach();
329+
330+
// When there was no merge we need to check and strip disallowed attributes in all nested nodes of
331+
// just inserted node because some attributes could end up in a place where are disallowed.
332+
if ( !mergeLeft && !mergeRight ) {
333+
removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), this.schema, this.batch );
334+
}
314335
}
315336

316337
/**
@@ -325,10 +346,17 @@ class Insertion {
325346
// Do not autoparagraph if the paragraph won't be allowed there,
326347
// cause that would lead to an infinite loop. The paragraph would be rejected in
327348
// the next _handleNode() call and we'd be here again.
328-
if ( this._getAllowedIn( paragraph, this.position.parent ) && this._checkIsAllowed( node, [ paragraph ] ) ) {
329-
paragraph.appendChildren( node );
349+
if ( this._getAllowedIn( paragraph, this.position.parent ) ) {
350+
// When node is a text and is disallowed by schema it means that contains disallowed attributes
351+
// and we need to remove them.
352+
if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) {
353+
removeDisallowedAttributes( [ node ], [ paragraph ], this.schema );
354+
}
330355

331-
this._handleNode( paragraph, context );
356+
if ( this._checkIsAllowed( node, [ paragraph ] ) ) {
357+
paragraph.appendChildren( node );
358+
this._handleNode( paragraph, context );
359+
}
332360
}
333361
}
334362

@@ -402,31 +430,59 @@ class Insertion {
402430
*/
403431
_checkIsAllowed( node, path ) {
404432
return this.schema.check( {
405-
name: this._getNodeSchemaName( node ),
433+
name: getNodeSchemaName( node ),
406434
attributes: Array.from( node.getAttributeKeys() ),
407435
inside: path
408436
} );
409437
}
410438

411439
/**
412-
* Checks wether according to the schema this is an object type element.
440+
* Checks whether according to the schema this is an object type element.
413441
*
414442
* @param {module:engine/model/node~Node} node The node to check.
415443
*/
416444
_checkIsObject( node ) {
417-
return this.schema.objects.has( this._getNodeSchemaName( node ) );
445+
return this.schema.objects.has( getNodeSchemaName( node ) );
418446
}
447+
}
419448

420-
/**
421-
* Gets a name under which we should check this node in the schema.
422-
*
423-
* @param {module:engine/model/node~Node} node The node.
424-
*/
425-
_getNodeSchemaName( node ) {
426-
if ( node.is( 'text' ) ) {
427-
return '$text';
449+
// Gets a name under which we should check this node in the schema.
450+
//
451+
// @param {module:engine/model/node~Node} node The node.
452+
// @returns {String} Node name.
453+
function getNodeSchemaName( node ) {
454+
return node.is( 'text' ) ? '$text' : node.name;
455+
}
456+
457+
// Removes disallowed by schema attributes from given nodes. When batch parameter is provided then
458+
// attributes will be removed by creating AttributeDeltas otherwise attributes will be removed
459+
// directly from provided nodes.
460+
//
461+
// @param {Array<module:engine/model/node~Node>} nodes Nodes that will be filtered.
462+
// @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked.
463+
// @param {module:engine/model/schema~Schema} schema Schema instance uses for element validation.
464+
// @param {module:engine/model/batch~Batch} [batch] Batch to which the deltas will be added.
465+
function removeDisallowedAttributes( nodes, inside, schema, batch ) {
466+
for ( const node of nodes ) {
467+
const name = getNodeSchemaName( node );
468+
469+
// When node with attributes is not allowed in current position.
470+
if ( !schema.check( { name, inside, attributes: Array.from( node.getAttributeKeys() ) } ) ) {
471+
// Let's remove attributes one by one.
472+
// This should be improved to check all combination of attributes.
473+
for ( const attribute of node.getAttributeKeys() ) {
474+
if ( !schema.check( { name, inside, attributes: attribute } ) ) {
475+
if ( batch ) {
476+
batch.removeAttribute( node, attribute );
477+
} else {
478+
node.removeAttribute( attribute );
479+
}
480+
}
481+
}
428482
}
429483

430-
return node.name;
484+
if ( node.is( 'element' ) ) {
485+
removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), schema, batch );
486+
}
431487
}
432488
}

tests/controller/deletecontent.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@ describe( 'DataController', () => {
155155

156156
schema.registerItem( 'paragraph', '$block' );
157157
schema.registerItem( 'heading1', '$block' );
158+
schema.registerItem( 'image', '$inline' );
158159
schema.registerItem( 'pchild' );
159160
schema.registerItem( 'pparent' );
160-
schema.registerItem( 'image', '$inline' );
161161

162162
schema.allow( { name: 'pchild', inside: 'paragraph' } );
163163
schema.allow( { name: '$text', inside: 'pchild' } );
@@ -188,12 +188,6 @@ describe( 'DataController', () => {
188188
{ leaveUnmerged: true }
189189
);
190190

191-
test(
192-
'merges second element into the first one (same name)',
193-
'<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
194-
'<paragraph>x</paragraph><paragraph>fo[]ar</paragraph><paragraph>y</paragraph>'
195-
);
196-
197191
test(
198192
'merges second element into the first one (different name)',
199193
'<paragraph>x</paragraph><heading1>fo[o</heading1><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
@@ -436,6 +430,51 @@ describe( 'DataController', () => {
436430
'<paragraph>ba[]</paragraph><blockWidget><nestedEditable>oo</nestedEditable></blockWidget>'
437431
);
438432
} );
433+
434+
describe( 'filtering out', () => {
435+
beforeEach( () => {
436+
const schema = doc.schema;
437+
438+
schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'paragraph' } );
439+
schema.allow( { name: '$text', attributes: [ 'b', 'c' ], inside: 'pchild' } );
440+
schema.allow( { name: 'pchild', inside: 'pchild' } );
441+
schema.disallow( { name: '$text', attributes: [ 'c' ], inside: 'pchild pchild' } );
442+
} );
443+
444+
test(
445+
'filters out disallowed attributes after left merge',
446+
'<paragraph>x<pchild>fo[o</pchild></paragraph><paragraph>y]<$text a="1" b="1">z</$text></paragraph>',
447+
'<paragraph>x<pchild>fo[]<$text b="1">z</$text></pchild></paragraph>'
448+
);
449+
450+
test(
451+
'filters out disallowed attributes from nested nodes after left merge',
452+
'<paragraph>' +
453+
'x' +
454+
'<pchild>fo[o</pchild>' +
455+
'</paragraph>' +
456+
'<paragraph>' +
457+
'b]a<$text a="1" b="1">r</$text>' +
458+
'<pchild>b<$text b="1" c="1">i</$text>z</pchild>' +
459+
'y' +
460+
'</paragraph>',
461+
462+
'<paragraph>' +
463+
'x' +
464+
'<pchild>' +
465+
'fo[]a<$text b="1">r</$text>' +
466+
'<pchild>b<$text b="1">i</$text>z</pchild>' +
467+
'y' +
468+
'</pchild>' +
469+
'</paragraph>'
470+
);
471+
472+
test(
473+
'filters out disallowed attributes after right merge',
474+
'<paragraph>fo[o</paragraph><paragraph><pchild>x<$text b="1" c="1">y]z</$text></pchild></paragraph>',
475+
'<paragraph>fo[]<$text b="1">z</$text></paragraph>'
476+
);
477+
} );
439478
} );
440479

441480
describe( 'in element selections scenarios', () => {

0 commit comments

Comments
 (0)