-
Notifications
You must be signed in to change notification settings - Fork 444
/
gm-global-wrapper.js
191 lines (185 loc) · 6.81 KB
/
gm-global-wrapper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import { FastLookup, safeConcat } from './util';
const scopeSym = SafeSymbol.unscopables;
const globalKeysSet = FastLookup();
const globalKeys = (function makeGlobalKeys() {
const kWrappedJSObject = 'wrappedJSObject';
const isContentMode = !PAGE_MODE_HANDSHAKE;
const names = builtinGlobals[0]; // `window` keys
const numFrames = window::getWindowLength();
// True if `names` is usable as is, but FF is bugged: its names have duplicates
let ok = !IS_FIREFOX;
for (const key of names) {
if (+key >= 0 && key < numFrames
|| isContentMode && (
key === process.env.INIT_FUNC_NAME || key === 'browser' || key === 'chrome'
)
) {
ok = false;
} else {
globalKeysSet.set(key, 1);
}
}
/* Chrome and FF page mode: `global` is `window`
FF content mode: `global` is different, some props e.g. `isFinite` are defined only there */
if (global !== window) {
builtinGlobals[1]::forEach(key => {
if (!(+key >= 0 && key < numFrames)) { // keep the `!` inversion to avoid safe-guarding isNaN
globalKeysSet.set(key, -1);
ok = false;
}
});
}
// wrappedJSObject is not included in getOwnPropertyNames so we add it explicitly.
if (IS_FIREFOX
&& !PAGE_MODE_HANDSHAKE
&& kWrappedJSObject in global
&& !globalKeysSet.get(kWrappedJSObject)) {
globalKeysSet.set(kWrappedJSObject, 1);
if (ok) setOwnProp(names, names.length, kWrappedJSObject);
}
return ok ? names : globalKeysSet.toArray();
}());
const inheritedKeys = createNullObj();
const globalDesc = createNullObj();
const updateGlobalDesc = name => {
let src;
let desc;
let fn;
if ((src = inheritedKeys[name])
|| (src = globalKeysSet.get(name)) && (src = src > 0 ? window : global)) {
if ((desc = describeProperty(src, name))) {
desc = nullObjFrom(desc);
/* ~45 enumerable action functions belong to `window` and need to be bound to it,
* the non-enum ~10 can be unbound, and `eval` MUST be unbound to run in scope. */
if (name >= 'a' && desc.enumerable && isFunction(fn = desc.value)) {
// TODO: switch to SafeProxy and preserve thisArg when it's not our wrapper or its cache?
fn = safeBind(fn, src === global ? global : window);
desc.value = defineProperty(fn, 'name', { __proto__: null, value: name });
}
// Using `!` to avoid the need to use and safe-guard isNaN
if (!(+name >= 0 && name < window::getWindowLength())) {
globalDesc[name] = desc;
}
return desc;
}
}
};
[SafeEventTarget, Object]::forEach(src => {
reflectOwnKeys(src = src[PROTO])::forEach(key => {
inheritedKeys[key] = src;
});
});
builtinGlobals = null; // eslint-disable-line no-global-assign
/**
* @desc Wrap helpers to prevent unexpected modifications.
*/
export function makeGlobalWrapper(local) {
let globals = globalKeysSet; // will be copied only if modified
/* Browsers may return [object Object] for Object.prototype.toString(window)
on our `window` proxy so jQuery libs see it as a plain object and throw
when trying to clone its recursive properties like `self` and `window`. */
setOwnProp(local, toStringTagSym, () => 'Window', false, 'get');
const events = createNullObj();
const wrapper = new SafeProxy(local, {
__proto__: null,
defineProperty(_, name, desc) {
if (name in local
|| !(_ = globalDesc[name] || updateGlobalDesc(name))
|| _.configurable) {
/* It's up to caller to protect proto */// eslint-disable-next-line no-restricted-syntax
return defineProperty(local, name, desc);
}
},
deleteProperty(_, name) {
if ((_ = delete local[name])
&& (_ = globalDesc[name] || updateGlobalDesc(name))
&& (_ = _.configurable)) {
if (globals === globalKeysSet) {
globals = globalKeysSet.clone();
}
globals.delete(name);
}
return !!_;
},
get: (_, name) => {
if (name === 'undefined' || name === scopeSym) return;
if ((_ = local[name]) !== undefined || name in local) return _;
return proxyDescribe(local, name, wrapper, events) && local[name];
},
getOwnPropertyDescriptor: (_, name) => describeProperty(local, name)
|| proxyDescribe(local, name, wrapper, events),
has: (_, name) => name in globalDesc || name in local || updateGlobalDesc(name),
ownKeys: () => makeOwnKeys(local, globals),
preventExtensions() {},
set(_, name, value) {
if (!(name in local)) proxyDescribe(local, name, wrapper, events);
local[name] = value;
return true;
},
});
return wrapper;
}
function makeOwnKeys(local, globals) {
/** Note that arrays can be eavesdropped via prototype setters like '0','1',...
* on `push` and `arr[i] = 123`, as well as via getters if you read beyond
* its length or from an unassigned `hole`. */
const frameIndexes = [];
const len = window::getWindowLength();
for (let i = 0, str; i < len && getOwnProp(window, str = `${i}`); i += 1) {
if (!(str in local)) safePush(frameIndexes, str);
}
return safeConcat(
frameIndexes,
globals === globalKeysSet ? globalKeys : globals.toArray(),
reflectOwnKeys(local)::filter(notIncludedIn, globals.get),
);
}
function proxyDescribe(local, name, wrapper, events) {
let desc = globalDesc[name] || updateGlobalDesc(name);
if (!desc) return;
const { get, set, value } = desc;
const isWindow = value === window
|| name === 'window'
|| name === 'self'
|| name === 'globalThis'
|| name === 'top' && window === top // `top` is unforgeable
|| name === 'parent' && window === window::getWindowParent();
if (isWindow) {
desc.value = wrapper;
delete desc.get;
delete desc.set;
} else if (get && set && typeof name === 'string'
// Spoofed String index getters won't be called within length, length itself is unforgeable
&& name.length >= 3 && name[0] === 'o' && name[1] === 'n'
) {
setWindowEvent(desc, name, events, wrapper);
} else {
if (get) desc.get = safeBind(get, window);
if (set) desc.set = safeBind(set, window);
}
defineProperty(local, name, desc); /* proto is null */// eslint-disable-line no-restricted-syntax
return desc;
}
function setWindowEvent(desc, name, events, wrapper) {
name = name::slice(2);
desc.get = () => events[name] || null;
desc.set = fn => {
window::off(name, events[name]);
if (isFunction(fn)) {
// the handler will be unique so that one script couldn't remove something global
// like console.log set by another script
window::on(name, events[name] = (
// FF chokes on safeBind because the result belongs to Vault's window
IS_FIREFOX && PAGE_MODE_HANDSHAKE
? evt => wrapper::fn(evt)
: safeBind(fn, wrapper)
));
} else {
delete events[name];
}
};
}
/** @this {FastLookup.get} */
function notIncludedIn(key) {
return !this(key);
}