--- a/browser/components/extensions/.eslintrc.js
+++ b/browser/components/extensions/.eslintrc.js
@@ -1,23 +1,23 @@
"use strict";
module.exports = { // eslint-disable-line no-undef
"extends": "../../../toolkit/components/extensions/.eslintrc.js",
"globals": {
- "AllWindowEvents": true,
+ "EventEmitter": true,
+ "IconDetails": true,
+ "PanelPopup": true,
+ "Tab": true,
+ "TabContext": true,
+ "ViewPopup": true,
+ "Window": true,
+ "WindowEventManager": true,
"browserActionFor": true,
- "currentWindow": true,
- "EventEmitter": true,
"getBrowserInfo": true,
"getCookieStoreIdForTab": true,
- "IconDetails": true,
"makeWidgetId": true,
"pageActionFor": true,
- "PanelPopup": true,
- "TabContext": true,
- "ViewPopup": true,
- "WindowEventManager": true,
- "WindowListManager": true,
- "WindowManager": true,
+ "tabTracker": true,
+ "windowTracker": true,
},
};
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -46,17 +46,17 @@ function BrowserAction(options, extensio
let widgetId = makeWidgetId(extension.id);
this.id = `${widgetId}-browser-action`;
this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
this.widget = null;
this.pendingPopup = null;
this.pendingPopupTimeout = null;
- this.tabManager = TabManager.for(extension);
+ this.tabManager = extension.tabManager;
this.defaults = {
enabled: true,
title: options.default_title || extension.name,
badgeText: "",
badgeBackgroundColor: null,
icon: IconDetails.normalize({path: options.default_icon}, extension),
popup: options.default_popup || "",
@@ -381,17 +381,17 @@ BrowserAction.prototype = {
// title, badge, etc. If it only changes a parameter for a single
// tab, |tab| will be that tab. Otherwise it will be null.
updateOnChange(tab) {
if (tab) {
if (tab.selected) {
this.updateWindow(tab.ownerGlobal);
}
} else {
- for (let window of WindowListManager.browserWindows()) {
+ for (let window of windowTracker.browserWindows()) {
this.updateWindow(window);
}
}
},
// tab is allowed to be null.
// prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
setProperty(tab, prop, value) {
@@ -439,107 +439,116 @@ extensions.on("shutdown", (type, extensi
browserActionMap.get(extension).shutdown();
browserActionMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("browserAction", "addon_parent", context => {
let {extension} = context;
+
+ let {tabManager} = extension;
+
+ function getTab(tabId) {
+ if (tabId !== null) {
+ return tabTracker.getTab(tabId);
+ }
+ return null;
+ }
+
return {
browserAction: {
onClicked: new EventManager(context, "browserAction.onClicked", fire => {
let listener = () => {
- let tab = TabManager.activeTab;
- fire(TabManager.convert(extension, tab));
+ fire(tabManager.convert(tabTracker.activeTab));
};
BrowserAction.for(extension).on("click", listener);
return () => {
BrowserAction.for(extension).off("click", listener);
};
}).api(),
enable: function(tabId) {
- let tab = tabId !== null ? TabManager.getTab(tabId, context) : null;
+ let tab = getTab(tabId);
BrowserAction.for(extension).setProperty(tab, "enabled", true);
},
disable: function(tabId) {
- let tab = tabId !== null ? TabManager.getTab(tabId, context) : null;
+ let tab = getTab(tabId);
BrowserAction.for(extension).setProperty(tab, "enabled", false);
},
setTitle: function(details) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
let title = details.title;
// Clear the tab-specific title when given a null string.
if (tab && title == "") {
title = null;
}
BrowserAction.for(extension).setProperty(tab, "title", title);
},
getTitle: function(details) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
let title = BrowserAction.for(extension).getProperty(tab, "title");
return Promise.resolve(title);
},
setIcon: function(details) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
let icon = IconDetails.normalize(details, extension, context);
BrowserAction.for(extension).setProperty(tab, "icon", icon);
},
setBadgeText: function(details) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
BrowserAction.for(extension).setProperty(tab, "badgeText", details.text);
},
getBadgeText: function(details) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
let text = BrowserAction.for(extension).getProperty(tab, "badgeText");
return Promise.resolve(text);
},
setPopup: function(details) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
// Note: Chrome resolves arguments to setIcon relative to the calling
// context, but resolves arguments to setPopup relative to the extension
// root.
// For internal consistency, we currently resolve both relative to the
// calling context.
let url = details.popup && context.uri.resolve(details.popup);
BrowserAction.for(extension).setProperty(tab, "popup", url);
},
getPopup: function(details) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
let popup = BrowserAction.for(extension).getProperty(tab, "popup");
return Promise.resolve(popup);
},
setBadgeBackgroundColor: function(details) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
let color = details.color;
if (!Array.isArray(color)) {
let col = DOMUtils.colorToRGBA(color);
color = col && [col.r, col.g, col.b, Math.round(col.a * 255)];
}
BrowserAction.for(extension).setProperty(tab, "badgeBackgroundColor", color);
},
getBadgeBackgroundColor: function(details, callback) {
- let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
+ let tab = getTab(details.tabId);
let color = BrowserAction.for(extension).getProperty(tab, "badgeBackgroundColor");
return Promise.resolve(color || [0xd9, 0, 0, 255]);
},
},
};
});
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -31,41 +31,41 @@ function CommandList(manifest, extension
}
CommandList.prototype = {
/**
* Registers the commands to all open windows and to any which
* are later created.
*/
register() {
- for (let window of WindowListManager.browserWindows()) {
+ for (let window of windowTracker.browserWindows()) {
this.registerKeysToDocument(window);
}
this.windowOpenListener = (window) => {
if (!this.keysetsMap.has(window)) {
this.registerKeysToDocument(window);
}
};
- WindowListManager.addOpenListener(this.windowOpenListener);
+ windowTracker.addOpenListener(this.windowOpenListener);
},
/**
* Unregisters the commands from all open windows and stops commands
* from being registered to windows which are later created.
*/
unregister() {
- for (let window of WindowListManager.browserWindows()) {
+ for (let window of windowTracker.browserWindows()) {
if (this.keysetsMap.has(window)) {
this.keysetsMap.get(window).remove();
}
}
- WindowListManager.removeOpenListener(this.windowOpenListener);
+ windowTracker.removeOpenListener(this.windowOpenListener);
},
/**
* Creates a Map from commands for each command in the manifest.commands object.
*
* @param {Object} manifest The manifest JSON object.
* @returns {Map<string, object>}
*/
@@ -127,18 +127,18 @@ CommandList.prototype = {
keyElement.addEventListener("command", (event) => {
if (name == "_execute_page_action") {
let win = event.target.ownerDocument.defaultView;
pageActionFor(this.extension).triggerAction(win);
} else if (name == "_execute_browser_action") {
let win = event.target.ownerDocument.defaultView;
browserActionFor(this.extension).triggerAction(win);
} else {
- TabManager.for(this.extension)
- .addActiveTabPermission(TabManager.activeTab);
+ this.extension.tabManager
+ .addActiveTabPermission();
this.emit("command", name);
}
});
/* eslint-enable mozilla/balanced-listeners */
return keyElement;
},
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -84,17 +84,17 @@ var gMenuBuilder = {
this.itemsToCleanUp.add(rootElement);
}
},
// Builds a context menu for browserAction and pageAction buttons.
buildActionContextMenu(contextData) {
const {menu} = contextData;
- contextData.tab = TabManager.activeTab;
+ contextData.tab = tabTracker.activeTab;
contextData.pageUrl = contextData.tab.linkedBrowser.currentURI.spec;
const root = gRootItems.get(contextData.extension);
const children = this.buildChildren(root, contextData);
const visible = children.slice(0, ACTION_MENU_TOP_LEVEL_LIMIT);
if (visible.length) {
this.xulMenu = menu;
@@ -315,17 +315,17 @@ function getContexts(contextData) {
return contexts;
}
function MenuItem(extension, createProperties, isRoot = false) {
this.extension = extension;
this.children = [];
this.parent = null;
- this.tabManager = TabManager.for(extension);
+ this.tabManager = extension.tabManager;
this.setDefaults();
this.setProps(createProperties);
if (!this.hasOwnProperty("_id")) {
this.id = gNextMenuItemID++;
}
// If the item is not the root and has no parent
@@ -533,46 +533,46 @@ MenuItem.prototype = {
},
};
// While any extensions are active, this Tracker registers to observe/listen
// for contex-menu events from both content and chrome.
const contextMenuTracker = {
register() {
Services.obs.addObserver(this, "on-build-contextmenu", false);
- for (const window of WindowListManager.browserWindows()) {
+ for (const window of windowTracker.browserWindows()) {
this.onWindowOpen(window);
}
- WindowListManager.addOpenListener(this.onWindowOpen);
+ windowTracker.addOpenListener(this.onWindowOpen);
},
unregister() {
Services.obs.removeObserver(this, "on-build-contextmenu");
- for (const window of WindowListManager.browserWindows()) {
+ for (const window of windowTracker.browserWindows()) {
const menu = window.document.getElementById("tabContextMenu");
menu.removeEventListener("popupshowing", this);
}
- WindowListManager.removeOpenListener(this.onWindowOpen);
+ windowTracker.removeOpenListener(this.onWindowOpen);
},
observe(subject, topic, data) {
subject = subject.wrappedJSObject;
gMenuBuilder.build(subject);
},
onWindowOpen(window) {
const menu = window.document.getElementById("tabContextMenu");
menu.addEventListener("popupshowing", contextMenuTracker);
},
handleEvent(event) {
const menu = event.target;
if (menu.id === "tabContextMenu") {
const trigger = menu.triggerNode;
- const tab = trigger.localName === "tab" ? trigger : TabManager.activeTab;
+ const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
const pageUrl = tab.linkedBrowser.currentURI.spec;
gMenuBuilder.build({menu, tab, pageUrl, onTab: true});
}
},
};
var gExtensionCount = 0;
/* eslint-disable mozilla/balanced-listeners */
--- a/browser/components/extensions/ext-desktop-runtime.js
+++ b/browser/components/extensions/ext-desktop-runtime.js
@@ -1,20 +1,20 @@
"use strict";
/* eslint-disable mozilla/balanced-listeners */
extensions.on("uninstall", (msg, extension) => {
if (extension.uninstallURL) {
- let browser = WindowManager.topWindow.gBrowser;
+ let browser = windowTracker.topWindow.gBrowser;
browser.addTab(extension.uninstallURL, {relatedToCurrent: true});
}
});
global.openOptionsPage = (extension) => {
- let window = WindowManager.topWindow;
+ let window = windowTracker.topWindow;
if (!window) {
return Promise.reject({message: "No browser window available"});
}
if (extension.manifest.options_ui.open_in_tab) {
window.switchToTabHavingURI(extension.manifest.options_ui.page, true);
return Promise.resolve();
}
--- a/browser/components/extensions/ext-devtools.js
+++ b/browser/components/extensions/ext-devtools.js
@@ -76,17 +76,17 @@ global.getTargetTabIdForToolbox = (toolb
if (!target.isLocalTab) {
throw new Error("Unexpected target type: only local tabs are currently supported.");
}
let parentWindow = target.tab.linkedBrowser.ownerDocument.defaultView;
let tab = parentWindow.gBrowser.getTabForBrowser(target.tab.linkedBrowser);
- return TabManager.getId(tab);
+ return tabTracker.getId(tab);
};
/**
* The DevToolsPage represents the "devtools_page" related to a particular
* Toolbox and WebExtension.
*
* The devtools_page contexts are invisible WebExtensions contexts, similar to the
* background page, associated to a single developer toolbox (e.g. If an add-on
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -13,17 +13,17 @@ var {
var pageActionMap = new WeakMap();
// Handles URL bar icons, including the |page_action| manifest entry
// and associated API.
function PageAction(options, extension) {
this.extension = extension;
this.id = makeWidgetId(extension.id) + "-page-action";
- this.tabManager = TabManager.for(extension);
+ this.tabManager = extension.tabManager;
this.defaults = {
show: false,
title: options.default_title || extension.name,
icon: IconDetails.normalize({path: options.default_icon}, extension),
popup: options.default_popup || "",
};
@@ -206,17 +206,17 @@ PageAction.prototype = {
this.tabContext.clear(tab);
}
this.updateButton(tab.ownerGlobal);
},
shutdown() {
this.tabContext.shutdown();
- for (let window of WindowListManager.browserWindows()) {
+ for (let window of windowTracker.browserWindows()) {
if (this.buttons.has(window)) {
this.buttons.get(window).remove();
window.removeEventListener("popupshowing", this);
}
}
},
};
@@ -237,74 +237,77 @@ extensions.on("shutdown", (type, extensi
PageAction.for = extension => {
return pageActionMap.get(extension);
};
global.pageActionFor = PageAction.for;
extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
let {extension} = context;
+
+ const {tabManager} = extension;
+
return {
pageAction: {
onClicked: new EventManager(context, "pageAction.onClicked", fire => {
let listener = (evt, tab) => {
- fire(TabManager.convert(extension, tab));
+ fire(tabManager.convert(tab));
};
let pageAction = PageAction.for(extension);
pageAction.on("click", listener);
return () => {
pageAction.off("click", listener);
};
}).api(),
show(tabId) {
- let tab = TabManager.getTab(tabId, context);
+ let tab = tabTracker.getTab(tabId);
PageAction.for(extension).setProperty(tab, "show", true);
},
hide(tabId) {
- let tab = TabManager.getTab(tabId, context);
+ let tab = tabTracker.getTab(tabId);
PageAction.for(extension).setProperty(tab, "show", false);
},
setTitle(details) {
- let tab = TabManager.getTab(details.tabId, context);
+ let tab = tabTracker.getTab(details.tabId);
// Clear the tab-specific title when given a null string.
PageAction.for(extension).setProperty(tab, "title", details.title || null);
},
getTitle(details) {
- let tab = TabManager.getTab(details.tabId, context);
+ let tab = tabTracker.getTab(details.tabId);
let title = PageAction.for(extension).getProperty(tab, "title");
return Promise.resolve(title);
},
setIcon(details) {
- let tab = TabManager.getTab(details.tabId, context);
+ let tab = tabTracker.getTab(details.tabId);
let icon = IconDetails.normalize(details, extension, context);
PageAction.for(extension).setProperty(tab, "icon", icon);
},
setPopup(details) {
- let tab = TabManager.getTab(details.tabId, context);
+ let tab = tabTracker.getTab(details.tabId);
// Note: Chrome resolves arguments to setIcon relative to the calling
// context, but resolves arguments to setPopup relative to the extension
// root.
// For internal consistency, we currently resolve both relative to the
// calling context.
let url = details.popup && context.uri.resolve(details.popup);
PageAction.for(extension).setProperty(tab, "popup", url);
},
getPopup(details) {
- let tab = TabManager.getTab(details.tabId, context);
+ let tab = tabTracker.getTab(details.tabId);
let popup = PageAction.for(extension).getProperty(tab, "popup");
return Promise.resolve(popup);
},
},
};
});
--- a/browser/components/extensions/ext-sessions.js
+++ b/browser/components/extensions/ext-sessions.js
@@ -16,46 +16,46 @@ const SS_ON_CLOSED_OBJECTS_CHANGED = "se
function getRecentlyClosed(maxResults, extension) {
let recentlyClosed = [];
// Get closed windows
let closedWindowData = SessionStore.getClosedWindowData(false);
for (let window of closedWindowData) {
recentlyClosed.push({
lastModified: window.closedAt,
- window: WindowManager.convertFromSessionStoreClosedData(window, extension)});
+ window: Window.convertFromSessionStoreClosedData(extension, window)});
}
// Get closed tabs
- for (let window of WindowListManager.browserWindows()) {
+ for (let window of windowTracker.browserWindows()) {
let closedTabData = SessionStore.getClosedTabData(window, false);
for (let tab of closedTabData) {
recentlyClosed.push({
lastModified: tab.closedAt,
- tab: TabManager.for(extension).convertFromSessionStoreClosedData(tab, window)});
+ tab: Tab.convertFromSessionStoreClosedData(extension, tab, window)});
}
}
// Sort windows and tabs
recentlyClosed.sort((a, b) => b.lastModified - a.lastModified);
return recentlyClosed.slice(0, maxResults);
}
function createSession(restored, extension, sessionId) {
if (!restored) {
return Promise.reject({message: `Could not restore object using sessionId ${sessionId}.`});
}
let sessionObj = {lastModified: Date.now()};
if (restored instanceof Ci.nsIDOMChromeWindow) {
return promiseObserved("sessionstore-single-window-restored", subject => subject == restored).then(() => {
- sessionObj.window = WindowManager.convert(extension, restored, {populate: true});
+ sessionObj.window = extension.windowManager.convert(restored, {populate: true});
return Promise.resolve([sessionObj]);
});
}
- sessionObj.tab = TabManager.for(extension).convert(restored);
+ sessionObj.tab = extension.tabManager.convert(restored);
return Promise.resolve([sessionObj]);
}
extensions.registerSchemaAPI("sessions", "addon_parent", context => {
let {extension} = context;
return {
sessions: {
getRecentlyClosed: function(filter) {
@@ -70,17 +70,17 @@ extensions.registerSchemaAPI("sessions",
session = SessionStore.undoCloseById(closedId);
} else if (SessionStore.lastClosedObjectType == "window") {
// If the most recently closed object is a window, just undo closing the most recent window.
session = SessionStore.undoCloseWindow(0);
} else {
// It is a tab, and we cannot call SessionStore.undoCloseTab without a window,
// so we must find the tab in which case we can just use its closedId.
let recentlyClosedTabs = [];
- for (let window of WindowListManager.browserWindows()) {
+ for (let window of windowTracker.browserWindows()) {
let closedTabData = SessionStore.getClosedTabData(window, false);
for (let tab of closedTabData) {
recentlyClosedTabs.push(tab);
}
}
// Sort the tabs.
recentlyClosedTabs.sort((a, b) => b.closedAt - a.closedAt);
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -32,19 +32,19 @@ function getSender(extension, target, se
// page-open listener below).
tabId = sender.tabId;
delete sender.tabId;
} else if (target instanceof Ci.nsIDOMXULElement) {
tabId = getBrowserInfo(target).tabId;
}
if (tabId) {
- let tab = TabManager.getTab(tabId, null, null);
+ let tab = extension.tabManager.get(tabId, null);
if (tab) {
- sender.tab = TabManager.convert(extension, tab);
+ sender.tab = tab.convert();
}
}
}
// Used by Extension.jsm
global.tabGetSender = getSender;
/* eslint-disable mozilla/balanced-listeners */
@@ -73,161 +73,24 @@ extensions.on("fill-browser-data", (type
({tabId, windowId} = getBrowserInfo(browser));
}
data.tabId = tabId || -1;
data.windowId = windowId || -1;
});
/* eslint-enable mozilla/balanced-listeners */
-global.currentWindow = function(context) {
- let {xulWindow} = context;
- if (xulWindow && context.viewType != "background") {
- return xulWindow;
- }
- return WindowManager.topWindow;
-};
-
let tabListener = {
- init() {
- if (this.initialized) {
- return;
- }
-
- this.adoptedTabs = new WeakMap();
-
- this.handleWindowOpen = this.handleWindowOpen.bind(this);
- this.handleWindowClose = this.handleWindowClose.bind(this);
-
- AllWindowEvents.addListener("TabClose", this);
- AllWindowEvents.addListener("TabOpen", this);
- WindowListManager.addOpenListener(this.handleWindowOpen);
- WindowListManager.addCloseListener(this.handleWindowClose);
-
- EventEmitter.decorate(this);
-
- this.initialized = true;
- },
-
- handleEvent(event) {
- switch (event.type) {
- case "TabOpen":
- if (event.detail.adoptedTab) {
- this.adoptedTabs.set(event.detail.adoptedTab, event.target);
- }
-
- // We need to delay sending this event until the next tick, since the
- // tab does not have its final index when the TabOpen event is dispatched.
- Promise.resolve().then(() => {
- if (event.detail.adoptedTab) {
- this.emitAttached(event.originalTarget);
- } else {
- this.emitCreated(event.originalTarget);
- }
- });
- break;
-
- case "TabClose":
- let tab = event.originalTarget;
-
- if (event.detail.adoptedBy) {
- this.emitDetached(tab, event.detail.adoptedBy);
- } else {
- this.emitRemoved(tab, false);
- }
- break;
- }
- },
-
- handleWindowOpen(window) {
- if (window.arguments && window.arguments[0] instanceof window.XULElement) {
- // If the first window argument is a XUL element, it means the
- // window is about to adopt a tab from another window to replace its
- // initial tab.
- //
- // Note that this event handler depends on running before the
- // delayed startup code in browser.js, which is currently triggered
- // by the first MozAfterPaint event. That code handles finally
- // adopting the tab, and clears it from the arguments list in the
- // process, so if we run later than it, we're too late.
- let tab = window.arguments[0];
- this.adoptedTabs.set(tab, window.gBrowser.tabs[0]);
-
- // We need to be sure to fire this event after the onDetached event
- // for the original tab.
- let listener = (event, details) => {
- if (details.tab == tab) {
- this.off("tab-detached", listener);
-
- Promise.resolve().then(() => {
- this.emitAttached(details.adoptedBy);
- });
- }
- };
-
- this.on("tab-detached", listener);
- } else {
- for (let tab of window.gBrowser.tabs) {
- this.emitCreated(tab);
- }
- }
- },
-
- handleWindowClose(window) {
- for (let tab of window.gBrowser.tabs) {
- if (this.adoptedTabs.has(tab)) {
- this.emitDetached(tab, this.adoptedTabs.get(tab));
- } else {
- this.emitRemoved(tab, true);
- }
- }
- },
-
- emitAttached(tab) {
- let newWindowId = WindowManager.getId(tab.ownerGlobal);
- let tabId = TabManager.getId(tab);
-
- this.emit("tab-attached", {tab, tabId, newWindowId, newPosition: tab._tPos});
- },
-
- emitDetached(tab, adoptedBy) {
- let oldWindowId = WindowManager.getId(tab.ownerGlobal);
- let tabId = TabManager.getId(tab);
-
- this.emit("tab-detached", {tab, adoptedBy, tabId, oldWindowId, oldPosition: tab._tPos});
- },
-
- emitCreated(tab) {
- this.emit("tab-created", {tab});
- },
-
- emitRemoved(tab, isWindowClosing) {
- let windowId = WindowManager.getId(tab.ownerGlobal);
- let tabId = TabManager.getId(tab);
-
- // When addons run in-process, `window.close()` is synchronous. Most other
- // addon-invoked calls are asynchronous since they go through a proxy
- // context via the message manager. This includes event registrations such
- // as `tabs.onRemoved.addListener`.
- // So, even if `window.close()` were to be called (in-process) after calling
- // `tabs.onRemoved.addListener`, then the tab would be closed before the
- // event listener is registered. To make sure that the event listener is
- // notified, we dispatch `tabs.onRemoved` asynchronously.
- Services.tm.mainThread.dispatch(() => {
- this.emit("tab-removed", {tab, tabId, windowId, isWindowClosing});
- }, Ci.nsIThread.DISPATCH_NORMAL);
- },
-
tabReadyInitialized: false,
tabReadyPromises: new WeakMap(),
initializingTabs: new WeakSet(),
initTabReady() {
if (!this.tabReadyInitialized) {
- AllWindowEvents.addListener("progress", this);
+ windowTracker.addListener("progress", this);
this.tabReadyInitialized = true;
}
},
onLocationChange(browser, webProgress, request, locationURI, flags) {
if (webProgress.isTopLevel) {
let gBrowser = browser.ownerGlobal.gBrowser;
@@ -264,87 +127,91 @@ let tabListener = {
this.initTabReady();
this.tabReadyPromises.set(tab, deferred);
}
}
return deferred.promise;
},
};
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("startup", () => {
- tabListener.init();
-});
-/* eslint-enable mozilla/balanced-listeners */
-
extensions.registerSchemaAPI("tabs", "addon_parent", context => {
let {extension} = context;
+
+ let {tabManager} = extension;
+
+ function getTabOrActive(tabId) {
+ if (tabId !== null) {
+ return tabTracker.getTab(tabId);
+ }
+ return tabTracker.activeTab;
+ }
+
let self = {
tabs: {
onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
let tab = event.originalTarget;
- let tabId = TabManager.getId(tab);
- let windowId = WindowManager.getId(tab.ownerGlobal);
+ let tabId = tabTracker.getId(tab);
+ let windowId = windowTracker.getId(tab.ownerGlobal);
fire({tabId, windowId});
}).api(),
onCreated: new EventManager(context, "tabs.onCreated", fire => {
let listener = (eventName, event) => {
- fire(TabManager.convert(extension, event.tab));
+ fire(tabManager.convert(event.tab));
};
- tabListener.on("tab-created", listener);
+ tabTracker.on("tab-created", listener);
return () => {
- tabListener.off("tab-created", listener);
+ tabTracker.off("tab-created", listener);
};
}).api(),
/**
* Since multiple tabs currently can't be highlighted, onHighlighted
* essentially acts an alias for self.tabs.onActivated but returns
* the tabId in an array to match the API.
* @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
*/
onHighlighted: new WindowEventManager(context, "tabs.onHighlighted", "TabSelect", (fire, event) => {
let tab = event.originalTarget;
- let tabIds = [TabManager.getId(tab)];
- let windowId = WindowManager.getId(tab.ownerGlobal);
+ let tabIds = [tabTracker.getId(tab)];
+ let windowId = windowTracker.getId(tab.ownerGlobal);
fire({tabIds, windowId});
}).api(),
onAttached: new EventManager(context, "tabs.onAttached", fire => {
let listener = (eventName, event) => {
fire(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
};
- tabListener.on("tab-attached", listener);
+ tabTracker.on("tab-attached", listener);
return () => {
- tabListener.off("tab-attached", listener);
+ tabTracker.off("tab-attached", listener);
};
}).api(),
onDetached: new EventManager(context, "tabs.onDetached", fire => {
let listener = (eventName, event) => {
fire(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
};
- tabListener.on("tab-detached", listener);
+ tabTracker.on("tab-detached", listener);
return () => {
- tabListener.off("tab-detached", listener);
+ tabTracker.off("tab-detached", listener);
};
}).api(),
onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
let listener = (eventName, event) => {
fire(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
};
- tabListener.on("tab-removed", listener);
+ tabTracker.on("tab-removed", listener);
return () => {
- tabListener.off("tab-removed", listener);
+ tabTracker.off("tab-removed", listener);
};
}).api(),
onReplaced: ignoreEvent(context, "tabs.onReplaced"),
onMoved: new EventManager(context, "tabs.onMoved", fire => {
// There are certain circumstances where we need to ignore a move event.
//
@@ -368,28 +235,28 @@ extensions.registerSchemaAPI("tabs", "ad
let moveListener = event => {
let tab = event.originalTarget;
if (ignoreNextMove.has(tab)) {
ignoreNextMove.delete(tab);
return;
}
- fire(TabManager.getId(tab), {
- windowId: WindowManager.getId(tab.ownerGlobal),
+ fire(tabTracker.getId(tab), {
+ windowId: windowTracker.getId(tab.ownerGlobal),
fromIndex: event.detail,
toIndex: tab._tPos,
});
};
- AllWindowEvents.addListener("TabMove", moveListener);
- AllWindowEvents.addListener("TabOpen", openListener);
+ windowTracker.addListener("TabMove", moveListener);
+ windowTracker.addListener("TabOpen", openListener);
return () => {
- AllWindowEvents.removeListener("TabMove", moveListener);
- AllWindowEvents.removeListener("TabOpen", openListener);
+ windowTracker.removeListener("TabMove", moveListener);
+ windowTracker.removeListener("TabOpen", openListener);
};
}).api(),
onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
const restricted = ["url", "favIconUrl", "title"];
function sanitize(extension, changeInfo) {
let result = {};
@@ -404,17 +271,17 @@ extensions.registerSchemaAPI("tabs", "ad
}
let fireForBrowser = (browser, changed) => {
let [needed, changeInfo] = sanitize(extension, changed);
if (needed) {
let gBrowser = browser.ownerGlobal.gBrowser;
let tabElem = gBrowser.getTabForBrowser(browser);
- let tab = TabManager.convert(extension, tabElem);
+ let tab = tabManager.convert(tabElem);
fire(tab.id, changeInfo, tab);
}
};
let listener = event => {
let needed = [];
if (event.type == "TabAttrModified") {
let changed = event.detail.changed;
@@ -436,17 +303,17 @@ extensions.registerSchemaAPI("tabs", "ad
needed.push("pinned");
}
if (needed.length && !extension.hasPermission("tabs")) {
needed = needed.filter(attr => !restricted.includes(attr));
}
if (needed.length) {
- let tab = TabManager.convert(extension, event.originalTarget);
+ let tab = tabManager.convert(event.originalTarget);
let changeInfo = {};
for (let prop of needed) {
changeInfo[prop] = tab[prop];
}
fire(tab.id, changeInfo, tab);
}
};
@@ -478,34 +345,35 @@ extensions.registerSchemaAPI("tabs", "ad
fireForBrowser(browser, {
status: webProgress.isLoadingDocument ? "loading" : "complete",
url: locationURI.spec,
});
},
};
- AllWindowEvents.addListener("progress", progressListener);
- AllWindowEvents.addListener("TabAttrModified", listener);
- AllWindowEvents.addListener("TabPinned", listener);
- AllWindowEvents.addListener("TabUnpinned", listener);
+ windowTracker.addListener("progress", progressListener);
+ windowTracker.addListener("TabAttrModified", listener);
+ windowTracker.addListener("TabPinned", listener);
+ windowTracker.addListener("TabUnpinned", listener);
return () => {
- AllWindowEvents.removeListener("progress", progressListener);
- AllWindowEvents.removeListener("TabAttrModified", listener);
- AllWindowEvents.removeListener("TabPinned", listener);
- AllWindowEvents.removeListener("TabUnpinned", listener);
+ windowTracker.removeListener("progress", progressListener);
+ windowTracker.removeListener("TabAttrModified", listener);
+ windowTracker.removeListener("TabPinned", listener);
+ windowTracker.removeListener("TabUnpinned", listener);
};
}).api(),
- create: function(createProperties) {
+ create(createProperties) {
return new Promise((resolve, reject) => {
let window = createProperties.windowId !== null ?
- WindowManager.getWindow(createProperties.windowId, context) :
- WindowManager.topWindow;
+ windowTracker.getWindow(createProperties.windowId, context) :
+ windowTracker.topWindow;
+
if (!window.gBrowser) {
let obs = (finishedWindow, topic, data) => {
if (finishedWindow != window) {
return;
}
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
resolve(window);
};
@@ -531,21 +399,21 @@ extensions.registerSchemaAPI("tabs", "ad
let options = {};
if (createProperties.cookieStoreId) {
if (!global.isValidCookieStoreId(createProperties.cookieStoreId)) {
return Promise.reject({message: `Illegal cookieStoreId: ${createProperties.cookieStoreId}`});
}
let privateWindow = PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser);
if (privateWindow && !global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
- return Promise.reject({message: `Illegal to set non-private cookieStorageId in a private window`});
+ return Promise.reject({message: `Illegal to set non-private cookieStoreId in a private window`});
}
if (!privateWindow && global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
- return Promise.reject({message: `Illegal to set private cookieStorageId in a non-private window`});
+ return Promise.reject({message: `Illegal to set private cookieStoreId in a non-private window`});
}
if (global.isContainerCookieStoreId(createProperties.cookieStoreId)) {
let containerId = global.getContainerForCookieStoreId(createProperties.cookieStoreId);
if (!containerId) {
return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
}
@@ -583,35 +451,33 @@ extensions.registerSchemaAPI("tabs", "ad
// Mark the tab as initializing, so that operations like
// `executeScript` wait until the requested URL is loaded in
// the tab before dispatching messages to the inner window
// that contains the URL we're attempting to load.
tabListener.initializingTabs.add(tab);
}
- return TabManager.convert(extension, tab);
+ return tabManager.convert(tab);
});
},
- remove: function(tabs) {
+ async remove(tabs) {
if (!Array.isArray(tabs)) {
tabs = [tabs];
}
for (let tabId of tabs) {
- let tab = TabManager.getTab(tabId, context);
+ let tab = tabTracker.getTab(tabId);
tab.ownerGlobal.gBrowser.removeTab(tab);
}
-
- return Promise.resolve();
},
- update: function(tabId, updateProperties) {
- let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+ async update(tabId, updateProperties) {
+ let tab = getTabOrActive(tabId);
let tabbrowser = tab.ownerGlobal.gBrowser;
if (updateProperties.url !== null) {
let url = context.uri.resolve(updateProperties.url);
if (!context.checkLoadURL(url, {dontReportErrors: true})) {
return Promise.reject({message: `Illegal URL: ${url}`});
@@ -636,134 +502,65 @@ extensions.registerSchemaAPI("tabs", "ad
if (updateProperties.pinned) {
tabbrowser.pinTab(tab);
} else {
tabbrowser.unpinTab(tab);
}
}
// FIXME: highlighted/selected, openerTabId
- return Promise.resolve(TabManager.convert(extension, tab));
+ return tabManager.convert(tab);
},
- reload: function(tabId, reloadProperties) {
- let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+ async reload(tabId, reloadProperties) {
+ let tab = getTabOrActive(tabId);
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
if (reloadProperties && reloadProperties.bypassCache) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
}
tab.linkedBrowser.reloadWithFlags(flags);
-
- return Promise.resolve();
},
- get: function(tabId) {
- let tab = TabManager.getTab(tabId, context);
+ async get(tabId) {
+ let tab = tabTracker.getTab(tabId);
- return Promise.resolve(TabManager.convert(extension, tab));
+ return tabManager.convert(tab);
},
getCurrent() {
let tab;
if (context.tabId) {
- tab = TabManager.convert(extension, TabManager.getTab(context.tabId, context));
+ tab = tabManager.get(context.tabId).convert();
}
return Promise.resolve(tab);
},
- query: function(queryInfo) {
- let pattern = null;
+ async query(queryInfo) {
if (queryInfo.url !== null) {
if (!extension.hasPermission("tabs")) {
return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
}
- pattern = new MatchPattern(queryInfo.url);
- }
-
- function matches(tab) {
- let props = ["active", "pinned", "highlighted", "status", "title", "index"];
- for (let prop of props) {
- if (queryInfo[prop] !== null && queryInfo[prop] != tab[prop]) {
- return false;
- }
- }
-
- if (queryInfo.audible !== null) {
- if (queryInfo.audible != tab.audible) {
- return false;
- }
- }
-
- if (queryInfo.muted !== null) {
- if (queryInfo.muted != tab.mutedInfo.muted) {
- return false;
- }
- }
-
- if (queryInfo.cookieStoreId !== null &&
- tab.cookieStoreId != queryInfo.cookieStoreId) {
- return false;
- }
-
- if (pattern && !pattern.matches(Services.io.newURI(tab.url))) {
- return false;
- }
-
- return true;
+ queryInfo = Object.assign({}, queryInfo);
+ queryInfo.url = new MatchPattern(queryInfo.url);
}
- let result = [];
- for (let window of WindowListManager.browserWindows()) {
- let lastFocused = window === WindowManager.topWindow;
- if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow !== lastFocused) {
- continue;
- }
-
- let windowType = WindowManager.windowType(window);
- if (queryInfo.windowType !== null && queryInfo.windowType !== windowType) {
- continue;
- }
-
- if (queryInfo.windowId !== null) {
- if (queryInfo.windowId === WindowManager.WINDOW_ID_CURRENT) {
- if (currentWindow(context) !== window) {
- continue;
- }
- } else if (queryInfo.windowId !== WindowManager.getId(window)) {
- continue;
- }
- }
-
- if (queryInfo.currentWindow !== null) {
- let eq = window === currentWindow(context);
- if (queryInfo.currentWindow != eq) {
- continue;
- }
- }
-
- let tabs = TabManager.for(extension).getTabs(window);
- for (let tab of tabs) {
- if (matches(tab)) {
- result.push(tab);
- }
- }
- }
- return Promise.resolve(result);
+ return Array.from(tabManager.query(queryInfo, context),
+ tab => tab.convert());
},
- captureVisibleTab: function(windowId, options) {
+ captureVisibleTab(windowId, options) {
if (!extension.hasPermission("<all_urls>")) {
return Promise.reject({message: "The <all_urls> permission is required to use the captureVisibleTab API"});
}
let window = windowId == null ?
- WindowManager.topWindow :
- WindowManager.getWindow(windowId, context);
+ windowTracker.topWindow :
+ windowTracker.getWindow(windowId, context);
let tab = window.gBrowser.selectedTab;
return tabListener.awaitTabReady(tab).then(() => {
let browser = tab.linkedBrowser;
let recipient = {
innerWindowID: browser.innerWindowID,
};
@@ -783,48 +580,48 @@ extensions.registerSchemaAPI("tabs", "ad
height: browser.clientHeight,
};
return context.sendMessage(browser.messageManager, "Extension:Capture",
message, {recipient});
});
},
- detectLanguage: function(tabId) {
- let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+ async detectLanguage(tabId) {
+ let tab = getTabOrActive(tabId);
return tabListener.awaitTabReady(tab).then(() => {
let browser = tab.linkedBrowser;
let recipient = {innerWindowID: browser.innerWindowID};
return context.sendMessage(browser.messageManager, "Extension:DetectLanguage",
{}, {recipient});
});
},
// Used to executeScript, insertCSS and removeCSS.
_execute: function(tabId, details, kind, method) {
- let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+ let tab = getTabOrActive(tabId);
let options = {
js: [],
css: [],
remove_css: method == "removeCSS",
};
// We require a `code` or a `file` property, but we can't accept both.
if ((details.code === null) == (details.file === null)) {
return Promise.reject({message: `${method} requires either a 'code' or a 'file' property, but not both`});
}
if (details.frameId !== null && details.allFrames) {
return Promise.reject({message: `'frameId' and 'allFrames' are mutually exclusive`});
}
- if (TabManager.for(extension).hasActiveTabPermission(tab)) {
+ if (tabManager.hasActiveTabPermission(tab)) {
// If we have the "activeTab" permission for this tab, ignore
// the host whitelist.
options.matchesHost = ["<all_urls>"];
} else {
options.matchesHost = extension.whiteListedHosts.serialize();
}
if (details.code !== null) {
@@ -874,42 +671,42 @@ extensions.registerSchemaAPI("tabs", "ad
insertCSS: function(tabId, details) {
return self.tabs._execute(tabId, details, "css", "insertCSS").then(() => {});
},
removeCSS: function(tabId, details) {
return self.tabs._execute(tabId, details, "css", "removeCSS").then(() => {});
},
- move: function(tabIds, moveProperties) {
+ async move(tabIds, moveProperties) {
let index = moveProperties.index;
let tabsMoved = [];
if (!Array.isArray(tabIds)) {
tabIds = [tabIds];
}
let destinationWindow = null;
if (moveProperties.windowId !== null) {
- destinationWindow = WindowManager.getWindow(moveProperties.windowId, context);
+ destinationWindow = windowTracker.getWindow(moveProperties.windowId);
// Fail on an invalid window.
if (!destinationWindow) {
return Promise.reject({message: `Invalid window ID: ${moveProperties.windowId}`});
}
}
/*
Indexes are maintained on a per window basis so that a call to
move([tabA, tabB], {index: 0})
-> tabA to 0, tabB to 1 if tabA and tabB are in the same window
move([tabA, tabB], {index: 0})
-> tabA to 0, tabB to 0 if tabA and tabB are in different windows
*/
let indexMap = new Map();
- let tabs = tabIds.map(tabId => TabManager.getTab(tabId, context));
+ let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
for (let tab of tabs) {
// If the window is not specified, use the window from the tab.
let window = destinationWindow || tab.ownerGlobal;
let gBrowser = window.gBrowser;
let insertionPoint = indexMap.get(window) || index;
// If the index is -1 it should go to the end of the tabs.
if (insertionPoint == -1) {
@@ -934,60 +731,60 @@ extensions.registerSchemaAPI("tabs", "ad
tab = gBrowser.adoptTab(tab, insertionPoint, false);
} else {
// If the window we are moving is the same, just move the tab.
gBrowser.moveTabTo(tab, insertionPoint);
}
tabsMoved.push(tab);
}
- return Promise.resolve(tabsMoved.map(tab => TabManager.convert(extension, tab)));
+ return tabsMoved.map(tab => tabManager.convert(tab));
},
- duplicate: function(tabId) {
- let tab = TabManager.getTab(tabId, context);
+ duplicate(tabId) {
+ let tab = tabTracker.getTab(tabId);
let gBrowser = tab.ownerGlobal.gBrowser;
let newTab = gBrowser.duplicateTab(tab);
return new Promise(resolve => {
// We need to use SSTabRestoring because any attributes set before
// are ignored. SSTabRestored is too late and results in a jump in
// the UI. See http://bit.ly/session-store-api for more information.
newTab.addEventListener("SSTabRestoring", function listener() {
// As the tab is restoring, move it to the correct position.
- newTab.removeEventListener("SSTabRestoring", listener);
+
// Pinned tabs that are duplicated are inserted
// after the existing pinned tab and pinned.
if (tab.pinned) {
gBrowser.pinTab(newTab);
}
gBrowser.moveTabTo(newTab, tab._tPos + 1);
- });
+ }, {once: true});
newTab.addEventListener("SSTabRestored", function listener() {
// Once it has been restored, select it and return the promise.
- newTab.removeEventListener("SSTabRestored", listener);
gBrowser.selectedTab = newTab;
- return resolve(TabManager.convert(extension, newTab));
- });
+
+ return resolve(tabManager.convert(newTab));
+ }, {once: true});
});
},
getZoom(tabId) {
- let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+ let tab = getTabOrActive(tabId);
let {ZoomManager} = tab.ownerGlobal;
let zoom = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
return Promise.resolve(zoom);
},
setZoom(tabId, zoom) {
- let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+ let tab = getTabOrActive(tabId);
let {FullZoom, ZoomManager} = tab.ownerGlobal;
if (zoom === 0) {
// A value of zero means use the default zoom factor.
return FullZoom.reset(tab.linkedBrowser);
} else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
FullZoom.setZoom(zoom, tab.linkedBrowser);
@@ -996,33 +793,33 @@ extensions.registerSchemaAPI("tabs", "ad
message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
});
}
return Promise.resolve();
},
_getZoomSettings(tabId) {
- let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+ let tab = getTabOrActive(tabId);
let {FullZoom} = tab.ownerGlobal;
return {
mode: "automatic",
scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
defaultZoomFactor: 1,
};
},
getZoomSettings(tabId) {
return Promise.resolve(this._getZoomSettings(tabId));
},
setZoomSettings(tabId, settings) {
- let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+ let tab = getTabOrActive(tabId);
let currentSettings = this._getZoomSettings(tab.id);
if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
}
return Promise.resolve();
},
@@ -1034,17 +831,17 @@ extensions.registerSchemaAPI("tabs", "ad
return ZoomManager.getZoomForBrowser(browser);
};
// Stores the last known zoom level for each tab's browser.
// WeakMap[<browser> -> number]
let zoomLevels = new WeakMap();
// Store the zoom level for all existing tabs.
- for (let window of WindowListManager.browserWindows()) {
+ for (let window of windowTracker.browserWindows()) {
for (let tab of window.gBrowser.tabs) {
let browser = tab.linkedBrowser;
zoomLevels.set(browser, getZoomLevel(browser));
}
}
let tabCreated = (eventName, event) => {
let browser = event.tab.linkedBrowser;
@@ -1071,35 +868,35 @@ extensions.registerSchemaAPI("tabs", "ad
}
let oldZoomFactor = zoomLevels.get(browser);
let newZoomFactor = getZoomLevel(browser);
if (oldZoomFactor != newZoomFactor) {
zoomLevels.set(browser, newZoomFactor);
- let tabId = TabManager.getId(tab);
+ let tabId = tabTracker.getId(tab);
fire({
tabId,
oldZoomFactor,
newZoomFactor,
zoomSettings: self.tabs._getZoomSettings(tabId),
});
}
};
- tabListener.on("tab-attached", tabCreated);
- tabListener.on("tab-created", tabCreated);
+ tabTracker.on("tab-attached", tabCreated);
+ tabTracker.on("tab-created", tabCreated);
- AllWindowEvents.addListener("FullZoomChange", zoomListener);
- AllWindowEvents.addListener("TextZoomChange", zoomListener);
+ windowTracker.addListener("FullZoomChange", zoomListener);
+ windowTracker.addListener("TextZoomChange", zoomListener);
return () => {
- tabListener.off("tab-attached", tabCreated);
- tabListener.off("tab-created", tabCreated);
+ tabTracker.off("tab-attached", tabCreated);
+ tabTracker.off("tab-created", tabCreated);
- AllWindowEvents.removeListener("FullZoomChange", zoomListener);
- AllWindowEvents.removeListener("TextZoomChange", zoomListener);
+ windowTracker.removeListener("FullZoomChange", zoomListener);
+ windowTracker.removeListener("TextZoomChange", zoomListener);
};
}).api(),
},
};
return self;
});
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -1,42 +1,48 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
+/* globals TabBase, WindowBase, TabTrackerBase, WindowTrackerBase, TabManagerBase, WindowManagerBase */
+Cu.import("resource://gre/modules/ExtensionTabs.jsm");
+
XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
"@mozilla.org/content/style-sheet-service;1",
"nsIStyleSheetService");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
const POPUP_LOAD_TIMEOUT_MS = 200;
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
var {
DefaultWeakMap,
EventManager,
+ ExtensionError,
+ defineLazyGetter,
promiseEvent,
} = ExtensionUtils;
+let tabTracker;
+let windowTracker;
+
// This file provides some useful code for the |tabs| and |windows|
// modules. All of the code is installed on |global|, which is a scope
// shared among the different ext-*.js scripts.
global.makeWidgetId = id => {
id = id.toLowerCase();
// FIXME: This allows for collisions.
return id.replace(/[^a-z0-9_-]/g, "_");
@@ -552,18 +558,18 @@ Object.assign(global, {PanelPopup, ViewP
// across all windows.
global.TabContext = function TabContext(getDefaults, extension) {
this.extension = extension;
this.getDefaults = getDefaults;
this.tabData = new WeakMap();
this.lastLocation = new WeakMap();
- AllWindowEvents.addListener("progress", this);
- AllWindowEvents.addListener("TabSelect", this);
+ windowTracker.addListener("progress", this);
+ windowTracker.addListener("TabSelect", this);
EventEmitter.decorate(this);
};
TabContext.prototype = {
get(tab) {
if (!this.tabData.has(tab)) {
this.tabData.set(tab, this.getDefaults(tab));
@@ -600,143 +606,18 @@ TabContext.prototype = {
!(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) {
let tab = gBrowser.getTabForBrowser(browser);
this.emit("location-change", tab, true);
}
this.lastLocation.set(browser, browser.currentURI);
},
shutdown() {
- AllWindowEvents.removeListener("progress", this);
- AllWindowEvents.removeListener("TabSelect", this);
- },
-};
-
-// Manages tab mappings and permissions for a specific extension.
-function ExtensionTabManager(extension) {
- this.extension = extension;
-
- // A mapping of tab objects to the inner window ID the extension currently has
- // the active tab permission for. The active permission for a given tab is
- // valid only for the inner window that was active when the permission was
- // granted. If the tab navigates, the inner window ID changes, and the
- // permission automatically becomes stale.
- //
- // WeakMap[tab => inner-window-id<int>]
- this.hasTabPermissionFor = new WeakMap();
-}
-
-ExtensionTabManager.prototype = {
- addActiveTabPermission(tab = TabManager.activeTab) {
- if (this.extension.hasPermission("activeTab")) {
- // Note that, unlike Chrome, we don't currently clear this permission with
- // the tab navigates. If the inner window is revived from BFCache before
- // we've granted this permission to a new inner window, the extension
- // maintains its permissions for it.
- this.hasTabPermissionFor.set(tab, tab.linkedBrowser.innerWindowID);
- }
- },
-
- revokeActiveTabPermission(tab = TabManager.activeTab) {
- this.hasTabPermissionFor.delete(tab);
- },
-
- // Returns true if the extension has the "activeTab" permission for this tab.
- // This is somewhat more permissive than the generic "tabs" permission, as
- // checked by |hasTabPermission|, in that it also allows programmatic script
- // injection without an explicit host permission.
- hasActiveTabPermission(tab) {
- // This check is redundant with addTabPermission, but cheap.
- if (this.extension.hasPermission("activeTab")) {
- return (this.hasTabPermissionFor.has(tab) &&
- this.hasTabPermissionFor.get(tab) === tab.linkedBrowser.innerWindowID);
- }
- return false;
- },
-
- hasTabPermission(tab) {
- return this.extension.hasPermission("tabs") || this.hasActiveTabPermission(tab);
- },
-
- convert(tab) {
- let window = tab.ownerGlobal;
- let browser = tab.linkedBrowser;
-
- let mutedInfo = {muted: tab.muted};
- if (tab.muteReason === null) {
- mutedInfo.reason = "user";
- } else if (tab.muteReason) {
- mutedInfo.reason = "extension";
- mutedInfo.extensionId = tab.muteReason;
- }
-
- let result = {
- id: TabManager.getId(tab),
- index: tab._tPos,
- windowId: WindowManager.getId(window),
- selected: tab.selected,
- highlighted: tab.selected,
- active: tab.selected,
- pinned: tab.pinned,
- status: TabManager.getStatus(tab),
- incognito: WindowManager.isBrowserPrivate(browser),
- width: browser.frameLoader.lazyWidth || browser.clientWidth,
- height: browser.frameLoader.lazyHeight || browser.clientHeight,
- audible: tab.soundPlaying,
- mutedInfo,
- };
- if (this.extension.hasPermission("cookies")) {
- result.cookieStoreId = getCookieStoreIdForTab(result, tab);
- }
-
- if (this.hasTabPermission(tab)) {
- result.url = browser.currentURI.spec;
- let title = browser.contentTitle || tab.label;
- if (title) {
- result.title = title;
- }
- let icon = window.gBrowser.getIcon(tab);
- if (icon) {
- result.favIconUrl = icon;
- }
- }
-
- return result;
- },
-
- // Converts tabs returned from SessionStore.getClosedTabData and
- // SessionStore.getClosedWindowData into API tab objects
- convertFromSessionStoreClosedData(tab, window) {
- let result = {
- sessionId: String(tab.closedId),
- index: tab.pos ? tab.pos : 0,
- windowId: WindowManager.getId(window),
- selected: false,
- highlighted: false,
- active: false,
- pinned: false,
- incognito: Boolean(tab.state && tab.state.isPrivate),
- };
-
- if (this.hasTabPermission(tab)) {
- let entries = tab.state ? tab.state.entries : tab.entries;
- result.url = entries[0].url;
- result.title = entries[0].title;
- if (tab.image) {
- result.favIconUrl = tab.image;
- }
- }
-
- return result;
- },
-
- getTabs(window) {
- return Array.from(window.gBrowser.tabs)
- .filter(tab => !tab.closing)
- .map(tab => this.convert(tab));
+ windowTracker.removeListener("progress", this);
+ windowTracker.removeListener("TabSelect", this);
},
};
function getBrowserInfo(browser) {
if (!browser.ownerGlobal.gBrowser) {
// When we're loaded into a <browser> inside about:addons, we need to go up
// one more level.
browser = browser.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -749,20 +630,20 @@ function getBrowserInfo(browser) {
}
let result = {};
let window = browser.ownerGlobal;
if (window.gBrowser) {
let tab = window.gBrowser.getTabForBrowser(browser);
if (tab) {
- result.tabId = TabManager.getId(tab);
+ result.tabId = tabTracker.getId(tab);
}
- result.windowId = WindowManager.getId(window);
+ result.windowId = windowTracker.getId(window);
}
return result;
}
global.getBrowserInfo = getBrowserInfo;
// Sends the tab and windowId upon request. This is primarily used to support
// the synchronous `browser.extension.getViews` API.
@@ -778,252 +659,446 @@ let onGetTabAndWindowId = {
}
},
};
/* eslint-disable mozilla/balanced-listeners */
Services.mm.addMessageListener("Extension:GetTabAndWindowId", onGetTabAndWindowId);
/* eslint-enable mozilla/balanced-listeners */
-// Manages global mappings between XUL tabs and extension tab IDs.
-global.TabManager = {
- _tabs: new WeakMap(),
- _nextId: 1,
- _initialized: false,
+class WindowTracker extends WindowTrackerBase {
+ addProgressListener(window, listener) {
+ window.gBrowser.addTabsProgressListener(listener);
+ }
+
+ removeProgressListener(window, listener) {
+ window.gBrowser.removeTabsProgressListener(listener);
+ }
+}
+
+global.WindowEventManager = class extends EventManager {
+ constructor(context, name, event, listener) {
+ super(context, name, fire => {
+ let listener2 = listener.bind(null, fire);
- // We begin listening for TabOpen and TabClose events once we've started
- // assigning IDs to tabs, so that we can remap the IDs of tabs which are moved
- // between windows.
- initListener() {
- if (this._initialized) {
+ windowTracker.addListener(event, listener2);
+ return () => {
+ windowTracker.removeListener(event, listener2);
+ };
+ });
+ }
+};
+
+class TabTracker extends TabTrackerBase {
+ constructor() {
+ super();
+
+ this._tabs = new WeakMap();
+ this._tabIds = new Map();
+ this._nextId = 1;
+
+ this.handleTabDestroyed = this.handleTabDestroyed.bind(this);
+ }
+
+ init() {
+ if (this.initialized) {
return;
}
+ this.initialized = true;
- AllWindowEvents.addListener("TabOpen", this);
- AllWindowEvents.addListener("TabClose", this);
- WindowListManager.addOpenListener(this.handleWindowOpen.bind(this));
+ this.adoptedTabs = new WeakMap();
+
+ this.handleWindowOpen = this.handleWindowOpen.bind(this);
+ this.handleWindowClose = this.handleWindowClose.bind(this);
+
+ windowTracker.addListener("TabClose", this);
+ windowTracker.addListener("TabOpen", this);
+ windowTracker.addOpenListener(this.handleWindowOpen);
+ windowTracker.addCloseListener(this.handleWindowClose);
+
+ /* eslint-disable mozilla/balanced-listeners */
+ this.on("tab-detached", this.handleTabDestroyed);
+ this.on("tab-removed", this.handleTabDestroyed);
+ /* eslint-enable mozilla/balanced-listeners */
+ }
+
+ getId(tab) {
+ if (this._tabs.has(tab)) {
+ return this._tabs.get(tab);
+ }
+
+ this.init();
+
+ let id = this._nextId++;
+ this.setId(tab, id);
+ return id;
+ }
+
+ setId(tab, id) {
+ this._tabs.set(tab, id);
+ this._tabIds.set(id, tab);
+ }
- this._initialized = true;
- },
+ handleTabDestroyed(event, {tab}) {
+ let id = this._tabs.get(tab);
+ if (id) {
+ this._tabs.delete(tab);
+ if (this._tabIds.get(id) === tab) {
+ this._tabIds.delete(tab);
+ }
+ }
+ }
+
+ /**
+ * Returns the XUL <tab> element associated with the given tab ID. If no tab
+ * with the given ID exists, and no default value is provided, an error is
+ * raised, belonging to the scope of the given context.
+ *
+ * @param {integer} tabId
+ * The ID of the tab to retrieve.
+ * @param {*} default_
+ * The value to return if no tab exists with the given ID.
+ * @returns {Element<tab>}
+ * A XUL <tab> element.
+ */
+ getTab(tabId, default_ = undefined) {
+ let tab = this._tabIds.get(tabId);
+ if (tab) {
+ return tab;
+ }
+ if (default_ !== undefined) {
+ return default_;
+ }
+ throw new ExtensionError(`Invalid tab ID: ${tabId}`);
+ }
handleEvent(event) {
- if (event.type == "TabOpen") {
- let {adoptedTab} = event.detail;
- if (adoptedTab) {
- // This tab is being created to adopt a tab from a different window.
- // Copy the ID from the old tab to the new.
- let tab = event.target;
- this._tabs.set(tab, this.getId(adoptedTab));
+ let tab = event.target;
+
+ switch (event.type) {
+ case "TabOpen":
+ let {adoptedTab} = event.detail;
+ if (adoptedTab) {
+ this.adoptedTabs.set(adoptedTab, event.target);
+
+ // This tab is being created to adopt a tab from a different window.
+ // Copy the ID from the old tab to the new.
+ this.setId(tab, this.getId(adoptedTab));
- tab.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", {
- windowId: WindowManager.getId(tab.ownerGlobal),
+ adoptedTab.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", {
+ windowId: windowTracker.getId(tab.ownerGlobal),
+ });
+ }
+
+ // We need to delay sending this event until the next tick, since the
+ // tab does not have its final index when the TabOpen event is dispatched.
+ Promise.resolve().then(() => {
+ if (event.detail.adoptedTab) {
+ this.emitAttached(event.originalTarget);
+ } else {
+ this.emitCreated(event.originalTarget);
+ }
});
- }
- } else if (event.type == "TabClose") {
- let {adoptedBy} = event.detail;
- if (adoptedBy) {
- // This tab is being closed because it was adopted by a new window.
- // Copy its ID to the new tab, in case it was created as the first tab
- // of a new window, and did not have an `adoptedTab` detail when it was
- // opened.
- this._tabs.set(adoptedBy, this.getId(event.target));
+ break;
- adoptedBy.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", {
- windowId: WindowManager.getId(adoptedBy),
- });
- }
+ case "TabClose":
+ let {adoptedBy} = event.detail;
+ if (adoptedBy) {
+ // This tab is being closed because it was adopted by a new window.
+ // Copy its ID to the new tab, in case it was created as the first tab
+ // of a new window, and did not have an `adoptedTab` detail when it was
+ // opened.
+ this.setId(adoptedBy, this.getId(tab));
+
+ this.emitDetached(tab, adoptedBy);
+ } else {
+ this.emitRemoved(tab, false);
+ }
+ break;
}
- },
+ }
handleWindowOpen(window) {
if (window.arguments && window.arguments[0] instanceof window.XULElement) {
// If the first window argument is a XUL element, it means the
// window is about to adopt a tab from another window to replace its
// initial tab.
- let adoptedTab = window.arguments[0];
+ //
+ // Note that this event handler depends on running before the
+ // delayed startup code in browser.js, which is currently triggered
+ // by the first MozAfterPaint event. That code handles finally
+ // adopting the tab, and clears it from the arguments list in the
+ // process, so if we run later than it, we're too late.
+ let tab = window.arguments[0];
+ let adoptedBy = window.gBrowser.tabs[0];
+
+ this.adoptedTabs.set(tab, adoptedBy);
+ this.setId(adoptedBy, this.getId(tab));
- this._tabs.set(window.gBrowser.tabs[0], this.getId(adoptedTab));
+ // We need to be sure to fire this event after the onDetached event
+ // for the original tab.
+ let listener = (event, details) => {
+ if (details.tab === tab) {
+ this.off("tab-detached", listener);
+
+ Promise.resolve().then(() => {
+ this.emitAttached(details.adoptedBy);
+ });
+ }
+ };
+
+ this.on("tab-detached", listener);
+ } else {
+ for (let tab of window.gBrowser.tabs) {
+ this.emitCreated(tab);
+ }
}
- },
+ }
- getId(tab) {
- if (this._tabs.has(tab)) {
- return this._tabs.get(tab);
+ handleWindowClose(window) {
+ for (let tab of window.gBrowser.tabs) {
+ if (this.adoptedTabs.has(tab)) {
+ this.emitDetached(tab, this.adoptedTabs.get(tab));
+ } else {
+ this.emitRemoved(tab, true);
+ }
}
- this.initListener();
+ }
+
+ emitAttached(tab) {
+ let newWindowId = windowTracker.getId(tab.ownerGlobal);
+ let tabId = this.getId(tab);
+
+ this.emit("tab-attached", {tab, tabId, newWindowId, newPosition: tab._tPos});
+ }
+
+ emitDetached(tab, adoptedBy) {
+ let oldWindowId = windowTracker.getId(tab.ownerGlobal);
+ let tabId = this.getId(tab);
+
+ this.emit("tab-detached", {tab, adoptedBy, tabId, oldWindowId, oldPosition: tab._tPos});
+ }
- let id = this._nextId++;
- this._tabs.set(tab, id);
- return id;
- },
+ emitCreated(tab) {
+ this.emit("tab-created", {tab});
+ }
+
+ emitRemoved(tab, isWindowClosing) {
+ let windowId = windowTracker.getId(tab.ownerGlobal);
+ let tabId = this.getId(tab);
+
+ // When addons run in-process, `window.close()` is synchronous. Most other
+ // addon-invoked calls are asynchronous since they go through a proxy
+ // context via the message manager. This includes event registrations such
+ // as `tabs.onRemoved.addListener`.
+ //
+ // So, even if `window.close()` were to be called (in-process) after calling
+ // `tabs.onRemoved.addListener`, then the tab would be closed before the
+ // event listener is registered. To make sure that the event listener is
+ // notified, we dispatch `tabs.onRemoved` asynchronously.
+ Services.tm.mainThread.dispatch(() => {
+ this.emit("tab-removed", {tab, tabId, windowId, isWindowClosing});
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ }
getBrowserId(browser) {
- let gBrowser = browser.ownerGlobal.gBrowser;
+ let {gBrowser} = browser.ownerGlobal;
// Some non-browser windows have gBrowser but not
// getTabForBrowser!
if (gBrowser && gBrowser.getTabForBrowser) {
let tab = gBrowser.getTabForBrowser(browser);
if (tab) {
return this.getId(tab);
}
}
return -1;
- },
-
- /**
- * Returns the XUL <tab> element associated with the given tab ID. If no tab
- * with the given ID exists, and no default value is provided, an error is
- * raised, belonging to the scope of the given context.
- *
- * @param {integer} tabId
- * The ID of the tab to retrieve.
- * @param {ExtensionContext} context
- * The context of the caller.
- * This value may be omitted if `default_` is not `undefined`.
- * @param {*} default_
- * The value to return if no tab exists with the given ID.
- * @returns {Element<tab>}
- * A XUL <tab> element.
- */
- getTab(tabId, context, default_ = undefined) {
- // FIXME: Speed this up without leaking memory somehow.
- for (let window of WindowListManager.browserWindows()) {
- if (!window.gBrowser) {
- continue;
- }
- for (let tab of window.gBrowser.tabs) {
- if (this.getId(tab) == tabId) {
- return tab;
- }
- }
- }
- if (default_ !== undefined) {
- return default_;
- }
- throw new context.cloneScope.Error(`Invalid tab ID: ${tabId}`);
- },
+ }
get activeTab() {
- let window = WindowManager.topWindow;
+ let window = windowTracker.topWindow;
if (window && window.gBrowser) {
return window.gBrowser.selectedTab;
}
return null;
- },
-
- getStatus(tab) {
- return tab.getAttribute("busy") == "true" ? "loading" : "complete";
- },
-
- convert(extension, tab) {
- return TabManager.for(extension).convert(tab);
- },
-};
-
-// WeakMap[Extension -> ExtensionTabManager]
-let tabManagers = new WeakMap();
-
-// Returns the extension-specific tab manager for the given extension, or
-// creates one if it doesn't already exist.
-TabManager.for = function(extension) {
- if (!tabManagers.has(extension)) {
- tabManagers.set(extension, new ExtensionTabManager(extension));
}
- return tabManagers.get(extension);
-};
-
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("shutdown", (type, extension) => {
- tabManagers.delete(extension);
-});
-/* eslint-enable mozilla/balanced-listeners */
-
-function memoize(fn) {
- let weakMap = new DefaultWeakMap(fn);
- return weakMap.get.bind(weakMap);
}
-// Manages mapping between XUL windows and extension window IDs.
-global.WindowManager = {
- // Note: These must match the values in windows.json.
- WINDOW_ID_NONE: -1,
- WINDOW_ID_CURRENT: -2,
+windowTracker = new WindowTracker();
+tabTracker = new TabTracker();
+
+Object.assign(global, {tabTracker, windowTracker});
+
+class Tab extends TabBase {
+ get _favIconUrl() {
+ return this.window.gBrowser.getIcon(this.tab);
+ }
- get topWindow() {
- return Services.wm.getMostRecentWindow("navigator:browser");
- },
+ get audible() {
+ return this.tab.soundPlaying;
+ }
+
+ get browser() {
+ return this.tab.linkedBrowser;
+ }
+
+ get cookieStoreId() {
+ return getCookieStoreIdForTab(this, this.tab);
+ }
- windowType(window) {
- // TODO: Make this work.
+ get height() {
+ return this.browser.clientHeight;
+ }
+
+ get index() {
+ return this.tab._tPos;
+ }
- let {chromeFlags} = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIXULWindow);
+ get innerWindowID() {
+ return this.browser.innerWindowID;
+ }
+
+ get mutedInfo() {
+ let tab = this.tab;
- if (chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) {
- return "popup";
+ let mutedInfo = {muted: tab.muted};
+ if (tab.muteReason === null) {
+ mutedInfo.reason = "user";
+ } else if (tab.muteReason) {
+ mutedInfo.reason = "extension";
+ mutedInfo.extensionId = tab.muteReason;
}
- return "normal";
- },
+ return mutedInfo;
+ }
+
+ get pinned() {
+ return this.tab.pinned;
+ }
+
+ get active() {
+ return this.tab.selected;
+ }
+
+ get selected() {
+ return this.tab.selected;
+ }
+
+ get status() {
+ if (this.tab.getAttribute("busy") === "true") {
+ return "loading";
+ }
+ return "complete";
+ }
+
+ get width() {
+ return this.browser.clientWidth;
+ }
+
+ get window() {
+ return this.tab.ownerGlobal;
+ }
- updateGeometry(window, options) {
+ get windowId() {
+ return windowTracker.getId(this.window);
+ }
+
+ static convertFromSessionStoreClosedData(extension, tab, window = null) {
+ let result = {
+ sessionId: String(tab.closedId),
+ index: tab.pos ? tab.pos : 0,
+ windowId: window && windowTracker.getId(window),
+ selected: false,
+ highlighted: false,
+ active: false,
+ pinned: false,
+ incognito: Boolean(tab.state && tab.state.isPrivate),
+ };
+
+ if (extension.tabManager.hasTabPermission(tab)) {
+ let entries = tab.state ? tab.state.entries : tab.entries;
+ result.url = entries[0].url;
+ result.title = entries[0].title;
+ if (tab.image) {
+ result.favIconUrl = tab.image;
+ }
+ }
+
+ return result;
+ }
+}
+
+class Window extends WindowBase {
+ updateGeometry(options) {
+ let {window} = this;
+
if (options.left !== null || options.top !== null) {
let left = options.left !== null ? options.left : window.screenX;
let top = options.top !== null ? options.top : window.screenY;
window.moveTo(left, top);
}
if (options.width !== null || options.height !== null) {
let width = options.width !== null ? options.width : window.outerWidth;
let height = options.height !== null ? options.height : window.outerHeight;
window.resizeTo(width, height);
}
- },
+ }
- isBrowserPrivate: memoize(browser => {
- return PrivateBrowsingUtils.isBrowserPrivate(browser);
- }),
+ get focused() {
+ return this.window.document.hasFocus();
+ }
- getId: memoize(window => {
- if (window instanceof Ci.nsIInterfaceRequestor) {
- return window.getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
- }
- return null;
- }),
+ get top() {
+ return this.window.screenY;
+ }
+
+ get left() {
+ return this.window.screenX;
+ }
+
+ get width() {
+ return this.window.outerWidth;
+ }
- getWindow(id, context) {
- if (id == this.WINDOW_ID_CURRENT) {
- return currentWindow(context);
- }
+ get height() {
+ return this.window.outerHeight;
+ }
+
+ get incognito() {
+ return PrivateBrowsingUtils.isWindowPrivate(this.window);
+ }
- for (let window of WindowListManager.browserWindows(true)) {
- if (this.getId(window) == id) {
- return window;
- }
- }
- return null;
- },
+ get alwaysOnTop() {
+ return this.xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ;
+ }
- getState(window) {
+ get isLastFocused() {
+ return this.window === windowTracker.topWindow;
+ }
+
+ static getState(window) {
const STATES = {
[window.STATE_MAXIMIZED]: "maximized",
[window.STATE_MINIMIZED]: "minimized",
[window.STATE_NORMAL]: "normal",
};
let state = STATES[window.windowState];
if (window.fullScreen) {
state = "fullscreen";
}
return state;
- },
+ }
- setState(window, state) {
- if (state != "fullscreen" && window.fullScreen) {
+ get state() {
+ return Window.getState(this.window);
+ }
+
+ set state(state) {
+ let {window} = this;
+ if (state !== "fullscreen" && window.fullScreen) {
window.fullScreen = false;
}
switch (state) {
case "maximized":
window.maximize();
break;
@@ -1032,266 +1107,106 @@ global.WindowManager = {
window.minimize();
break;
case "normal":
// Restore sometimes returns the window to its previous state, rather
// than to the "normal" state, so it may need to be called anywhere from
// zero to two times.
window.restore();
- if (window.windowState != window.STATE_NORMAL) {
+ if (window.windowState !== window.STATE_NORMAL) {
window.restore();
}
- if (window.windowState != window.STATE_NORMAL) {
+ if (window.windowState !== window.STATE_NORMAL) {
// And on OS-X, where normal vs. maximized is basically a heuristic,
// we need to cheat.
window.sizeToContent();
}
break;
case "fullscreen":
window.fullScreen = true;
break;
default:
throw new Error(`Unexpected window state: ${state}`);
}
- },
+ }
- convert(extension, window, getInfo) {
- let xulWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIXULWindow);
+ * getTabs() {
+ let {tabManager} = this.extension;
- let result = {
- id: this.getId(window),
- focused: window.document.hasFocus(),
- top: window.screenY,
- left: window.screenX,
- width: window.outerWidth,
- height: window.outerHeight,
- incognito: PrivateBrowsingUtils.isWindowPrivate(window),
- type: this.windowType(window),
- state: this.getState(window),
- alwaysOnTop: xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ,
- };
+ for (let tab of this.window.gBrowser.tabs) {
+ yield tabManager.getWrapper(tab);
+ }
+ }
- if (getInfo && getInfo.populate) {
- result.tabs = TabManager.for(extension).getTabs(window);
- }
-
- return result;
- },
-
- // Converts windows returned from SessionStore.getClosedWindowData
- // into API window objects
- convertFromSessionStoreClosedData(window, extension) {
+ static convertFromSessionStoreClosedData(extension, window) {
let result = {
sessionId: String(window.closedId),
focused: false,
incognito: false,
type: "normal", // this is always "normal" for a closed window
+ // Surely this does not actually work?
state: this.getState(window),
alwaysOnTop: false,
};
if (window.tabs.length) {
- result.tabs = [];
- window.tabs.forEach((tab, index) => {
- result.tabs.push(TabManager.for(extension).convertFromSessionStoreClosedData(tab, window, index));
+ result.tabs = window.tabs.map(tab => {
+ return Tab.convertFromSessionStoreClosedData(extension, tab);
});
}
return result;
- },
-};
-
-// Manages listeners for window opening and closing. A window is
-// considered open when the "load" event fires on it. A window is
-// closed when a "domwindowclosed" notification fires for it.
-global.WindowListManager = {
- _openListeners: new Set(),
- _closeListeners: new Set(),
+ }
+}
- // Returns an iterator for all browser windows. Unless |includeIncomplete| is
- // true, only fully-loaded windows are returned.
- * browserWindows(includeIncomplete = false) {
- // The window type parameter is only available once the window's document
- // element has been created. This means that, when looking for incomplete
- // browser windows, we need to ignore the type entirely for windows which
- // haven't finished loading, since we would otherwise skip browser windows
- // in their early loading stages.
- // This is particularly important given that the "domwindowcreated" event
- // fires for browser windows when they're in that in-between state, and just
- // before we register our own "domwindowcreated" listener.
-
- let e = Services.wm.getEnumerator("");
- while (e.hasMoreElements()) {
- let window = e.getNext();
-
- let ok = includeIncomplete;
- if (window.document.readyState == "complete") {
- ok = window.document.documentElement.getAttribute("windowtype") == "navigator:browser";
- }
-
- if (ok) {
- yield window;
- }
- }
- },
-
- addOpenListener(listener) {
- if (this._openListeners.size == 0 && this._closeListeners.size == 0) {
- Services.ww.registerNotification(this);
- }
- this._openListeners.add(listener);
+Object.assign(global, {Tab, Window});
- for (let window of this.browserWindows(true)) {
- if (window.document.readyState != "complete") {
- window.addEventListener("load", this);
- }
- }
- },
-
- removeOpenListener(listener) {
- this._openListeners.delete(listener);
- if (this._openListeners.size == 0 && this._closeListeners.size == 0) {
- Services.ww.unregisterNotification(this);
- }
- },
-
- addCloseListener(listener) {
- if (this._openListeners.size == 0 && this._closeListeners.size == 0) {
- Services.ww.registerNotification(this);
- }
- this._closeListeners.add(listener);
- },
+class TabManager extends TabManagerBase {
+ get(tabId, default_ = undefined) {
+ let tab = tabTracker.getTab(tabId, default_);
- removeCloseListener(listener) {
- this._closeListeners.delete(listener);
- if (this._openListeners.size == 0 && this._closeListeners.size == 0) {
- Services.ww.unregisterNotification(this);
- }
- },
-
- handleEvent(event) {
- event.currentTarget.removeEventListener(event.type, this);
- let window = event.target.defaultView;
- if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
- return;
- }
-
- for (let listener of this._openListeners) {
- listener(window);
- }
- },
-
- observe(window, topic, data) {
- if (topic == "domwindowclosed") {
- if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
- return;
- }
-
- window.removeEventListener("load", this);
- for (let listener of this._closeListeners) {
- listener(window);
- }
- } else {
- window.addEventListener("load", this);
+ if (tab) {
+ return this.getWrapper(tab);
}
- },
-};
-
-// Provides a facility to listen for DOM events across all XUL windows.
-global.AllWindowEvents = {
- _listeners: new Map(),
+ return default_;
+ }
- // If |type| is a normal event type, invoke |listener| each time
- // that event fires in any open window. If |type| is "progress", add
- // a web progress listener that covers all open windows.
- addListener(type, listener) {
- if (type == "domwindowopened") {
- return WindowListManager.addOpenListener(listener);
- } else if (type == "domwindowclosed") {
- return WindowListManager.addCloseListener(listener);
- }
-
- if (this._listeners.size == 0) {
- WindowListManager.addOpenListener(this.openListener);
- }
+ addActiveTabPermission(tab = tabTracker.activeTab) {
+ return super.addActiveTabPermission(tab);
+ }
- if (!this._listeners.has(type)) {
- this._listeners.set(type, new Set());
- }
- let list = this._listeners.get(type);
- list.add(listener);
-
- // Register listener on all existing windows.
- for (let window of WindowListManager.browserWindows()) {
- this.addWindowListener(window, type, listener);
- }
- },
+ revokeActiveTabPermission(tab = tabTracker.activeTab) {
+ return super.revokeActiveTabPermission(tab);
+ }
- removeListener(eventType, listener) {
- if (eventType == "domwindowopened") {
- return WindowListManager.removeOpenListener(listener);
- } else if (eventType == "domwindowclosed") {
- return WindowListManager.removeCloseListener(listener);
- }
+ wrapTab(tab) {
+ return new Tab(this.extension, tab, tabTracker.getId(tab));
+ }
+}
- let listeners = this._listeners.get(eventType);
- listeners.delete(listener);
- if (listeners.size == 0) {
- this._listeners.delete(eventType);
- if (this._listeners.size == 0) {
- WindowListManager.removeOpenListener(this.openListener);
- }
- }
+class WindowManager extends WindowManagerBase {
+ get(windowId, context) {
+ let window = windowTracker.getWindow(windowId, context);
- // Unregister listener from all existing windows.
- let useCapture = eventType === "focus" || eventType === "blur";
- for (let window of WindowListManager.browserWindows()) {
- if (eventType == "progress") {
- window.gBrowser.removeTabsProgressListener(listener);
- } else {
- window.removeEventListener(eventType, listener, useCapture);
- }
- }
- },
+ return this.getWrapper(window);
+ }
- /* eslint-disable mozilla/balanced-listeners */
- addWindowListener(window, eventType, listener) {
- let useCapture = eventType === "focus" || eventType === "blur";
-
- if (eventType == "progress") {
- window.gBrowser.addTabsProgressListener(listener);
- } else {
- window.addEventListener(eventType, listener, useCapture);
+ * getAll() {
+ for (let window of windowTracker.browserWindows()) {
+ yield this.getWrapper(window);
}
- },
- /* eslint-enable mozilla/balanced-listeners */
+ }
- // Runs whenever the "load" event fires for a new window.
- openListener(window) {
- for (let [eventType, listeners] of AllWindowEvents._listeners) {
- for (let listener of listeners) {
- this.addWindowListener(window, eventType, listener);
- }
- }
- },
-};
+ wrapWindow(window) {
+ return new Window(this.extension, window, windowTracker.getId(window));
+ }
+}
-AllWindowEvents.openListener = AllWindowEvents.openListener.bind(AllWindowEvents);
-// Subclass of EventManager where we just need to call
-// add/removeEventListener on each XUL window.
-global.WindowEventManager = function(context, name, event, listener) {
- EventManager.call(this, context, name, fire => {
- let listener2 = (...args) => listener(fire, ...args);
- AllWindowEvents.addListener(event, listener2);
- return () => {
- AllWindowEvents.removeListener(event, listener2);
- };
- });
-};
-
-WindowEventManager.prototype = Object.create(EventManager.prototype);
+extensions.on("startup", (type, extension) => { // eslint-disable-line mozilla/balanced-listeners
+ defineLazyGetter(extension, "tabManager",
+ () => new TabManager(extension));
+ defineLazyGetter(extension, "windowManager",
+ () => new WindowManager(extension));
+});
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -17,73 +17,76 @@ var {
} = ExtensionUtils;
function onXULFrameLoaderCreated({target}) {
target.messageManager.sendAsyncMessage("AllowScriptsToClose", {});
}
extensions.registerSchemaAPI("windows", "addon_parent", context => {
let {extension} = context;
+
+ const {windowManager} = extension;
+
return {
windows: {
onCreated:
new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
- fire(WindowManager.convert(extension, window));
+ fire(windowManager.convert(window));
}).api(),
onRemoved:
new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
- fire(WindowManager.getId(window));
+ fire(windowTracker.getId(window));
}).api(),
onFocusChanged: new EventManager(context, "windows.onFocusChanged", 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 window = Services.focus.activeWindow;
- let windowId = window ? WindowManager.getId(window) : WindowManager.WINDOW_ID_NONE;
+ let windowId = window ? windowTracker.getId(window) : Window.WINDOW_ID_NONE;
if (windowId !== lastOnFocusChangedWindowId) {
fire(windowId);
lastOnFocusChangedWindowId = windowId;
}
});
};
- AllWindowEvents.addListener("focus", listener);
- AllWindowEvents.addListener("blur", listener);
+ windowTracker.addListener("focus", listener);
+ windowTracker.addListener("blur", listener);
return () => {
- AllWindowEvents.removeListener("focus", listener);
- AllWindowEvents.removeListener("blur", listener);
+ windowTracker.removeListener("focus", listener);
+ windowTracker.removeListener("blur", listener);
};
}).api(),
get: function(windowId, getInfo) {
- let window = WindowManager.getWindow(windowId, context);
+ let window = windowTracker.getWindow(windowId, context);
if (!window) {
return Promise.reject({message: `Invalid window ID: ${windowId}`});
}
- return Promise.resolve(WindowManager.convert(extension, window, getInfo));
+ return Promise.resolve(windowManager.convert(window, getInfo));
},
getCurrent: function(getInfo) {
- let window = currentWindow(context);
- return Promise.resolve(WindowManager.convert(extension, window, getInfo));
+ let window = context.currentWindow || windowTracker.topWindow;
+ return Promise.resolve(windowManager.convert(window, getInfo));
},
getLastFocused: function(getInfo) {
- let window = WindowManager.topWindow;
- return Promise.resolve(WindowManager.convert(extension, window, getInfo));
+ let window = windowTracker.topWindow;
+ return Promise.resolve(windowManager.convert(window, getInfo));
},
getAll: function(getInfo) {
- let windows = Array.from(WindowListManager.browserWindows(),
- window => WindowManager.convert(extension, window, getInfo));
+ let windows = Array.from(windowManager.getAll(), win => win.convert(getInfo));
+
return Promise.resolve(windows);
},
create: function(createData) {
let needResize = (createData.left !== null || createData.top !== null ||
createData.width !== null || createData.height !== null);
if (needResize) {
@@ -105,17 +108,17 @@ extensions.registerSchemaAPI("windows",
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 = TabManager.getTab(createData.tabId, context);
+ let tab = tabTracker.getTab(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;
@@ -155,80 +158,81 @@ extensions.registerSchemaAPI("windows",
let {allowScriptsToClose, url} = createData;
if (allowScriptsToClose === null) {
allowScriptsToClose = typeof url === "string" && url.startsWith("moz-extension://");
}
let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
features.join(","), args);
- WindowManager.updateGeometry(window, createData);
+ let win = windowManager.getWrapper(window);
+ win.updateGeometry(createData);
// TODO: focused, type
return new Promise(resolve => {
window.addEventListener("load", function listener() {
window.removeEventListener("load", listener);
if (["maximized", "normal"].includes(createData.state)) {
window.document.documentElement.setAttribute("sizemode", createData.state);
}
resolve(promiseObserved("browser-delayed-startup-finished", win => win == window));
});
}).then(() => {
// Some states only work after delayed-startup-finished
if (["minimized", "fullscreen", "docked"].includes(createData.state)) {
- WindowManager.setState(window, createData.state);
+ win.state = createData.state;
}
if (allowScriptsToClose) {
for (let {linkedBrowser} of window.gBrowser.tabs) {
onXULFrameLoaderCreated({target: linkedBrowser});
linkedBrowser.addEventListener( // eslint-disable-line mozilla/balanced-listeners
"XULFrameLoaderCreated", onXULFrameLoaderCreated);
}
}
- return WindowManager.convert(extension, window, {populate: true});
+ 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 window = WindowManager.getWindow(windowId, context);
+ let win = windowManager.get(windowId, context);
if (updateInfo.focused) {
- Services.focus.activeWindow = window;
+ Services.focus.activeWindow = win.window;
}
if (updateInfo.state !== null) {
- WindowManager.setState(window, updateInfo.state);
+ win.state = updateInfo.state;
}
if (updateInfo.drawAttention) {
// Bug 1257497 - Firefox can't cancel attention actions.
- window.getAttention();
+ win.window.getAttention();
}
- WindowManager.updateGeometry(window, updateInfo);
+ win.updateGeometry(updateInfo);
// TODO: All the other properties, focused=false...
- return Promise.resolve(WindowManager.convert(extension, window));
+ return Promise.resolve(win.convert());
},
remove: function(windowId) {
- let window = WindowManager.getWindow(windowId, context);
+ let window = windowTracker.getWindow(windowId, context);
window.close();
return new Promise(resolve => {
let listener = () => {
- AllWindowEvents.removeListener("domwindowclosed", listener);
+ windowTracker.removeListener("domwindowclosed", listener);
resolve();
};
- AllWindowEvents.addListener("domwindowclosed", listener);
+ windowTracker.addListener("domwindowclosed", listener);
});
},
},
};
});
--- a/browser/components/extensions/test/browser/browser_ext_currentWindow.js
+++ b/browser/components/extensions/test/browser/browser_ext_currentWindow.js
@@ -85,20 +85,20 @@ add_task(function* () {
"popup.js": genericChecker,
},
background: genericChecker,
});
yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
- let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ let {Management: {global: {windowTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
- let winId1 = WindowManager.getId(win1);
- let winId2 = WindowManager.getId(win2);
+ let winId1 = windowTracker.getId(win1);
+ let winId2 = windowTracker.getId(win2);
function* checkWindow(kind, winId, name) {
extension.sendMessage(kind + "-check-current1");
is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 1) [${kind}]`);
extension.sendMessage(kind + "-check-current2");
is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 2) [${kind}]`);
extension.sendMessage(kind + "-check-current3");
is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 3) [${kind}]`);
--- a/browser/components/extensions/test/browser/browser_ext_getViews.js
+++ b/browser/components/extensions/test/browser/browser_ext_getViews.js
@@ -99,20 +99,20 @@ add_task(function* () {
background: genericChecker,
});
yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
info("started");
- let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ let {Management: {global: {windowTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
- let winId1 = WindowManager.getId(win1);
- let winId2 = WindowManager.getId(win2);
+ let winId1 = windowTracker.getId(win1);
+ let winId2 = windowTracker.getId(win2);
function* openTab(winId) {
extension.sendMessage("background-open-tab", winId);
yield extension.awaitMessage("tab-ready");
}
function* checkViews(kind, tabCount, popupCount, kindCount, windowId = undefined, windowCount = 0) {
extension.sendMessage(kind + "-check-views", windowId);
--- a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
@@ -27,18 +27,18 @@ add_task(function* test_sessions_get_rec
background,
});
// Open a private browsing window.
let privateWin = yield BrowserTestUtils.openNewBrowserWindow({private: true});
yield extension.startup();
- let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
- let privateWinId = WindowManager.getId(privateWin);
+ let {Management: {global: {windowTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ let privateWinId = windowTracker.getId(privateWin);
extension.sendMessage("check-sessions");
let recentlyClosed = yield extension.awaitMessage("recentlyClosed");
recordInitialTimestamps(recentlyClosed.map(item => item.lastModified));
// Open and close two tabs in the private window
let tab = yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, "http://example.com");
yield BrowserTestUtils.removeTab(tab);
--- a/browser/components/extensions/test/browser/browser_ext_sessions_restore.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_restore.js
@@ -50,20 +50,20 @@ add_task(function* test_sessions_restore
function* assertNotificationCount(expected) {
let notificationCount = yield extension.awaitMessage("notificationCount");
is(notificationCount, expected, "the expected number of notifications was fired");
}
yield extension.startup();
- let {Management: {global: {WindowManager, TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ let {Management: {global: {windowTracker, tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
function checkLocalTab(tab, expectedUrl) {
- let realTab = TabManager.getTab(tab.id);
+ let realTab = tabTracker.getTab(tab.id);
let tabState = JSON.parse(SessionStore.getTabState(realTab));
is(tabState.entries[0].url, expectedUrl, "restored tab has the expected url");
}
yield extension.awaitMessage("ready");
let win = yield BrowserTestUtils.openNewBrowserWindow();
yield BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:config");
@@ -87,17 +87,17 @@ add_task(function* test_sessions_restore
is(restored.length, 1, "restore returned the expected number of sessions");
is(restored[0].window.tabs.length, 3, "restore returned a window with the expected number of tabs");
checkLocalTab(restored[0].window.tabs[0], "about:config");
checkLocalTab(restored[0].window.tabs[1], "about:robots");
checkLocalTab(restored[0].window.tabs[2], "about:mozilla");
// Close the window again.
- let window = WindowManager.getWindow(restored[0].window.id);
+ let window = windowTracker.getWindow(restored[0].window.id);
yield BrowserTestUtils.closeWindow(window);
yield assertNotificationCount(3);
// Restore the window using the sessionId.
extension.sendMessage("check-sessions");
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
extension.sendMessage("restore", recentlyClosed[0].window.sessionId);
yield assertNotificationCount(4);
@@ -105,17 +105,17 @@ add_task(function* test_sessions_restore
is(restored.length, 1, "restore returned the expected number of sessions");
is(restored[0].window.tabs.length, 3, "restore returned a window with the expected number of tabs");
checkLocalTab(restored[0].window.tabs[0], "about:config");
checkLocalTab(restored[0].window.tabs[1], "about:robots");
checkLocalTab(restored[0].window.tabs[2], "about:mozilla");
// Close the window again.
- window = WindowManager.getWindow(restored[0].window.id);
+ window = windowTracker.getWindow(restored[0].window.id);
yield BrowserTestUtils.closeWindow(window);
// notificationCount = yield extension.awaitMessage("notificationCount");
yield assertNotificationCount(5);
// Open and close a tab.
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots");
yield TabStateFlusher.flush(tab.linkedBrowser);
yield BrowserTestUtils.removeTab(tab);
@@ -127,34 +127,34 @@ add_task(function* test_sessions_restore
restored = yield extension.awaitMessage("restored");
is(restored.length, 1, "restore returned the expected number of sessions");
tab = restored[0].tab;
ok(tab, "restore returned a tab");
checkLocalTab(tab, "about:robots");
// Close the tab again.
- let realTab = TabManager.getTab(tab.id);
+ let realTab = tabTracker.getTab(tab.id);
yield BrowserTestUtils.removeTab(realTab);
yield assertNotificationCount(8);
// Restore the tab using the sessionId.
extension.sendMessage("check-sessions");
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
extension.sendMessage("restore", recentlyClosed[0].tab.sessionId);
yield assertNotificationCount(9);
restored = yield extension.awaitMessage("restored");
is(restored.length, 1, "restore returned the expected number of sessions");
tab = restored[0].tab;
ok(tab, "restore returned a tab");
checkLocalTab(tab, "about:robots");
// Close the tab again.
- realTab = TabManager.getTab(tab.id);
+ realTab = tabTracker.getTab(tab.id);
yield BrowserTestUtils.removeTab(realTab);
yield assertNotificationCount(10);
// Try to restore something with an invalid sessionId.
extension.sendMessage("restore-reject");
restored = yield extension.awaitMessage("restore-rejected");
yield extension.unload();
--- a/browser/components/extensions/test/browser/browser_ext_tabs_audio.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_audio.js
@@ -156,19 +156,19 @@ add_task(function* () {
manifest: {
"permissions": ["tabs"],
},
background,
});
extension.onMessage("change-tab", (tabId, attr, on) => {
- let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ let {Management: {global: {tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
- let tab = TabManager.getTab(tabId);
+ let tab = tabTracker.getTab(tabId);
if (attr == "muted") {
// Ideally we'd simulate a click on the tab audio icon for this, but the
// handler relies on CSS :hover states, which are complicated and fragile
// to simulate.
if (tab.muted != on) {
tab.toggleMuteAudio();
}
@@ -179,17 +179,17 @@ add_task(function* () {
} else {
browser.audioPlaybackStopped();
}
} else if (attr == "duplicate") {
// This is a bit of a hack. It won't be necessary once we have
// `tabs.duplicate`.
let newTab = gBrowser.duplicateTab(tab);
BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(() => {
- extension.sendMessage("change-tab-done", tabId, TabManager.getId(newTab));
+ extension.sendMessage("change-tab-done", tabId, tabTracker.getId(newTab));
});
return;
}
extension.sendMessage("change-tab-done", tabId);
});
yield extension.startup();
--- a/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
@@ -89,24 +89,24 @@ add_task(function* testDuplicateTabLazil
manifest: {
"permissions": ["tabs"],
},
background,
});
extension.onMessage("duplicate-tab", tabId => {
- let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ let {Management: {global: {tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
- let tab = TabManager.getTab(tabId);
+ let tab = tabTracker.getTab(tabId);
// This is a bit of a hack to load a tab in the background.
let newTab = gBrowser.duplicateTab(tab, true);
BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(() => {
- extension.sendMessage("duplicate-tab-done", TabManager.getId(newTab));
+ extension.sendMessage("duplicate-tab-done", tabTracker.getId(newTab));
});
});
yield extension.startup();
yield extension.awaitFinish("tabs.hasCorrectTabTitle");
yield extension.unload();
});
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
@@ -16,17 +16,17 @@ add_task(function* testExecuteScriptAtOn
function background() {
// Using variables to prevent listeners from running more than once, instead
// of removing the listener. This is to minimize any IPC, since the bug that
// is being tested is sensitive to timing.
let ignore = false;
let url;
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
- if (changeInfo.status === "loading" && tab.url === url && !ignore) {
+ if (url && changeInfo.status === "loading" && tab.url === url && !ignore) {
ignore = true;
browser.tabs.executeScript(tabId, {
code: "document.URL",
}).then(results => {
browser.test.assertEq(url, results[0], "Content script should run");
browser.test.notifyPass("executeScript-at-onUpdated");
}, error => {
browser.test.fail(`Unexpected error: ${error} :: ${error.stack}`);
--- a/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
@@ -184,24 +184,24 @@ add_task(function* () {
manifest: {
"permissions": ["tabs"],
},
background,
});
extension.onMessage("msg", (id, msg, ...args) => {
- let {Management: {global: {TabManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ let {Management: {global: {tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
let resp;
if (msg == "get-zoom") {
- let tab = TabManager.getTab(args[0]);
+ let tab = tabTracker.getTab(args[0]);
resp = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
} else if (msg == "set-zoom") {
- let tab = TabManager.getTab(args[0]);
+ let tab = tabTracker.getTab(args[0]);
ZoomManager.setZoomForBrowser(tab.linkedBrowser);
} else if (msg == "enlarge") {
FullZoom.enlarge();
} else if (msg == "site-specific") {
if (args[0] == null) {
SpecialPowers.clearUserPref(SITE_SPECIFIC_PREF);
} else {
SpecialPowers.setBoolPref(SITE_SPECIFIC_PREF, args[0]);
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
@@ -1,23 +1,23 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* testWebNavigationGetNonExistentTab() {
let extension = ExtensionTestUtils.loadExtension({
background: async function() {
- // There is no "tabId = 0" because the id assigned by TabManager (defined in ext-utils.js)
+ // There is no "tabId = 0" because the id assigned by tabTracker (defined in ext-utils.js)
// starts from 1.
await browser.test.assertRejects(
browser.webNavigation.getAllFrames({tabId: 0}),
"Invalid tab ID: 0",
"getAllFrames rejected Promise should pass the expected error");
- // There is no "tabId = 0" because the id assigned by TabManager (defined in ext-utils.js)
+ // There is no "tabId = 0" because the id assigned by tabTracker (defined in ext-utils.js)
// starts from 1, processId is currently marked as optional and it is ignored.
await browser.test.assertRejects(
browser.webNavigation.getFrame({tabId: 0, frameId: 15, processId: 20}),
"Invalid tab ID: 0",
"getFrame rejected Promise should pass the expected error");
browser.test.sendMessage("getNonExistentTab.done");
},
--- a/browser/components/extensions/test/browser/browser_ext_windows_events.js
+++ b/browser/components/extensions/test/browser/browser_ext_windows_events.js
@@ -56,20 +56,20 @@ add_task(function* testWindowsEvents() {
let extension = ExtensionTestUtils.loadExtension({
background: `(${background})()`,
});
yield extension.startup();
yield extension.awaitMessage("ready");
- let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ let {Management: {global: {windowTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
let currentWindow = window;
- let currentWindowId = WindowManager.getId(currentWindow);
+ let currentWindowId = windowTracker.getId(currentWindow);
info(`Current window ID: ${currentWindowId}`);
info(`Create browser window 1`);
let win1 = yield BrowserTestUtils.openNewBrowserWindow();
let win1Id = yield extension.awaitMessage("window-created");
info(`Window 1 ID: ${win1Id}`);
// This shouldn't be necessary, but tests intermittently fail, so let's give
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -25,18 +25,17 @@ module.exports = { // eslint-disable-lin
"isValidCookieStoreId": true,
"NetUtil": true,
"openOptionsPage": true,
"require": false,
"runSafe": true,
"runSafeSync": true,
"runSafeSyncWithoutClone": true,
"Services": true,
- "TabManager": true,
- "WindowListManager": true,
+ "tabTracker": false,
"XPCOMUtils": true,
},
"rules": {
// Rules from the mozilla plugin
"mozilla/balanced-listeners": "error",
"mozilla/no-aArgs": "error",
"mozilla/no-cpows-in-tests": "warn",
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -186,18 +186,18 @@ ProxyMessenger = {
* @param {Extension} extension
* @returns {object|null} The message manager matching the recipient if found.
*/
getMessageManagerForRecipient(recipient) {
let {tabId} = recipient;
// tabs.sendMessage / tabs.connect
if (tabId) {
// `tabId` being set implies that the tabs API is supported, so we don't
- // need to check whether `TabManager` exists.
- let tab = apiManager.global.TabManager.getTab(tabId, null, null);
+ // need to check whether `tabTracker` exists.
+ let tab = apiManager.global.tabTracker.getTab(tabId, null);
return tab && tab.linkedBrowser.messageManager;
}
// runtime.sendMessage / runtime.connect
let extension = GlobalManager.extensionMap.get(recipient.extensionId);
if (extension) {
return extension.parentMessageManager;
}
@@ -349,22 +349,29 @@ class ExtensionPageContextParent extends
extension.emit("extension-proxy-context-load", this);
}
// The window that contains this context. This may change due to moving tabs.
get xulWindow() {
return this.xulBrowser.ownerGlobal;
}
+ get currentWindow() {
+ if (this.viewType !== "background") {
+ return this.xulWindow;
+ }
+ }
+
get windowId() {
- if (!apiManager.global.WindowManager || this.viewType == "background") {
- return;
+ let {currentWindow} = this;
+ let {windowTracker} = apiManager.global;
+
+ if (currentWindow && windowTracker) {
+ return windowTracker.getId(currentWindow);
}
- // viewType popup or tab:
- return apiManager.global.WindowManager.getId(this.xulWindow);
}
get tabId() {
let {getBrowserInfo} = apiManager.global;
if (getBrowserInfo) {
// This is currently only available on desktop Firefox.
return getBrowserInfo(this.xulBrowser).tabId;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -0,0 +1,547 @@
+/* -*- 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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* exported TabTrackerBase, TabManagerBase, TabBase, WindowTrackerBase, WindowManagerBase, WindowBase */
+
+var EXPORTED_SYMBOLS = ["TabTrackerBase", "TabManagerBase", "TabBase", "WindowTrackerBase", "WindowManagerBase", "WindowBase"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+const {
+ DefaultMap,
+ DefaultWeakMap,
+ EventEmitter,
+ ExtensionError,
+} = ExtensionUtils;
+
+class TabBase {
+ constructor(extension, tab, id) {
+ this.extension = extension;
+ this.tabManager = extension.tabManager;
+ this.id = id;
+ this.tab = tab;
+ this.activeTabWindowId = null;
+ }
+
+ get innerWindowId() {
+ return this.browser.innerWindowId;
+ }
+
+ get hasTabPermission() {
+ return this.extension.hasPermission("tabs") || this.hasActiveTabPermission;
+ }
+
+ get hasActiveTabPermission() {
+ return (this.extension.hasPermission("activeTab") &&
+ this.activeTabWindowId !== null &&
+ this.activeTabWindowId === this.innerWindowId);
+ }
+
+ get incognito() {
+ return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ }
+
+ get _url() {
+ return this.browser.currentURI.spec;
+ }
+
+ get url() {
+ if (this.hasTabPermission) {
+ return this._url;
+ }
+ }
+
+ get uri() {
+ if (this.hasTabPermission) {
+ return this.browser.currentURI;
+ }
+ }
+
+ get _title() {
+ return this.browser.contentTitle || this.tab.label;
+ }
+
+
+ get title() {
+ if (this.hasTabPermission) {
+ return this._title;
+ }
+ }
+
+ get favIconUrl() {
+ if (this.hasTabPermission) {
+ return this._favIconUrl;
+ }
+ }
+
+ matches(queryInfo) {
+ const PROPS = ["active", "audible", "cookieStoreId", "highlighted", "index", "pinned", "status", "title"];
+
+ if (PROPS.some(prop => queryInfo[prop] !== null && queryInfo[prop] !== this[prop])) {
+ return false;
+ }
+
+ if (queryInfo.muted !== null) {
+ if (queryInfo.muted !== this.mutedInfo.muted) {
+ return false;
+ }
+ }
+
+ if (queryInfo.url && !queryInfo.url.matches(this.uri)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ convert() {
+ let result = {
+ id: this.id,
+ index: this.index,
+ windowId: this.windowId,
+ selected: this.selected,
+ highlighted: this.selected,
+ active: this.selected,
+ pinned: this.pinned,
+ status: this.status,
+ incognito: this.incognito,
+ width: this.width,
+ height: this.height,
+ audible: this.audible,
+ mutedInfo: this.mutedInfo,
+ };
+
+ if (this.extension.hasPermission("cookies")) {
+ result.cookieStoreId = this.cookieStoreId;
+ }
+
+ if (this.hasTabPermission) {
+ for (let prop of ["url", "title", "favIconUrl"]) {
+ let val = this[`_${prop}`];
+ if (val) {
+ result[prop] = val;
+ }
+ }
+ }
+
+ return result;
+ }
+}
+
+// Note: These must match the values in windows.json.
+const WINDOW_ID_NONE = -1;
+const WINDOW_ID_CURRENT = -2;
+
+class WindowBase {
+ constructor(extension, window, id) {
+ this.extension = extension;
+ this.window = window;
+ this.id = id;
+ }
+
+ get xulWindow() {
+ return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIXULWindow);
+ }
+
+ isCurrentFor(context) {
+ if (context && context.currentWindow) {
+ return this.window === context.currentWindow;
+ }
+ return this.isLastFocused;
+ }
+
+ get type() {
+ let {chromeFlags} = this.xulWindow;
+
+ if (chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) {
+ return "popup";
+ }
+
+ return "normal";
+ }
+
+ convert(getInfo) {
+ let result = {
+ id: this.id,
+ focused: this.focused,
+ top: this.top,
+ left: this.left,
+ width: this.width,
+ height: this.height,
+ incognito: this.incognito,
+ type: this.type,
+ state: this.state,
+ alwaysOnTop: this.alwaysOnTop,
+ };
+
+ if (getInfo && getInfo.populate) {
+ result.tabs = Array.from(this.getTabs(), tab => tab.convert());
+ }
+
+ return result;
+ }
+
+ matches(queryInfo, context) {
+ if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow !== this.isLastFocused) {
+ return false;
+ }
+
+ if (queryInfo.windowType !== null && queryInfo.windowType !== this.type) {
+ return false;
+ }
+
+ if (queryInfo.windowId !== null) {
+ if (queryInfo.windowId === WINDOW_ID_CURRENT) {
+ if (!this.isCurrentFor(context)) {
+ return false;
+ }
+ } else if (queryInfo.windowId !== this.id) {
+ return false;
+ }
+ }
+
+ if (queryInfo.currentWindow !== null && queryInfo.currentWindow !== this.isCurrentFor(context)) {
+ return false;
+ }
+
+ return true;
+ }
+}
+
+Object.assign(WindowBase, {WINDOW_ID_NONE, WINDOW_ID_CURRENT});
+
+class TabTrackerBase extends EventEmitter {
+ on(...args) {
+ if (!this.initialized) {
+ this.init();
+ }
+
+ return super.on(...args); // eslint-disable-line mozilla/balanced-listeners
+ }
+}
+
+class WindowTrackerBase extends EventEmitter {
+ constructor() {
+ super();
+
+ this.handleWindowOpened = this.handleWindowOpened.bind(this);
+
+ this._openListeners = new Set();
+ this._closeListeners = new Set();
+
+ this._listeners = new DefaultMap(() => new Set());
+
+ this._windowIds = new DefaultWeakMap(window => {
+ window.QueryInterface(Ci.nsIInterfaceRequestor);
+
+ return window.getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ });
+ }
+
+ isBrowserWindow(window) {
+ let {documentElement} = window.document;
+
+ return documentElement.getAttribute("windowtype") === "navigator:browser";
+ }
+
+ // Returns an iterator for all browser windows. Unless |includeIncomplete| is
+ // true, only fully-loaded windows are returned.
+ * browserWindows(includeIncomplete = false) {
+ // The window type parameter is only available once the window's document
+ // element has been created. This means that, when looking for incomplete
+ // browser windows, we need to ignore the type entirely for windows which
+ // haven't finished loading, since we would otherwise skip browser windows
+ // in their early loading stages.
+ // This is particularly important given that the "domwindowcreated" event
+ // fires for browser windows when they're in that in-between state, and just
+ // before we register our own "domwindowcreated" listener.
+
+ let e = Services.wm.getEnumerator("");
+ while (e.hasMoreElements()) {
+ let window = e.getNext();
+
+ let ok = includeIncomplete;
+ if (window.document.readyState === "complete") {
+ ok = this.isBrowserWindow(window);
+ }
+
+ if (ok) {
+ yield window;
+ }
+ }
+ }
+
+ get topWindow() {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+ }
+
+ getId(window) {
+ return this._windowIds.get(window);
+ }
+
+ getCurrentWindow(context) {
+ let {xulWindow} = context;
+ if (xulWindow && context.viewType !== "background") {
+ return xulWindow;
+ }
+ return this.topWindow;
+ }
+
+ getWindow(id, context) {
+ if (id === WINDOW_ID_CURRENT) {
+ return this.getCurrentWindow(context);
+ }
+
+ for (let window of this.browserWindows(true)) {
+ if (this.getId(window) === id) {
+ return window;
+ }
+ }
+ throw new ExtensionError(`Invalid window ID: ${id}`);
+ }
+
+ get haveListeners() {
+ return this._openListeners.size > 0 || this._closeListeners.size > 0;
+ }
+
+ addOpenListener(listener) {
+ if (!this.haveListeners) {
+ Services.ww.registerNotification(this);
+ }
+
+ this._openListeners.add(listener);
+
+ for (let window of this.browserWindows(true)) {
+ if (window.document.readyState !== "complete") {
+ window.addEventListener("load", this);
+ }
+ }
+ }
+
+ removeOpenListener(listener) {
+ this._openListeners.delete(listener);
+
+ if (!this.haveListeners) {
+ Services.ww.unregisterNotification(this);
+ }
+ }
+
+ addCloseListener(listener) {
+ if (!this.haveListeners) {
+ Services.ww.registerNotification(this);
+ }
+
+ this._closeListeners.add(listener);
+ }
+
+ removeCloseListener(listener) {
+ this._closeListeners.delete(listener);
+
+ if (!this.haveListeners) {
+ Services.ww.unregisterNotification(this);
+ }
+ }
+
+ handleEvent(event) {
+ if (event.type === "load") {
+ event.currentTarget.removeEventListener(event.type, this);
+
+ let window = event.target.defaultView;
+ if (!this.isBrowserWindow(window)) {
+ return;
+ }
+
+ for (let listener of this._openListeners) {
+ try {
+ listener(window);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+ }
+
+ observe(window, topic, data) {
+ if (topic === "domwindowclosed") {
+ if (!this.isBrowserWindow(window)) {
+ return;
+ }
+
+ window.removeEventListener("load", this);
+ for (let listener of this._closeListeners) {
+ try {
+ listener(window);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ } else if (topic === "domwindowopened") {
+ window.addEventListener("load", this);
+ }
+ }
+
+ // If |type| is a normal event type, invoke |listener| each time
+ // that event fires in any open window. If |type| is "progress", add
+ // a web progress listener that covers all open windows.
+ addListener(type, listener) {
+ if (type === "domwindowopened") {
+ return this.addOpenListener(listener);
+ } else if (type === "domwindowclosed") {
+ return this.addCloseListener(listener);
+ }
+
+ if (this._listeners.size === 0) {
+ this.addOpenListener(this.handleWindowOpened);
+ }
+
+ this._listeners.get(type).add(listener);
+
+ // Register listener on all existing windows.
+ for (let window of this.browserWindows()) {
+ this.addWindowListener(window, type, listener);
+ }
+ }
+
+ removeListener(eventType, listener) {
+ if (eventType === "domwindowopened") {
+ return this.removeOpenListener(listener);
+ } else if (eventType === "domwindowclosed") {
+ return this.removeCloseListener(listener);
+ }
+
+ let listeners = this._listeners.get(eventType);
+ listeners.delete(listener);
+
+ if (listeners.size === 0) {
+ this._listeners.delete(eventType);
+ if (this._listeners.size === 0) {
+ this.removeOpenListener(this.handleWindowOpened);
+ }
+ }
+
+ // Unregister listener from all existing windows.
+ let useCapture = eventType === "focus" || eventType === "blur";
+ for (let window of this.browserWindows()) {
+ if (eventType === "progress") {
+ this.removeProgressListener(window, listener);
+ } else {
+ window.removeEventListener(eventType, listener, useCapture);
+ }
+ }
+ }
+
+ addWindowListener(window, eventType, listener) {
+ let useCapture = eventType === "focus" || eventType === "blur";
+
+ if (eventType === "progress") {
+ this.addProgressListener(window, listener);
+ } else {
+ window.addEventListener(eventType, listener, useCapture);
+ }
+ }
+
+ handleWindowOpened(window) {
+ for (let [eventType, listeners] of this._listeners) {
+ for (let listener of listeners) {
+ this.addWindowListener(window, eventType, listener);
+ }
+ }
+ }
+}
+
+class TabManagerBase {
+ constructor(extension) {
+ this.extension = extension;
+
+ this._tabs = new DefaultWeakMap(tab => this.wrapTab(tab));
+ }
+
+ addActiveTabPermission(tab) {
+ if (this.extension.hasPermission("activeTab")) {
+ // Note that, unlike Chrome, we don't currently clear this permission with
+ // the tab navigates. If the inner window is revived from BFCache before
+ // we've granted this permission to a new inner window, the extension
+ // maintains its permissions for it.
+ tab = this.getWrapper(tab);
+ tab.activeTabWindowId = tab.innerWindowId;
+ }
+ }
+
+ revokeActiveTabPermission(tab) {
+ this.getWrapper(tab).activeTabWindowId = null;
+ }
+
+ // Returns true if the extension has the "activeTab" permission for this tab.
+ // This is somewhat more permissive than the generic "tabs" permission, as
+ // checked by |hasTabPermission|, in that it also allows programmatic script
+ // injection without an explicit host permission.
+ hasActiveTabPermission(tab) {
+ return this.getWrapper(tab).hasActiveTabPermission;
+ }
+
+ hasTabPermission(tab) {
+ return this.getWrapper(tab).hasTabPermission;
+ }
+
+ getWrapper(tab) {
+ return this._tabs.get(tab);
+ }
+
+ * query(queryInfo = null, context = null) {
+ for (let window of this.extension.windowManager.query(queryInfo, context)) {
+ for (let tab of window.getTabs()) {
+ if (!queryInfo || tab.matches(queryInfo)) {
+ yield tab;
+ }
+ }
+ }
+ }
+
+ convert(tab) {
+ return this.getWrapper(tab).convert();
+ }
+
+ wrapTab(tab) {
+ throw new Error("Not implemented");
+ }
+}
+
+class WindowManagerBase {
+ constructor(extension) {
+ this.extension = extension;
+
+ this._windows = new DefaultWeakMap(window => this.wrapWindow(window));
+ }
+
+ convert(window, ...args) {
+ return this.getWrapper(window).convert(...args);
+ }
+
+ getWrapper(tab) {
+ return this._windows.get(tab);
+ }
+
+ * query(queryInfo = null, context = null) {
+ for (let window of this.getAll()) {
+ if (!queryInfo || window.matches(queryInfo, context)) {
+ yield window;
+ }
+ }
+ }
+}
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -416,24 +416,21 @@ extensions.registerSchemaAPI("cookies",
});
}
return Promise.resolve(null);
},
getAllCookieStores: function() {
let data = {};
- for (let window of WindowListManager.browserWindows()) {
- let tabs = TabManager.for(extension).getTabs(window);
- for (let tab of tabs) {
- if (!(tab.cookieStoreId in data)) {
- data[tab.cookieStoreId] = [];
- }
- data[tab.cookieStoreId].push(tab.id);
+ for (let tab of extension.tabManager.query()) {
+ if (!(tab.cookieStoreId in data)) {
+ data[tab.cookieStoreId] = [];
}
+ data[tab.cookieStoreId].push(tab.id);
}
let result = [];
for (let key in data) {
result.push({id: key, tabIds: data[key], incognito: key == PRIVATE_STORE});
}
return Promise.resolve(result);
},
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -147,44 +147,46 @@ function convertGetFrameResult(tabId, da
url: data.url,
tabId,
frameId: ExtensionManagement.getFrameId(data.windowId),
parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
};
}
extensions.registerSchemaAPI("webNavigation", "addon_parent", context => {
+ let {tabManager} = context.extension;
+
return {
webNavigation: {
onTabReplaced: ignoreEvent(context, "webNavigation.onTabReplaced"),
onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
getAllFrames(details) {
- let tab = TabManager.getTab(details.tabId, context);
+ let tab = tabManager.get(details.tabId);
- let {innerWindowID, messageManager} = tab.linkedBrowser;
+ let {innerWindowID, messageManager} = tab.browser;
let recipient = {innerWindowID};
return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, {recipient})
.then((results) => results.map(convertGetFrameResult.bind(null, details.tabId)));
},
getFrame(details) {
- let tab = TabManager.getTab(details.tabId, context);
+ let tab = tabManager.get(details.tabId);
let recipient = {
- innerWindowID: tab.linkedBrowser.innerWindowID,
+ innerWindowID: tab.browser.innerWindowID,
};
- let mm = tab.linkedBrowser.messageManager;
+ let mm = tab.browser.messageManager;
return context.sendMessage(mm, "WebNavigation:GetFrame", {options: details}, {recipient})
.then((result) => {
return result ?
convertGetFrameResult(details.tabId, result) :
Promise.reject({message: `No frame found with frameId: ${details.frameId}`});
});
},
},
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -9,16 +9,17 @@ EXTRA_JS_MODULES += [
'ExtensionAPI.jsm',
'ExtensionChild.jsm',
'ExtensionCommon.jsm',
'ExtensionContent.jsm',
'ExtensionManagement.jsm',
'ExtensionParent.jsm',
'ExtensionStorage.jsm',
'ExtensionStorageSync.jsm',
+ 'ExtensionTabs.jsm',
'ExtensionUtils.jsm',
'LegacyExtensionsUtils.jsm',
'MessageChannel.jsm',
'NativeMessaging.jsm',
'Schemas.jsm',
]
EXTRA_PP_COMPONENTS += [