bug 1301227 - add allowScriptsToClose option to windows.create(), r?kmag draft 1301227-allowScriptsToClose
authorTomislav Jovanovic <tomica@gmail.com>
Fri, 23 Sep 2016 23:40:24 +0200
changeset 421850 e03aaaf333f0301d6b9c7ab8cdfd01fcf6e051c9
parent 421258 e8fa13708c070d1fadf488ed9d951464745b4e17
child 533191 a914a742562adb25bafeb7bbbf4fbd4e758b6f81
push id31620
push userbmo:tomica@gmail.com
push dateFri, 07 Oct 2016 00:48:47 +0000
reviewerskmag
bugs1301227
milestone52.0a1
bug 1301227 - add allowScriptsToClose option to windows.create(), r?kmag MozReview-Commit-ID: 4p8A1y2FALX
browser/base/content/tab-content.js
browser/components/extensions/ext-windows.js
browser/components/extensions/schemas/windows.json
browser/components/extensions/test/browser/browser_ext_windows_create.js
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -925,12 +925,18 @@ var UserContextIdNotifier = {
 UserContextIdNotifier.init();
 
 ExtensionContent.init(this);
 addEventListener("unload", () => {
   ExtensionContent.uninit(this);
   RefreshBlocker.uninit();
 });
 
+addMessageListener("AllowScriptsToClose", () => {
+  content.QueryInterface(Ci.nsIInterfaceRequestor)
+         .getInterface(Ci.nsIDOMWindowUtils)
+         .allowScriptsToClose();
+});
+
 addEventListener("MozAfterPaint", function onFirstPaint() {
   removeEventListener("MozAfterPaint", onFirstPaint);
   sendAsyncMessage("Browser:FirstPaint");
 });
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -8,18 +8,23 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
+  promiseObserved,
 } = ExtensionUtils;
 
+function onXULFrameLoaderCreated({target}) {
+  target.messageManager.sendAsyncMessage("AllowScriptsToClose", {});
+}
+
 extensions.registerSchemaAPI("windows", "addon_parent", context => {
   let {extension} = context;
   return {
     windows: {
       onCreated:
       new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
         fire(WindowManager.convert(extension, window));
       }).api(),
@@ -89,16 +94,20 @@ extensions.registerSchemaAPI("windows", 
 
         let args = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
 
         if (createData.tabId !== null) {
           if (createData.url !== null) {
             return Promise.reject({message: "`tabId` may not be used in conjunction with `url`"});
           }
 
+          if (createData.allowScriptsToClose) {
+            return Promise.reject({message: "`tabId` may not be used in conjunction with `allowScriptsToClose`"});
+          }
+
           let tab = TabManager.getTab(createData.tabId, context);
 
           // Private browsing tabs can only be moved to private browsing
           // windows.
           let incognito = PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser);
           if (createData.incognito !== null && createData.incognito != incognito) {
             return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
           }
@@ -131,48 +140,52 @@ extensions.registerSchemaAPI("windows", 
         if (createData.incognito !== null) {
           if (createData.incognito) {
             features.push("private");
           } else {
             features.push("non-private");
           }
         }
 
+        let {allowScriptsToClose, url} = createData;
+        if (allowScriptsToClose === null) {
+          allowScriptsToClose = typeof url === "string" && url.startsWith("moz-extension://");
+        }
+
         let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
                                             features.join(","), args);
 
         WindowManager.updateGeometry(window, createData);
 
         // TODO: focused, type
 
         return new Promise(resolve => {
           window.addEventListener("load", function listener() {
             window.removeEventListener("load", listener);
 
             if (createData.state == "maximized" || createData.state == "normal" ||
                 (createData.state == "fullscreen" && AppConstants.platform != "macosx")) {
               window.document.documentElement.setAttribute("sizemode", createData.state);
             } else if (createData.state !== null) {
-              // window.minimize() has no useful effect until the window has
-              // been shown.
-
-              let obs = doc => {
-                if (doc === window.document) {
-                  Services.obs.removeObserver(obs, "document-shown");
-                  WindowManager.setState(window, createData.state);
-                  resolve();
-                }
-              };
-              Services.obs.addObserver(obs, "document-shown", false);
-              return;
+              // window.minimize() has no effect until the window has been shown.
+              return promiseObserved("document-shown", doc => doc == window.document).then(() => {
+                WindowManager.setState(window, createData.state);
+                resolve();
+              });
             }
-
             resolve();
           });
         }).then(() => {
+          if (allowScriptsToClose) {
+            for (let {linkedBrowser} of window.gBrowser.tabs) {
+              onXULFrameLoaderCreated({target: linkedBrowser});
+              linkedBrowser.addEventListener( // eslint-disable-line mozilla/balanced-listeners
+                                             "XULFrameLoaderCreated", onXULFrameLoaderCreated);
+            }
+          }
           return WindowManager.convert(extension, window);
         });
       },
 
       update: function(windowId, updateInfo) {
         if (updateInfo.state !== null && updateInfo.state != "normal") {
           if (updateInfo.left !== null || updateInfo.top !== null ||
               updateInfo.width !== null || updateInfo.height !== null) {
--- a/browser/components/extensions/schemas/windows.json
+++ b/browser/components/extensions/schemas/windows.json
@@ -323,16 +323,21 @@
                 "$ref": "CreateType",
                 "optional": true,
                 "description": "Specifies what type of browser window to create. The 'panel' and 'detached_panel' types create a popup unless the '--enable-panels' flag is set."
               },
               "state": {
                 "$ref": "WindowState",
                 "optional": true,
                 "description": "The initial state of the window. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined with 'left', 'top', 'width' or 'height'."
+              },
+              "allowScriptsToClose": {
+                "type": "boolean",
+                "optional": true,
+                "description": "Allow scripts to close the window."
               }
             },
             "optional": true
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
--- a/browser/components/extensions/test/browser/browser_ext_windows_create.js
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create.js
@@ -160,8 +160,65 @@ add_task(function* testWindowCreateParam
     },
   });
 
   yield extension.startup();
   yield extension.awaitFinish("window-create-params");
   yield extension.unload();
 });
 
