forked from mirrors/gecko-dev
Backed out changeset 8781a0d1254d (bug 1810141) Backed out changeset 131037295784 (bug 1810141) Backed out changeset 3852fbe290f4 (bug 1810141) Backed out changeset 118f131a524a (bug 1810141) Backed out changeset ab5d76846e10 (bug 1810141) Backed out changeset dce3aa683445 (bug 1810141) Backed out changeset 4dc41d90dbb3 (bug 1810141) Backed out changeset 50b57ba1a061 (bug 1810141) Backed out changeset 569de94781e4 (bug 1810141)
1629 lines
54 KiB
JavaScript
1629 lines
54 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"BrowserUIUtils",
|
|
"resource:///modules/BrowserUIUtils.jsm"
|
|
);
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
|
|
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
|
});
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"ExtensionControlledPopup",
|
|
"resource:///modules/ExtensionControlledPopup.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
|
|
return Services.strings.createBundle(
|
|
"chrome://global/locale/extensions.properties"
|
|
);
|
|
});
|
|
|
|
var { DefaultMap, ExtensionError } = ExtensionUtils;
|
|
|
|
const TAB_HIDE_CONFIRMED_TYPE = "tabHideNotification";
|
|
|
|
const TAB_ID_NONE = -1;
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "tabHidePopup", () => {
|
|
return new ExtensionControlledPopup({
|
|
confirmedType: TAB_HIDE_CONFIRMED_TYPE,
|
|
anchorId: "alltabs-button",
|
|
popupnotificationId: "extension-tab-hide-notification",
|
|
descriptionId: "extension-tab-hide-notification-description",
|
|
descriptionMessageId: "tabHideControlled.message",
|
|
getLocalizedDescription: (doc, message, addonDetails) => {
|
|
let image = doc.createXULElement("image");
|
|
image.setAttribute("class", "extension-controlled-icon alltabs-icon");
|
|
return BrowserUIUtils.getLocalizedFragment(
|
|
doc,
|
|
message,
|
|
addonDetails,
|
|
image
|
|
);
|
|
},
|
|
learnMoreMessageId: "tabHideControlled.learnMore",
|
|
learnMoreLink: "extension-hiding-tabs",
|
|
});
|
|
});
|
|
|
|
function showHiddenTabs(id) {
|
|
for (let win of Services.wm.getEnumerator("navigator:browser")) {
|
|
if (win.closed || !win.gBrowser) {
|
|
continue;
|
|
}
|
|
|
|
for (let tab of win.gBrowser.tabs) {
|
|
if (
|
|
tab.hidden &&
|
|
tab.ownerGlobal &&
|
|
SessionStore.getCustomTabValue(tab, "hiddenBy") === id
|
|
) {
|
|
win.gBrowser.showTab(tab);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let tabListener = {
|
|
tabReadyInitialized: false,
|
|
// Map[tab -> Promise]
|
|
tabBlockedPromises: new WeakMap(),
|
|
// Map[tab -> Deferred]
|
|
tabReadyPromises: new WeakMap(),
|
|
initializingTabs: new WeakSet(),
|
|
|
|
initTabReady() {
|
|
if (!this.tabReadyInitialized) {
|
|
windowTracker.addListener("progress", this);
|
|
|
|
this.tabReadyInitialized = true;
|
|
}
|
|
},
|
|
|
|
onLocationChange(browser, webProgress, request, locationURI, flags) {
|
|
if (webProgress.isTopLevel) {
|
|
let { gBrowser } = browser.ownerGlobal;
|
|
let nativeTab = gBrowser.getTabForBrowser(browser);
|
|
|
|
// Now we are certain that the first page in the tab was loaded.
|
|
this.initializingTabs.delete(nativeTab);
|
|
|
|
// browser.innerWindowID is now set, resolve the promises if any.
|
|
let deferred = this.tabReadyPromises.get(nativeTab);
|
|
if (deferred) {
|
|
deferred.resolve(nativeTab);
|
|
this.tabReadyPromises.delete(nativeTab);
|
|
}
|
|
}
|
|
},
|
|
|
|
blockTabUntilRestored(nativeTab) {
|
|
let promise = ExtensionUtils.promiseEvent(nativeTab, "SSTabRestored").then(
|
|
({ target }) => {
|
|
this.tabBlockedPromises.delete(target);
|
|
return target;
|
|
}
|
|
);
|
|
|
|
this.tabBlockedPromises.set(nativeTab, promise);
|
|
},
|
|
|
|
/**
|
|
* 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} nativeTab The <tab> element.
|
|
* @returns {Promise} Resolves with the given tab once ready.
|
|
*/
|
|
awaitTabReady(nativeTab) {
|
|
let deferred = this.tabReadyPromises.get(nativeTab);
|
|
if (!deferred) {
|
|
let promise = this.tabBlockedPromises.get(nativeTab);
|
|
if (promise) {
|
|
return promise;
|
|
}
|
|
deferred = PromiseUtils.defer();
|
|
if (
|
|
!this.initializingTabs.has(nativeTab) &&
|
|
(nativeTab.linkedBrowser.innerWindowID ||
|
|
nativeTab.linkedBrowser.currentURI.spec === "about:blank")
|
|
) {
|
|
deferred.resolve(nativeTab);
|
|
} else {
|
|
this.initTabReady();
|
|
this.tabReadyPromises.set(nativeTab, deferred);
|
|
}
|
|
}
|
|
return deferred.promise;
|
|
},
|
|
};
|
|
|
|
const allAttrs = new Set([
|
|
"attention",
|
|
"audible",
|
|
"favIconUrl",
|
|
"mutedInfo",
|
|
"sharingState",
|
|
"title",
|
|
]);
|
|
const allProperties = new Set([
|
|
"attention",
|
|
"audible",
|
|
"discarded",
|
|
"favIconUrl",
|
|
"hidden",
|
|
"isArticle",
|
|
"mutedInfo",
|
|
"pinned",
|
|
"sharingState",
|
|
"status",
|
|
"title",
|
|
"url",
|
|
]);
|
|
const restricted = new Set(["url", "favIconUrl", "title"]);
|
|
|
|
this.tabs = class extends ExtensionAPIPersistent {
|
|
static onUpdate(id, manifest) {
|
|
if (!manifest.permissions || !manifest.permissions.includes("tabHide")) {
|
|
showHiddenTabs(id);
|
|
}
|
|
}
|
|
|
|
static onDisable(id) {
|
|
showHiddenTabs(id);
|
|
tabHidePopup.clearConfirmation(id);
|
|
}
|
|
|
|
static onUninstall(id) {
|
|
tabHidePopup.clearConfirmation(id);
|
|
}
|
|
|
|
tabEventRegistrar({ event, listener }) {
|
|
let { extension } = this;
|
|
let { tabManager } = extension;
|
|
return ({ fire }) => {
|
|
let listener2 = (eventName, eventData, ...args) => {
|
|
if (!tabManager.canAccessTab(eventData.nativeTab)) {
|
|
return;
|
|
}
|
|
|
|
listener(fire, eventData, ...args);
|
|
};
|
|
|
|
tabTracker.on(event, listener2);
|
|
return {
|
|
unregister() {
|
|
tabTracker.off(event, listener2);
|
|
},
|
|
convert(_fire) {
|
|
fire = _fire;
|
|
},
|
|
};
|
|
};
|
|
}
|
|
|
|
PERSISTENT_EVENTS = {
|
|
onActivated: this.tabEventRegistrar({
|
|
event: "tab-activated",
|
|
listener: (fire, event) => {
|
|
let { extension } = this;
|
|
let { tabId, windowId, previousTabId, previousTabIsPrivate } = event;
|
|
if (previousTabIsPrivate && !extension.privateBrowsingAllowed) {
|
|
previousTabId = undefined;
|
|
}
|
|
fire.async({ tabId, previousTabId, windowId });
|
|
},
|
|
}),
|
|
onAttached: this.tabEventRegistrar({
|
|
event: "tab-attached",
|
|
listener: (fire, event) => {
|
|
fire.async(event.tabId, {
|
|
newWindowId: event.newWindowId,
|
|
newPosition: event.newPosition,
|
|
});
|
|
},
|
|
}),
|
|
onCreated: this.tabEventRegistrar({
|
|
event: "tab-created",
|
|
listener: (fire, event) => {
|
|
let { tabManager } = this.extension;
|
|
fire.async(tabManager.convert(event.nativeTab, event.currentTabSize));
|
|
},
|
|
}),
|
|
onDetached: this.tabEventRegistrar({
|
|
event: "tab-detached",
|
|
listener: (fire, event) => {
|
|
fire.async(event.tabId, {
|
|
oldWindowId: event.oldWindowId,
|
|
oldPosition: event.oldPosition,
|
|
});
|
|
},
|
|
}),
|
|
onRemoved: this.tabEventRegistrar({
|
|
event: "tab-removed",
|
|
listener: (fire, event) => {
|
|
fire.async(event.tabId, {
|
|
windowId: event.windowId,
|
|
isWindowClosing: event.isWindowClosing,
|
|
});
|
|
},
|
|
}),
|
|
onMoved({ fire }) {
|
|
let { tabManager } = this.extension;
|
|
let moveListener = event => {
|
|
let nativeTab = event.originalTarget;
|
|
if (tabManager.canAccessTab(nativeTab)) {
|
|
fire.async(tabTracker.getId(nativeTab), {
|
|
windowId: windowTracker.getId(nativeTab.ownerGlobal),
|
|
fromIndex: event.detail,
|
|
toIndex: nativeTab._tPos,
|
|
});
|
|
}
|
|
};
|
|
|
|
windowTracker.addListener("TabMove", moveListener);
|
|
return {
|
|
unregister() {
|
|
windowTracker.removeListener("TabMove", moveListener);
|
|
},
|
|
convert(_fire) {
|
|
fire = _fire;
|
|
},
|
|
};
|
|
},
|
|
|
|
onHighlighted({ fire, context }) {
|
|
let { windowManager } = this.extension;
|
|
let highlightListener = (eventName, event) => {
|
|
// TODO see if we can avoid "context" here
|
|
let window = windowTracker.getWindow(event.windowId, context, false);
|
|
if (!window) {
|
|
return;
|
|
}
|
|
let windowWrapper = windowManager.getWrapper(window);
|
|
if (!windowWrapper) {
|
|
return;
|
|
}
|
|
let tabIds = Array.from(
|
|
windowWrapper.getHighlightedTabs(),
|
|
tab => tab.id
|
|
);
|
|
fire.async({ tabIds: tabIds, windowId: event.windowId });
|
|
};
|
|
|
|
tabTracker.on("tabs-highlighted", highlightListener);
|
|
return {
|
|
unregister() {
|
|
tabTracker.off("tabs-highlighted", highlightListener);
|
|
},
|
|
convert(_fire, _context) {
|
|
fire = _fire;
|
|
context = _context;
|
|
},
|
|
};
|
|
},
|
|
|
|
onUpdated({ fire, context }, params) {
|
|
let { extension } = this;
|
|
let { tabManager } = extension;
|
|
let [filterProps] = params;
|
|
let filter = { ...filterProps };
|
|
if (filter.urls) {
|
|
filter.urls = new MatchPatternSet(filter.urls, {
|
|
restrictSchemes: false,
|
|
});
|
|
}
|
|
let needsModified = true;
|
|
if (filter.properties) {
|
|
// Default is to listen for all events.
|
|
needsModified = filter.properties.some(p => allAttrs.has(p));
|
|
filter.properties = new Set(filter.properties);
|
|
} else {
|
|
filter.properties = allProperties;
|
|
}
|
|
|
|
function sanitize(tab, changeInfo) {
|
|
let result = {};
|
|
let nonempty = false;
|
|
for (let prop in changeInfo) {
|
|
// In practice, changeInfo contains at most one property from
|
|
// restricted. Therefore it is not necessary to cache the value
|
|
// of tab.hasTabPermission outside the loop.
|
|
// Unnecessarily accessing tab.hasTabPermission can cause bugs, see
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1694699#c21
|
|
if (!restricted.has(prop) || tab.hasTabPermission) {
|
|
nonempty = true;
|
|
result[prop] = changeInfo[prop];
|
|
}
|
|
}
|
|
return nonempty && result;
|
|
}
|
|
|
|
function getWindowID(windowId) {
|
|
if (windowId === Window.WINDOW_ID_CURRENT) {
|
|
let window = windowTracker.getTopWindow(context);
|
|
if (!window) {
|
|
return undefined;
|
|
}
|
|
return windowTracker.getId(window);
|
|
}
|
|
return windowId;
|
|
}
|
|
|
|
function matchFilters(tab, changed) {
|
|
if (!filterProps) {
|
|
return true;
|
|
}
|
|
if (filter.tabId != null && tab.id != filter.tabId) {
|
|
return false;
|
|
}
|
|
if (
|
|
filter.windowId != null &&
|
|
tab.windowId != getWindowID(filter.windowId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (filter.urls) {
|
|
return filter.urls.matches(tab._uri) && tab.hasTabPermission;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
let fireForTab = (tab, changed, nativeTab) => {
|
|
// Tab may be null if private and not_allowed.
|
|
if (!tab || !matchFilters(tab, changed)) {
|
|
return;
|
|
}
|
|
|
|
let changeInfo = sanitize(tab, changed);
|
|
if (changeInfo) {
|
|
tabTracker.maybeWaitForTabOpen(nativeTab).then(() => {
|
|
if (!nativeTab.parentNode) {
|
|
// If the tab is already be destroyed, do nothing.
|
|
return;
|
|
}
|
|
fire.async(tab.id, changeInfo, tab.convert());
|
|
});
|
|
}
|
|
};
|
|
|
|
let listener = event => {
|
|
// Ignore any events prior to TabOpen
|
|
// and events that are triggered while tabs are swapped between windows.
|
|
if (event.originalTarget.initializingTab) {
|
|
return;
|
|
}
|
|
if (!extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
|
|
return;
|
|
}
|
|
let needed = [];
|
|
if (event.type == "TabAttrModified") {
|
|
let changed = event.detail.changed;
|
|
if (
|
|
changed.includes("image") &&
|
|
filter.properties.has("favIconUrl")
|
|
) {
|
|
needed.push("favIconUrl");
|
|
}
|
|
if (changed.includes("muted") && filter.properties.has("mutedInfo")) {
|
|
needed.push("mutedInfo");
|
|
}
|
|
if (
|
|
changed.includes("soundplaying") &&
|
|
filter.properties.has("audible")
|
|
) {
|
|
needed.push("audible");
|
|
}
|
|
if (changed.includes("label") && filter.properties.has("title")) {
|
|
needed.push("title");
|
|
}
|
|
if (
|
|
changed.includes("sharing") &&
|
|
filter.properties.has("sharingState")
|
|
) {
|
|
needed.push("sharingState");
|
|
}
|
|
if (
|
|
changed.includes("attention") &&
|
|
filter.properties.has("attention")
|
|
) {
|
|
needed.push("attention");
|
|
}
|
|
} else if (event.type == "TabPinned") {
|
|
needed.push("pinned");
|
|
} else if (event.type == "TabUnpinned") {
|
|
needed.push("pinned");
|
|
} else if (event.type == "TabBrowserInserted") {
|
|
// This may be an adopted tab. Bail early to avoid asking tabManager
|
|
// about the tab before we run the adoption logic in ext-browser.js.
|
|
if (event.detail.insertedOnTabCreation) {
|
|
return;
|
|
}
|
|
needed.push("discarded");
|
|
} else if (event.type == "TabBrowserDiscarded") {
|
|
needed.push("discarded");
|
|
} else if (event.type == "TabShow") {
|
|
needed.push("hidden");
|
|
} else if (event.type == "TabHide") {
|
|
needed.push("hidden");
|
|
}
|
|
|
|
let tab = tabManager.getWrapper(event.originalTarget);
|
|
|
|
let changeInfo = {};
|
|
for (let prop of needed) {
|
|
changeInfo[prop] = tab[prop];
|
|
}
|
|
|
|
fireForTab(tab, changeInfo, event.originalTarget);
|
|
};
|
|
|
|
let statusListener = ({ browser, status, url }) => {
|
|
let { gBrowser } = browser.ownerGlobal;
|
|
let tabElem = gBrowser.getTabForBrowser(browser);
|
|
if (tabElem) {
|
|
if (!extension.canAccessWindow(tabElem.ownerGlobal)) {
|
|
return;
|
|
}
|
|
|
|
let changed = {};
|
|
if (filter.properties.has("status")) {
|
|
changed.status = status;
|
|
}
|
|
if (url && filter.properties.has("url")) {
|
|
changed.url = url;
|
|
}
|
|
|
|
fireForTab(tabManager.wrapTab(tabElem), changed, tabElem);
|
|
}
|
|
};
|
|
|
|
let isArticleChangeListener = (messageName, message) => {
|
|
let { gBrowser } = message.target.ownerGlobal;
|
|
let nativeTab = gBrowser.getTabForBrowser(message.target);
|
|
|
|
if (nativeTab && extension.canAccessWindow(nativeTab.ownerGlobal)) {
|
|
let tab = tabManager.getWrapper(nativeTab);
|
|
fireForTab(tab, { isArticle: message.data.isArticle }, nativeTab);
|
|
}
|
|
};
|
|
|
|
let listeners = new Map();
|
|
if (filter.properties.has("status") || filter.properties.has("url")) {
|
|
listeners.set("status", statusListener);
|
|
}
|
|
if (needsModified) {
|
|
listeners.set("TabAttrModified", listener);
|
|
}
|
|
if (filter.properties.has("pinned")) {
|
|
listeners.set("TabPinned", listener);
|
|
listeners.set("TabUnpinned", listener);
|
|
}
|
|
if (filter.properties.has("discarded")) {
|
|
listeners.set("TabBrowserInserted", listener);
|
|
listeners.set("TabBrowserDiscarded", listener);
|
|
}
|
|
if (filter.properties.has("hidden")) {
|
|
listeners.set("TabShow", listener);
|
|
listeners.set("TabHide", listener);
|
|
}
|
|
|
|
for (let [name, listener] of listeners) {
|
|
windowTracker.addListener(name, listener);
|
|
}
|
|
|
|
if (filter.properties.has("isArticle")) {
|
|
tabTracker.on("tab-isarticle", isArticleChangeListener);
|
|
}
|
|
|
|
return {
|
|
unregister() {
|
|
for (let [name, listener] of listeners) {
|
|
windowTracker.removeListener(name, listener);
|
|
}
|
|
|
|
if (filter.properties.has("isArticle")) {
|
|
tabTracker.off("tab-isarticle", isArticleChangeListener);
|
|
}
|
|
},
|
|
convert(_fire, _context) {
|
|
fire = _fire;
|
|
context = _context;
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
getAPI(context) {
|
|
let { extension } = context;
|
|
let { tabManager, windowManager } = extension;
|
|
let extensionApi = this;
|
|
let module = "tabs";
|
|
|
|
function getTabOrActive(tabId) {
|
|
let tab =
|
|
tabId !== null ? tabTracker.getTab(tabId) : tabTracker.activeTab;
|
|
if (!tabManager.canAccessTab(tab)) {
|
|
throw new ExtensionError(
|
|
tabId === null
|
|
? "Cannot access activeTab"
|
|
: `Invalid tab ID: ${tabId}`
|
|
);
|
|
}
|
|
return tab;
|
|
}
|
|
|
|
function getNativeTabsFromIDArray(tabIds) {
|
|
if (!Array.isArray(tabIds)) {
|
|
tabIds = [tabIds];
|
|
}
|
|
return tabIds.map(tabId => {
|
|
let tab = tabTracker.getTab(tabId);
|
|
if (!tabManager.canAccessTab(tab)) {
|
|
throw new ExtensionError(`Invalid tab ID: ${tabId}`);
|
|
}
|
|
return tab;
|
|
});
|
|
}
|
|
|
|
async function promiseTabWhenReady(tabId) {
|
|
let tab;
|
|
if (tabId !== null) {
|
|
tab = tabManager.get(tabId);
|
|
} else {
|
|
tab = tabManager.getWrapper(tabTracker.activeTab);
|
|
}
|
|
if (!tab) {
|
|
throw new ExtensionError(
|
|
tabId == null ? "Cannot access activeTab" : `Invalid tab ID: ${tabId}`
|
|
);
|
|
}
|
|
|
|
await tabListener.awaitTabReady(tab.nativeTab);
|
|
|
|
return tab;
|
|
}
|
|
|
|
function setContentTriggeringPrincipal(url, browser, options) {
|
|
// For urls that we want to allow an extension to open in a tab, but
|
|
// that it may not otherwise have access to, we set the triggering
|
|
// principal to the url that is being opened. This is used for newtab,
|
|
// about: and moz-extension: protocols.
|
|
options.triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal(
|
|
Services.io.newURI(url),
|
|
{
|
|
userContextId: options.userContextId,
|
|
privateBrowsingId: PrivateBrowsingUtils.isBrowserPrivate(browser)
|
|
? 1
|
|
: 0,
|
|
}
|
|
);
|
|
}
|
|
|
|
let tabsApi = {
|
|
tabs: {
|
|
onActivated: new EventManager({
|
|
context,
|
|
module,
|
|
event: "onActivated",
|
|
extensionApi,
|
|
}).api(),
|
|
|
|
onCreated: new EventManager({
|
|
context,
|
|
module,
|
|
event: "onCreated",
|
|
extensionApi,
|
|
}).api(),
|
|
|
|
onHighlighted: new EventManager({
|
|
context,
|
|
module,
|
|
event: "onHighlighted",
|
|
extensionApi,
|
|
}).api(),
|
|
|
|
onAttached: new EventManager({
|
|
context,
|
|
module,
|
|
event: "onAttached",
|
|
extensionApi,
|
|
}).api(),
|
|
|
|
onDetached: new EventManager({
|
|
context,
|
|
module,
|
|
event: "onDetached",
|
|
extensionApi,
|
|
}).api(),
|
|
|
|
onRemoved: new EventManager({
|
|
context,
|
|
module,
|
|
event: "onRemoved",
|
|
extensionApi,
|
|
}).api(),
|
|
|
|
onReplaced: new EventManager({
|
|
context,
|
|
name: "tabs.onReplaced",
|
|
register: fire => {
|
|
return () => {};
|
|
},
|
|
}).api(),
|
|
|
|
onMoved: new EventManager({
|
|
context,
|
|
module,
|
|
event: "onMoved",
|
|
extensionApi,
|
|
}).api(),
|
|
|
|
onUpdated: new EventManager({
|
|
context,
|
|
module,
|
|
event: "onUpdated",
|
|
extensionApi,
|
|
}).api(),
|
|
|
|
create(createProperties) {
|
|
return new Promise((resolve, reject) => {
|
|
let window =
|
|
createProperties.windowId !== null
|
|
? windowTracker.getWindow(createProperties.windowId, context)
|
|
: windowTracker.getTopNormalWindow(context);
|
|
if (!window || !context.canAccessWindow(window)) {
|
|
throw new Error(
|
|
"Not allowed to create tabs on the target window"
|
|
);
|
|
}
|
|
let { gBrowserInit } = window;
|
|
if (!gBrowserInit || !gBrowserInit.delayedStartupFinished) {
|
|
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");
|
|
} else {
|
|
resolve(window);
|
|
}
|
|
}).then(window => {
|
|
let url;
|
|
|
|
let options = { triggeringPrincipal: context.principal };
|
|
if (createProperties.cookieStoreId) {
|
|
// May throw if validation fails.
|
|
options.userContextId = getUserContextIdForCookieStoreId(
|
|
extension,
|
|
createProperties.cookieStoreId,
|
|
PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser)
|
|
);
|
|
}
|
|
|
|
if (createProperties.url !== null) {
|
|
url = context.uri.resolve(createProperties.url);
|
|
|
|
if (
|
|
!url.startsWith("moz-extension://") &&
|
|
!context.checkLoadURL(url, { dontReportErrors: true })
|
|
) {
|
|
return Promise.reject({ message: `Illegal URL: ${url}` });
|
|
}
|
|
|
|
if (createProperties.openInReaderMode) {
|
|
url = `about:reader?url=${encodeURIComponent(url)}`;
|
|
}
|
|
} else {
|
|
url = window.BROWSER_NEW_TAB_URL;
|
|
}
|
|
let discardable = url && !url.startsWith("about:");
|
|
// Handle moz-ext separately from the discardable flag to retain prior behavior.
|
|
if (!discardable || url.startsWith("moz-extension://")) {
|
|
setContentTriggeringPrincipal(url, window.gBrowser, options);
|
|
}
|
|
|
|
tabListener.initTabReady();
|
|
const currentTab = window.gBrowser.selectedTab;
|
|
const { frameLoader } = currentTab.linkedBrowser;
|
|
const currentTabSize = {
|
|
width: frameLoader.lazyWidth,
|
|
height: frameLoader.lazyHeight,
|
|
};
|
|
|
|
if (createProperties.openerTabId !== null) {
|
|
options.ownerTab = tabTracker.getTab(
|
|
createProperties.openerTabId
|
|
);
|
|
options.openerBrowser = options.ownerTab.linkedBrowser;
|
|
if (options.ownerTab.ownerGlobal !== window) {
|
|
return Promise.reject({
|
|
message:
|
|
"Opener tab must be in the same window as the tab being created",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Simple properties
|
|
const properties = ["index", "pinned"];
|
|
for (let prop of properties) {
|
|
if (createProperties[prop] != null) {
|
|
options[prop] = createProperties[prop];
|
|
}
|
|
}
|
|
|
|
let active =
|
|
createProperties.active !== null
|
|
? createProperties.active
|
|
: !createProperties.discarded;
|
|
if (createProperties.discarded) {
|
|
if (active) {
|
|
return Promise.reject({
|
|
message: `Active tabs cannot be created and discarded.`,
|
|
});
|
|
}
|
|
if (createProperties.pinned) {
|
|
return Promise.reject({
|
|
message: `Pinned tabs cannot be created and discarded.`,
|
|
});
|
|
}
|
|
if (!discardable) {
|
|
return Promise.reject({
|
|
message: `Cannot create a discarded new tab or "about" urls.`,
|
|
});
|
|
}
|
|
options.createLazyBrowser = true;
|
|
options.lazyTabTitle = createProperties.title;
|
|
} else if (createProperties.title) {
|
|
return Promise.reject({
|
|
message: `Title may only be set for discarded tabs.`,
|
|
});
|
|
}
|
|
|
|
let nativeTab = window.gBrowser.addTab(url, options);
|
|
|
|
if (active) {
|
|
window.gBrowser.selectedTab = nativeTab;
|
|
if (!createProperties.url) {
|
|
window.gURLBar.select();
|
|
}
|
|
}
|
|
|
|
if (
|
|
createProperties.url &&
|
|
createProperties.url !== window.BROWSER_NEW_TAB_URL
|
|
) {
|
|
// 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(nativeTab);
|
|
}
|
|
|
|
if (createProperties.muted) {
|
|
nativeTab.toggleMuteAudio(extension.id);
|
|
}
|
|
|
|
return tabManager.convert(nativeTab, currentTabSize);
|
|
});
|
|
},
|
|
|
|
async remove(tabIds) {
|
|
let nativeTabs = getNativeTabsFromIDArray(tabIds);
|
|
|
|
if (nativeTabs.length === 1) {
|
|
nativeTabs[0].ownerGlobal.gBrowser.removeTab(nativeTabs[0]);
|
|
return;
|
|
}
|
|
|
|
// Or for multiple tabs, first group them by window
|
|
let windowTabMap = new DefaultMap(() => []);
|
|
for (let nativeTab of nativeTabs) {
|
|
windowTabMap.get(nativeTab.ownerGlobal).push(nativeTab);
|
|
}
|
|
|
|
// Then make one call to removeTabs() for each window, to keep the
|
|
// count accurate for SessionStore.getLastClosedTabCount().
|
|
// Note: always pass options to disable animation and the warning
|
|
// dialogue box, so that way all tabs are actually closed when the
|
|
// browser.tabs.remove() promise resolves
|
|
for (let [eachWindow, tabsToClose] of windowTabMap.entries()) {
|
|
eachWindow.gBrowser.removeTabs(tabsToClose, {
|
|
animate: false,
|
|
suppressWarnAboutClosingWindow: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
async discard(tabIds) {
|
|
for (let nativeTab of getNativeTabsFromIDArray(tabIds)) {
|
|
nativeTab.ownerGlobal.gBrowser.discardBrowser(nativeTab);
|
|
}
|
|
},
|
|
|
|
async update(tabId, updateProperties) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
|
|
let tabbrowser = nativeTab.ownerGlobal.gBrowser;
|
|
|
|
if (updateProperties.url !== null) {
|
|
let url = context.uri.resolve(updateProperties.url);
|
|
|
|
let options = {
|
|
flags: updateProperties.loadReplace
|
|
? Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY
|
|
: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
|
|
triggeringPrincipal: context.principal,
|
|
};
|
|
|
|
if (!context.checkLoadURL(url, { dontReportErrors: true })) {
|
|
// We allow loading top level tabs for "other" extensions.
|
|
if (url.startsWith("moz-extension://")) {
|
|
setContentTriggeringPrincipal(url, tabbrowser, options);
|
|
} else {
|
|
return Promise.reject({ message: `Illegal URL: ${url}` });
|
|
}
|
|
}
|
|
|
|
let browser = nativeTab.linkedBrowser;
|
|
if (nativeTab.linkedPanel) {
|
|
browser.loadURI(url, options);
|
|
} else {
|
|
// Shift to fully loaded browser and make
|
|
// sure load handler is instantiated.
|
|
nativeTab.addEventListener(
|
|
"SSTabRestoring",
|
|
() => browser.loadURI(url, options),
|
|
{ once: true }
|
|
);
|
|
tabbrowser._insertBrowser(nativeTab);
|
|
}
|
|
}
|
|
|
|
if (updateProperties.active) {
|
|
tabbrowser.selectedTab = nativeTab;
|
|
}
|
|
if (updateProperties.highlighted !== null) {
|
|
if (updateProperties.highlighted) {
|
|
if (!nativeTab.selected && !nativeTab.multiselected) {
|
|
tabbrowser.addToMultiSelectedTabs(nativeTab);
|
|
// Select the highlighted tab unless active:false is provided.
|
|
// Note that Chrome selects it even in that case.
|
|
if (updateProperties.active !== false) {
|
|
tabbrowser.lockClearMultiSelectionOnce();
|
|
tabbrowser.selectedTab = nativeTab;
|
|
}
|
|
}
|
|
} else {
|
|
tabbrowser.removeFromMultiSelectedTabs(nativeTab);
|
|
}
|
|
}
|
|
if (updateProperties.muted !== null) {
|
|
if (nativeTab.muted != updateProperties.muted) {
|
|
nativeTab.toggleMuteAudio(extension.id);
|
|
}
|
|
}
|
|
if (updateProperties.pinned !== null) {
|
|
if (updateProperties.pinned) {
|
|
tabbrowser.pinTab(nativeTab);
|
|
} else {
|
|
tabbrowser.unpinTab(nativeTab);
|
|
}
|
|
}
|
|
if (updateProperties.openerTabId !== null) {
|
|
let opener = tabTracker.getTab(updateProperties.openerTabId);
|
|
if (opener.ownerDocument !== nativeTab.ownerDocument) {
|
|
return Promise.reject({
|
|
message:
|
|
"Opener tab must be in the same window as the tab being updated",
|
|
});
|
|
}
|
|
tabTracker.setOpener(nativeTab, opener);
|
|
}
|
|
if (updateProperties.successorTabId !== null) {
|
|
let successor = null;
|
|
if (updateProperties.successorTabId !== TAB_ID_NONE) {
|
|
successor = tabTracker.getTab(
|
|
updateProperties.successorTabId,
|
|
null
|
|
);
|
|
if (!successor) {
|
|
throw new ExtensionError("Invalid successorTabId");
|
|
}
|
|
// This also ensures "privateness" matches.
|
|
if (successor.ownerDocument !== nativeTab.ownerDocument) {
|
|
throw new ExtensionError(
|
|
"Successor tab must be in the same window as the tab being updated"
|
|
);
|
|
}
|
|
}
|
|
tabbrowser.setSuccessor(nativeTab, successor);
|
|
}
|
|
|
|
return tabManager.convert(nativeTab);
|
|
},
|
|
|
|
async reload(tabId, reloadProperties) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
|
|
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
|
if (reloadProperties && reloadProperties.bypassCache) {
|
|
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
|
|
}
|
|
nativeTab.linkedBrowser.reloadWithFlags(flags);
|
|
},
|
|
|
|
async warmup(tabId) {
|
|
let nativeTab = tabTracker.getTab(tabId);
|
|
if (!tabManager.canAccessTab(nativeTab)) {
|
|
throw new ExtensionError(`Invalid tab ID: ${tabId}`);
|
|
}
|
|
let tabbrowser = nativeTab.ownerGlobal.gBrowser;
|
|
tabbrowser.warmupTab(nativeTab);
|
|
},
|
|
|
|
async get(tabId) {
|
|
return tabManager.get(tabId).convert();
|
|
},
|
|
|
|
getCurrent() {
|
|
let tabData;
|
|
if (context.tabId) {
|
|
tabData = tabManager.get(context.tabId).convert();
|
|
}
|
|
return Promise.resolve(tabData);
|
|
},
|
|
|
|
async query(queryInfo) {
|
|
return Array.from(tabManager.query(queryInfo, context), tab =>
|
|
tab.convert()
|
|
);
|
|
},
|
|
|
|
async captureTab(tabId, options) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
await tabListener.awaitTabReady(nativeTab);
|
|
|
|
let browser = nativeTab.linkedBrowser;
|
|
let window = browser.ownerGlobal;
|
|
let zoom = window.ZoomManager.getZoomForBrowser(browser);
|
|
|
|
let tab = tabManager.wrapTab(nativeTab);
|
|
return tab.capture(context, zoom, options);
|
|
},
|
|
|
|
async captureVisibleTab(windowId, options) {
|
|
let window =
|
|
windowId == null
|
|
? windowTracker.getTopWindow(context)
|
|
: windowTracker.getWindow(windowId, context);
|
|
|
|
let tab = tabManager.wrapTab(window.gBrowser.selectedTab);
|
|
await tabListener.awaitTabReady(tab.nativeTab);
|
|
|
|
let zoom = window.ZoomManager.getZoomForBrowser(
|
|
tab.nativeTab.linkedBrowser
|
|
);
|
|
return tab.capture(context, zoom, options);
|
|
},
|
|
|
|
async detectLanguage(tabId) {
|
|
let tab = await promiseTabWhenReady(tabId);
|
|
let results = await tab.queryContent("DetectLanguage", {});
|
|
return results[0];
|
|
},
|
|
|
|
async executeScript(tabId, details) {
|
|
let tab = await promiseTabWhenReady(tabId);
|
|
return tab.executeScript(context, details);
|
|
},
|
|
|
|
async insertCSS(tabId, details) {
|
|
let tab = await promiseTabWhenReady(tabId);
|
|
return tab.insertCSS(context, details);
|
|
},
|
|
|
|
async removeCSS(tabId, details) {
|
|
let tab = await promiseTabWhenReady(tabId);
|
|
return tab.removeCSS(context, details);
|
|
},
|
|
|
|
async move(tabIds, moveProperties) {
|
|
let tabsMoved = [];
|
|
if (!Array.isArray(tabIds)) {
|
|
tabIds = [tabIds];
|
|
}
|
|
|
|
let destinationWindow = null;
|
|
if (moveProperties.windowId !== null) {
|
|
destinationWindow = windowTracker.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 lastInsertionMap = new Map();
|
|
|
|
for (let nativeTab of getNativeTabsFromIDArray(tabIds)) {
|
|
// If the window is not specified, use the window from the tab.
|
|
let window = destinationWindow || nativeTab.ownerGlobal;
|
|
let isSameWindow = nativeTab.ownerGlobal == window;
|
|
let gBrowser = window.gBrowser;
|
|
|
|
// If we are not moving the tab to a different window, and the window
|
|
// only has one tab, do nothing.
|
|
if (isSameWindow && gBrowser.tabs.length === 1) {
|
|
lastInsertionMap.set(window, 0);
|
|
continue;
|
|
}
|
|
// If moving between windows, be sure privacy matches. While gBrowser
|
|
// prevents this, we want to silently ignore it.
|
|
if (
|
|
!isSameWindow &&
|
|
PrivateBrowsingUtils.isBrowserPrivate(gBrowser) !=
|
|
PrivateBrowsingUtils.isBrowserPrivate(
|
|
nativeTab.ownerGlobal.gBrowser
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
let insertionPoint;
|
|
let lastInsertion = lastInsertionMap.get(window);
|
|
if (lastInsertion == null) {
|
|
insertionPoint = moveProperties.index;
|
|
let maxIndex = gBrowser.tabs.length - (isSameWindow ? 1 : 0);
|
|
if (insertionPoint == -1) {
|
|
// If the index is -1 it should go to the end of the tabs.
|
|
insertionPoint = maxIndex;
|
|
} else {
|
|
insertionPoint = Math.min(insertionPoint, maxIndex);
|
|
}
|
|
} else if (isSameWindow && nativeTab._tPos <= lastInsertion) {
|
|
// lastInsertion is the current index of the last inserted tab.
|
|
// insertionPoint is the desired index of the current tab *after* moving it.
|
|
// When the tab is moved, the last inserted tab will no longer be at index
|
|
// lastInsertion, but (lastInsertion - 1). To position the tabs adjacent to
|
|
// each other, the tab should therefore be at index (lastInsertion - 1 + 1).
|
|
insertionPoint = lastInsertion;
|
|
} else {
|
|
// In this case the last inserted tab will stay at index lastInsertion,
|
|
// so we should move the current tab to index (lastInsertion + 1).
|
|
insertionPoint = lastInsertion + 1;
|
|
}
|
|
|
|
// 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 = nativeTab.pinned
|
|
? insertionPoint <= numPinned
|
|
: insertionPoint >= numPinned;
|
|
if (!ok) {
|
|
continue;
|
|
}
|
|
|
|
if (isSameWindow) {
|
|
// If the window we are moving is the same, just move the tab.
|
|
gBrowser.moveTabTo(nativeTab, insertionPoint);
|
|
} else {
|
|
// If the window we are moving the tab in is different, then move the tab
|
|
// to the new window.
|
|
nativeTab = gBrowser.adoptTab(nativeTab, insertionPoint, false);
|
|
}
|
|
lastInsertionMap.set(window, nativeTab._tPos);
|
|
tabsMoved.push(nativeTab);
|
|
}
|
|
|
|
return tabsMoved.map(nativeTab => tabManager.convert(nativeTab));
|
|
},
|
|
|
|
duplicate(tabId, duplicateProperties) {
|
|
const { active, index } = duplicateProperties || {};
|
|
const inBackground = active === undefined ? false : !active;
|
|
|
|
// Schema requires tab id.
|
|
let nativeTab = getTabOrActive(tabId);
|
|
|
|
let gBrowser = nativeTab.ownerGlobal.gBrowser;
|
|
let newTab = gBrowser.duplicateTab(nativeTab, true, {
|
|
inBackground,
|
|
index,
|
|
});
|
|
|
|
tabListener.blockTabUntilRestored(newTab);
|
|
return new Promise(resolve => {
|
|
// Use SSTabRestoring to ensure that the tab's URL is ready before
|
|
// resolving the promise.
|
|
newTab.addEventListener(
|
|
"SSTabRestoring",
|
|
() => resolve(tabManager.convert(newTab)),
|
|
{ once: true }
|
|
);
|
|
});
|
|
},
|
|
|
|
getZoom(tabId) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
|
|
let { ZoomManager } = nativeTab.ownerGlobal;
|
|
let zoom = ZoomManager.getZoomForBrowser(nativeTab.linkedBrowser);
|
|
|
|
return Promise.resolve(zoom);
|
|
},
|
|
|
|
setZoom(tabId, zoom) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
|
|
let { FullZoom, ZoomManager } = nativeTab.ownerGlobal;
|
|
|
|
if (zoom === 0) {
|
|
// A value of zero means use the default zoom factor.
|
|
return FullZoom.reset(nativeTab.linkedBrowser);
|
|
} else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
|
|
FullZoom.setZoom(zoom, nativeTab.linkedBrowser);
|
|
} else {
|
|
return Promise.reject({
|
|
message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
|
|
});
|
|
}
|
|
|
|
return Promise.resolve();
|
|
},
|
|
|
|
async getZoomSettings(tabId) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
|
|
let { FullZoom, ZoomUI } = nativeTab.ownerGlobal;
|
|
|
|
return {
|
|
mode: "automatic",
|
|
scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
|
|
defaultZoomFactor: await ZoomUI.getGlobalValue(),
|
|
};
|
|
},
|
|
|
|
async setZoomSettings(tabId, settings) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
|
|
let currentSettings = await this.getZoomSettings(
|
|
tabTracker.getId(nativeTab)
|
|
);
|
|
|
|
if (
|
|
!Object.keys(settings).every(
|
|
key => settings[key] === currentSettings[key]
|
|
)
|
|
) {
|
|
throw new ExtensionError(
|
|
`Unsupported zoom settings: ${JSON.stringify(settings)}`
|
|
);
|
|
}
|
|
},
|
|
|
|
onZoomChange: new EventManager({
|
|
context,
|
|
name: "tabs.onZoomChange",
|
|
register: 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 windowTracker.browserWindows()) {
|
|
if (!context.canAccessWindow(window)) {
|
|
continue;
|
|
}
|
|
for (let nativeTab of window.gBrowser.tabs) {
|
|
let browser = nativeTab.linkedBrowser;
|
|
zoomLevels.set(browser, getZoomLevel(browser));
|
|
}
|
|
}
|
|
|
|
let tabCreated = (eventName, event) => {
|
|
let browser = event.nativeTab.linkedBrowser;
|
|
if (!event.isPrivate || context.privateBrowsingAllowed) {
|
|
zoomLevels.set(browser, getZoomLevel(browser));
|
|
}
|
|
};
|
|
|
|
let zoomListener = async event => {
|
|
let browser = event.originalTarget;
|
|
|
|
// For non-remote browsers, this event is dispatched on the document
|
|
// rather than on the <browser>. But either way we have a node here.
|
|
if (browser.nodeType == browser.DOCUMENT_NODE) {
|
|
browser = browser.docShell.chromeEventHandler;
|
|
}
|
|
|
|
if (!context.canAccessWindow(browser.ownerGlobal)) {
|
|
return;
|
|
}
|
|
|
|
let { gBrowser } = browser.ownerGlobal;
|
|
let nativeTab = gBrowser.getTabForBrowser(browser);
|
|
if (!nativeTab) {
|
|
// 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 = tabTracker.getId(nativeTab);
|
|
fire.async({
|
|
tabId,
|
|
oldZoomFactor,
|
|
newZoomFactor,
|
|
zoomSettings: await tabsApi.tabs.getZoomSettings(tabId),
|
|
});
|
|
}
|
|
};
|
|
|
|
tabTracker.on("tab-attached", tabCreated);
|
|
tabTracker.on("tab-created", tabCreated);
|
|
|
|
windowTracker.addListener("FullZoomChange", zoomListener);
|
|
windowTracker.addListener("TextZoomChange", zoomListener);
|
|
return () => {
|
|
tabTracker.off("tab-attached", tabCreated);
|
|
tabTracker.off("tab-created", tabCreated);
|
|
|
|
windowTracker.removeListener("FullZoomChange", zoomListener);
|
|
windowTracker.removeListener("TextZoomChange", zoomListener);
|
|
};
|
|
},
|
|
}).api(),
|
|
|
|
print() {
|
|
let activeTab = getTabOrActive(null);
|
|
let { PrintUtils } = activeTab.ownerGlobal;
|
|
PrintUtils.startPrintWindow(activeTab.linkedBrowser.browsingContext);
|
|
},
|
|
|
|
// Legacy API
|
|
printPreview() {
|
|
return Promise.resolve(this.print());
|
|
},
|
|
|
|
saveAsPDF(pageSettings) {
|
|
let activeTab = getTabOrActive(null);
|
|
let picker = Cc["@mozilla.org/filepicker;1"].createInstance(
|
|
Ci.nsIFilePicker
|
|
);
|
|
let title = strBundle.GetStringFromName(
|
|
"saveaspdf.saveasdialog.title"
|
|
);
|
|
let filename;
|
|
if (
|
|
pageSettings.toFileName !== null &&
|
|
pageSettings.toFileName != ""
|
|
) {
|
|
filename = pageSettings.toFileName;
|
|
} else if (activeTab.linkedBrowser.contentTitle != "") {
|
|
filename = activeTab.linkedBrowser.contentTitle;
|
|
} else {
|
|
let url = new URL(activeTab.linkedBrowser.currentURI.spec);
|
|
let path = decodeURIComponent(url.pathname);
|
|
path = path.replace(/\/$/, "");
|
|
filename = path.split("/").pop();
|
|
if (filename == "") {
|
|
filename = url.hostname;
|
|
}
|
|
}
|
|
filename = DownloadPaths.sanitize(filename);
|
|
|
|
picker.init(activeTab.ownerGlobal, title, Ci.nsIFilePicker.modeSave);
|
|
picker.appendFilter("PDF", "*.pdf");
|
|
picker.defaultExtension = "pdf";
|
|
picker.defaultString = filename;
|
|
|
|
return new Promise(resolve => {
|
|
picker.open(function(retval) {
|
|
if (retval == 0 || retval == 2) {
|
|
// OK clicked (retval == 0) or replace confirmed (retval == 2)
|
|
|
|
// Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
|
|
// the print progress listener is never called. This workaround ensures that a correct status is always returned.
|
|
try {
|
|
let fstream = Cc[
|
|
"@mozilla.org/network/file-output-stream;1"
|
|
].createInstance(Ci.nsIFileOutputStream);
|
|
fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
|
|
fstream.close();
|
|
} catch (e) {
|
|
resolve(retval == 0 ? "not_saved" : "not_replaced");
|
|
return;
|
|
}
|
|
|
|
let psService = Cc[
|
|
"@mozilla.org/gfx/printsettings-service;1"
|
|
].getService(Ci.nsIPrintSettingsService);
|
|
let printSettings = psService.createNewPrintSettings();
|
|
|
|
printSettings.printerName = "";
|
|
printSettings.isInitializedFromPrinter = true;
|
|
printSettings.isInitializedFromPrefs = true;
|
|
|
|
printSettings.outputDestination =
|
|
Ci.nsIPrintSettings.kOutputDestinationFile;
|
|
printSettings.toFileName = picker.file.path;
|
|
|
|
printSettings.printSilent = true;
|
|
|
|
printSettings.outputFormat =
|
|
Ci.nsIPrintSettings.kOutputFormatPDF;
|
|
|
|
if (pageSettings.paperSizeUnit !== null) {
|
|
printSettings.paperSizeUnit = pageSettings.paperSizeUnit;
|
|
}
|
|
if (pageSettings.paperWidth !== null) {
|
|
printSettings.paperWidth = pageSettings.paperWidth;
|
|
}
|
|
if (pageSettings.paperHeight !== null) {
|
|
printSettings.paperHeight = pageSettings.paperHeight;
|
|
}
|
|
if (pageSettings.orientation !== null) {
|
|
printSettings.orientation = pageSettings.orientation;
|
|
}
|
|
if (pageSettings.scaling !== null) {
|
|
printSettings.scaling = pageSettings.scaling;
|
|
}
|
|
if (pageSettings.shrinkToFit !== null) {
|
|
printSettings.shrinkToFit = pageSettings.shrinkToFit;
|
|
}
|
|
if (pageSettings.showBackgroundColors !== null) {
|
|
printSettings.printBGColors =
|
|
pageSettings.showBackgroundColors;
|
|
}
|
|
if (pageSettings.showBackgroundImages !== null) {
|
|
printSettings.printBGImages =
|
|
pageSettings.showBackgroundImages;
|
|
}
|
|
if (pageSettings.edgeLeft !== null) {
|
|
printSettings.edgeLeft = pageSettings.edgeLeft;
|
|
}
|
|
if (pageSettings.edgeRight !== null) {
|
|
printSettings.edgeRight = pageSettings.edgeRight;
|
|
}
|
|
if (pageSettings.edgeTop !== null) {
|
|
printSettings.edgeTop = pageSettings.edgeTop;
|
|
}
|
|
if (pageSettings.edgeBottom !== null) {
|
|
printSettings.edgeBottom = pageSettings.edgeBottom;
|
|
}
|
|
if (pageSettings.marginLeft !== null) {
|
|
printSettings.marginLeft = pageSettings.marginLeft;
|
|
}
|
|
if (pageSettings.marginRight !== null) {
|
|
printSettings.marginRight = pageSettings.marginRight;
|
|
}
|
|
if (pageSettings.marginTop !== null) {
|
|
printSettings.marginTop = pageSettings.marginTop;
|
|
}
|
|
if (pageSettings.marginBottom !== null) {
|
|
printSettings.marginBottom = pageSettings.marginBottom;
|
|
}
|
|
if (pageSettings.headerLeft !== null) {
|
|
printSettings.headerStrLeft = pageSettings.headerLeft;
|
|
}
|
|
if (pageSettings.headerCenter !== null) {
|
|
printSettings.headerStrCenter = pageSettings.headerCenter;
|
|
}
|
|
if (pageSettings.headerRight !== null) {
|
|
printSettings.headerStrRight = pageSettings.headerRight;
|
|
}
|
|
if (pageSettings.footerLeft !== null) {
|
|
printSettings.footerStrLeft = pageSettings.footerLeft;
|
|
}
|
|
if (pageSettings.footerCenter !== null) {
|
|
printSettings.footerStrCenter = pageSettings.footerCenter;
|
|
}
|
|
if (pageSettings.footerRight !== null) {
|
|
printSettings.footerStrRight = pageSettings.footerRight;
|
|
}
|
|
|
|
activeTab.linkedBrowser.browsingContext
|
|
.print(printSettings)
|
|
.then(() => resolve(retval == 0 ? "saved" : "replaced"))
|
|
.catch(() =>
|
|
resolve(retval == 0 ? "not_saved" : "not_replaced")
|
|
);
|
|
} else {
|
|
// Cancel clicked (retval == 1)
|
|
resolve("canceled");
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
async toggleReaderMode(tabId) {
|
|
let tab = await promiseTabWhenReady(tabId);
|
|
if (!tab.isInReaderMode && !tab.isArticle) {
|
|
throw new ExtensionError(
|
|
"The specified tab cannot be placed into reader mode."
|
|
);
|
|
}
|
|
let nativeTab = getTabOrActive(tabId);
|
|
|
|
nativeTab.linkedBrowser.sendMessageToActor(
|
|
"Reader:ToggleReaderMode",
|
|
{},
|
|
"AboutReader"
|
|
);
|
|
},
|
|
|
|
moveInSuccession(tabIds, tabId, options) {
|
|
const { insert, append } = options || {};
|
|
const tabIdSet = new Set(tabIds);
|
|
if (tabIdSet.size !== tabIds.length) {
|
|
throw new ExtensionError(
|
|
"IDs must not occur more than once in tabIds"
|
|
);
|
|
}
|
|
if ((append || insert) && tabIdSet.has(tabId)) {
|
|
throw new ExtensionError(
|
|
"Value of tabId must not occur in tabIds if append or insert is true"
|
|
);
|
|
}
|
|
|
|
const referenceTab = tabTracker.getTab(tabId, null);
|
|
let referenceWindow = referenceTab && referenceTab.ownerGlobal;
|
|
if (referenceWindow && !context.canAccessWindow(referenceWindow)) {
|
|
throw new ExtensionError(`Invalid tab ID: ${tabId}`);
|
|
}
|
|
let previousTab, lastSuccessor;
|
|
if (append) {
|
|
previousTab = referenceTab;
|
|
lastSuccessor =
|
|
(insert && referenceTab && referenceTab.successor) || null;
|
|
} else {
|
|
lastSuccessor = referenceTab;
|
|
}
|
|
|
|
let firstTab;
|
|
for (const tabId of tabIds) {
|
|
const tab = tabTracker.getTab(tabId, null);
|
|
if (tab === null) {
|
|
continue;
|
|
}
|
|
if (!tabManager.canAccessTab(tab)) {
|
|
throw new ExtensionError(`Invalid tab ID: ${tabId}`);
|
|
}
|
|
if (referenceWindow === null) {
|
|
referenceWindow = tab.ownerGlobal;
|
|
} else if (tab.ownerGlobal !== referenceWindow) {
|
|
continue;
|
|
}
|
|
referenceWindow.gBrowser.replaceInSuccession(tab, tab.successor);
|
|
if (append && tab === lastSuccessor) {
|
|
lastSuccessor = tab.successor;
|
|
}
|
|
if (previousTab) {
|
|
referenceWindow.gBrowser.setSuccessor(previousTab, tab);
|
|
} else {
|
|
firstTab = tab;
|
|
}
|
|
previousTab = tab;
|
|
}
|
|
|
|
if (previousTab) {
|
|
if (!append && insert && lastSuccessor !== null) {
|
|
referenceWindow.gBrowser.replaceInSuccession(
|
|
lastSuccessor,
|
|
firstTab
|
|
);
|
|
}
|
|
referenceWindow.gBrowser.setSuccessor(previousTab, lastSuccessor);
|
|
}
|
|
},
|
|
|
|
show(tabIds) {
|
|
for (let tab of getNativeTabsFromIDArray(tabIds)) {
|
|
if (tab.ownerGlobal) {
|
|
tab.ownerGlobal.gBrowser.showTab(tab);
|
|
}
|
|
}
|
|
},
|
|
|
|
hide(tabIds) {
|
|
let hidden = [];
|
|
for (let tab of getNativeTabsFromIDArray(tabIds)) {
|
|
if (tab.ownerGlobal && !tab.hidden) {
|
|
tab.ownerGlobal.gBrowser.hideTab(tab, extension.id);
|
|
if (tab.hidden) {
|
|
hidden.push(tabTracker.getId(tab));
|
|
}
|
|
}
|
|
}
|
|
if (hidden.length) {
|
|
let win = Services.wm.getMostRecentWindow("navigator:browser");
|
|
tabHidePopup.open(win, extension.id);
|
|
}
|
|
return hidden;
|
|
},
|
|
|
|
highlight(highlightInfo) {
|
|
let { windowId, tabs, populate } = highlightInfo;
|
|
if (windowId == null) {
|
|
windowId = Window.WINDOW_ID_CURRENT;
|
|
}
|
|
let window = windowTracker.getWindow(windowId, context);
|
|
if (!context.canAccessWindow(window)) {
|
|
throw new ExtensionError(`Invalid window ID: ${windowId}`);
|
|
}
|
|
|
|
if (!Array.isArray(tabs)) {
|
|
tabs = [tabs];
|
|
} else if (!tabs.length) {
|
|
throw new ExtensionError("No highlighted tab.");
|
|
}
|
|
window.gBrowser.selectedTabs = tabs.map(tabIndex => {
|
|
let tab = window.gBrowser.tabs[tabIndex];
|
|
if (!tab || !tabManager.canAccessTab(tab)) {
|
|
throw new ExtensionError("No tab at index: " + tabIndex);
|
|
}
|
|
return tab;
|
|
});
|
|
return windowManager.convert(window, { populate });
|
|
},
|
|
|
|
goForward(tabId) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
nativeTab.linkedBrowser.goForward();
|
|
},
|
|
|
|
goBack(tabId) {
|
|
let nativeTab = getTabOrActive(tabId);
|
|
nativeTab.linkedBrowser.goBack();
|
|
},
|
|
},
|
|
};
|
|
return tabsApi;
|
|
}
|
|
};
|