forked from mirrors/gecko-dev
		
	Credits to Jonathan Mayer for submitting the original patch. The patch changes the onFocusChanged extension event handler so that it no longer bails out if focus is moving to a private window, ensuring that the onFocusChanged event gets generated with the proper WINDOW_ID_NONE identifier. Differential Revision: https://phabricator.services.mozilla.com/D104732
		
			
				
	
	
		
			443 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			443 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | 
						|
/* vim: set sts=2 sw=2 et tw=80: */
 | 
						|
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | 
						|
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "HomePage",
 | 
						|
  "resource:///modules/HomePage.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "PrivateBrowsingUtils",
 | 
						|
  "resource://gre/modules/PrivateBrowsingUtils.jsm"
 | 
						|
);
 | 
						|
 | 
						|
var { promiseObserved } = ExtensionUtils;
 | 
						|
 | 
						|
/**
 | 
						|
 * An event manager API provider which listens for a DOM event in any browser
 | 
						|
 * window, and calls the given listener function whenever an event is received.
 | 
						|
 * That listener function receives a `fire` object, which it can use to dispatch
 | 
						|
 * events to the extension, and a DOM event object.
 | 
						|
 *
 | 
						|
 * @param {BaseContext} context
 | 
						|
 *        The extension context which the event manager belongs to.
 | 
						|
 * @param {string} name
 | 
						|
 *        The API name of the event manager, e.g.,"runtime.onMessage".
 | 
						|
 * @param {string} event
 | 
						|
 *        The name of the DOM event to listen for.
 | 
						|
 * @param {function} listener
 | 
						|
 *        The listener function to call when a DOM event is received.
 | 
						|
 *
 | 
						|
 * @returns {object} An injectable api for the new event.
 | 
						|
 */
 | 
						|
