michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: const hiddenFrames = require("sdk/frame/hidden-frame");
michael@0: const { create: makeFrame } = require("sdk/frame/utils");
michael@0: const { window } = require("sdk/addon/window");
michael@0: const { Loader } = require('sdk/test/loader');
michael@0: const { URL } = require("sdk/url");
michael@0: const testURI = require("./fixtures").url("test.html");
michael@0: const testHost = URL(testURI).scheme + '://' + URL(testURI).host;
michael@0:
michael@0: /*
michael@0: * Utility function that allow to easily run a proxy test with a clean
michael@0: * new HTML document. See first unit test for usage.
michael@0: */
michael@0: function createProxyTest(html, callback) {
michael@0: return function (assert, done) {
michael@0: let url = 'data:text/html;charset=utf-8,' + encodeURIComponent(html);
michael@0: let principalLoaded = false;
michael@0:
michael@0: let element = makeFrame(window.document, {
michael@0: nodeName: "iframe",
michael@0: type: "content",
michael@0: allowJavascript: true,
michael@0: allowPlugins: true,
michael@0: allowAuth: true,
michael@0: uri: testURI
michael@0: });
michael@0:
michael@0: element.addEventListener("DOMContentLoaded", onDOMReady, false);
michael@0:
michael@0: function onDOMReady() {
michael@0: // Reload frame after getting principal from `testURI`
michael@0: if (!principalLoaded) {
michael@0: element.setAttribute("src", url);
michael@0: principalLoaded = true;
michael@0: return;
michael@0: }
michael@0:
michael@0: assert.equal(element.getAttribute("src"), url, "correct URL loaded");
michael@0: element.removeEventListener("DOMContentLoaded", onDOMReady,
michael@0: false);
michael@0: let xrayWindow = element.contentWindow;
michael@0: let rawWindow = xrayWindow.wrappedJSObject;
michael@0:
michael@0: let isDone = false;
michael@0: let helper = {
michael@0: xrayWindow: xrayWindow,
michael@0: rawWindow: rawWindow,
michael@0: createWorker: function (contentScript) {
michael@0: return createWorker(assert, xrayWindow, contentScript, helper.done);
michael@0: },
michael@0: done: function () {
michael@0: if (isDone)
michael@0: return;
michael@0: isDone = true;
michael@0: element.parentNode.removeChild(element);
michael@0: done();
michael@0: }
michael@0: };
michael@0: callback(helper, assert);
michael@0: }
michael@0: };
michael@0: }
michael@0:
michael@0: function createWorker(assert, xrayWindow, contentScript, done) {
michael@0: let loader = Loader(module);
michael@0: let Worker = loader.require("sdk/content/worker").Worker;
michael@0: let worker = Worker({
michael@0: window: xrayWindow,
michael@0: contentScript: [
michael@0: 'new ' + function () {
michael@0: assert = function assert(v, msg) {
michael@0: self.port.emit("assert", {assertion:v, msg:msg});
michael@0: }
michael@0: done = function done() {
michael@0: self.port.emit("done");
michael@0: }
michael@0: },
michael@0: contentScript
michael@0: ]
michael@0: });
michael@0:
michael@0: worker.port.on("done", done);
michael@0: worker.port.on("assert", function (data) {
michael@0: assert.ok(data.assertion, data.msg);
michael@0: });
michael@0:
michael@0: return worker;
michael@0: }
michael@0:
michael@0: /* Examples for the `createProxyTest` uses */
michael@0:
michael@0: let html = "";
michael@0:
michael@0: exports["test Create Proxy Test"] = createProxyTest(html, function (helper, assert) {
michael@0: // You can get access to regular `test` object in second argument of
michael@0: // `createProxyTest` method:
michael@0: assert.ok(helper.rawWindow.documentGlobal,
michael@0: "You have access to a raw window reference via `helper.rawWindow`");
michael@0: assert.ok(!("documentGlobal" in helper.xrayWindow),
michael@0: "You have access to an XrayWrapper reference via `helper.xrayWindow`");
michael@0:
michael@0: // If you do not create a Worker, you have to call helper.done(),
michael@0: // in order to say when your test is finished
michael@0: helper.done();
michael@0: });
michael@0:
michael@0: exports["test Create Proxy Test With Worker"] = createProxyTest("", function (helper) {
michael@0:
michael@0: helper.createWorker(
michael@0: "new " + function WorkerScope() {
michael@0: assert(true, "You can do assertions in your content script");
michael@0: // And if you create a worker, you either have to call `done`
michael@0: // from content script or helper.done()
michael@0: done();
michael@0: }
michael@0: );
michael@0:
michael@0: });
michael@0:
michael@0: exports["test Create Proxy Test With Events"] = createProxyTest("", function (helper, assert) {
michael@0:
michael@0: let worker = helper.createWorker(
michael@0: "new " + function WorkerScope() {
michael@0: self.port.emit("foo");
michael@0: }
michael@0: );
michael@0:
michael@0: worker.port.on("foo", function () {
michael@0: assert.pass("You can use events");
michael@0: // And terminate your test with helper.done:
michael@0: helper.done();
michael@0: });
michael@0:
michael@0: });
michael@0:
michael@0: // Bug 714778: There was some issue around `toString` functions
michael@0: // that ended up being shared between content scripts
michael@0: exports["test Shared To String Proxies"] = createProxyTest("", function(helper) {
michael@0:
michael@0: let worker = helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: // We ensure that `toString` can't be modified so that nothing could
michael@0: // leak to/from the document and between content scripts
michael@0: // It only applies to JS proxies, there isn't any such issue with xrays.
michael@0: //document.location.toString = function foo() {};
michael@0: document.location.toString.foo = "bar";
michael@0: assert("foo" in document.location.toString, "document.location.toString can be modified");
michael@0: assert(document.location.toString() == "data:text/html;charset=utf-8,",
michael@0: "First document.location.toString()");
michael@0: self.postMessage("next");
michael@0: }
michael@0: );
michael@0: worker.on("message", function () {
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope2() {
michael@0: assert(!("foo" in document.location.toString),
michael@0: "document.location.toString is different for each content script");
michael@0: assert(document.location.toString() == "data:text/html;charset=utf-8,",
michael@0: "Second document.location.toString()");
michael@0: done();
michael@0: }
michael@0: );
michael@0: });
michael@0: });
michael@0:
michael@0:
michael@0: // Ensure that postMessage is working correctly across documents with an iframe
michael@0: let html = '';
michael@0: exports["test postMessage"] = createProxyTest(html, function (helper, assert) {
michael@0: let ifWindow = helper.xrayWindow.document.getElementById("iframe").contentWindow;
michael@0: // Listen without proxies, to check that it will work in regular case
michael@0: // simulate listening from a web document.
michael@0: ifWindow.addEventListener("message", function listener(event) {
michael@0: ifWindow.removeEventListener("message", listener, false);
michael@0: // As we are in system principal, event is an XrayWrapper
michael@0: // xrays use current compartments when calling postMessage method.
michael@0: // Whereas js proxies was using postMessage method compartment,
michael@0: // not the caller one.
michael@0: assert.strictEqual(event.source, helper.xrayWindow,
michael@0: "event.source is the top window");
michael@0: assert.equal(event.origin, testHost, "origin matches testHost");
michael@0:
michael@0: assert.equal(event.data, "{\"foo\":\"bar\\n \\\"escaped\\\".\"}",
michael@0: "message data is correct");
michael@0:
michael@0: helper.done();
michael@0: }, false);
michael@0:
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: var json = JSON.stringify({foo : "bar\n \"escaped\"."});
michael@0:
michael@0: document.getElementById("iframe").contentWindow.postMessage(json, "*");
michael@0: }
michael@0: );
michael@0: });
michael@0:
michael@0: let html = '';
michael@0: exports["test Object Listener"] = createProxyTest(html, function (helper) {
michael@0:
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: // Test objects being given as event listener
michael@0: let input = document.getElementById("input2");
michael@0: let myClickListener = {
michael@0: called: false,
michael@0: handleEvent: function(event) {
michael@0: assert(this === myClickListener, "`this` is the original object");
michael@0: assert(!this.called, "called only once");
michael@0: this.called = true;
michael@0: assert(event.target, input, "event.target is the wrapped window");
michael@0: done();
michael@0: }
michael@0: };
michael@0:
michael@0: window.addEventListener("click", myClickListener, true);
michael@0: input.click();
michael@0: window.removeEventListener("click", myClickListener, true);
michael@0: }
michael@0: );
michael@0:
michael@0: });
michael@0:
michael@0: exports["test Object Listener 2"] = createProxyTest("", function (helper) {
michael@0:
michael@0: helper.createWorker(
michael@0: ('new ' + function ContentScriptScope() {
michael@0: // variable replaced with `testHost`
michael@0: let testHost = "TOKEN";
michael@0: // Verify object as DOM event listener
michael@0: let myMessageListener = {
michael@0: called: false,
michael@0: handleEvent: function(event) {
michael@0: window.removeEventListener("message", myMessageListener, true);
michael@0:
michael@0: assert(this == myMessageListener, "`this` is the original object");
michael@0: assert(!this.called, "called only once");
michael@0: this.called = true;
michael@0: assert(event.target == document.defaultView, "event.target is the wrapped window");
michael@0: assert(event.source == document.defaultView, "event.source is the wrapped window");
michael@0: assert(event.origin == testHost, "origin matches testHost");
michael@0: assert(event.data == "ok", "message data is correct");
michael@0: done();
michael@0: }
michael@0: };
michael@0:
michael@0: window.addEventListener("message", myMessageListener, true);
michael@0: document.defaultView.postMessage("ok", '*');
michael@0: }
michael@0: ).replace("TOKEN", testHost));
michael@0:
michael@0: });
michael@0:
michael@0: let html = '' +
michael@0: '';
michael@0:
michael@0: exports.testStringOverload = createProxyTest(html, function (helper, assert) {
michael@0: // Proxy - toString error
michael@0: let originalString = "string";
michael@0: let p = Proxy.create({
michael@0: get: function(receiver, name) {
michael@0: if (name == "binded")
michael@0: return originalString.toString.bind(originalString);
michael@0: return originalString[name];
michael@0: }
michael@0: });
michael@0: assert.throws(function () {
michael@0: p.toString();
michael@0: },
michael@0: /toString method called on incompatible Proxy/,
michael@0: "toString can't be called with this being the proxy");
michael@0: assert.equal(p.binded(), "string", "but it works if we bind this to the original string");
michael@0:
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: // RightJS is hacking around String.prototype, and do similar thing:
michael@0: // Pass `this` from a String prototype method.
michael@0: // It is funny because typeof this == "object"!
michael@0: // So that when we pass `this` to a native method,
michael@0: // our proxy code can fail on another even more crazy thing.
michael@0: // See following test to see what fails around proxies.
michael@0: String.prototype.update = function () {
michael@0: assert(typeof this == "object", "in update, `this` is an object");
michael@0: assert(this.toString() == "input", "in update, `this.toString works");
michael@0: return document.querySelectorAll(this);
michael@0: };
michael@0: assert("input".update().length == 3, "String.prototype overload works");
michael@0: done();
michael@0: }
michael@0: );
michael@0: });
michael@0:
michael@0: exports["test MozMatchedSelector"] = createProxyTest("", function (helper) {
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: // Check mozMatchesSelector XrayWrappers bug:
michael@0: // mozMatchesSelector returns bad results when we are not calling it from the node itself
michael@0: // SEE BUG 658909: mozMatchesSelector returns incorrect results with XrayWrappers
michael@0: assert(document.createElement( "div" ).mozMatchesSelector("div"),
michael@0: "mozMatchesSelector works while being called from the node");
michael@0: assert(document.documentElement.mozMatchesSelector.call(
michael@0: document.createElement( "div" ),
michael@0: "div"
michael@0: ),
michael@0: "mozMatchesSelector works while being called from a " +
michael@0: "function reference to " +
michael@0: "document.documentElement.mozMatchesSelector.call");
michael@0: done();
michael@0: }
michael@0: );
michael@0: });
michael@0:
michael@0: exports["test Events Overload"] = createProxyTest("", function (helper) {
michael@0:
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: // If we add a "____proxy" attribute on XrayWrappers in order to store
michael@0: // the related proxy to create an unique proxy for each wrapper;
michael@0: // we end up setting this attribute to prototype objects :x
michael@0: // And so, instances created with such prototype will be considered
michael@0: // as equal to the prototype ...
michael@0: // // Internal method that return the proxy for a given XrayWrapper
michael@0: // function proxify(obj) {
michael@0: // if (obj._proxy) return obj._proxy;
michael@0: // return obj._proxy = Proxy.create(...);
michael@0: // }
michael@0: //
michael@0: // // Get a proxy of an XrayWrapper prototype object
michael@0: // let proto = proxify(xpcProto);
michael@0: //
michael@0: // // Use this proxy as a prototype
michael@0: // function Constr() {}
michael@0: // Constr.proto = proto;
michael@0: //
michael@0: // // Try to create an instance using this prototype
michael@0: // let xpcInstance = new Constr();
michael@0: // let wrapper = proxify(xpcInstance)
michael@0: //
michael@0: // xpcProto._proxy = proto and as xpcInstance.__proto__ = xpcProto,
michael@0: // xpcInstance._proxy = proto ... and profixy(xpcInstance) = proto :(
michael@0: //
michael@0: let proto = window.document.createEvent('HTMLEvents').__proto__;
michael@0: window.Event.prototype = proto;
michael@0: let event = document.createEvent('HTMLEvents');
michael@0: assert(event !== proto, "Event should not be equal to its prototype");
michael@0: event.initEvent('dataavailable', true, true);
michael@0: assert(event.type === 'dataavailable', "Events are working fine");
michael@0: done();
michael@0: }
michael@0: );
michael@0:
michael@0: });
michael@0:
michael@0: exports["test Nested Attributes"] = createProxyTest("", function (helper) {
michael@0:
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: // XrayWrappers has a bug when you set an attribute on it,
michael@0: // in some cases, it creates an unnecessary wrapper that introduces
michael@0: // a different object that refers to the same original object
michael@0: // Check that our wrappers don't reproduce this bug
michael@0: // SEE BUG 658560: Fix identity problem with CrossOriginWrappers
michael@0: let o = {sandboxObject:true};
michael@0: window.nested = o;
michael@0: o.foo = true;
michael@0: assert(o === window.nested, "Nested attribute to sandbox object should not be proxified");
michael@0: window.nested = document;
michael@0: assert(window.nested === document, "Nested attribute to proxy should not be double proxified");
michael@0: done();
michael@0: }
michael@0: );
michael@0:
michael@0: });
michael@0:
michael@0: exports["test Form nodeName"] = createProxyTest("", function (helper) {
michael@0:
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: let body = document.body;
michael@0: // Check form[nodeName]
michael@0: let form = document.createElement("form");
michael@0: let input = document.createElement("input");
michael@0: input.setAttribute("name", "test");
michael@0: form.appendChild(input);
michael@0: body.appendChild(form);
michael@0: assert(form.test == input, "form[nodeName] is valid");
michael@0: body.removeChild(form);
michael@0: done();
michael@0: }
michael@0: );
michael@0:
michael@0: });
michael@0:
michael@0: exports["test localStorage"] = createProxyTest("", function (helper, assert) {
michael@0:
michael@0: let worker = helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: // Check localStorage:
michael@0: assert(window.localStorage, "has access to localStorage");
michael@0: window.localStorage.name = 1;
michael@0: assert(window.localStorage.name == 1, "localStorage appears to work");
michael@0:
michael@0: self.port.on("step2", function () {
michael@0: window.localStorage.clear();
michael@0: assert(window.localStorage.name == undefined, "localStorage really, really works");
michael@0: done();
michael@0: });
michael@0: self.port.emit("step1");
michael@0: }
michael@0: );
michael@0:
michael@0: worker.port.on("step1", function () {
michael@0: assert.equal(helper.rawWindow.localStorage.name, 1, "localStorage really works");
michael@0: worker.port.emit("step2");
michael@0: });
michael@0:
michael@0: });
michael@0:
michael@0: exports["test Auto Unwrap Custom Attributes"] = createProxyTest("", function (helper) {
michael@0:
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: let body = document.body;
michael@0: // Setting a custom object to a proxy attribute is not wrapped when we get it afterward
michael@0: let object = {custom: true, enumerable: false};
michael@0: body.customAttribute = object;
michael@0: assert(object === body.customAttribute, "custom JS attributes are not wrapped");
michael@0: done();
michael@0: }
michael@0: );
michael@0:
michael@0: });
michael@0:
michael@0: exports["test Object Tag"] = createProxyTest("", function (helper) {
michael@0:
michael@0: helper.createWorker(
michael@0: 'new ' + function ContentScriptScope() {
michael@0: //