summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/parent/ext-windows.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /browser/components/extensions/parent/ext-windows.js
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/extensions/parent/ext-windows.js')
-rw-r--r--browser/components/extensions/parent/ext-windows.js443
1 files changed, 443 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..dde88dc880
--- /dev/null
+++ b/browser/components/extensions/parent/ext-windows.js
@@ -0,0 +1,443 @@
+/* -*- 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);
+ });
+ },
+ },
+ };
+ }
+};