Files
tubestation/browser/components/extensions/parent/ext-windows.js
Emilio Cobos Álvarez 79c93cc546 Bug 581863 - Web ext fixes. r=robwu
* In ext-windows.js, set the position and size on creation. This moves
   the burden of doing so to widget, and could avoid flickering.

 * In browser_ext_windows_size.js, we cope with the fact that on linux
   the specified size might be the inner rather than outer size. This
   was already the case before, but outer{Width,Height} reported always
   the inner size so it was papered.

 * In browser_unified_extensions_overflowable_toolbar.js and the
   relevant head file, we make the code a bit more reliable. This is all
   to workaround a mutter but on automation, where if a window was
   already maximized and you request a resize, mutter will (correctly)
   restore() the window, but will lose the resize information. This
   doesn't happen on newer Mutter versions. Still, it seems easier to
   use window.maximize(), etc than manually trying to maximize the
   window.

 * In head_unified_extensions.js we also remove the code to ensure
   stable positions since it was broken (it was checking
   win.screen.{top,left}, rather than win.screen{X,Y}). We can always
   bring it back fixed if really needed.

Differential Revision: https://phabricator.services.mozilla.com/D234903
2025-02-03 17:08:45 +00:00

557 lines
19 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.defineESModuleGetters(this, {
HomePage: "resource:///modules/HomePage.sys.mjs",
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 = () => {
// 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);
if (createData.width !== null) {
features.push("outerWidth=" + createData.width);
}
if (createData.height !== null) {
features.push("outerHeight=" + createData.height);
}
if (createData.left !== null) {
features.push("left=" + createData.left);
}
if (createData.top !== null) {
features.push("top=" + createData.top);
}
let window = Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
"_blank",
features.join(","),
args
);
let win = windowManager.getWrapper(window);
// 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);
});
},
},
};
}
};