fune/browser/components/extensions/ext-tabs.js
Wes Kocher 4e5f6472fd Backed out 15 changesets (bug 1317101) for e10s jsreftest failures a=backout CLOSED TREE
Backed out changeset 17757ba4c0e8 (bug 1317101)
Backed out changeset 61f8a4084bbd (bug 1317101)
Backed out changeset a8cdc81cdcce (bug 1317101)
Backed out changeset e06d269a5d4f (bug 1317101)
Backed out changeset 1e1bfb578dcd (bug 1317101)
Backed out changeset 0f8144296a9d (bug 1317101)
Backed out changeset b7892d3fb0ca (bug 1317101)
Backed out changeset 039d63d5fef7 (bug 1317101)
Backed out changeset ef7e061b37bf (bug 1317101)
Backed out changeset af7b81d7a5cc (bug 1317101)
Backed out changeset 225ad2535585 (bug 1317101)
Backed out changeset b0521588011d (bug 1317101)
Backed out changeset 07321664430a (bug 1317101)
Backed out changeset 47d283897283 (bug 1317101)
Backed out changeset ffc63be3557c (bug 1317101)
2016-11-16 16:44:30 -08:00

1098 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 {
EventManager,
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) {
if ("tabId" in sender) {
// The message came from an ExtensionContext. In that case, it should
// include a tabId property (which is filled in by the page-open
// listener below).
let tab = TabManager.getTab(sender.tabId, null, null);
delete sender.tabId;
if (tab) {
sender.tab = TabManager.convert(extension, tab);
return;
}
}
if (target instanceof Ci.nsIDOMXULElement) {
// If the message was sent from a content script to a <browser> element,
// then we can just get the `tab` from `target`.
let tabbrowser = target.ownerGlobal.gBrowser;
if (tabbrowser) {
let tab = tabbrowser.getTabForBrowser(target);
// `tab` can be `undefined`, e.g. for extension popups. This condition is
// reached if `getSender` is called for a popup without a valid `tabId`.
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 gBrowser = browser && browser.ownerGlobal.gBrowser;
let tab = gBrowser && gBrowser.getTabForBrowser(browser);
data.tabId = tab ? TabManager.getId(tab) : -1;
data.windowId = tab ? WindowManager.getId(tab.ownerGlobal) : -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
* changed to the requested URL. Other tabs are always assumed to be ready.
*
* @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.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({tabId, windowId});
}).api(),
onCreated: new EventManager(context, "tabs.onCreated", fire => {
let listener = (eventName, event) => {
fire(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({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);
return () => {
tabListener.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);
return () => {
tabListener.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);
return () => {
tabListener.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.
//
// 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(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 EventManager(context, "tabs.onUpdated", fire => {
function sanitize(extension, changeInfo) {
let result = {};
let nonempty = false;
for (let prop in changeInfo) {
if ((prop != "favIconUrl" && prop != "url") || extension.hasPermission("tabs")) {
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(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");
}
} 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 => attr != "url" && attr != "favIconUrl");
}
if (needed.length) {
let tab = TabManager.convert(extension, event.originalTarget);
let changeInfo = {};
for (let prop of needed) {
changeInfo[prop] = tab[prop];
}
fire(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(window, tab) {
let props = ["active", "pinned", "highlighted", "status", "title", "index"];
for (let prop of props) {
if (queryInfo[prop] !== null && queryInfo[prop] != tab[prop]) {
return false;
}
}
let lastFocused = window == WindowManager.topWindow;
if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow != lastFocused) {
return false;
}
let windowType = WindowManager.windowType(window);
if (queryInfo.windowType !== null && queryInfo.windowType != windowType) {
return false;
}
if (queryInfo.windowId !== null) {
if (queryInfo.windowId == WindowManager.WINDOW_ID_CURRENT) {
if (currentWindow(context) != window) {
return false;
}
} else if (queryInfo.windowId != tab.windowId) {
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.currentWindow !== null) {
let eq = window == currentWindow(context);
if (queryInfo.currentWindow != eq) {
return false;
}
}
if (queryInfo.cookieStoreId !== null &&
tab.cookieStoreId != queryInfo.cookieStoreId) {
return false;
}
if (pattern && !pattern.matches(Services.io.newURI(tab.url, null, null))) {
return false;
}
return true;
}
let result = [];
for (let window of WindowListManager.browserWindows()) {
let tabs = TabManager.for(extension).getTabs(window);
for (let tab of tabs) {
if (matches(window, 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";
}
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 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);
});
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));
});
});
},
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 EventManager(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({
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;
});