mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-07-10 22:02:45 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d43bd40e83 | |||
| d5a0ea58bd |
@@ -243,8 +243,28 @@ exports.slowInSlowOut = function(t) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.copyObjectPropertiesSafe = function(object) {
|
exports.copyObjectPropertiesSafe = function(object) {
|
||||||
const seen = new Set(),
|
const seen = new Set();
|
||||||
isDOMElement = (value) => value instanceof Node || value instanceof Window;
|
|
||||||
|
function isDOMElement(value) {
|
||||||
|
if(!value || typeof value !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-realm DOM nodes
|
||||||
|
if(typeof value.nodeType === "number" &&
|
||||||
|
typeof value.nodeName === "string") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-realm Window objects
|
||||||
|
if(value.window === value &&
|
||||||
|
value.document &&
|
||||||
|
value.location) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function safeCopy(obj) {
|
function safeCopy(obj) {
|
||||||
// skip circular references
|
// skip circular references
|
||||||
@@ -255,10 +275,6 @@ exports.copyObjectPropertiesSafe = function(object) {
|
|||||||
if(typeof obj !== "object" || obj === null) {
|
if(typeof obj !== "object" || obj === null) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
// skip DOM elements
|
|
||||||
if(isDOMElement(obj)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
// copy arrays, preserving positions
|
// copy arrays, preserving positions
|
||||||
if(Array.isArray(obj)) {
|
if(Array.isArray(obj)) {
|
||||||
return obj.map((item) => {
|
return obj.map((item) => {
|
||||||
@@ -266,7 +282,11 @@ exports.copyObjectPropertiesSafe = function(object) {
|
|||||||
return value === undefined ? null : value;
|
return value === undefined ? null : value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// skip DOM elements
|
||||||
|
if(isDOMElement(obj)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
seen.add(obj);
|
seen.add(obj);
|
||||||
const copy = {};
|
const copy = {};
|
||||||
let key,
|
let key,
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/*\
|
||||||
|
title: test-utils-copyObjectPropertiesSafe.js
|
||||||
|
type: application/javascript
|
||||||
|
tags: [[$:/tags/test-spec]]
|
||||||
|
|
||||||
|
Tests $tw.utils.copyObjectPropertiesSafe, the root cause of #9869.
|
||||||
|
|
||||||
|
$eventcatcher serialises DOM events via JSON.stringify(copyObjectPropertiesSafe(event)).
|
||||||
|
|
||||||
|
The original bug was caused by instanceof Node/Window being realm-specific.
|
||||||
|
When an event originated from a different browser window, DOM objects from that
|
||||||
|
window were not detected and JSON.stringify() could throw "Illegal invocation".
|
||||||
|
|
||||||
|
The implementation must:
|
||||||
|
- skip DOM nodes and Window objects from other realms
|
||||||
|
- preserve normal event data
|
||||||
|
- preserve CustomEvent.detail payloads
|
||||||
|
- continue copying enumerable properties from non-DOM objects
|
||||||
|
- preserve arrays and break circular references
|
||||||
|
|
||||||
|
The tests use substitutes for foreign DOM objects because headless Node does not
|
||||||
|
provide a second browser realm. These objects model the important characteristics:
|
||||||
|
DOM nodes have nodeType/nodeName, and Window objects have window/self/document.
|
||||||
|
\*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("copyObjectPropertiesSafe (#9869)", function() {
|
||||||
|
|
||||||
|
var cops = $tw.utils.copyObjectPropertiesSafe;
|
||||||
|
|
||||||
|
// Simulates a DOM node from another realm.
|
||||||
|
// The important characteristics are:
|
||||||
|
// - nodeType/nodeName identify it as a DOM node
|
||||||
|
// - circular parentNode references resemble real DOM trees
|
||||||
|
function fakeElement(tagName,extra) {
|
||||||
|
var node = $tw.utils.extend({
|
||||||
|
nodeType: 1,
|
||||||
|
nodeName: tagName,
|
||||||
|
tagName: tagName
|
||||||
|
},extra || {});
|
||||||
|
|
||||||
|
node.parentNode = node;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulates a Window object from another realm.
|
||||||
|
function fakeWindow() {
|
||||||
|
var win = {
|
||||||
|
document: {},
|
||||||
|
location: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
win.window = win;
|
||||||
|
win.self = win;
|
||||||
|
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An event carrying foreign DOM nodes and a Window.
|
||||||
|
function fakeEvent(type,target,extra) {
|
||||||
|
return $tw.utils.extend({
|
||||||
|
type: type,
|
||||||
|
target: target,
|
||||||
|
currentTarget: target,
|
||||||
|
view: fakeWindow(),
|
||||||
|
detail: 0,
|
||||||
|
isTrusted: true
|
||||||
|
},extra || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
it("does not throw serialising any event type from a secondary window", function() {
|
||||||
|
var events = [
|
||||||
|
fakeEvent("focusin",fakeElement("INPUT")),
|
||||||
|
fakeEvent("change",fakeElement("SELECT")),
|
||||||
|
fakeEvent("click",fakeElement("BUTTON"),{
|
||||||
|
button: 0,
|
||||||
|
clientX: 5,
|
||||||
|
clientY: 9,
|
||||||
|
relatedTarget: null
|
||||||
|
}),
|
||||||
|
fakeEvent("mouseover",fakeElement("DIV"),{
|
||||||
|
relatedTarget: fakeElement("SPAN")
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
events.forEach(function(event) {
|
||||||
|
expect(function() {
|
||||||
|
JSON.stringify(cops(event));
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("drops DOM nodes and Window but keeps serialisable event data", function() {
|
||||||
|
var event = fakeEvent("click",fakeElement("BUTTON"),{
|
||||||
|
button: 0,
|
||||||
|
clientX: 5,
|
||||||
|
clientY: 9
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = JSON.parse(JSON.stringify(cops(event)));
|
||||||
|
|
||||||
|
expect(result.target).toBeUndefined();
|
||||||
|
expect(result.currentTarget).toBeUndefined();
|
||||||
|
expect(result.view).toBeUndefined();
|
||||||
|
|
||||||
|
expect(result.type).toBe("click");
|
||||||
|
expect(result.detail).toBe(0);
|
||||||
|
expect(result.isTrusted).toBe(true);
|
||||||
|
expect(result.button).toBe(0);
|
||||||
|
expect(result.clientX).toBe(5);
|
||||||
|
expect(result.clientY).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("preserves nested objects, arrays and circular reference handling", function() {
|
||||||
|
var event = fakeEvent("custom",fakeElement("DIV"),{
|
||||||
|
detail: {
|
||||||
|
nested: {
|
||||||
|
a: 1,
|
||||||
|
b: [2,3]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
path: [
|
||||||
|
fakeElement("DIV"),
|
||||||
|
fakeWindow(),
|
||||||
|
42
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
event.self = event;
|
||||||
|
|
||||||
|
var result = JSON.parse(JSON.stringify(cops(event)));
|
||||||
|
|
||||||
|
expect(result.detail).toEqual({
|
||||||
|
nested: {
|
||||||
|
a: 1,
|
||||||
|
b: [2,3]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.path).toEqual([
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
42
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.self).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("preserves CustomEvent.detail objects including custom object instances", function() {
|
||||||
|
function DetailObject() {
|
||||||
|
this.name = "example";
|
||||||
|
this.values = [1,2,3];
|
||||||
|
}
|
||||||
|
|
||||||
|
var event = fakeEvent("custom",fakeElement("DIV"),{
|
||||||
|
detail: new DetailObject()
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = JSON.parse(JSON.stringify(cops(event)));
|
||||||
|
|
||||||
|
expect(result.detail).toEqual({
|
||||||
|
name: "example",
|
||||||
|
values: [1,2,3]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("preserves CustomEvent.detail objects while dropping DOM objects inside them", function() {
|
||||||
|
var event = fakeEvent("custom",fakeElement("DIV"),{
|
||||||
|
detail: {
|
||||||
|
value: 123,
|
||||||
|
nested: {
|
||||||
|
target: fakeElement("SPAN"),
|
||||||
|
kept: "yes"
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
fakeElement("BUTTON"),
|
||||||
|
42
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = JSON.parse(JSON.stringify(cops(event)));
|
||||||
|
|
||||||
|
expect(result.detail.value).toBe(123);
|
||||||
|
|
||||||
|
expect(result.detail.nested.target).toBeUndefined();
|
||||||
|
expect(result.detail.nested.kept).toBe("yes");
|
||||||
|
|
||||||
|
expect(result.detail.items).toEqual([
|
||||||
|
null,
|
||||||
|
42
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("accepts primitive, array and object arguments", function() {
|
||||||
|
var nullProto = Object.create(null);
|
||||||
|
nullProto.kept = true;
|
||||||
|
|
||||||
|
expect(cops(42)).toBe(42);
|
||||||
|
expect(cops(null)).toBe(null);
|
||||||
|
|
||||||
|
expect(cops([
|
||||||
|
1,
|
||||||
|
"two",
|
||||||
|
{three: 3}
|
||||||
|
])).toEqual([
|
||||||
|
1,
|
||||||
|
"two",
|
||||||
|
{three: 3}
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(cops({
|
||||||
|
a: 1,
|
||||||
|
b: {
|
||||||
|
c: 2
|
||||||
|
}
|
||||||
|
})).toEqual({
|
||||||
|
a: 1,
|
||||||
|
b: {
|
||||||
|
c: 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(cops({
|
||||||
|
nullProto: nullProto
|
||||||
|
})))).toEqual({
|
||||||
|
nullProto: {
|
||||||
|
kept: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
title: $:/changenotes/5.4.1/#9905
|
||||||
|
created: 20260709142414000
|
||||||
|
modified: 20260709142414000
|
||||||
|
description: Fix errors in eventcatcher while serializing event properties
|
||||||
|
release: 5.4.1
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
change-type: bugfix
|
||||||
|
change-category: internal
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9905
|
||||||
|
github-contributors: saqimtiaz
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
Fixes an issue in utils.copyObjectPropertiesSafe which did not correctly handle DOM objects from other windows resulting in an error.
|
||||||
Reference in New Issue
Block a user