Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(utils/clone): don't try to clone elements from different window context #4072

Merged
merged 9 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 35 additions & 23 deletions lib/core/utils/clone.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
/**
* Deeply clones an object or array
* Deeply clones an object or array. DOM nodes or collections of DOM nodes are not deeply cloned and are instead returned as is.
* @param {Mixed} obj The object/array to clone
* @return {Mixed} A clone of the initial object or array
* @return {Mixed} A clone of the initial object or array
*/
function clone(obj) {
/* eslint guard-for-in: 0*/
var index,
length,
out = obj;
// DOM nodes cannot be cloned.
export default function clone(obj) {
return cloneRecused(obj, new Map());
}

// internal function to hide non-user facing parameters
function cloneRecused(obj, seen) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

// don't clone DOM nodes. since we can pass nodes from different window contexts
// we'll also use duck typing to determine what is a DOM node
if (
(window?.Node && obj instanceof window.Node) ||
(window?.HTMLCollection && obj instanceof window.HTMLCollection)
(window?.HTMLCollection && obj instanceof window.HTMLCollection) ||
('nodeName' in obj && 'nodeType' in obj && 'ownerDocument' in obj)
) {
return obj;
}

if (obj !== null && typeof obj === 'object') {
if (Array.isArray(obj)) {
out = [];
for (index = 0, length = obj.length; index < length; index++) {
out[index] = clone(obj[index]);
}
} else {
out = {};
for (index in obj) {
out[index] = clone(obj[index]);
}
}
// handle circular references by caching the cloned object and returning it
if (seen.has(obj)) {
return seen.get(obj);
}

if (Array.isArray(obj)) {
const out = [];
seen.set(obj, out);
obj.forEach(value => {
out.push(cloneRecused(value, seen));
});
return out;
}

const out = {};
seen.set(obj, out);
// eslint-disable-next-line guard-for-in
for (const key in obj) {
out[key] = cloneRecused(obj[key], seen);
}
return out;
}

export default clone;
108 changes: 85 additions & 23 deletions test/core/utils/clone.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
describe('utils.clone', function () {
'use strict';
var clone = axe.utils.clone;
describe('utils.clone', () => {
const clone = axe.utils.clone;
const fixture = document.querySelector('#fixture');

it('should clone an object', function () {
var obj = {
it('should clone an object', () => {
const obj = {
cats: true,
dogs: 2,
fish: [0, 1, 2]
fish: [0, 1, { one: 'two' }]
};
var c = clone(obj);
const c = clone(obj);

obj.cats = false;
obj.dogs = 1;
obj.fish[0] = 'stuff';
obj.fish[2].one = 'three';

assert.strictEqual(c.cats, true);
assert.strictEqual(c.dogs, 2);
assert.deepEqual(c.fish, [0, 1, 2]);
assert.deepEqual(c.fish, [0, 1, { one: 'two' }]);
});

it('should clone nested objects', function () {
var obj = {
it('should clone nested objects', () => {
const obj = {
cats: {
fred: 1,
billy: 2,
Expand All @@ -33,7 +33,7 @@ describe('utils.clone', function () {
},
fish: [0, 1, 2]
};
var c = clone(obj);
const c = clone(obj);

obj.cats.fred = 47;
obj.dogs = 47;
Expand All @@ -54,45 +54,107 @@ describe('utils.clone', function () {
assert.deepEqual(c.fish, [0, 1, 2]);
});

it('should clone objects with methods', function () {
var obj = {
cats: function () {
it('should clone objects with methods', () => {
const obj = {
cats: () => {
return 'meow';
},
dogs: function () {
dogs: () => {
return 'woof';
}
};
var c = clone(obj);
const c = clone(obj);

assert.strictEqual(obj.cats, c.cats);
assert.strictEqual(obj.dogs, c.dogs);

obj.cats = function () {};
obj.dogs = function () {};
obj.cats = () => {};
obj.dogs = () => {};

assert.notStrictEqual(obj.cats, c.cats);
assert.notStrictEqual(obj.dogs, c.dogs);
});

it('should clone prototypes', function () {
it('should clone prototypes', () => {
function Cat(name) {
this.name = name;
}

Cat.prototype.meow = function () {
Cat.prototype.meow = () => {
return 'meow';
};

Cat.prototype.bark = function () {
Cat.prototype.bark = () => {
return 'cats dont bark';
};

var cat = new Cat('Fred'),
const cat = new Cat('Fred'),
c = clone(cat);

assert.deepEqual(cat.name, c.name);
assert.deepEqual(Cat.prototype.bark, c.bark);
assert.deepEqual(Cat.prototype.meow, c.meow);
});

it('should clone circular objects while keeping the circular reference', () => {
const obj = { cats: true };
obj.child = obj;
const c = clone(obj);

obj.cats = false;

assert.deepEqual(c, {
cats: true,
child: c
});
assert.strictEqual(c, c.child);
});

it('should not return the same object when cloned twice', () => {
const obj = { cats: true };
const c1 = clone(obj);
const c2 = clone(obj);

assert.notStrictEqual(c1, c2);
});

it('should not return the same object when nested', () => {
const obj = { dogs: true };
const obj1 = { cats: true, child: { prop: obj } };
const obj2 = { fish: [0, 1, 2], child: { prop: obj } };

const c1 = clone(obj1);
const c2 = clone(obj2);

assert.notStrictEqual(c1.child.prop, c2.child.prop);
});

it('should not clone HTML elements', () => {
const obj = {
cats: true,
node: document.createElement('div')
};
const c = clone(obj);

obj.cats = false;

assert.equal(c.cats, true);
assert.strictEqual(c.node, obj.node);
});

it('should not clone HTML elements from different windows', () => {
fixture.innerHTML = '<iframe id="target"></iframe>';
const iframe = fixture.querySelector('#target');

const obj = {
cats: true,
node: iframe.contentDocument
};
const c = clone(obj);

obj.cats = false;

assert.equal(c.cats, true);
assert.strictEqual(c.node, obj.node);
});
});