612 lines
18 KiB
JavaScript
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,
|
|
});
|
|
}
|