var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; var Cr = Components.results; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/BrowserUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); const baseURL = "http://mochi.test:8888/browser/" + "toolkit/components/addoncompat/tests/browser/"; var contentSecManager = Cc["@mozilla.org/contentsecuritymanager;1"] .getService(Ci.nsIContentSecurityManager); function forEachWindow(f) { let wins = Services.wm.getEnumerator("navigator:browser"); while (wins.hasMoreElements()) { let win = wins.getNext(); f(win); } } function addLoadListener(target, listener) { target.addEventListener("load", function handler(event) { target.removeEventListener("load", handler, true); return listener(event); }, true); } var gWin; var gBrowser; var ok, is, info; function removeTab(tab, done) { // Remove the tab in a different turn of the event loop. This way // the nested event loop in removeTab doesn't conflict with the // event listener shims. gWin.setTimeout(() => { gBrowser.removeTab(tab); done(); }, 0); } // Make sure that the shims for window.content, browser.contentWindow, // and browser.contentDocument are working. function testContentWindow() { return new Promise(function(resolve, reject) { const url = baseURL + "browser_addonShims_testpage.html"; let tab = gBrowser.addTab(url); gBrowser.selectedTab = tab; let browser = tab.linkedBrowser; addLoadListener(browser, function handler() { ok(gWin.content, "content is defined on chrome window"); ok(browser.contentWindow, "contentWindow is defined"); ok(browser.contentDocument, "contentWindow is defined"); is(gWin.content, browser.contentWindow, "content === contentWindow"); ok(browser.webNavigation.sessionHistory, "sessionHistory is defined"); ok(browser.contentDocument.getElementById("link"), "link present in document"); // FIXME: Waiting on bug 1073631. // is(browser.contentWindow.wrappedJSObject.global, 3, "global available on document"); removeTab(tab, resolve); }); }); } // Test for bug 1060046 and bug 1072607. We want to make sure that // adding and removing listeners works as expected. function testListeners() { return new Promise(function(resolve, reject) { const url1 = baseURL + "browser_addonShims_testpage.html"; const url2 = baseURL + "browser_addonShims_testpage2.html"; let tab = gBrowser.addTab(url2); let browser = tab.linkedBrowser; addLoadListener(browser, function handler() { function dummyHandler() {} // Test that a removed listener stays removed (bug // 1072607). We're looking to make sure that adding and removing // a listener here doesn't cause later listeners to fire more // than once. for (let i = 0; i < 5; i++) { gBrowser.addEventListener("load", dummyHandler, true); gBrowser.removeEventListener("load", dummyHandler, true); } // We also want to make sure that this listener doesn't fire // after it's removed. let loadWithRemoveCount = 0; addLoadListener(browser, function handler1(event) { loadWithRemoveCount++; is(event.target.documentURI, url1, "only fire for first url"); }); // Load url1 and then url2. We want to check that: // 1. handler1 only fires for url1. // 2. handler2 only fires once for url1 (so the second time it // fires should be for url2). let loadCount = 0; browser.addEventListener("load", function handler2(event) { loadCount++; if (loadCount == 1) { is(event.target.documentURI, url1, "first load is for first page loaded"); browser.loadURI(url2); } else { gBrowser.removeEventListener("load", handler2, true); is(event.target.documentURI, url2, "second load is for second page loaded"); is(loadWithRemoveCount, 1, "load handler is only called once"); removeTab(tab, resolve); } }, true); browser.loadURI(url1); }); }); } // Test for bug 1059207. We want to make sure that adding a capturing // listener and a non-capturing listener to the same element works as // expected. function testCapturing() { return new Promise(function(resolve, reject) { let capturingCount = 0; let nonCapturingCount = 0; function capturingHandler(event) { is(capturingCount, 0, "capturing handler called once"); is(nonCapturingCount, 0, "capturing handler called before bubbling handler"); capturingCount++; } function nonCapturingHandler(event) { is(capturingCount, 1, "bubbling handler called after capturing handler"); is(nonCapturingCount, 0, "bubbling handler called once"); nonCapturingCount++; } gBrowser.addEventListener("mousedown", capturingHandler, true); gBrowser.addEventListener("mousedown", nonCapturingHandler, false); const url = baseURL + "browser_addonShims_testpage.html"; let tab = gBrowser.addTab(url); let browser = tab.linkedBrowser; addLoadListener(browser, function handler() { let win = browser.contentWindow; let event = win.document.createEvent("MouseEvents"); event.initMouseEvent("mousedown", true, false, win, 1, 1, 0, 0, 0, // screenX, screenY, clientX, clientY false, false, false, false, // ctrlKey, altKey, shiftKey, metaKey 0, null); // buttonCode, relatedTarget let element = win.document.getElementById("output"); element.dispatchEvent(event); is(capturingCount, 1, "capturing handler fired"); is(nonCapturingCount, 1, "bubbling handler fired"); gBrowser.removeEventListener("mousedown", capturingHandler, true); gBrowser.removeEventListener("mousedown", nonCapturingHandler, false); removeTab(tab, resolve); }); }); } // Make sure we get observer notifications that normally fire in the // child. function testObserver() { return new Promise(function(resolve, reject) { let observerFired = 0; function observer(subject, topic, data) { Services.obs.removeObserver(observer, "document-element-inserted"); observerFired++; } Services.obs.addObserver(observer, "document-element-inserted", false); let count = 0; const url = baseURL + "browser_addonShims_testpage.html"; let tab = gBrowser.addTab(url); let browser = tab.linkedBrowser; browser.addEventListener("load", function handler() { count++; if (count == 1) { browser.reload(); } else { browser.removeEventListener("load", handler); is(observerFired, 1, "got observer notification"); removeTab(tab, resolve); } }, true); }); } // Test for bug 1072472. Make sure that creating a sandbox to run code // in the content window works. This is essentially a test for // Greasemonkey. function testSandbox() { return new Promise(function(resolve, reject) { const url = baseURL + "browser_addonShims_testpage.html"; let tab = gBrowser.addTab(url); let browser = tab.linkedBrowser; browser.addEventListener("load", function handler() { browser.removeEventListener("load", handler); let sandbox = Cu.Sandbox(browser.contentWindow, {sandboxPrototype: browser.contentWindow, wantXrays: false}); Cu.evalInSandbox("const unsafeWindow = window;", sandbox); Cu.evalInSandbox("document.getElementById('output').innerHTML = 'hello';", sandbox); is(browser.contentDocument.getElementById("output").innerHTML, "hello", "sandbox code ran successfully"); // Now try a sandbox with expanded principals. sandbox = Cu.Sandbox([browser.contentWindow], {sandboxPrototype: browser.contentWindow, wantXrays: false}); Cu.evalInSandbox("const unsafeWindow = window;", sandbox); Cu.evalInSandbox("document.getElementById('output').innerHTML = 'hello2';", sandbox); is(browser.contentDocument.getElementById("output").innerHTML, "hello2", "EP sandbox code ran successfully"); removeTab(tab, resolve); }, true); }); } // Test for bug 1095305. We just want to make sure that loading some // unprivileged content from an add-on package doesn't crash. function testAddonContent() { let chromeRegistry = Components.classes["@mozilla.org/chrome/chrome-registry;1"] .getService(Components.interfaces.nsIChromeRegistry); let base = chromeRegistry.convertChromeURL(BrowserUtils.makeURI("chrome://addonshim1/content/")); let res = Services.io.getProtocolHandler("resource") .QueryInterface(Ci.nsIResProtocolHandler); res.setSubstitution("addonshim1", base); return new Promise(function(resolve, reject) { const url = "resource://addonshim1/page.html"; let tab = gBrowser.addTab(url); let browser = tab.linkedBrowser; addLoadListener(browser, function handler() { res.setSubstitution("addonshim1", null); removeTab(tab, resolve); }); }); } // Test for bug 1102410. We check that multiple nsIAboutModule's can be // registered in the parent, and that the child can browse to each of // the registered about: pages. function testAboutModuleRegistration() { let Registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); let modulesToUnregister = new Map(); function TestChannel(uri, aLoadInfo, aboutName) { this.aboutName = aboutName; this.loadInfo = aLoadInfo; this.URI = this.originalURI = uri; } TestChannel.prototype = { asyncOpen: function(listener, context) { let stream = this.open(); let runnable = { run: () => { try { listener.onStartRequest(this, context); } catch (e) {} try { listener.onDataAvailable(this, context, stream, 0, stream.available()); } catch (e) {} try { listener.onStopRequest(this, context, Cr.NS_OK); } catch (e) {} } }; Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL); }, asyncOpen2: function(listener) { // throws an error if security checks fail var outListener = contentSecManager.performSecurityCheck(this, listener); return this.asyncOpen(outListener, null); }, open: function() { function getWindow(channel) { try { if (channel.notificationCallbacks) return channel.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow; } catch (e) {} try { if (channel.loadGroup && channel.loadGroup.notificationCallbacks) return channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow; } catch (e) {} return null; } let data = `

${this.aboutName}

`; let wnd = getWindow(this); if (!wnd) throw Cr.NS_ERROR_UNEXPECTED; let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); stream.setData(data, data.length); return stream; }, open2: function() { // throws an error if security checks fail contentSecManager.performSecurityCheck(this, null); return this.open(); }, isPending: function() { return false; }, cancel: function() { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, suspend: function() { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, resume: function() { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest]) }; /** * This function creates a new nsIAboutModule and registers it. Callers * should also call unregisterModules after using this function to clean * up the nsIAboutModules at the end of this test. * * @param aboutName * This will be the string after about: used to refer to this module. * For example, if aboutName is foo, you can refer to this module by * browsing to about:foo. * * @param uuid * A unique identifer string for this module. For example, * "5f3a921b-250f-4ac5-a61c-8f79372e6063" */ let createAndRegisterAboutModule = function(aboutName, uuid) { let AboutModule = function() {}; AboutModule.prototype = { classID: Components.ID(uuid), classDescription: `Testing About Module for about:${aboutName}`, contractID: `@mozilla.org/network/protocol/about;1?what=${aboutName}`, QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]), newChannel: (aURI, aLoadInfo) => { return new TestChannel(aURI, aLoadInfo, aboutName); }, getURIFlags: (aURI) => { return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT | Ci.nsIAboutModule.ALLOW_SCRIPT; }, }; let factory = { createInstance: function(outer, iid) { if (outer) { throw Cr.NS_ERROR_NO_AGGREGATION; } return new AboutModule(); }, }; Registrar.registerFactory(AboutModule.prototype.classID, AboutModule.prototype.classDescription, AboutModule.prototype.contractID, factory); modulesToUnregister.set(AboutModule.prototype.classID, factory); }; /** * Unregisters any nsIAboutModules registered with * createAndRegisterAboutModule. */ let unregisterModules = () => { for (let [classID, factory] of modulesToUnregister) { Registrar.unregisterFactory(classID, factory); } }; /** * Takes a browser, and sends it a framescript to attempt to * load some about: pages. The frame script will send a test:result * message on completion, passing back a data object with: * * { * pass: true * } * * on success, and: * * { * pass: false, * errorMsg: message, * } * * on failure. * * @param browser * The browser to send the framescript to. */ let testAboutModulesWork = (browser) => { let testConnection = () => { let request = new content.XMLHttpRequest(); try { request.open("GET", "about:test1", false); request.send(null); if (request.status != 200) { throw (`about:test1 response had status ${request.status} - expected 200`); } if (request.responseText.indexOf("test1") == -1) { throw (`about:test1 response had result ${request.responseText}`); } request = new content.XMLHttpRequest(); request.open("GET", "about:test2", false); request.send(null); if (request.status != 200) { throw (`about:test2 response had status ${request.status} - expected 200`); } if (request.responseText.indexOf("test2") == -1) { throw (`about:test2 response had result ${request.responseText}`); } sendAsyncMessage("test:result", { pass: true, }); } catch (e) { sendAsyncMessage("test:result", { pass: false, errorMsg: e.toString(), }); } }; return new Promise((resolve, reject) => { let mm = browser.messageManager; mm.addMessageListener("test:result", function onTestResult(message) { mm.removeMessageListener("test:result", onTestResult); if (message.data.pass) { ok(true, "Connections to about: pages were successful"); } else { ok(false, message.data.errorMsg); } resolve(); }); mm.loadFrameScript("data:,(" + testConnection.toString() + ")();", false); }); } // Here's where the actual test is performed. return new Promise((resolve, reject) => { createAndRegisterAboutModule("test1", "5f3a921b-250f-4ac5-a61c-8f79372e6063"); createAndRegisterAboutModule("test2", "d7ec0389-1d49-40fa-b55c-a1fc3a6dbf6f"); // This needs to be a chrome-privileged page that loads in the // content process. It needs chrome privs because otherwise the // XHRs for about:test[12] will fail with a privilege error // despite the presence of URI_SAFE_FOR_UNTRUSTED_CONTENT. let newTab = gBrowser.addTab("chrome://addonshim1/content/page.html"); gBrowser.selectedTab = newTab; let browser = newTab.linkedBrowser; addLoadListener(browser, function() { testAboutModulesWork(browser).then(() => { unregisterModules(); removeTab(newTab, resolve); }); }); }); } function testProgressListener() { const url = baseURL + "browser_addonShims_testpage.html"; let sawGlobalLocChange = false; let sawTabsLocChange = false; let globalListener = { onLocationChange: function(webProgress, request, uri) { if (uri.spec == url) { sawGlobalLocChange = true; ok(request instanceof Ci.nsIHttpChannel, "Global listener channel is an HTTP channel"); } }, }; let tabsListener = { onLocationChange: function(browser, webProgress, request, uri) { if (uri.spec == url) { sawTabsLocChange = true; ok(request instanceof Ci.nsIHttpChannel, "Tab listener channel is an HTTP channel"); } }, }; gBrowser.addProgressListener(globalListener); gBrowser.addTabsProgressListener(tabsListener); info("Added progress listeners"); return new Promise(function(resolve, reject) { let tab = gBrowser.addTab(url); gBrowser.selectedTab = tab; addLoadListener(tab.linkedBrowser, function handler() { ok(sawGlobalLocChange, "Saw global onLocationChange"); ok(sawTabsLocChange, "Saw tabs onLocationChange"); gBrowser.removeProgressListener(globalListener); gBrowser.removeTabsProgressListener(tabsListener); removeTab(tab, resolve); }); }); } function testRootTreeItem() { return new Promise(function(resolve, reject) { const url = baseURL + "browser_addonShims_testpage.html"; let tab = gBrowser.addTab(url); gBrowser.selectedTab = tab; let browser = tab.linkedBrowser; addLoadListener(browser, function handler() { let win = browser.contentWindow; // Add-ons love this crap. let root = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) .getInterface(Components.interfaces.nsIWebNavigation) .QueryInterface(Components.interfaces.nsIDocShellTreeItem) .rootTreeItem .QueryInterface(Components.interfaces.nsIInterfaceRequestor) .getInterface(Components.interfaces.nsIDOMWindow); is(root, gWin, "got correct chrome window"); removeTab(tab, resolve); }); }); } function testImportNode() { return new Promise(function(resolve, reject) { const url = baseURL + "browser_addonShims_testpage.html"; let tab = gBrowser.addTab(url); gBrowser.selectedTab = tab; let browser = tab.linkedBrowser; addLoadListener(browser, function handler() { let node = gWin.document.createElement("div"); let doc = browser.contentDocument; let result; try { result = doc.importNode(node, false); } catch (e) { ok(false, "importing threw an exception"); } if (browser.isRemoteBrowser) { is(result, node, "got expected import result"); } removeTab(tab, resolve); }); }); } function runTests(win, funcs) { ok = funcs.ok; is = funcs.is; info = funcs.info; gWin = win; gBrowser = win.gBrowser; return testContentWindow(). then(testListeners). then(testCapturing). then(testObserver). then(testSandbox). then(testAddonContent). then(testAboutModuleRegistration). then(testProgressListener). then(testRootTreeItem). then(testImportNode). then(Promise.resolve()); } /* bootstrap.js API */ function startup(aData, aReason) { forEachWindow(win => { win.runAddonShimTests = (funcs) => runTests(win, funcs); }); } function shutdown(aData, aReason) { forEachWindow(win => { delete win.runAddonShimTests; }); } function install(aData, aReason) { } function uninstall(aData, aReason) { }