+// Tests allowScriptsToClose option
+add_task(function* test_allowScriptsToClose() {
+  const files = {
+    "dummy.html": "<meta charset=utf-8><script src=close.js></script>",
+    "close.js": function() {
+      window.close();
+      if (!window.closed) {
+        browser.test.sendMessage("close-failed");
+      }
+    },
+  };
+
+  function background() {
+    browser.test.onMessage.addListener((msg, options) => {
+      function listener(_, {status}, {url}) {
+        if (status == "complete" && url == options.url) {
+          browser.tabs.onUpdated.removeListener(listener);
+          browser.tabs.executeScript({file: "close.js"});
+        }
+      }
+      options.url = browser.runtime.getURL(options.url);
+      browser.windows.create(options);
+      if (msg === "create+execute") {
+        browser.tabs.onUpdated.addListener(listener);
+      }
+    });
+    browser.test.notifyPass();
+  }
+
+  const example = "http://example.com/";
+  const manifest = {permissions: ["tabs", example]};
+
+  const extension = ExtensionTestUtils.loadExtension({files, background, manifest});
+  yield SpecialPowers.pushPrefEnv({set: [["dom.allow_scripts_to_close_windows", false]]});
+
+  yield extension.startup();
+  yield extension.awaitFinish();
+
+  extension.sendMessage("create", {url: "dummy.html"});
+  let win = yield BrowserTestUtils.waitForNewWindow();
+  yield BrowserTestUtils.windowClosed(win);
+  info("script allowed to close the window");
+
+  extension.sendMessage("create+execute", {url: example});
+  win = yield BrowserTestUtils.waitForNewWindow();
+  yield extension.awaitMessage("close-failed");
+  info("script prevented from closing the window");
+  win.close();
+
+  extension.sendMessage("create+execute", {url: example, allowScriptsToClose: true});
+  win = yield BrowserTestUtils.waitForNewWindow();
+  yield BrowserTestUtils.windowClosed(win);
+  info("script allowed to close the window");
+
+  yield SpecialPowers.popPrefEnv();
+  yield extension.unload();
+});