Bug 702353 - Implement Proxy-Based XPCOM Wrappers in SpecialPowers. r=mrbkap,ted

This commit is contained in:
Bobby Holley 2012-01-18 19:10:04 -08:00
parent 91a2b92f7f
commit 2e8f439fa1
2 changed files with 331 additions and 0 deletions

View File

@ -132,8 +132,295 @@ Observer.prototype = {
},
};
function isWrappable(x) {
if (typeof x === "object")
return x !== null;
return typeof x === "function";
};
function isWrapper(x) {
return isWrappable(x) && (typeof x.SpecialPowers_wrappedObject !== "undefined");
};
function unwrapIfWrapped(x) {
return isWrapper(x) ? unwrapPrivileged(x) : x;
};
function isXrayWrapper(x) {
return /XrayWrapper/.exec(x.toString());
}
// We can't call apply() directy on Xray-wrapped functions, so we have to be
// clever.
function doApply(fun, invocant, args) {
return Function.prototype.apply.call(fun, invocant, args);
}
function wrapPrivileged(obj) {
// Primitives pass straight through.
if (!isWrappable(obj))
return obj;
// No double wrapping.
if (isWrapper(obj))
throw "Trying to double-wrap object!";
// Make our core wrapper object.
var handler = new SpecialPowersHandler(obj);
// If the object is callable, make a function proxy.
if (typeof obj === "function") {
var callTrap = function() {
// The invocant and arguments may or may not be wrappers. Unwrap them if necessary.
var invocant = unwrapIfWrapped(this);
var unwrappedArgs = Array.prototype.slice.call(arguments).map(unwrapIfWrapped);
return wrapPrivileged(doApply(obj, invocant, unwrappedArgs));
};
var constructTrap = function() {
// The arguments may or may not be wrappers. Unwrap them if necessary.
var unwrappedArgs = Array.prototype.slice.call(arguments).map(unwrapIfWrapped);
// Constructors are tricky, because we can't easily call apply on them.
// As a workaround, we create a wrapper constructor with the same
// |prototype| property.
var FakeConstructor = function() {
doApply(obj, this, unwrappedArgs);
};
FakeConstructor.prototype = obj.prototype;
return wrapPrivileged(new FakeConstructor());
};
return Proxy.createFunction(handler, callTrap, constructTrap);
}
// Otherwise, just make a regular object proxy.
return Proxy.create(handler);
};
function unwrapPrivileged(x) {
// We don't wrap primitives, so sometimes we have a primitive where we'd
// expect to have a wrapper. The proxy pretends to be the type that it's
// emulating, so we can just as easily check isWrappable() on a proxy as
// we can on an unwrapped object.
if (!isWrappable(x))
return x;
// If we have a wrappable type, make sure it's wrapped.
if (!isWrapper(x))
throw "Trying to unwrap a non-wrapped object!";
// Unwrap.
return x.SpecialPowers_wrappedObject;
};
function crawlProtoChain(obj, fn) {
var rv = fn(obj);
if (rv !== undefined)
return rv;
if (Object.getPrototypeOf(obj))
return crawlProtoChain(Object.getPrototypeOf(obj), fn);
};
function SpecialPowersHandler(obj) {
this.wrappedObject = obj;
};
// Allow us to transitively maintain the membrane by wrapping descriptors
// we return.
SpecialPowersHandler.prototype.doGetPropertyDescriptor = function(name, own) {
// Handle our special API.
if (name == "SpecialPowers_wrappedObject")
return { value: this.wrappedObject, writeable: false, configurable: false, enumerable: false };
// In general, we want Xray wrappers for content DOM objects, because waiving
// Xray gives us Xray waiver wrappers that clamp the principal when we cross
// compartment boundaries. However, Xray adds some gunk to toString(), which
// has the potential to confuse consumers that aren't expecting Xray wrappers.
// Since toString() is a non-privileged method that returns only strings, we
// can just waive Xray for that case.
var obj = name == 'toString' ? XPCNativeWrapper.unwrap(this.wrappedObject)
: this.wrappedObject;
//
// Call through to the wrapped object.
//
// Note that we have several cases here, each of which requires special handling.
//
var desc;
// Case 1: Own Properties.
//
// This one is easy, thanks to Object.getOwnPropertyDescriptor().
if (own)
desc = Object.getOwnPropertyDescriptor(obj, name);
// Case 2: Not own, not Xray-wrapped.
//
// Here, we can just crawl the prototype chain, calling
// Object.getOwnPropertyDescriptor until we find what we want.
//
// NB: Make sure to check this.wrappedObject here, rather than obj, because
// we may have waived Xray on obj above.
else if (!isXrayWrapper(this.wrappedObject))
desc = crawlProtoChain(obj, function(o) {return Object.getOwnPropertyDescriptor(o, name);});
// Case 3: Not own, Xray-wrapped.
//
// This one is harder, because we Xray wrappers are flattened and don't have
// a prototype. Xray wrappers are proxies themselves, so we'd love to just call
// through to XrayWrapper<Base>::getPropertyDescriptor(). Unfortunately though,
// we don't have any way to do that. :-(
//
// So we first try with a call to getOwnPropertyDescriptor(). If that fails,
// we make up a descriptor, using some assumptions about what kinds of things
// tend to live on the prototypes of Xray-wrapped objects.
else {
desc = Object.getOwnPropertyDescriptor(obj, name);
if (!desc) {
var getter = Object.prototype.__lookupGetter__.call(obj, name);
var setter = Object.prototype.__lookupSetter__.call(obj, name);
if (getter || setter)
desc = {get: getter, set: setter, configurable: true, enumerable: true};
else if (name in obj)
desc = {value: obj[name], writable: false, configurable: true, enumerable: true};
}
}
// Bail if we've got nothing.
if (typeof desc === 'undefined')
return undefined;
// When accessors are implemented as JSPropertyOps rather than JSNatives (ie,
// QuickStubs), the js engine does the wrong thing and treats it as a value
// descriptor rather than an accessor descriptor. Jorendorff suggested this
// little hack to work around it. See bug 520882.
if (desc && 'value' in desc && desc.value === undefined)
desc.value = obj[name];
// A trapping proxy's properties must always be configurable, but sometimes
// this we get non-configurable properties from Object.getOwnPropertyDescriptor().
// Tell a white lie.
desc.configurable = true;
// Transitively maintain the wrapper membrane.
function wrapIfExists(key) { if (key in desc) desc[key] = wrapPrivileged(desc[key]); };
wrapIfExists('value');
wrapIfExists('get');
wrapIfExists('set');
return desc;
};
SpecialPowersHandler.prototype.getOwnPropertyDescriptor = function(name) {
return this.doGetPropertyDescriptor(name, true);
};
SpecialPowersHandler.prototype.getPropertyDescriptor = function(name) {
return this.doGetPropertyDescriptor(name, false);
};
function doGetOwnPropertyNames(obj, props) {
// Insert our special API. It's not enumerable, but getPropertyNames()
// includes non-enumerable properties.
var specialAPI = 'SpecialPowers_wrappedObject';
if (props.indexOf(specialAPI) == -1)
props.push(specialAPI);
// Do the normal thing.
var flt = function(a) { return props.indexOf(a) == -1; };
props = props.concat(Object.getOwnPropertyNames(obj).filter(flt));
// If we've got an Xray wrapper, include the expandos as well.
if ('wrappedJSObject' in obj)
props = props.concat(Object.getOwnPropertyNames(obj.wrappedJSObject)
.filter(flt));
return props;
}
SpecialPowersHandler.prototype.getOwnPropertyNames = function() {
return doGetOwnPropertyNames(this.wrappedObject, []);
};
SpecialPowersHandler.prototype.getPropertyNames = function() {
// Manually walk the prototype chain, making sure to add only property names
// that haven't been overridden.
//
// There's some trickiness here with Xray wrappers. Xray wrappers don't have
// a prototype, so we need to unwrap them if we want to get all of the names
// with Object.getOwnPropertyNames(). But we don't really want to unwrap the
// base object, because that will include expandos that are inaccessible via
// our implementation of get{,Own}PropertyDescriptor(). So we unwrap just
// before accessing the prototype. This ensures that we get Xray vision on
// the base object, and no Xray vision for the rest of the way up.
var obj = this.wrappedObject;
var props = [];
while (obj) {
props = doGetOwnPropertyNames(obj, props);
obj = Object.getPrototypeOf(XPCNativeWrapper.unwrap(obj));
}
return props;
};
SpecialPowersHandler.prototype.defineProperty = function(name, desc) {
return Object.defineProperty(this.wrappedObject, name, desc);
};
SpecialPowersHandler.prototype.delete = function(name) {
return delete this.wrappedObject[name];
};
SpecialPowersHandler.prototype.fix = function() { return undefined; /* Throws a TypeError. */ };
// Per the ES5 spec this is a derived trap, but it's fundamental in spidermonkey
// for some reason. See bug 665198.
SpecialPowersHandler.prototype.enumerate = function() {
var t = this;
var filt = function(name) { return t.getPropertyDescriptor(name).enumerable; };
return this.getPropertyNames().filter(filt);
};
SpecialPowersAPI.prototype = {
/*
* Privileged object wrapping API
*
* Usage:
* var wrapper = SpecialPowers.wrap(obj);
* wrapper.privilegedMethod(); wrapper.privilegedProperty;
* obj === SpecialPowers.unwrap(wrapper);
*
* These functions provide transparent access to privileged objects using
* various pieces of deep SpiderMagic. Conceptually, a wrapper is just an
* object containing a reference to the underlying object, where all method
* calls and property accesses are transparently performed with the System
* Principal. Moreover, objects obtained from the wrapper (including properties
* and method return values) are wrapped automatically. Thus, after a single
* call to SpecialPowers.wrap(), the wrapper layer is transitively maintained.
*
* Known Issues:
*
* - The wrapping function does not preserve identity, so
* SpecialPowers.wrap(foo) !== SpecialPowers.wrap(foo). See bug 718543.
*
* - The wrapper cannot see expando properties on unprivileged DOM objects.
* That is to say, the wrapper uses Xray delegation.
*
* - The wrapper sometimes guesses certain ES5 attributes for returned
* properties. This is explained in a comment in the wrapper code above,
* and shouldn't be a problem.
*/
wrap: wrapPrivileged,
unwrap: unwrapPrivileged,
get MockFilePicker() {
return MockFilePicker
},

View File

@ -90,6 +90,50 @@ function starttest(){
//try to run garbage collection
SpecialPowers.gc();
//
// Test the SpecialPowers wrapper.
//
// Try some basic stuff with XHR.
var xhr2 = SpecialPowers.wrap(Components).classes["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Components.interfaces.nsIXMLHttpRequest);
is(xhr2.toString(), SpecialPowers.unwrap(xhr2).toString(), "toString should be transparently delegated");
is(xhr.readyState, XMLHttpRequest.UNSENT, "Should be able to get props off privileged objects");
var testURI = SpecialPowers.wrap(Components).classes['@mozilla.org/network/standard-url;1']
.createInstance(Components.interfaces.nsIURI);
testURI.spec = "http://www.foobar.org/";
is(testURI.spec, "http://www.foobar.org/", "Getters/Setters should work correctly");
is(SpecialPowers.wrap(document).getElementsByTagName('details').length, 0, "Should work with proxy-based DOM bindings.");
// Play with the window object.
var webnav = SpecialPowers.wrap(window).QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIWebNavigation);
webnav.QueryInterface(Components.interfaces.nsIDocShell);
ok(webnav.allowJavascript, "Able to pull properties off of docshell!");
// Make sure Xray-wrapped functions work.
try {
SpecialPowers.wrap(Components).ID('{00000000-0000-0000-0000-000000000000}');
ok(true, "Didn't throw");
}
catch (e) {
ok(false, "Threw while trying to call Xray-wrapped function.");
}
// Check functions that return null.
var returnsNull = function() { return null; }
is(SpecialPowers.wrap(returnsNull)(), null, "Should be able to handle functions that return null.");
// Play around with a JS object to check the non-xray path.
var noxray_proto = {a: 3, b: 12};
var noxray = {a: 5, c: 32};
noxray.__proto__ = noxray_proto;
var noxray_wrapper = SpecialPowers.wrap(noxray);
is(noxray_wrapper.c, 32, "Regular properties should work.");
is(noxray_wrapper.a, 5, "Shadow properties should work.");
is(noxray_wrapper.b, 12, "Proto properties should work.");
noxray.b = 122;
is(noxray_wrapper.b, 122, "Should be able to shadow.");
SimpleTest.info("\nProfile::SpecialPowersRunTime: " + (new Date() - startTime) + "\n");
SimpleTest.finish();
}