summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/parent/ext-windows.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/parent/ext-windows.js')
-rw-r--r--browser/components/extensions/parent/ext-windows.js548
1 files changed, 548 insertions, 0 deletions
diff --git a/browser/components/extensions/parent/ext-windows.js b/browser/components/extensions/parent/ext-windows.js
new file mode 100644
index 0000000000..37c837b99e
--- /dev/null
+++ b/browser/components/extensions/parent/ext-windows.js
@@ -0,0 +1,548 @@
+/* -*- 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.defineESModuleGetters(this, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+var { ExtensionError, promiseObserved } = ExtensionUtils;
+
+function sanitizePositionParams(params, window = null, positionOffset = 0) {
+ if (params.left === null && params.top === null) {
+ return;
+ }
+
+ if (params.left === null) {
+ const baseLeft = window ? window.screenX : 0;
+ params.left = baseLeft + positionOffset;
+ }
+ if (params.top === null) {
+ const baseTop = window ? window.screenY : 0;
+ params.top = baseTop + positionOffset;
+ }
+
+ // boundary check: don't put window out of visible area
+ const baseWidth = window ? window.outerWidth : 0;
+ const baseHeight = window ? window.outerHeight : 0;
+ // Secure minimum size of an window should be same to the one
+ // defined at nsGlobalWindowOuter::CheckSecurityWidthAndHeight.
+ const minWidth = 100;
+ const minHeight = 100;
+ const width = Math.max(
+ minWidth,
+ params.width !== null ? params.width : baseWidth
+ );
+ const height = Math.max(
+ minHeight,
+ params.height !== null ? params.height : baseHeight
+ );
+ const screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
+ Ci.nsIScreenManager
+ );
+ const screen = screenManager.screenForRect(
+ params.left,
+ params.top,
+ width,
+ height
+ );
+ const availDeviceLeft = {};
+ const availDeviceTop = {};
+ const availDeviceWidth = {};
+ const availDeviceHeight = {};
+ screen.GetAvailRect(
+ availDeviceLeft,
+ availDeviceTop,
+ availDeviceWidth,
+ availDeviceHeight
+ );
+ const slopX = window?.screenEdgeSlopX || 0;
+ const slopY = window?.screenEdgeSlopY || 0;
+ const factor = screen.defaultCSSScaleFactor;
+ const availLeft = Math.floor(availDeviceLeft.value / factor) - slopX;
+ const availTop = Math.floor(availDeviceTop.value / factor) - slopY;
+ const availWidth = Math.floor(availDeviceWidth.value / factor) + slopX;
+ const availHeight = Math.floor(availDeviceHeight.value / factor) + slopY;
+ params.left = Math.min(
+ availLeft + availWidth - width,
+ Math.max(availLeft, params.left)
+ );
+ params.top = Math.min(
+ availTop + availHeight - height,
+ Math.max(availTop, params.top)
+ );
+}
+
+this.windows = class extends ExtensionAPIPersistent {
+ windowEventRegistrar(event, listener) {
+ let { extension } = this;
+ return ({ fire }) => {
+ let listener2 = (window, ...args) => {
+ if (extension.canAccessWindow(window)) {
+ listener(fire, window, ...args);
+ }
+ };
+
+ windowTracker.addListener(event, listener2);
+ return {
+ unregister() {
+ windowTracker.removeListener(event, listener2);
+ },
+ convert(_fire) {
+ fire = _fire;
+ },
+ };
+ };
+ }
+
+ PERSISTENT_EVENTS = {
+ onCreated: this.windowEventRegistrar("domwindowopened", (fire, window) => {
+ fire.async(this.extension.windowManager.convert(window));
+ }),
+ onRemoved: this.windowEventRegistrar("domwindowclosed", (fire, window) => {
+ fire.async(windowTracker.getId(window));
+ }),
+ onFocusChanged({ fire }) {
+ let { extension } = this;
+ // 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 && extension.canAccessWindow(window)) {
+ windowId = windowTracker.getId(window);
+ }
+ if (windowId !== lastOnFocusChangedWindowId) {
+ fire.async(windowId);
+ lastOnFocusChangedWindowId = windowId;
+ }
+ });
+ };
+ windowTracker.addListener("focus", listener);
+ windowTracker.addListener("blur", listener);
+ return {
+ unregister() {
+ windowTracker.removeListener("focus", listener);
+ windowTracker.removeListener("blur", listener);
+ },
+ convert(_fire) {
+ fire = _fire;
+ },
+ };
+ },
+ };
+
+ getAPI(context) {
+ let { extension } = context;
+
+ const { windowManager } = extension;
+
+ return {
+ windows: {
+ onCreated: new EventManager({
+ context,
+ module: "windows",
+ event: "onCreated",
+ extensionApi: this,
+ }).api(),
+
+ onRemoved: new EventManager({
+ context,
+ module: "windows",
+ event: "onRemoved",
+ extensionApi: this,
+ }).api(),
+
+ onFocusChanged: new EventManager({
+ context,
+ module: "windows",
+ event: "onFocusChanged",
+ extensionApi: this,
+ }).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: async function (createData) {
+ let needResize =
+ createData.left !== null ||
+ createData.top !== null ||
+ createData.width !== null ||
+ createData.height !== null;
+ if (createData.incognito && !context.privateBrowsingAllowed) {
+ throw new ExtensionError(
+ "Extension does not have permission for incognito mode"
+ );
+ }
+
+ if (needResize) {
+ if (createData.state !== null && createData.state != "normal") {
+ throw new ExtensionError(
+ `"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
+ );
+
+ // Whether there is only one URL to load, and it is a moz-extension:-URL.
+ let isOnlyMozExtensionUrl = false;
+
+ // Creating a new window allows one single triggering principal for all tabs that
+ // are created in the window. Due to that, if we need a browser principal to load
+ // some urls, we fallback to using a content principal like we do in the tabs api.
+ // Throws if url is an array and any url can't be loaded by the extension principal.
+ let principal = context.principal;
+ function setContentTriggeringPrincipal(url) {
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(url),
+ {
+ // Note: privateBrowsingAllowed was already checked before.
+ privateBrowsingId: createData.incognito ? 1 : 0,
+ }
+ );
+ }
+
+ if (createData.tabId !== null) {
+ if (createData.url !== null) {
+ throw new ExtensionError(
+ "`tabId` may not be used in conjunction with `url`"
+ );
+ }
+
+ if (createData.allowScriptsToClose) {
+ throw new ExtensionError(
+ "`tabId` may not be used in conjunction with `allowScriptsToClose`"
+ );
+ }
+
+ let tab = tabTracker.getTab(createData.tabId);
+ if (!context.canAccessWindow(tab.ownerGlobal)) {
+ throw new ExtensionError(`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
+ ) {
+ throw new ExtensionError(
+ "`incognito` property must match the incognito state of tab"
+ );
+ }
+ createData.incognito = incognito;
+
+ if (
+ createData.cookieStoreId &&
+ createData.cookieStoreId !==
+ getCookieStoreIdForTab(createData, tab)
+ ) {
+ throw new ExtensionError(
+ "`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.map(u => context.uri.resolve(u))) {
+ // We can only provide a single triggering principal when
+ // opening a window, so if the extension cannot normally
+ // access a url, we fail. This includes about and moz-ext
+ // urls.
+ if (!context.checkLoadURL(url, { dontReportErrors: true })) {
+ return Promise.reject({ message: `Illegal URL: ${url}` });
+ }
+ array.appendElement(mkstr(url));
+ }
+ args.appendElement(array);
+ // TODO bug 1780583: support multiple triggeringPrincipals to
+ // avoid having to use the system principal here.
+ principal = Services.scriptSecurityManager.getSystemPrincipal();
+ } else {
+ let url = context.uri.resolve(createData.url);
+ args.appendElement(mkstr(url));
+ isOnlyMozExtensionUrl = url.startsWith("moz-extension://");
+ if (!context.checkLoadURL(url, { dontReportErrors: true })) {
+ if (isOnlyMozExtensionUrl) {
+ // For backwards-compatibility (also in tabs APIs), we allow
+ // extensions to open other moz-extension:-URLs even if that
+ // other resource is not listed in web_accessible_resources.
+ setContentTriggeringPrincipal(url);
+ } else {
+ throw new ExtensionError(`Illegal URL: ${url}`);
+ }
+ }
+ }
+ } else {
+ let url =
+ createData.incognito &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing
+ ? "about:privatebrowsing"
+ : HomePage.get().split("|", 1)[0];
+ args.appendElement(mkstr(url));
+ isOnlyMozExtensionUrl = url.startsWith("moz-extension://");
+
+ if (!context.checkLoadURL(url, { dontReportErrors: true })) {
+ // The extension principal cannot directly load about:-URLs,
+ // except for about:blank, or other moz-extension:-URLs that are
+ // not in web_accessible_resources. Ensure any page set as a home
+ // page will load by using a content principal.
+ setContentTriggeringPrincipal(url);
+ }
+ }
+
+ args.appendElement(null); // extraOptions
+ 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",
+ "titlebar",
+ "close"
+ );
+ if (createData.left === null && createData.top === null) {
+ features.push("centerscreen");
+ }
+ }
+
+ if (createData.incognito !== null) {
+ if (createData.incognito) {
+ if (!PrivateBrowsingUtils.enabled) {
+ throw new ExtensionError(
+ "`incognito` cannot be used if incognito mode is disabled"
+ );
+ }
+ features.push("private");
+ } else {
+ features.push("non-private");
+ }
+ }
+
+ const baseWindow = windowTracker.getTopNormalWindow(context);
+ // 10px offset is same to Chromium
+ sanitizePositionParams(createData, baseWindow, 10);
+
+ 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
+
+ const contentLoaded = new Promise(resolve => {
+ window.addEventListener(
+ "DOMContentLoaded",
+ function () {
+ let { allowScriptsToClose } = createData;
+ if (allowScriptsToClose === null && isOnlyMozExtensionUrl) {
+ allowScriptsToClose = true;
+ }
+ if (allowScriptsToClose) {
+ window.gBrowserAllowScriptsToCloseInitialTabs = true;
+ }
+ resolve();
+ },
+ { once: true }
+ );
+ });
+
+ const startupFinished = promiseObserved(
+ "browser-delayed-startup-finished",
+ win => win == window
+ );
+
+ await contentLoaded;
+ await startupFinished;
+
+ if (
+ [
+ "minimized",
+ "fullscreen",
+ "docked",
+ "normal",
+ "maximized",
+ ].includes(createData.state)
+ ) {
+ await win.setState(createData.state);
+ }
+
+ if (createData.titlePreface !== null) {
+ win.setTitlePreface(createData.titlePreface);
+ }
+ return win.convert({ populate: true });
+ },
+
+ update: async function (windowId, updateInfo) {
+ if (updateInfo.state !== null && updateInfo.state != "normal") {
+ if (
+ updateInfo.left !== null ||
+ updateInfo.top !== null ||
+ updateInfo.width !== null ||
+ updateInfo.height !== null
+ ) {
+ throw new ExtensionError(
+ `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`
+ );
+ }
+ }
+
+ let win = windowManager.get(windowId, context);
+ if (!win) {
+ throw new ExtensionError(`Invalid window ID: ${windowId}`);
+ }
+ if (updateInfo.focused) {
+ win.window.focus();
+ }
+
+ if (updateInfo.state !== null) {
+ await win.setState(updateInfo.state);
+ }
+
+ if (updateInfo.drawAttention) {
+ // Bug 1257497 - Firefox can't cancel attention actions.
+ win.window.getAttention();
+ }
+
+ sanitizePositionParams(updateInfo, win.window);
+ win.updateGeometry(updateInfo);
+
+ if (updateInfo.titlePreface !== null) {
+ win.setTitlePreface(updateInfo.titlePreface);
+ win.window.gBrowser.updateTitlebar();
+ }
+
+ // TODO: All the other properties, focused=false...
+
+ return 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);
+ });
+ },
+ },
+ };
+ }
+};