function WindowEventManager(context, name, event, listener) {
 | 
						|
  let register = fire => {
 | 
						|
    let listener2 = (window, ...args) => {
 | 
						|
      if (context.canAccessWindow(window)) {
 | 
						|
        listener(fire, window, ...args);
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    windowTracker.addListener(event, listener2);
 | 
						|
    return () => {
 | 
						|
      windowTracker.removeListener(event, listener2);
 | 
						|
    };
 | 
						|
  };
 | 
						|
 | 
						|
  return new EventManager({ context, name, register }).api();
 | 
						|
}
 | 
						|
 | 
						|
this.windows = class extends ExtensionAPI {
 | 
						|
  getAPI(context) {
 | 
						|
    let { extension } = context;
 | 
						|
 | 
						|
    const { windowManager } = extension;
 | 
						|
 | 
						|
    return {
 | 
						|
      windows: {
 | 
						|
        onCreated: WindowEventManager(
 | 
						|
          context,
 | 
						|
          "windows.onCreated",
 | 
						|
          "domwindowopened",
 | 
						|
          (fire, window) => {
 | 
						|
            fire.async(windowManager.convert(window));
 | 
						|
          }
 | 
						|
        ),
 | 
						|
 | 
						|
        onRemoved: WindowEventManager(
 | 
						|
          context,
 | 
						|
          "windows.onRemoved",
 | 
						|
          "domwindowclosed",
 | 
						|
          (fire, window) => {
 | 
						|
            fire.async(windowTracker.getId(window));
 | 
						|
          }
 | 
						|
        ),
 | 
						|
 | 
						|
        onFocusChanged: new EventManager({
 | 
						|
          context,
 | 
						|
          name: "windows.onFocusChanged",
 | 
						|
          register: fire => {
 | 
						|
            // Keep track of the last windowId used to fire an onFocusChanged event
 | 
						|
            let lastOnFocusChangedWindowId;
 | 
						|
 | 
						|
            let listener = event => {
 | 
						|
              // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
 | 
						|
              // event when switching focus between two Firefox windows.
 | 
						|
              Promise.resolve().then(() => {
 | 
						|
                let windowId = Window.WINDOW_ID_NONE;
 | 
						|
                let window = Services.focus.activeWindow;
 | 
						|
                if (window && context.canAccessWindow(window)) {
 | 
						|
                  windowId = windowTracker.getId(window);
 | 
						|
                }
 | 
						|
                if (windowId !== lastOnFocusChangedWindowId) {
 | 
						|
                  fire.async(windowId);
 | 
						|
                  lastOnFocusChangedWindowId = windowId;
 | 
						|
                }
 | 
						|
              });
 | 
						|
            };
 | 
						|
            windowTracker.addListener("focus", listener);
 | 
						|
            windowTracker.addListener("blur", listener);
 | 
						|
            return () => {
 | 
						|
              windowTracker.removeListener("focus", listener);
 | 
						|
              windowTracker.removeListener("blur", listener);
 | 
						|
            };
 | 
						|
          },
 | 
						|
        }).api(),
 | 
						|
 | 
						|
        get: function(windowId, getInfo) {
 | 
						|
          let window = windowTracker.getWindow(windowId, context);
 | 
						|
          if (!window || !context.canAccessWindow(window)) {
 | 
						|
            return Promise.reject({
 | 
						|
              message: `Invalid window ID: ${windowId}`,
 | 
						|
            });
 | 
						|
          }
 | 
						|
          return Promise.resolve(windowManager.convert(window, getInfo));
 | 
						|
        },
 | 
						|
 | 
						|
        getCurrent: function(getInfo) {
 | 
						|
          let window = context.currentWindow || windowTracker.topWindow;
 | 
						|
          if (!context.canAccessWindow(window)) {
 | 
						|
            return Promise.reject({ message: `Invalid window` });
 | 
						|
          }
 | 
						|
          return Promise.resolve(windowManager.convert(window, getInfo));
 | 
						|
        },
 | 
						|
 | 
						|
        getLastFocused: function(getInfo) {
 | 
						|
          let window = windowTracker.topWindow;
 | 
						|
          if (!context.canAccessWindow(window)) {
 | 
						|
            return Promise.reject({ message: `Invalid window` });
 | 
						|
          }
 | 
						|
          return Promise.resolve(windowManager.convert(window, getInfo));
 | 
						|
        },
 | 
						|
 | 
						|
        getAll: function(getInfo) {
 | 
						|
          let doNotCheckTypes =
 | 
						|
            getInfo === null || getInfo.windowTypes === null;
 | 
						|
          let windows = [];
 | 
						|
          // incognito access is checked in getAll
 | 
						|
          for (let win of windowManager.getAll()) {
 | 
						|
            if (doNotCheckTypes || getInfo.windowTypes.includes(win.type)) {
 | 
						|
              windows.push(win.convert(getInfo));
 | 
						|
            }
 | 
						|
          }
 | 
						|
          return windows;
 | 
						|
        },
 | 
						|
 | 
						|
        create: function(createData) {
 | 
						|
          let needResize =
 | 
						|
            createData.left !== null ||
 | 
						|
            createData.top !== null ||
 | 
						|
            createData.width !== null ||
 | 
						|
            createData.height !== null;
 | 
						|
          if (createData.incognito && !context.privateBrowsingAllowed) {
 | 
						|
            return Promise.reject({
 | 
						|
              message: "Extension does not have permission for incognito mode",
 | 
						|
            });
 | 
						|
          }
 | 
						|
 | 
						|
          if (needResize) {
 | 
						|
            if (createData.state !== null && createData.state != "normal") {
 | 
						|
              return Promise.reject({
 | 
						|
                message: `"state": "${createData.state}" may not be combined with "left", "top", "width", or "height"`,
 | 
						|
              });
 | 
						|
            }
 | 
						|
            createData.state = "normal";
 | 
						|
          }
 | 
						|
 | 
						|
          function mkstr(s) {
 | 
						|
            let result = Cc["@mozilla.org/supports-string;1"].createInstance(
 | 
						|
              Ci.nsISupportsString
 | 
						|
            );
 | 
						|
            result.data = s;
 | 
						|
            return result;
 | 
						|
          }
 | 
						|
 | 
						|
          let args = Cc["@mozilla.org/array;1"].createInstance(
 | 
						|
            Ci.nsIMutableArray
 | 
						|
          );
 | 
						|
 | 
						|
          let principal = context.principal;
 | 
						|
          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 = tabTracker.getTab(createData.tabId);
 | 
						|
            if (!context.canAccessWindow(tab.ownerGlobal)) {
 | 
						|
              return Promise.reject({
 | 
						|
                message: `Invalid tab ID: ${createData.tabId}`,
 | 
						|
              });
 | 
						|
            }
 | 
						|
            // 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",
 | 
						|
              });
 | 
						|
            }
 | 
						|
            createData.incognito = incognito;
 | 
						|
 | 
						|
            if (
 | 
						|
              createData.cookieStoreId &&
 | 
						|
              createData.cookieStoreId !==
 | 
						|
                getCookieStoreIdForTab(createData, tab)
 | 
						|
            ) {
 | 
						|
              return Promise.reject({
 | 
						|
                message: "`cookieStoreId` must match the tab's cookieStoreId",
 | 
						|
              });
 | 
						|
            }
 | 
						|
 | 
						|
            args.appendElement(tab);
 | 
						|
          } else if (createData.url !== null) {
 | 
						|
            if (Array.isArray(createData.url)) {
 | 
						|
              let array = Cc["@mozilla.org/array;1"].createInstance(
 | 
						|
                Ci.nsIMutableArray
 | 
						|
              );
 | 
						|
              for (let url of createData.url) {
 | 
						|
                array.appendElement(mkstr(url));
 | 
						|
              }
 | 
						|
              args.appendElement(array);
 | 
						|
            } else {
 | 
						|
              args.appendElement(mkstr(createData.url));
 | 
						|
            }
 | 
						|
          } else {
 | 
						|
            let url =
 | 
						|
              createData.incognito &&
 | 
						|
              !PrivateBrowsingUtils.permanentPrivateBrowsing
 | 
						|
                ? "about:privatebrowsing"
 | 
						|
                : HomePage.get().split("|", 1)[0];
 | 
						|
            args.appendElement(mkstr(url));
 | 
						|
 | 
						|
            if (
 | 
						|
              url.startsWith("about:") &&
 | 
						|
              !context.checkLoadURL(url, { dontReportErrors: true })
 | 
						|
            ) {
 | 
						|
              // The extension principal cannot directly load about:-URLs,
 | 
						|
              // except for about:blank. So use the system principal instead.
 | 
						|
              principal = Services.scriptSecurityManager.getSystemPrincipal();
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          args.appendElement(null); // unused
 | 
						|
          args.appendElement(null); // referrerInfo
 | 
						|
          args.appendElement(null); // postData
 | 
						|
          args.appendElement(null); // allowThirdPartyFixup
 | 
						|
 | 
						|
          if (createData.cookieStoreId) {
 | 
						|
            let userContextIdSupports = Cc[
 | 
						|
              "@mozilla.org/supports-PRUint32;1"
 | 
						|
            ].createInstance(Ci.nsISupportsPRUint32);
 | 
						|
            // May throw if validation fails.
 | 
						|
            userContextIdSupports.data = getUserContextIdForCookieStoreId(
 | 
						|
              extension,
 | 
						|
              createData.cookieStoreId,
 | 
						|
              createData.incognito
 | 
						|
            );
 | 
						|
            args.appendElement(userContextIdSupports); // userContextId
 | 
						|
          } else {
 | 
						|
            args.appendElement(null);
 | 
						|
          }
 | 
						|
 | 
						|
          args.appendElement(context.principal); // originPrincipal - not important.
 | 
						|
          args.appendElement(context.principal); // originStoragePrincipal - not important.
 | 
						|
          args.appendElement(principal); // triggeringPrincipal
 | 
						|
          args.appendElement(
 | 
						|
            Cc["@mozilla.org/supports-PRBool;1"].createInstance(
 | 
						|
              Ci.nsISupportsPRBool
 | 
						|
            )
 | 
						|
          ); // allowInheritPrincipal
 | 
						|
          // There is no CSP associated with this extension, hence we explicitly pass null as the CSP argument.
 | 
						|
          args.appendElement(null); // csp
 | 
						|
 | 
						|
          let features = ["chrome"];
 | 
						|
 | 
						|
          if (createData.type === null || createData.type == "normal") {
 | 
						|
            features.push("dialog=no", "all");
 | 
						|
          } else {
 | 
						|
            // All other types create "popup"-type windows by default.
 | 
						|
            features.push(
 | 
						|
              "dialog",
 | 
						|
              "resizable",
 | 
						|
              "minimizable",
 | 
						|
              "centerscreen",
 | 
						|
              "titlebar",
 | 
						|
              "close"
 | 
						|
            );
 | 
						|
          }
 | 
						|
 | 
						|
          if (createData.incognito !== null) {
 | 
						|
            if (createData.incognito) {
 | 
						|
              if (!PrivateBrowsingUtils.enabled) {
 | 
						|
                return Promise.reject({
 | 
						|
                  message:
 | 
						|
                    "`incognito` cannot be used if incognito mode is disabled",
 | 
						|
                });
 | 
						|
              }
 | 
						|
              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,
 | 
						|
            AppConstants.BROWSER_CHROME_URL,
 | 
						|
            "_blank",
 | 
						|
            features.join(","),
 | 
						|
            args
 | 
						|
          );
 | 
						|
 | 
						|
          let win = windowManager.getWrapper(window);
 | 
						|
          win.updateGeometry(createData);
 | 
						|
 | 
						|
          // TODO: focused, type
 | 
						|
 | 
						|
          return new Promise(resolve => {
 | 
						|
            window.addEventListener(
 | 
						|
              "DOMContentLoaded",
 | 
						|
              function() {
 | 
						|
                if (allowScriptsToClose) {
 | 
						|
                  window.gBrowserAllowScriptsToCloseInitialTabs = true;
 | 
						|
                }
 | 
						|
                resolve(
 | 
						|
                  promiseObserved(
 | 
						|
                    "browser-delayed-startup-finished",
 | 
						|
                    win => win == window
 | 
						|
                  )
 | 
						|
                );
 | 
						|
              },
 | 
						|
              { once: true }
 | 
						|
            );
 | 
						|
          }).then(() => {
 | 
						|
            if (
 | 
						|
              [
 | 
						|
                "minimized",
 | 
						|
                "fullscreen",
 | 
						|
                "docked",
 | 
						|
                "normal",
 | 
						|
                "maximized",
 | 
						|
              ].includes(createData.state)
 | 
						|
            ) {
 | 
						|
              win.state = createData.state;
 | 
						|
            }
 | 
						|
            if (createData.titlePreface !== null) {
 | 
						|
              win.setTitlePreface(createData.titlePreface);
 | 
						|
            }
 | 
						|
            return win.convert({ populate: true });
 | 
						|
          });
 | 
						|
        },
 | 
						|
 | 
						|
        update: function(windowId, updateInfo) {
 | 
						|
          if (updateInfo.state !== null && updateInfo.state != "normal") {
 | 
						|
            if (
 | 
						|
              updateInfo.left !== null ||
 | 
						|
              updateInfo.top !== null ||
 | 
						|
              updateInfo.width !== null ||
 | 
						|
              updateInfo.height !== null
 | 
						|
            ) {
 | 
						|
              return Promise.reject({
 | 
						|
                message: `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`,
 | 
						|
              });
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          let win = windowManager.get(windowId, context);
 | 
						|
          if (!win) {
 | 
						|
            return Promise.reject({
 | 
						|
              message: `Invalid window ID: ${windowId}`,
 | 
						|
            });
 | 
						|
          }
 | 
						|
          if (updateInfo.focused) {
 | 
						|
            win.window.focus();
 | 
						|
          }
 | 
						|
 | 
						|
          if (updateInfo.state !== null) {
 | 
						|
            win.state = updateInfo.state;
 | 
						|
          }
 | 
						|
 | 
						|
          if (updateInfo.drawAttention) {
 | 
						|
            // Bug 1257497 - Firefox can't cancel attention actions.
 | 
						|
            win.window.getAttention();
 | 
						|
          }
 | 
						|
 | 
						|
          win.updateGeometry(updateInfo);
 | 
						|
 | 
						|
          if (updateInfo.titlePreface !== null) {
 | 
						|
            win.setTitlePreface(updateInfo.titlePreface);
 | 
						|
            win.window.gBrowser.updateTitlebar();
 | 
						|
          }
 | 
						|
 | 
						|
          // TODO: All the other properties, focused=false...
 | 
						|
 | 
						|
          return Promise.resolve(win.convert());
 | 
						|
        },
 | 
						|
 | 
						|
        remove: function(windowId) {
 | 
						|
          let window = windowTracker.getWindow(windowId, context);
 | 
						|
          if (!context.canAccessWindow(window)) {
 | 
						|
            return Promise.reject({
 | 
						|
              message: `Invalid window ID: ${windowId}`,
 | 
						|
            });
 | 
						|
          }
 | 
						|
          window.close();
 | 
						|
 | 
						|
          return new Promise(resolve => {
 | 
						|
            let listener = () => {
 | 
						|
              windowTracker.removeListener("domwindowclosed", listener);
 | 
						|
              resolve();
 | 
						|
            };
 | 
						|
            windowTracker.addListener("domwindowclosed", listener);
 | 
						|
          });
 | 
						|
        },
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
};
 |