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 2 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
54 changes: 32 additions & 22 deletions lib/core/utils/clone.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
import cache from '../base/cache';

/**
* Deeply clones an object or array
* @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) {
// handle circular references by caching the cloned object and returning it
const seen = cache.get('utils.clone', () => new WeakMap());
if (seen.has(obj)) {
return seen.get(obj);
}

if (obj === null || typeof obj !== 'object') {
return obj;
}

// don't 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]);
}
}
if (Array.isArray(obj)) {
const out = [];
seen.set(obj, out);
obj.forEach(value => {
out.push(clone(value));
});
return out;
}

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

export default clone;
88 changes: 65 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,87 @@ 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', () => {
const obj = { cats: true };
obj.child = obj;
const c = clone(obj);

obj.cats = false;

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

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.equal(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.equal(c.node, obj.node);
});
});