Files
2026-07-13 13:23:39 +08:00

612 lines
18 KiB
JavaScript

// When in doubt, refer to https://github.com/nodejs/node/blob/main/doc/contributing/primordials.md
/* eslint-disable no-undef */
/* eslint-disable no-cond-assign */
/* eslint-disable prefer-rest-params */
/* eslint-disable no-shadow */
// TODO: https://developer.mozilla.org/en-US/docs/Web/API/Cookie_Store_API
// TODO: custom error messages for Firefox (for now it uses Chrome's)
// The only way to detect this "container" is to benchmark document.cookie or compare localStorage performance with sessionStorage (it's the same).
const isFirefox = navigator.userAgent.includes('Firefox');
const tabPrefix = `.${tabId}.`;
const {
String,
Array,
Set,
TypeError,
WeakMap,
Object,
Number,
Function,
Proxy,
IDBFactory,
IDBDatabase,
BroadcastChannel,
Storage,
// We don't have to implement StorageEvent because this implementation does not use localStorage at all.
} = globalThis;
const ObjectDefineProperty = Object.defineProperty;
const ObjectDefineProperties = Object.defineProperties;
const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
const ObjectGetPrototypeOf = Object.getPrototypeOf;
const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const ObjectCreate = Object.create;
const ObjectEntries = Object.entries;
const ReflectGet = Reflect.get;
const ReflectSet = Reflect.set;
const ObjectKeys = Object.keys;
const NumberIsFinite = Number.isFinite;
const clonePrototype = (from) => {
const target = ObjectCreate(null);
const prototype = ObjectGetOwnPropertyDescriptors(from.prototype);
const entries = ObjectEntries(prototype);
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const { 0: name, 1: descriptor } = entry;
target[name] = ObjectCreate(null);
if ('get' in descriptor) {
target[name].get = descriptor.get;
}
if ('set' in descriptor) {
target[name].set = descriptor.set;
}
if ('value' in descriptor) {
target[name] = descriptor.value;
}
}
return target;
};
const StringSplitSafe = (string, separator) => {
const result = [];
const separatorLength = separator.length;
if (separatorLength === 0) {
throw new Error('Separator must not be empty');
}
let startFrom = 0;
let index;
while ((index = StringPrototype.indexOf.call(string, separator, startFrom)) !== -1) {
ArrayPrototype.push.call(result, StringPrototype.slice.call(string, startFrom, index));
startFrom = index + separatorLength;
}
const lastChunk = StringPrototype.slice.call(string, startFrom);
ArrayPrototype.push.call(result, lastChunk);
return result;
};
const fixStack = (error) => {
const lines = StringSplitSafe(error.stack, '\n');
if (isFirefox) {
ArrayPrototype.splice.call(lines, 0, 1);
} else {
ArrayPrototype.splice.call(lines, 1, 1);
}
error.stack = ArrayPrototype.join.call(lines, '\n');
return error;
};
const SetPrototype = clonePrototype(Set);
const WeakMapPrototype = clonePrototype(WeakMap);
const ArrayPrototype = clonePrototype(Array);
const StringPrototype = clonePrototype(String);
const IDBFactoryPrototype = clonePrototype(IDBFactory);
const IDBDatabasePrototype = clonePrototype(IDBDatabase);
const StoragePrototype = clonePrototype(Storage);
const privates = new WeakMap();
let invocable = false;
const FakeStorage = class Storage {
constructor() {
if (invocable) {
throw fixStack(new TypeError('Illegal constructor'));
}
WeakMapPrototype.set.call(privates, this, arguments[0]);
}
get length() {
const priv = WeakMapPrototype.get.call(privates, this);
if (!priv) {
throw fixStack(new TypeError('Illegal invocation'));
}
const { storage, prefix } = priv;
const length = StoragePrototype.length.get.call(storage);
let fakeLength = 0;
for (let i = 0; i < length; i++) {
const storageKey = StoragePrototype.key.call(storage, i);
if (StringPrototype.startsWith.call(storageKey, prefix)) {
fakeLength++;
}
}
return fakeLength;
}
clear() {
const priv = WeakMapPrototype.get.call(privates, this);
if (!priv) {
throw fixStack(new TypeError('Illegal invocation'));
}
const { storage, prefix } = priv;
const length = StoragePrototype.length.get.call(storage);
const keys = [];
for (let i = 0; i < length; i++) {
ArrayPrototype.push.call(keys, StoragePrototype.key.call(storage, i));
}
for (let i = 0; i < length; i++) {
const storageKey = keys[i];
if (StringPrototype.startsWith.call(storageKey, prefix)) {
StoragePrototype.removeItem.call(storage, storageKey);
}
}
}
key(index) {
const priv = WeakMapPrototype.get.call(privates, this);
if (!priv) {
throw fixStack(new TypeError('Illegal invocation'));
}
if (arguments.length === 0) {
throw fixStack(
new TypeError(`Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present.`),
);
}
index = NumberIsFinite(index) ? index : 0;
const { storage, prefix } = priv;
const length = StoragePrototype.length.get.call(storage);
let fakeLength = 0;
for (let i = 0; i < length; i++) {
const storageKey = StoragePrototype.key.call(storage, i);
if (StringPrototype.startsWith.call(storageKey, prefix)) {
if (fakeLength === index) {
return StringPrototype.slice.call(storageKey, prefix.length);
}
fakeLength++;
}
}
return null;
}
getItem(key) {
const priv = WeakMapPrototype.get.call(privates, this);
if (!priv) {
throw fixStack(new TypeError('Illegal invocation'));
}
if (arguments.length === 0) {
throw fixStack(
new TypeError(`Failed to execute 'getItem' on 'Storage': 1 argument required, but only 0 present.`),
);
}
return StoragePrototype.getItem.call(priv.storage, priv.prefix + key);
}
removeItem(key) {
const priv = WeakMapPrototype.get.call(privates, this);
if (!priv) {
throw fixStack(new TypeError('Illegal invocation'));
}
if (arguments.length === 0) {
throw fixStack(
new TypeError(`Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present.`),
);
}
StoragePrototype.removeItem.call(priv.storage, priv.prefix + key);
}
setItem(key, value) {
const priv = WeakMapPrototype.get.call(privates, this);
if (!priv) {
throw fixStack(new TypeError('Illegal invocation'));
}
if (arguments.length === 0 || arguments.length === 1) {
throw fixStack(
new TypeError(
`Failed to execute 'setItem' on 'Storage': 2 arguments required, but only ${arguments.length} present.`,
),
);
}
StoragePrototype.setItem.call(priv.storage, priv.prefix + key, value);
}
};
const FakeStoragePrototype = clonePrototype(FakeStorage);
const createStorage = ({ storage, prefix }) => {
invocable = false;
const fake = new FakeStorage({ storage, prefix });
invocable = true;
const proxy = new Proxy(fake, {
__proto__: null,
// Default:
// apply: (target, thisArg, args) => {},
// construct(target, args) => {},
// setPrototypeOf: (target, proto) => {},
// getPrototypeOf: (target) => {},
defineProperty: (target, key, descriptor) => {
if ('set' in descriptor || 'get' in descriptor) {
throw fixStack(
new TypeError(`Failed to set a named property on 'Storage': Accessor properties are not allowed.`),
);
}
FakeStoragePrototype.setItem.call(target, key, descriptor.value);
},
deleteProperty: (target, key) => {
if (typeof key === 'symbol') {
delete target[key];
} else {
FakeStoragePrototype.removeItem.call(target, key);
}
return true;
},
get: (target, key) => {
if (typeof key === 'symbol') {
return target[key];
}
if (key in target) {
return ReflectGet(target, key);
}
return FakeStoragePrototype.getItem.call(target, key) ?? undefined;
},
set: (target, key, value) => {
if (typeof key === 'symbol') {
ObjectDefineProperty(target, key, {
__proto__: null,
value,
configurable: true,
writable: true,
enumerable: false,
});
return true;
}
if (key in target) {
return ReflectSet(target, key, value);
}
return FakeStoragePrototype.setItem.call(target, key, value) ?? true;
},
has: (target, key) => {
if (key in target) {
return true;
}
return FakeStoragePrototype.getItem.call(target, key) !== null;
},
isExtensible: () => {
return true;
},
preventExtensions: () => {
throw fixStack(new TypeError(`Cannot prevent extensions`));
},
getOwnPropertyDescriptor: (target, key) => {
if (key in target) {
return ObjectGetOwnPropertyDescriptor(ObjectGetPrototypeOf(target), key);
}
const value = FakeStoragePrototype.getItem.call(target, key);
if (value !== null) {
return {
value,
writable: true,
enumerable: true,
configurable: true,
};
}
},
ownKeys: (target) => {
const keys = [];
const { storage, prefix } = WeakMapPrototype.get.call(privates, target);
const length = StoragePrototype.length.get.call(storage);
for (let i = 0; i < length; i++) {
const storageKey = StoragePrototype.key.call(storage, i);
if (StringPrototype.startsWith.call(storageKey, prefix)) {
ArrayPrototype.push.call(keys, StringPrototype.slice.call(storageKey, prefix.length));
}
}
ArrayPrototype.push.apply(keys, ObjectKeys(target));
const set = new Set();
for (let i = 0; i < keys.length; i++) {
SetPrototype.add.call(set, keys[i]);
}
return ArrayPrototype.slice.call(set);
},
});
privates.set(proxy, privates.get(fake));
return proxy;
};
const toHide = new WeakMap();
for (const Type of [Function, Object, Array]) {
const create = (fallback) =>
function () {
if (this instanceof FakeStorage) {
return '[object Storage]';
}
if (WeakMapPrototype.has.call(toHide, this)) {
return `function ${WeakMapPrototype.get.call(toHide, this)}() { [native code] }`;
}
return fallback.call(this);
};
const toString = create(Type.prototype.toString);
const toLocaleString = create(Type.prototype.toLocaleString);
WeakMapPrototype.set.call(toHide, toString, 'toString');
WeakMapPrototype.set.call(toHide, toLocaleString, 'toLocaleString');
Object.defineProperty(Type.prototype, 'toString', {
__proto__: null,
value: toString,
});
Object.defineProperty(Type.prototype, 'toLocaleString', {
__proto__: null,
value: toLocaleString,
});
}
// https://stackoverflow.com/q/30481516
try {
// We use sessionStorage as the underlying storage for localStorage.
// This way we do not have to worry about clean up.
const { sessionStorage } = globalThis;
const fakeLocalStorage = createStorage({ storage: sessionStorage, prefix: 'l.' });
const fakeSessionStorage = createStorage({ storage: sessionStorage, prefix: 's.' });
const getLocalStorage = function localStorage() {
return fakeLocalStorage;
};
const getSessionStorage = function sessionStorage() {
return fakeSessionStorage;
};
WeakMapPrototype.set.call(toHide, FakeStorage, 'Storage');
WeakMapPrototype.set.call(toHide, FakeStoragePrototype.key, 'key');
WeakMapPrototype.set.call(toHide, FakeStoragePrototype.getItem, 'getItem');
WeakMapPrototype.set.call(toHide, FakeStoragePrototype.setItem, 'setItem');
WeakMapPrototype.set.call(toHide, FakeStoragePrototype.removeItem, 'removeItem');
WeakMapPrototype.set.call(toHide, FakeStoragePrototype.clear, 'clear');
WeakMapPrototype.set.call(toHide, getLocalStorage, 'get localStorage');
WeakMapPrototype.set.call(toHide, getSessionStorage, 'get sessionStorage');
ObjectDefineProperties(window, {
__proto__: null,
Storage: {
__proto__: null,
value: FakeStorage,
configurable: true,
enumerable: false,
writable: true,
},
localStorage: {
__proto__: null,
configurable: true,
enumerable: true,
get: getLocalStorage,
set: undefined,
},
sessionStorage: {
__proto__: null,
configurable: true,
enumerable: true,
get: getSessionStorage,
set: undefined,
},
});
} catch (error) {
console.error(error);
}
{
const { Document } = globalThis;
const realGetCookie = ObjectGetOwnPropertyDescriptor(Document.prototype, 'cookie').get;
const realSetCookie = ObjectGetOwnPropertyDescriptor(Document.prototype, 'cookie').set;
const getCookie = function cookie() {
try {
const cookies = StringSplitSafe(realGetCookie.call(this), '; ');
const filtered = ArrayPrototype.filter.call(cookies, (cookie) =>
StringPrototype.startsWith.call(cookie, tabPrefix),
);
const mapped = ArrayPrototype.map.call(filtered, (cookie) => {
const result = StringPrototype.slice.call(cookie, tabPrefix.length);
if (result[0] === '=') {
return StringPrototype.slice.call(result, 1);
}
return result;
});
return ArrayPrototype.join.call(mapped, '; ');
} catch (error) {
throw fixStack(error);
}
};
const setCookie = function cookie(cookieString) {
cookieString = StringPrototype.trimStart.call(String(cookieString));
const delimiterIndex = StringPrototype.indexOf.call(cookieString, ';');
const equalsIndex = StringPrototype.indexOf.call(cookieString, '=');
if (equalsIndex === -1 || (delimiterIndex !== -1 && equalsIndex > delimiterIndex)) {
cookieString = `=${cookieString}`;
}
try {
realSetCookie.call(this, tabPrefix + cookieString);
} catch (error) {
throw fixStack(error);
}
};
WeakMapPrototype.set.call(toHide, getCookie, 'get cookie');
WeakMapPrototype.set.call(toHide, setCookie, 'set cookie');
ObjectDefineProperty(Document.prototype, 'cookie', {
__proto__: null,
configurable: true,
enumerable: true,
get: getCookie,
set: setCookie,
});
}
{
const openDatabase = function open(name) {
try {
return IDBFactoryPrototype.open.call(this, tabPrefix + name);
} catch (error) {
throw fixStack(error);
}
};
const deleteDatabase = function deleteDatabase(name) {
try {
return IDBFactoryPrototype.deleteDatabase.call(this, tabPrefix + name);
} catch (error) {
throw fixStack(error);
}
};
const databaseName = function name() {
try {
return StringPrototype.slice.call(IDBDatabasePrototype.name.get.call(this), tabPrefix.length);
} catch (error) {
throw fixStack(error);
}
};
WeakMapPrototype.set.call(toHide, openDatabase, 'open');
WeakMapPrototype.set.call(toHide, deleteDatabase, 'deleteDatabase');
WeakMapPrototype.set.call(toHide, databaseName, 'get name');
ObjectDefineProperties(IDBFactory.prototype, {
__proto__: null,
open: {
__proto__: null,
writable: true,
configurable: true,
enumerable: true,
value: openDatabase,
},
deleteDatabase: {
__proto__: null,
writable: true,
configurable: true,
enumerable: true,
value: deleteDatabase,
},
name: {
__proto__: null,
configurable: true,
enumerable: true,
get: databaseName,
set: undefined,
},
});
}
{
ObjectDefineProperty(window, 'BroadcastChannel', {
__proto__: null,
configurable: true,
enumerable: false,
writable: true,
value: new Proxy(BroadcastChannel, {
__proto__: null,
construct: (Target, name) => {
return new Target(tabPrefix + name);
},
}),
});
WeakMapPrototype.set.call(toHide, window.BroadcastChannel, 'BroadcastChannel');
const getBroadcastChannelName = ObjectGetOwnPropertyDescriptor(BroadcastChannel.prototype, 'name').get;
const broadcastChannelName = function name() {
try {
const realName = getBroadcastChannelName.call(this);
if (StringPrototype.startsWith.call(realName, tabPrefix)) {
return StringPrototype.slice.call(realName, tabPrefix.length);
}
return realName;
} catch (error) {
throw fixStack(error);
}
};
WeakMapPrototype.set.call(toHide, broadcastChannelName, 'get name');
ObjectDefineProperty(BroadcastChannel.prototype, 'name', {
__proto__: null,
configurable: true,
enumerable: true,
get: broadcastChannelName,
set: undefined,
});
}