diff --git a/testing/mochitest/tests/SimpleTest/specialpowersAPI.js b/testing/mochitest/tests/SimpleTest/specialpowersAPI.js index 681a49d9015..e9196828d5a 100644 --- a/testing/mochitest/tests/SimpleTest/specialpowersAPI.js +++ b/testing/mochitest/tests/SimpleTest/specialpowersAPI.js @@ -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::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 }, diff --git a/testing/mochitest/tests/test_SpecialPowersExtension.html b/testing/mochitest/tests/test_SpecialPowersExtension.html index c00c5095d1e..fa4147ac49d 100644 --- a/testing/mochitest/tests/test_SpecialPowersExtension.html +++ b/testing/mochitest/tests/test_SpecialPowersExtension.html @@ -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(); }