I'm trying to trap calls to Storage. As far as I can tell there are two ways to call either setItem or getItem:
sessionStorage.setItem("foo", "bar");
let item = sessionStorage.getItem("foo");
Storage.prototype.setItem.call(sessionStorage, "foo", "bar");
let item2 = Storage.prototype.getItem.call(sessionStorage, "foo");
Using sessionStorage in Code Snippet throws a security error, so here's the code in JS Fiddle
TLDR: I can handle all cases if I do this, but it seems hacky. Is there a better/cleaner way to accomplish my goal? (NOTE: I don't have control over caller, this is why I'm covering both cases):
try {
console.log("\n\n");
let ss = new Proxy(sessionStorage, {
get: function (getTarget, p) {
if (p === "__this") {
// kinda hacky, but allows us to unwrap Proxy for binding
return getTarget;
}
console.log("sessionStorage.get proxy called")
return new Proxy(Reflect.get(getTarget, p), {
apply(applyTarget, thisArg, argArray) {
console.log("sessionStorage.get.apply called");
Reflect.apply(applyTarget, getTarget, argArray);
}
})
},
});
SP = new Proxy(Object.create(Storage.prototype), {
get: function (getTarget, p) {
console.log("Storage.get proxy called")
return new Proxy(Reflect.get(getTarget, p), {
apply(applyTarget, thisArg, argArray) {
console.log("Storage.get.apply called");
try {
return Reflect.apply(applyTarget, thisArg, argArray);
} catch (e) {
// unpack proxy if we're double-wrapped (both target and thisArg are Proxy)
return Reflect.apply(applyTarget, thisArg.__this, argArray);
}
}
})
},
});
SPW = {};
Object.defineProperty(SPW, 'prototype', {
value: SP,
configurable: false,
});
si = SPW.prototype.setItem;
gi = SPW.prototype.getItem;
si.call(ss, "foo", "3");
console.log(`Storage.prototype.getItem: ${gi.call(ss, "foo")}`);
console.log(`Storage.prototype Worked`)
} catch (e) {
console.log(`Storage.prototype Caught ${e.stack}`);
}
Result:
Storage.get proxy called
Storage.get proxy called
Storage.get.apply called
Storage.get.apply called
Storage.prototype.getItem: 3
Storage.prototype Worked
I don't see any other way than including a "hidden" __this property so that the caller can "unwrap" the Proxy object and get the reference to the original sessionStorage. Is there a better way to do this?
Background
If it helps here are examples of only wrapping either sessionStorage or Storage, but not both:
Wrap sessionStorage
For the apply trap, I have to pass getTarget instead of thisArg because the latter is Proxy object and if I pass it an Illegal Invocation error is thrown.
try {
let ss = new Proxy(sessionStorage, {
get: function (getTarget, p) {
console.log("sessionStorage.get called")
return new Proxy(Reflect.get(getTarget, p), {
apply(applyTarget, thisArg, argArray) {
console.log("sessionStorage.get.apply called");
Reflect.apply(applyTarget, getTarget, argArray);
}
})
},
});
ss.setItem("foo", "1");
console.log(`Proxy.sessionStorage.getItem: ${ss.getItem("foo")}`);
console.log(`Proxy.sessionStorage Worked`)
} catch (e) {
console.log(`Proxy.sessionStorage Caught ${e.stack}`);
}
Result:
sessionStorage.get called
sessionStorage.get.apply called
sessionStorage.get called
sessionStorage.get.apply called
sessionStorage.get called
sessionStorage.get.apply called
Proxy.sessionStorage.getItem: undefined
Proxy.sessionStorage Worked
Wrap Storage.prototype
Here I have to first create a new object with a separate prototype because the prototype property descriptor for Storage has configurable set to false. Once I've done that I essentially have to do the same thing as the previous case by passing the "unwrapped" getTarget instead of the Proxy instance pointed to by thisArg.
try {
console.log("\n\n");
SP = new Proxy(Object.create(Storage.prototype), {
get: function (getTarget, p) {
console.log("Storage.get proxy called")
return new Proxy(Reflect.get(getTarget, p), {
apply(applyTarget, thisArg, argArray) {
console.log("Storage.get.apply called");
try {
return Reflect.apply(applyTarget, thisArg, argArray);
} catch (e) {
console.log("apply failed when passing thisArg");
return Reflect.apply(applyTarget, getTarget, argArray);
}
}
})
},
});
SPW = {};
Object.defineProperty(SPW, 'prototype', {
value: SP,
configurable: false,
});
si = SPW.prototype.setItem;
gi = SPW.prototype.getItem;
si.call(sessionStorage, "foo", "2");
console.log(`Storage.prototype.getItem: ${gi.call(sessionStorage, "foo")}`);
console.log(`Storage.prototype Worked`)
} catch (e) {
console.log(`Storage.prototype Caught ${e.stack}`);
}
Result:
Storage.get proxy called
Storage.get proxy called
Storage.get.apply called
Storage.get.apply called
Storage.prototype.getItem: 2
Storage.prototype Worked
I think you're making this far too complicated. There's no reason to involve a proxy here, just monkey-patch the two methods: