Greasemonkey: isolates scripts' writes to window
Since the global namespace of javascript in the browser is accessible there where issues with scripts clobbering things that the page expected to be attributes of window pages clobbering things that userscripts saved to `window`. The latter was occuring with OneeChan. OneeChan was setting `window.$` to a function that took a css selector and the 4chan catalog script was setting `window.$` to some object. Since OneeChan was set to run at document-start this broke OneeChan, switching it to document-end broke scripts on 4chan. I used OneeChan and 4chan-X on 4chan as the test case for this and TamperMonkey as a guide for what is the correct way to handle scoping. I didn't manage to pick apart just how TamperMonkey does what it does (I think it might just be the environment that Chrome provides extensions actually) but I got close to the same behaviour. TamperMonkey provides a `window` object that appears to be what the global window looked like before the webpage modified it. The global scope though does have the pages modifications accessible. If the script assigns something to an attribute `window` it can see that attribute in the global scope. This implementation differs from that one in that, to the scipt, `window` and the global scope always look the same, and that is the same as the global scope looks in the environment provided by TamperMonkey. I am using the ES6 `Proxy` feature to allow the `window` object to look like the actual (unsafe) one while allowing writing to it that doesn't clobber the unsafe one. I am then using the ES4 `with` function to make attributes of that window (proxy) object visible in the scope chain. There may be other ways to do this without using `with` by using nested functions and setting `this` creatively. There are notes around alleging `with` to be various states of uncool[1]. I also ran into an issue where a userscript calling `window.addEventListener(...)` would fail with `TypeError: Illegal Execution` which is apparently due to `this` not being set correctly. I looked at the functions which threw that error and those that didn't and am using whether they have a `prototype` attribute or not to tell whether I need to bind them with `window` as `this`. I am not sure how correct that is but it worked for all the cases I ran into. [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with
This commit is contained in:
parent
046c0a7ea2
commit
ab50ad735b
@ -145,9 +145,53 @@
|
||||
}
|
||||
};
|
||||
|
||||
/* TamperMonkey allows assinging to `window` to change the visible global
|
||||
* scope, without breaking other scripts. Eg if the page has set
|
||||
* window.$ for an object for some reason (hello 4chan)
|
||||
* - typeof $ === 'object'
|
||||
* - typeof window.$ == 'undefined'
|
||||
* - window.$ = function() {}
|
||||
* - typeof $ === 'function'
|
||||
* - typeof window.$ == 'function'
|
||||
* Just shadowing `window` won't work because if you try to use '$'
|
||||
* from the global scope you will still get the pages one.
|
||||
* Additionally the userscript expects `window` to actually look
|
||||
* like a fully featured browser window object.
|
||||
*
|
||||
* So let's try to use a Proxy on window and the possibly deprecated
|
||||
* `with` function to make that proxy shadow the global scope.
|
||||
* unsafeWindow should still be the actual global page window.
|
||||
*/
|
||||
const unsafeWindow = window;
|
||||
let myWindow = {};
|
||||
var windowProxyHandler = {
|
||||
get: function(obj, prop) {
|
||||
if (prop in myWindow)
|
||||
return myWindow[prop];
|
||||
if (prop in obj) {
|
||||
if (typeof obj[prop] === 'function' && typeof obj[prop].prototype == 'undefined')
|
||||
// Getting TypeError: Illegal Execution when callers try to execute
|
||||
// eg addEventListener from here because they were returned
|
||||
// unbound
|
||||
return obj[prop].bind(obj);
|
||||
return obj[prop];
|
||||
}
|
||||
},
|
||||
set: function(target, prop, val) {
|
||||
return myWindow[prop] = val;
|
||||
}
|
||||
};
|
||||
var myProxy = new Proxy(unsafeWindow, windowProxyHandler);
|
||||
|
||||
// ====== The actual user script source ====== //
|
||||
with (myProxy) {
|
||||
// can't assign window directly in with() scope because proxy doesn't
|
||||
// allow assinging to things that a readonly on the target.
|
||||
function blarg() { // why can't this be anonymous?
|
||||
var window = myProxy;
|
||||
{{ scriptSource }}
|
||||
};
|
||||
blarg();
|
||||
};
|
||||
// ====== End User Script ====== //
|
||||
})();
|
||||
|
Loading…
Reference in New Issue
Block a user