fune/browser/components/extensions/parent/ext-windows.js

331 lines
13 KiB
JavaScript

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"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) {
if (!context.canAccessWindow(window)) {
return;
}
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); // referrer
args.appendElement(null); // postData
args.appendElement(null); // allowThirdPartyFixup
args.appendElement(null); // referrerPolicy
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(principal); // triggeringPrincipal
args.appendElement(Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool)); // allowInheritPrincipal
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) {
Services.focus.activeWindow = win.window;
}
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);
});
},
},
};
}
};