forked from mirrors/gecko-dev
CLOSED TREE Backed out changeset 2d42350d209a (bug 1203330) Backed out changeset 3a12c51c3eca (bug 1203330) Backed out changeset 31fac390e15d (bug 1203330)
1103 lines
38 KiB
JavaScript
1103 lines
38 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
"use strict";
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
|
"@mozilla.org/browser/aboutnewtab-service;1",
|
|
"nsIAboutNewTabService");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
|
"resource://gre/modules/MatchPattern.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
|
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
|
"resource://gre/modules/PromiseUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
|
"resource://gre/modules/Services.jsm");
|
|
|
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
|
|
var {
|
|
SingletonEventManager,
|
|
ignoreEvent,
|
|
} = ExtensionUtils;
|
|
|
|
// This function is pretty tightly tied to Extension.jsm.
|
|
// Its job is to fill in the |tab| property of the sender.
|
|
function getSender(extension, target, sender) {
|
|
let tabId;
|
|
if ("tabId" in sender) {
|
|
// The message came from a privileged extension page running in a tab. In
|
|
// that case, it should include a tabId property (which is filled in by the
|
|
// 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);
|
|
if (tab) {
|
|
sender.tab = TabManager.convert(extension, tab);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Used by Extension.jsm
|
|
global.tabGetSender = getSender;
|
|
|
|
/* eslint-disable mozilla/balanced-listeners */
|
|
|
|
extensions.on("page-shutdown", (type, context) => {
|
|
if (context.viewType == "tab") {
|
|
if (context.extension.id !== context.xulBrowser.contentPrincipal.addonId) {
|
|
// Only close extension tabs.
|
|
// This check prevents about:addons from closing when it contains a
|
|
// WebExtension as an embedded inline options page.
|
|
return;
|
|
}
|
|
let {gBrowser} = context.xulBrowser.ownerGlobal;
|
|
if (gBrowser) {
|
|
let tab = gBrowser.getTabForBrowser(context.xulBrowser);
|
|
if (tab) {
|
|
gBrowser.removeTab(tab);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
extensions.on("fill-browser-data", (type, browser, data) => {
|
|
let tabId, windowId;
|
|
if (browser) {
|
|
({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);
|
|
|
|
this.tabReadyInitialized = true;
|
|
}
|
|
},
|
|
|
|
onLocationChange(browser, webProgress, request, locationURI, flags) {
|
|
if (webProgress.isTopLevel) {
|
|
let gBrowser = browser.ownerGlobal.gBrowser;
|
|
let tab = gBrowser.getTabForBrowser(browser);
|
|
|
|
// Now we are certain that the first page in the tab was loaded.
|
|
this.initializingTabs.delete(tab);
|
|
|
|
// browser.innerWindowID is now set, resolve the promises if any.
|
|
let deferred = this.tabReadyPromises.get(tab);
|
|
if (deferred) {
|
|
deferred.resolve(tab);
|
|
this.tabReadyPromises.delete(tab);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a promise that resolves when the tab is ready.
|
|
* Tabs created via the `tabs.create` method are "ready" once the location
|
|
* changes to the requested URL. Other tabs are assumed to be ready once their
|
|
* inner window ID is known.
|
|
*
|
|
* @param {XULElement} tab The <tab> element.
|
|
* @returns {Promise} Resolves with the given tab once ready.
|
|
*/
|
|
awaitTabReady(tab) {
|
|
let deferred = this.tabReadyPromises.get(tab);
|
|
if (!deferred) {
|
|
deferred = PromiseUtils.defer();
|
|
if (!this.initializingTabs.has(tab) && tab.linkedBrowser.innerWindowID) {
|
|
deferred.resolve(tab);
|
|
} else {
|
|
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 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);
|
|
fire.async({tabId, windowId});
|
|
}).api(),
|
|
|
|
onCreated: new SingletonEventManager(context, "tabs.onCreated", fire => {
|
|
let listener = (eventName, event) => {
|
|
fire.async(TabManager.convert(extension, event.tab));
|
|
};
|
|
|
|
tabListener.on("tab-created", listener);
|
|
return () => {
|
|
tabListener.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);
|
|
fire.async({tabIds, windowId});
|
|
}).api(),
|
|
|
|
onAttached: new SingletonEventManager(context, "tabs.onAttached", fire => {
|
|
let listener = (eventName, event) => {
|
|
fire.async(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
|
|
};
|
|
|
|
tabListener.on("tab-attached", listener);
|
|
return () => {
|
|
tabListener.off("tab-attached", listener);
|
|
};
|
|
}).api(),
|
|
|
|
onDetached: new SingletonEventManager(context, "tabs.onDetached", fire => {
|
|
let listener = (eventName, event) => {
|
|
fire.async(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
|
|
};
|
|
|
|
tabListener.on("tab-detached", listener);
|
|
return () => {
|
|
tabListener.off("tab-detached", listener);
|
|
};
|
|
}).api(),
|
|
|
|
onRemoved: new SingletonEventManager(context, "tabs.onRemoved", fire => {
|
|
let listener = (eventName, event) => {
|
|
fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
|
|
};
|
|
|
|
tabListener.on("tab-removed", listener);
|
|
return () => {
|
|
tabListener.off("tab-removed", listener);
|
|
};
|
|
}).api(),
|
|
|
|
onReplaced: ignoreEvent(context, "tabs.onReplaced"),
|
|
|
|
onMoved: new SingletonEventManager(context, "tabs.onMoved", fire => {
|
|
// There are certain circumstances where we need to ignore a move event.
|
|
//
|
|
// Namely, the first time the tab is moved after it's created, we need
|
|
// to report the final position as the initial position in the tab's
|
|
// onAttached or onCreated event. This is because most tabs are inserted
|
|
// in a temporary location and then moved after the TabOpen event fires,
|
|
// which generates a TabOpen event followed by a TabMove event, which
|
|
// does not match the contract of our API.
|
|
let ignoreNextMove = new WeakSet();
|
|
|
|
let openListener = event => {
|
|
ignoreNextMove.add(event.target);
|
|
// Remove the tab from the set on the next tick, since it will already
|
|
// have been moved by then.
|
|
Promise.resolve().then(() => {
|
|
ignoreNextMove.delete(event.target);
|
|
});
|
|
};
|
|
|
|
let moveListener = event => {
|
|
let tab = event.originalTarget;
|
|
|
|
if (ignoreNextMove.has(tab)) {
|
|
ignoreNextMove.delete(tab);
|
|
return;
|
|
}
|
|
|
|
fire.async(TabManager.getId(tab), {
|
|
windowId: WindowManager.getId(tab.ownerGlobal),
|
|
fromIndex: event.detail,
|
|
toIndex: tab._tPos,
|
|
});
|
|
};
|
|
|
|
AllWindowEvents.addListener("TabMove", moveListener);
|
|
AllWindowEvents.addListener("TabOpen", openListener);
|
|
return () => {
|
|
AllWindowEvents.removeListener("TabMove", moveListener);
|
|
AllWindowEvents.removeListener("TabOpen", openListener);
|
|
};
|
|
}).api(),
|
|
|
|
onUpdated: new SingletonEventManager(context, "tabs.onUpdated", fire => {
|
|
const restricted = ["url", "favIconUrl", "title"];
|
|
|
|
function sanitize(extension, changeInfo) {
|
|
let result = {};
|
|
let nonempty = false;
|
|
for (let prop in changeInfo) {
|
|
if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
|
|
nonempty = true;
|
|
result[prop] = changeInfo[prop];
|
|
}
|
|
}
|
|
return [nonempty, result];
|
|
}
|
|
|
|
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);
|
|
fire.async(tab.id, changeInfo, tab);
|
|
}
|
|
};
|
|
|
|
let listener = event => {
|
|
let needed = [];
|
|
if (event.type == "TabAttrModified") {
|
|
let changed = event.detail.changed;
|
|
if (changed.includes("image")) {
|
|
needed.push("favIconUrl");
|
|
}
|
|
if (changed.includes("muted")) {
|
|
needed.push("mutedInfo");
|
|
}
|
|
if (changed.includes("soundplaying")) {
|
|
needed.push("audible");
|
|
}
|
|
if (changed.includes("label")) {
|
|
needed.push("title");
|
|
}
|
|
} else if (event.type == "TabPinned") {
|
|
needed.push("pinned");
|
|
} else if (event.type == "TabUnpinned") {
|
|
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 changeInfo = {};
|
|
for (let prop of needed) {
|
|
changeInfo[prop] = tab[prop];
|
|
}
|
|
fire.async(tab.id, changeInfo, tab);
|
|
}
|
|
};
|
|
let progressListener = {
|
|
onStateChange(browser, webProgress, request, stateFlags, statusCode) {
|
|
if (!webProgress.isTopLevel) {
|
|
return;
|
|
}
|
|
|
|
let status;
|
|
if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
|
|
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
|
|
status = "loading";
|
|
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
|
|
status = "complete";
|
|
}
|
|
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
|
|
statusCode == Cr.NS_BINDING_ABORTED) {
|
|
status = "complete";
|
|
}
|
|
|
|
fireForBrowser(browser, {status});
|
|
},
|
|
|
|
onLocationChange(browser, webProgress, request, locationURI, flags) {
|
|
if (!webProgress.isTopLevel) {
|
|
return;
|
|
}
|
|
|
|
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);
|
|
|
|
return () => {
|
|
AllWindowEvents.removeListener("progress", progressListener);
|
|
AllWindowEvents.removeListener("TabAttrModified", listener);
|
|
AllWindowEvents.removeListener("TabPinned", listener);
|
|
AllWindowEvents.removeListener("TabUnpinned", listener);
|
|
};
|
|
}).api(),
|
|
|
|
create: function(createProperties) {
|
|
return new Promise((resolve, reject) => {
|
|
let window = createProperties.windowId !== null ?
|
|
WindowManager.getWindow(createProperties.windowId, context) :
|
|
WindowManager.topWindow;
|
|
if (!window.gBrowser) {
|
|
let obs = (finishedWindow, topic, data) => {
|
|
if (finishedWindow != window) {
|
|
return;
|
|
}
|
|
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
|
|
resolve(window);
|
|
};
|
|
Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
|
|
} else {
|
|
resolve(window);
|
|
}
|
|
}).then(window => {
|
|
let url;
|
|
|
|
if (createProperties.url !== null) {
|
|
url = context.uri.resolve(createProperties.url);
|
|
|
|
if (!context.checkLoadURL(url, {dontReportErrors: true})) {
|
|
return Promise.reject({message: `Illegal URL: ${url}`});
|
|
}
|
|
}
|
|
|
|
if (createProperties.cookieStoreId && !extension.hasPermission("cookies")) {
|
|
return Promise.reject({message: `No permission for cookieStoreId: ${createProperties.cookieStoreId}`});
|
|
}
|
|
|
|
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`});
|
|
}
|
|
|
|
if (!privateWindow && global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
|
|
return Promise.reject({message: `Illegal to set private cookieStorageId 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}`});
|
|
}
|
|
|
|
options.userContextId = containerId;
|
|
}
|
|
}
|
|
|
|
// Make sure things like about:blank and data: URIs never inherit,
|
|
// and instead always get a NullPrincipal.
|
|
options.disallowInheritPrincipal = true;
|
|
|
|
tabListener.initTabReady();
|
|
let tab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
|
|
|
|
let active = true;
|
|
if (createProperties.active !== null) {
|
|
active = createProperties.active;
|
|
}
|
|
if (active) {
|
|
window.gBrowser.selectedTab = tab;
|
|
}
|
|
|
|
if (createProperties.index !== null) {
|
|
window.gBrowser.moveTabTo(tab, createProperties.index);
|
|
}
|
|
|
|
if (createProperties.pinned) {
|
|
window.gBrowser.pinTab(tab);
|
|
}
|
|
|
|
if (createProperties.url && !createProperties.url.startsWith("about:")) {
|
|
// We can't wait for a location change event for about:newtab,
|
|
// since it may be pre-rendered, in which case its initial
|
|
// location change event has already fired.
|
|
|
|
// 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);
|
|
});
|
|
},
|
|
|
|
remove: function(tabs) {
|
|
if (!Array.isArray(tabs)) {
|
|
tabs = [tabs];
|
|
}
|
|
|
|
for (let tabId of tabs) {
|
|
let tab = TabManager.getTab(tabId, context);
|
|
tab.ownerGlobal.gBrowser.removeTab(tab);
|
|
}
|
|
|
|
return Promise.resolve();
|
|
},
|
|
|
|
update: function(tabId, updateProperties) {
|
|
let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
|
|
|
|
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}`});
|
|
}
|
|
|
|
tab.linkedBrowser.loadURI(url);
|
|
}
|
|
|
|
if (updateProperties.active !== null) {
|
|
if (updateProperties.active) {
|
|
tabbrowser.selectedTab = tab;
|
|
} else {
|
|
// Not sure what to do here? Which tab should we select?
|
|
}
|
|
}
|
|
if (updateProperties.muted !== null) {
|
|
if (tab.muted != updateProperties.muted) {
|
|
tab.toggleMuteAudio(extension.uuid);
|
|
}
|
|
}
|
|
if (updateProperties.pinned !== null) {
|
|
if (updateProperties.pinned) {
|
|
tabbrowser.pinTab(tab);
|
|
} else {
|
|
tabbrowser.unpinTab(tab);
|
|
}
|
|
}
|
|
// FIXME: highlighted/selected, openerTabId
|
|
|
|
return Promise.resolve(TabManager.convert(extension, tab));
|
|
},
|
|
|
|
reload: function(tabId, reloadProperties) {
|
|
let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
|
|
|
|
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);
|
|
|
|
return Promise.resolve(TabManager.convert(extension, tab));
|
|
},
|
|
|
|
getCurrent() {
|
|
let tab;
|
|
if (context.tabId) {
|
|
tab = TabManager.convert(extension, TabManager.getTab(context.tabId, context));
|
|
}
|
|
return Promise.resolve(tab);
|
|
},
|
|
|
|
query: function(queryInfo) {
|
|
let pattern = null;
|
|
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;
|
|
}
|
|
|
|
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);
|
|
},
|
|
|
|
captureVisibleTab: function(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);
|
|
|
|
let tab = window.gBrowser.selectedTab;
|
|
return tabListener.awaitTabReady(tab).then(() => {
|
|
let browser = tab.linkedBrowser;
|
|
let recipient = {
|
|
innerWindowID: browser.innerWindowID,
|
|
};
|
|
|
|
if (!options) {
|
|
options = {};
|
|
}
|
|
if (options.format == null) {
|
|
options.format = "png";
|
|
}
|
|
if (options.quality == null) {
|
|
options.quality = 92;
|
|
}
|
|
|
|
let message = {
|
|
options,
|
|
width: browser.clientWidth,
|
|
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;
|
|
|
|
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 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 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) {
|
|
options[kind + "Code"] = details.code;
|
|
}
|
|
if (details.file !== null) {
|
|
let url = context.uri.resolve(details.file);
|
|
if (!extension.isExtensionURL(url)) {
|
|
return Promise.reject({message: "Files to be injected must be within the extension"});
|
|
}
|
|
options[kind].push(url);
|
|
}
|
|
if (details.allFrames) {
|
|
options.all_frames = details.allFrames;
|
|
}
|
|
if (details.frameId !== null) {
|
|
options.frame_id = details.frameId;
|
|
}
|
|
if (details.matchAboutBlank) {
|
|
options.match_about_blank = details.matchAboutBlank;
|
|
}
|
|
if (details.runAt !== null) {
|
|
options.run_at = details.runAt;
|
|
} else {
|
|
options.run_at = "document_idle";
|
|
}
|
|
if (details.cssOrigin !== null) {
|
|
options.css_origin = details.cssOrigin;
|
|
} else {
|
|
options.css_origin = "author";
|
|
}
|
|
|
|
return tabListener.awaitTabReady(tab).then(() => {
|
|
let browser = tab.linkedBrowser;
|
|
let recipient = {
|
|
innerWindowID: browser.innerWindowID,
|
|
};
|
|
|
|
return context.sendMessage(browser.messageManager, "Extension:Execute", {options}, {recipient});
|
|
});
|
|
},
|
|
|
|
executeScript: function(tabId, details) {
|
|
return self.tabs._execute(tabId, details, "js", "executeScript");
|
|
},
|
|
|
|
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) {
|
|
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);
|
|
// 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));
|
|
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) {
|
|
insertionPoint = gBrowser.tabs.length;
|
|
}
|
|
|
|
// We can only move pinned tabs to a point within, or just after,
|
|
// the current set of pinned tabs. Unpinned tabs, likewise, can only
|
|
// be moved to a position after the current set of pinned tabs.
|
|
// Attempts to move a tab to an illegal position are ignored.
|
|
let numPinned = gBrowser._numPinnedTabs;
|
|
let ok = tab.pinned ? insertionPoint <= numPinned : insertionPoint >= numPinned;
|
|
if (!ok) {
|
|
continue;
|
|
}
|
|
|
|
indexMap.set(window, insertionPoint + 1);
|
|
|
|
if (tab.ownerGlobal != window) {
|
|
// If the window we are moving the tab in is different, then move the tab
|
|
// to the new window.
|
|
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)));
|
|
},
|
|
|
|
duplicate: function(tabId) {
|
|
let tab = TabManager.getTab(tabId, context);
|
|
|
|
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() {
|
|
// As the tab is restoring, move it to the correct position.
|
|
// 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() {
|
|
// Once it has been restored, select it and return the promise.
|
|
gBrowser.selectedTab = newTab;
|
|
return resolve(TabManager.convert(extension, newTab));
|
|
}, {once: true});
|
|
});
|
|
},
|
|
|
|
getZoom(tabId) {
|
|
let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
|
|
|
|
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 {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);
|
|
} else {
|
|
return Promise.reject({
|
|
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 {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 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();
|
|
},
|
|
|
|
onZoomChange: new SingletonEventManager(context, "tabs.onZoomChange", fire => {
|
|
let getZoomLevel = browser => {
|
|
let {ZoomManager} = browser.ownerGlobal;
|
|
|
|
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 tab of window.gBrowser.tabs) {
|
|
let browser = tab.linkedBrowser;
|
|
zoomLevels.set(browser, getZoomLevel(browser));
|
|
}
|
|
}
|
|
|
|
let tabCreated = (eventName, event) => {
|
|
let browser = event.tab.linkedBrowser;
|
|
zoomLevels.set(browser, getZoomLevel(browser));
|
|
};
|
|
|
|
|
|
let zoomListener = event => {
|
|
let browser = event.originalTarget;
|
|
|
|
// For non-remote browsers, this event is dispatched on the document
|
|
// rather than on the <browser>.
|
|
if (browser instanceof Ci.nsIDOMDocument) {
|
|
browser = browser.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDocShell)
|
|
.chromeEventHandler;
|
|
}
|
|
|
|
let {gBrowser} = browser.ownerGlobal;
|
|
let tab = gBrowser.getTabForBrowser(browser);
|
|
if (!tab) {
|
|
// We only care about zoom events in the top-level browser of a tab.
|
|
return;
|
|
}
|
|
|
|
let oldZoomFactor = zoomLevels.get(browser);
|
|
let newZoomFactor = getZoomLevel(browser);
|
|
|
|
if (oldZoomFactor != newZoomFactor) {
|
|
zoomLevels.set(browser, newZoomFactor);
|
|
|
|
let tabId = TabManager.getId(tab);
|
|
fire.async({
|
|
tabId,
|
|
oldZoomFactor,
|
|
newZoomFactor,
|
|
zoomSettings: self.tabs._getZoomSettings(tabId),
|
|
});
|
|
}
|
|
};
|
|
|
|
tabListener.on("tab-attached", tabCreated);
|
|
tabListener.on("tab-created", tabCreated);
|
|
|
|
AllWindowEvents.addListener("FullZoomChange", zoomListener);
|
|
AllWindowEvents.addListener("TextZoomChange", zoomListener);
|
|
return () => {
|
|
tabListener.off("tab-attached", tabCreated);
|
|
tabListener.off("tab-created", tabCreated);
|
|
|
|
AllWindowEvents.removeListener("FullZoomChange", zoomListener);
|
|
AllWindowEvents.removeListener("TextZoomChange", zoomListener);
|
|
};
|
|
}).api(),
|
|
},
|
|
};
|
|
return self;
|
|
});
|