зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1175770 - New extension API (r=Mossop)
This commit is contained in:
Родитель
1ad7580e1a
Коммит
b1a00d7c72
|
@ -9,6 +9,7 @@ let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
|||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/ExtensionContent.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
|
||||
"resource:///modules/E10SUtils.jsm");
|
||||
|
@ -656,3 +657,8 @@ let DOMFullscreenHandler = {
|
|||
}
|
||||
};
|
||||
DOMFullscreenHandler.init();
|
||||
|
||||
ExtensionContent.init(this);
|
||||
addEventListener("unload", () => {
|
||||
ExtensionContent.uninit(this);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/* 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";
|
||||
|
||||
Components.utils.import("resource://gre/modules/Extension.jsm");
|
||||
|
||||
let extension;
|
||||
|
||||
function startup(data, reason)
|
||||
{
|
||||
extension = new Extension(data);
|
||||
extension.startup();
|
||||
}
|
||||
|
||||
function shutdown(data, reason)
|
||||
{
|
||||
extension.shutdown();
|
||||
}
|
|
@ -0,0 +1,326 @@
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
|
||||
"resource:///modules/CustomizableUI.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/devtools/event-emitter.js");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
EventManager,
|
||||
DefaultWeakMap,
|
||||
ignoreEvent,
|
||||
runSafe,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// WeakMap[Extension -> BrowserAction]
|
||||
let browserActionMap = new WeakMap();
|
||||
|
||||
function browserActionOf(extension)
|
||||
{
|
||||
return browserActionMap.get(extension);
|
||||
}
|
||||
|
||||
function makeWidgetId(id)
|
||||
{
|
||||
id = id.toLowerCase();
|
||||
return id.replace(/[^a-z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
let nextActionId = 0;
|
||||
|
||||
// Responsible for the browser_action section of the manifest as well
|
||||
// as the associated popup.
|
||||
function BrowserAction(options, extension)
|
||||
{
|
||||
this.extension = extension;
|
||||
this.id = makeWidgetId(extension.id) + "-browser-action";
|
||||
this.widget = null;
|
||||
|
||||
this.title = new DefaultWeakMap(extension.localize(options.default_title));
|
||||
this.badgeText = new DefaultWeakMap();
|
||||
this.badgeBackgroundColor = new DefaultWeakMap();
|
||||
this.icon = new DefaultWeakMap(options.default_icon);
|
||||
this.popup = new DefaultWeakMap(options.default_popup);
|
||||
|
||||
// Make the default something that won't compare equal to anything.
|
||||
this.prevPopups = new DefaultWeakMap({});
|
||||
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
BrowserAction.prototype = {
|
||||
build() {
|
||||
let widget = CustomizableUI.createWidget({
|
||||
id: this.id,
|
||||
type: "custom",
|
||||
removable: true,
|
||||
defaultArea: CustomizableUI.AREA_NAVBAR,
|
||||
onBuild: document => {
|
||||
let node = document.createElement("toolbarbutton");
|
||||
node.id = this.id;
|
||||
node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional badged-button");
|
||||
node.setAttribute("constrain-size", "true");
|
||||
|
||||
this.updateTab(null, node);
|
||||
|
||||
let tabbrowser = document.defaultView.gBrowser;
|
||||
tabbrowser.ownerDocument.addEventListener("TabSelect", () => {
|
||||
this.updateTab(tabbrowser.selectedTab, node);
|
||||
});
|
||||
|
||||
node.addEventListener("command", event => {
|
||||
if (node.getAttribute("type") != "panel") {
|
||||
this.emit("click");
|
||||
}
|
||||
});
|
||||
|
||||
return node;
|
||||
},
|
||||
});
|
||||
this.widget = widget;
|
||||
},
|
||||
|
||||
// Initialize the toolbar icon and popup given that |tab| is the
|
||||
// current tab and |node| is the CustomizableUI node. Note: |tab|
|
||||
// will be null if we don't know the current tab yet (during
|
||||
// initialization).
|
||||
updateTab(tab, node) {
|
||||
let window = node.ownerDocument.defaultView;
|
||||
|
||||
let title = this.getProperty(tab, "title");
|
||||
if (title) {
|
||||
node.setAttribute("tooltiptext", title);
|
||||
node.setAttribute("label", title);
|
||||
} else {
|
||||
node.removeAttribute("tooltiptext");
|
||||
node.removeAttribute("label");
|
||||
}
|
||||
|
||||
let badgeText = this.badgeText.get(tab);
|
||||
if (badgeText) {
|
||||
node.setAttribute("badge", badgeText);
|
||||
} else {
|
||||
node.removeAttribute("badge");
|
||||
}
|
||||
|
||||
function toHex(n) {
|
||||
return Math.floor(n / 16).toString(16) + (n % 16).toString(16);
|
||||
}
|
||||
|
||||
let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
|
||||
'class', 'toolbarbutton-badge');
|
||||
if (badgeNode) {
|
||||
let color = this.badgeBackgroundColor.get(tab);
|
||||
if (Array.isArray(color)) {
|
||||
color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
||||
}
|
||||
badgeNode.style.backgroundColor = color;
|
||||
}
|
||||
|
||||
let iconURL = this.getIcon(tab, node);
|
||||
node.setAttribute("image", iconURL);
|
||||
|
||||
let popup = this.getProperty(tab, "popup");
|
||||
|
||||
if (popup != this.prevPopups.get(window)) {
|
||||
this.prevPopups.set(window, popup);
|
||||
|
||||
let panel = node.querySelector("panel");
|
||||
if (panel) {
|
||||
panel.remove();
|
||||
}
|
||||
|
||||
if (popup) {
|
||||
let popupURL = this.extension.baseURI.resolve(popup);
|
||||
node.setAttribute("type", "panel");
|
||||
|
||||
let document = node.ownerDocument;
|
||||
let panel = document.createElement("panel");
|
||||
panel.setAttribute("class", "browser-action-panel");
|
||||
panel.setAttribute("type", "arrow");
|
||||
panel.setAttribute("flip", "slide");
|
||||
node.appendChild(panel);
|
||||
|
||||
let browser = document.createElementNS(XUL_NS, "browser");
|
||||
browser.setAttribute("type", "content");
|
||||
browser.setAttribute("disableglobalhistory", "true");
|
||||
browser.setAttribute("width", "500");
|
||||
browser.setAttribute("height", "500");
|
||||
panel.appendChild(browser);
|
||||
|
||||
let loadListener = () => {
|
||||
panel.removeEventListener("load", loadListener);
|
||||
|
||||
if (this.context) {
|
||||
this.context.unload();
|
||||
}
|
||||
|
||||
this.context = new ExtensionPage(this.extension, {
|
||||
type: "popup",
|
||||
contentWindow: browser.contentWindow,
|
||||
uri: Services.io.newURI(popupURL, null, null),
|
||||
docShell: browser.docShell,
|
||||
});
|
||||
GlobalManager.injectInDocShell(browser.docShell, this.extension, this.context);
|
||||
browser.setAttribute("src", popupURL);
|
||||
};
|
||||
panel.addEventListener("load", loadListener);
|
||||
} else {
|
||||
node.removeAttribute("type");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Note: tab is allowed to be null here.
|
||||
getIcon(tab, node) {
|
||||
let icon = this.icon.get(tab);
|
||||
|
||||
let url;
|
||||
if (typeof(icon) != "object") {
|
||||
url = icon;
|
||||
} else {
|
||||
let window = node.ownerDocument.defaultView;
|
||||
let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindowUtils);
|
||||
let res = {value: 1}
|
||||
utils.getResolution(res);
|
||||
|
||||
let size = res.value == 1 ? 19 : 38;
|
||||
url = icon[size];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
return this.extension.baseURI.resolve(url);
|
||||
} else {
|
||||
return "chrome://browser/content/extension.svg";
|
||||
}
|
||||
},
|
||||
|
||||
// Update the toolbar button for a given window.
|
||||
updateWindow(window) {
|
||||
let tab = window.gBrowser ? window.gBrowser.selectedTab : null;
|
||||
let node = CustomizableUI.getWidget(this.id).forWindow(window).node;
|
||||
this.updateTab(tab, node);
|
||||
},
|
||||
|
||||
// Update the toolbar button when the extension changes the icon,
|
||||
// title, badge, etc. If it only changes a parameter for a single
|
||||
// tab, |tab| will be that tab. Otherwise it will be null.
|
||||
updateOnChange(tab) {
|
||||
if (tab) {
|
||||
if (tab.selected) {
|
||||
this.updateWindow(tab.ownerDocument.defaultView);
|
||||
}
|
||||
} else {
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
if (window.gBrowser) {
|
||||
this.updateWindow(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// tab is allowed to be null.
|
||||
// prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
|
||||
setProperty(tab, prop, value) {
|
||||
this[prop].set(tab, value);
|
||||
this.updateOnChange(tab);
|
||||
},
|
||||
|
||||
// tab is allowed to be null.
|
||||
// prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
|
||||
getProperty(tab, prop) {
|
||||
return this[prop].get(tab);
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
CustomizableUI.destroyWidget(this.id);
|
||||
},
|
||||
};
|
||||
|
||||
EventEmitter.decorate(BrowserAction.prototype);
|
||||
|
||||
extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
|
||||
let browserAction = new BrowserAction(manifest.browser_action, extension);
|
||||
browserAction.build();
|
||||
browserActionMap.set(extension, browserAction);
|
||||
});
|
||||
|
||||
extensions.on("shutdown", (type, extension) => {
|
||||
if (browserActionMap.has(extension)) {
|
||||
browserActionMap.get(extension).shutdown();
|
||||
browserActionMap.delete(extension);
|
||||
}
|
||||
});
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
browserAction: {
|
||||
onClicked: new EventManager(context, "browserAction.onClicked", fire => {
|
||||
let listener = () => {
|
||||
let tab = TabManager.activeTab;
|
||||
fire(TabManager.convert(extension, tab));
|
||||
};
|
||||
browserActionOf(extension).on("click", listener);
|
||||
return () => {
|
||||
browserActionOf(extension).off("click", listener);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
setTitle: function(details) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
browserActionOf(extension).setProperty(tab, "title", details.title);
|
||||
},
|
||||
|
||||
getTitle: function(details, callback) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
let title = browserActionOf(extension).getProperty(tab, "title");
|
||||
runSafe(context, callback, title);
|
||||
},
|
||||
|
||||
setIcon: function(details, callback) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
if (details.imageData) {
|
||||
// FIXME: Support the imageData attribute.
|
||||
return;
|
||||
}
|
||||
browserActionOf(extension).setProperty(tab, "icon", details.path);
|
||||
},
|
||||
|
||||
setBadgeText: function(details) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
browserActionOf(extension).setProperty(tab, "badgeText", details.text);
|
||||
},
|
||||
|
||||
getBadgeText: function(details, callback) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
let text = browserActionOf(extension).getProperty(tab, "badgeText");
|
||||
runSafe(context, callback, text);
|
||||
},
|
||||
|
||||
setPopup: function(details) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
browserActionOf(extension).setProperty(tab, "popup", details.popup);
|
||||
},
|
||||
|
||||
getPopup: function(details, callback) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
let popup = browserActionOf(extension).getProperty(tab, "popup");
|
||||
runSafe(context, callback, popup);
|
||||
},
|
||||
|
||||
setBadgeBackgroundColor: function(details) {
|
||||
let color = details.color;
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
browserActionOf(extension).setProperty(tab, "badgeBackgroundColor", details.color);
|
||||
},
|
||||
|
||||
getBadgeBackgroundColor: function(details, callback) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
let color = browserActionOf(extension).getProperty(tab, "badgeBackgroundColor");
|
||||
runSafe(context, callback, color);
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
extensions.registerPrivilegedAPI("contextMenus", (extension, context) => {
|
||||
return {
|
||||
contextMenus: {
|
||||
create() {},
|
||||
removeAll() {},
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,486 @@
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "NewTabURL",
|
||||
"resource:///modules/NewTabURL.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
EventManager,
|
||||
ignoreEvent,
|
||||
runSafe,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// This function is pretty tightly tied to Extension.jsm.
|
||||
// Its job is to fill in the |tab| property of the sender.
|
||||
function getSender(context, target, sender)
|
||||
{
|
||||
// The message was sent from a content script to a <browser> element.
|
||||
// We can just get the |tab| from |target|.
|
||||
if (target instanceof Ci.nsIDOMXULElement) {
|
||||
// The message came from a content script.
|
||||
let tabbrowser = target.ownerDocument.defaultView.gBrowser;
|
||||
if (!tabbrowser) {
|
||||
return;
|
||||
}
|
||||
let tab = tabbrowser.getTabForBrowser(target);
|
||||
|
||||
sender.tab = TabManager.convert(context.extension, tab);
|
||||
} else {
|
||||
// The message came from an ExtensionPage. In that case, it should
|
||||
// include a tabId property (which is filled in by the page-open
|
||||
// listener below).
|
||||
if ("tabId" in sender) {
|
||||
sender.tab = TabManager.convert(context.extension, TabManager.getTab(sender.tabId));
|
||||
delete sender.tabId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WeakMap[ExtensionPage -> {tab, parentWindow}]
|
||||
let pageDataMap = new WeakMap();
|
||||
|
||||
// This listener fires whenever an extension page opens in a tab
|
||||
// (either initiated by the extension or the user). Its job is to fill
|
||||
// in some tab-specific details and keep data around about the
|
||||
// ExtensionPage.
|
||||
extensions.on("page-load", (type, page, params, sender, delegate) => {
|
||||
if (params.type == "tab") {
|
||||
let browser = params.docShell.chromeEventHandler;
|
||||
let parentWindow = browser.ownerDocument.defaultView;
|
||||
let tab = parentWindow.gBrowser.getTabForBrowser(browser);
|
||||
sender.tabId = TabManager.getId(tab);
|
||||
|
||||
pageDataMap.set(page, {tab, parentWindow});
|
||||
}
|
||||
|
||||
delegate.getSender = getSender;
|
||||
});
|
||||
|
||||
extensions.on("page-unload", (type, page) => {
|
||||
pageDataMap.delete(page);
|
||||
});
|
||||
|
||||
extensions.on("page-shutdown", (type, page) => {
|
||||
if (pageDataMap.has(page)) {
|
||||
let {tab, parentWindow} = pageDataMap.get(page);
|
||||
pageDataMap.delete(page);
|
||||
|
||||
parentWindow.gBrowser.removeTab(tab);
|
||||
}
|
||||
});
|
||||
|
||||
extensions.on("fill-browser-data", (type, browser, data, result) => {
|
||||
let tabId = TabManager.getBrowserId(browser);
|
||||
if (tabId == -1) {
|
||||
result.cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
data.tabId = tabId;
|
||||
});
|
||||
|
||||
// TODO: activeTab permission
|
||||
|
||||
extensions.registerAPI((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.ownerDocument.defaultView);
|
||||
fire({tabId, windowId});
|
||||
}).api(),
|
||||
|
||||
onCreated: new EventManager(context, "tabs.onCreated", fire => {
|
||||
let listener = event => {
|
||||
let tab = event.originalTarget;
|
||||
fire({tab: TabManager.convert(extension, tab)});
|
||||
};
|
||||
|
||||
let windowListener = window => {
|
||||
for (let tab of window.gBrowser.tabs) {
|
||||
fire({tab: TabManager.convert(extension, tab)});
|
||||
}
|
||||
};
|
||||
|
||||
WindowListManager.addOpenListener(windowListener, false);
|
||||
AllWindowEvents.addListener("TabOpen", listener);
|
||||
return () => {
|
||||
WindowListManager.removeOpenListener(windowListener);
|
||||
AllWindowEvents.removeListener("TabOpen", listener);
|
||||
};
|
||||
}).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 listener = event => {
|
||||
let tab = event.originalTarget;
|
||||
let window = tab.ownerDocument.defaultView;
|
||||
let tabId = TabManager.getId(tab);
|
||||
|
||||
let changeInfo = {};
|
||||
let needed = false;
|
||||
if (event.type == "TabAttrModified") {
|
||||
if (event.detail.changed.indexOf("image") != -1) {
|
||||
changeInfo.favIconUrl = window.gBrowser.getIcon(tab);
|
||||
needed = true;
|
||||
}
|
||||
} else if (event.type == "TabPinned") {
|
||||
changeInfo.pinned = true;
|
||||
needed = true;
|
||||
} else if (event.type == "TabUnpinned") {
|
||||
changeInfo.pinned = false;
|
||||
needed = true;
|
||||
}
|
||||
|
||||
[needed, changeInfo] = sanitize(extension, changeInfo);
|
||||
if (needed) {
|
||||
fire(tabId, changeInfo, TabManager.convert(extension, 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";
|
||||
}
|
||||
|
||||
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
|
||||
let tab = gBrowser.getTabForBrowser(browser);
|
||||
let tabId = TabManager.getId(tab);
|
||||
let [needed, changeInfo] = sanitize(extension, {status});
|
||||
fire(tabId, changeInfo, TabManager.convert(extension, tab));
|
||||
},
|
||||
|
||||
onLocationChange(browser, webProgress, request, locationURI, flags) {
|
||||
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
|
||||
let tab = gBrowser.getTabForBrowser(browser);
|
||||
let tabId = TabManager.getId(tab);
|
||||
let [needed, changeInfo] = sanitize(extension, {url: locationURI.spec});
|
||||
if (needed) {
|
||||
fire(tabId, changeInfo, TabManager.convert(extension, tab));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
AllWindowEvents.addListener("progress", progressListener);
|
||||
AllWindowEvents.addListener("TabAttrModified", listener);
|
||||
AllWindowEvents.addListener("TabPinned", listener);
|
||||
AllWindowEvents.addListener("TabUnpinned", listener);
|
||||
return () => {
|
||||
AllWindowEvents.removeListener("progress", progressListener);
|
||||
AllWindowEvents.addListener("TabAttrModified", listener);
|
||||
AllWindowEvents.addListener("TabPinned", listener);
|
||||
AllWindowEvents.addListener("TabUnpinned", listener);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
onReplaced: ignoreEvent(),
|
||||
|
||||
onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
|
||||
let tabListener = event => {
|
||||
let tab = event.originalTarget;
|
||||
let tabId = TabManager.getId(tab);
|
||||
let windowId = WindowManager.getId(tab.ownerDocument.defaultView);
|
||||
let removeInfo = {windowId, isWindowClosing: false};
|
||||
fire(tabId, removeInfo);
|
||||
};
|
||||
|
||||
let windowListener = window => {
|
||||
for (let tab of window.gBrowser.tabs) {
|
||||
let tabId = TabManager.getId(tab);
|
||||
let windowId = WindowManager.getId(window);
|
||||
let removeInfo = {windowId, isWindowClosing: true};
|
||||
fire(tabId, removeInfo);
|
||||
}
|
||||
};
|
||||
|
||||
WindowListManager.addCloseListener(windowListener);
|
||||
AllWindowEvents.addListener("TabClose", tabListener);
|
||||
return () => {
|
||||
WindowListManager.removeCloseListener(windowListener);
|
||||
AllWindowEvents.removeListener("TabClose", tabListener);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
create: function(createProperties, callback) {
|
||||
if (!createProperties) {
|
||||
createProperties = {};
|
||||
}
|
||||
|
||||
let url = createProperties.url || NewTabURL.get();
|
||||
url = extension.baseURI.resolve(url);
|
||||
|
||||
function createInWindow(window) {
|
||||
let tab = window.gBrowser.addTab(url);
|
||||
|
||||
let active = true;
|
||||
if ("active" in createProperties) {
|
||||
active = createProperties.active;
|
||||
} else if ("selected" in createProperties) {
|
||||
active = createProperties.selected;
|
||||
}
|
||||
if (active) {
|
||||
window.gBrowser.selectedTab = tab;
|
||||
}
|
||||
|
||||
if ("index" in createProperties) {
|
||||
window.gBrowser.moveTabTo(tab, createProperties.index);
|
||||
}
|
||||
|
||||
if (createProperties.pinned) {
|
||||
window.gBrowser.pinTab(tab);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
runSafe(context, callback, TabManager.convert(extension, tab));
|
||||
}
|
||||
}
|
||||
|
||||
let window = createProperties.windowId ?
|
||||
WindowManager.getWindow(createProperties.windowId) :
|
||||
WindowManager.topWindow;
|
||||
if (!window.gBrowser) {
|
||||
let obs = (finishedWindow, topic, data) => {
|
||||
if (finishedWindow != window) {
|
||||
return;
|
||||
}
|
||||
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
|
||||
createInWindow(window);
|
||||
};
|
||||
Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
|
||||
} else {
|
||||
createInWindow(window);
|
||||
}
|
||||
},
|
||||
|
||||
remove: function(tabs, callback) {
|
||||
if (!Array.isArray(tabs)) {
|
||||
tabs = [tabs];
|
||||
}
|
||||
|
||||
for (let tabId of tabs) {
|
||||
let tab = TabManager.getTab(tabId);
|
||||
tab.ownerDocument.defaultView.gBrowser.removeTab(tab);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
runSafe(context, callback);
|
||||
}
|
||||
},
|
||||
|
||||
update: function(...args) {
|
||||
let tabId, updateProperties, callback;
|
||||
if (args.length == 1) {
|
||||
updateProperties = args[0];
|
||||
} else {
|
||||
[tabId, updateProperties, callback] = args;
|
||||
}
|
||||
|
||||
let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
|
||||
let tabbrowser = tab.ownerDocument.gBrowser;
|
||||
if ("url" in updateProperties) {
|
||||
tab.linkedBrowser.loadURI(updateProperties.url);
|
||||
}
|
||||
if ("active" in updateProperties) {
|
||||
if (updateProperties.active) {
|
||||
tabbrowser.selectedTab = tab;
|
||||
} else {
|
||||
// Not sure what to do here? Which tab should we select?
|
||||
}
|
||||
}
|
||||
if ("pinned" in updateProperties) {
|
||||
if (updateProperties.pinned) {
|
||||
tabbrowser.pinTab(tab);
|
||||
} else {
|
||||
tabbrowser.unpinTab(tab);
|
||||
}
|
||||
}
|
||||
// FIXME: highlighted/selected, openerTabId
|
||||
|
||||
if (callback) {
|
||||
runSafe(context, callback, TabManager.convert(extension, tab));
|
||||
}
|
||||
},
|
||||
|
||||
reload: function(tabId, reloadProperties, callback) {
|
||||
let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
|
||||
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
||||
if (reloadProperties && reloadProperties.bypassCache) {
|
||||
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
|
||||
}
|
||||
tab.linkedBrowser.reloadWithFlags(flags);
|
||||
|
||||
if (callback) {
|
||||
runSafe(context, callback);
|
||||
}
|
||||
},
|
||||
|
||||
get: function(tabId, callback) {
|
||||
let tab = TabManager.getTab(tabId);
|
||||
runSafe(context, callback, TabManager.convert(extension, tab));
|
||||
},
|
||||
|
||||
getAllInWindow: function(...args) {
|
||||
let window, callback;
|
||||
if (args.length == 1) {
|
||||
callbacks = args[0];
|
||||
} else {
|
||||
window = WindowManager.getWindow(args[0]);
|
||||
callback = args[1];
|
||||
}
|
||||
|
||||
if (!window) {
|
||||
window = WindowManager.topWindow;
|
||||
}
|
||||
|
||||
return self.tabs.query({windowId: WindowManager.getId(window)}, callback);
|
||||
},
|
||||
|
||||
query: function(queryInfo, callback) {
|
||||
if (!queryInfo) {
|
||||
queryInfo = {};
|
||||
}
|
||||
|
||||
function matches(window, tab) {
|
||||
let props = ["active", "pinned", "highlighted", "status", "title", "url", "index"];
|
||||
for (let prop of props) {
|
||||
if (prop in queryInfo && queryInfo[prop] != tab[prop]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let lastFocused = window == WindowManager.topWindow;
|
||||
if ("lastFocusedWindow" in queryInfo && queryInfo.lastFocusedWindow != lastFocused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let windowType = WindowManager.windowType(window);
|
||||
if ("windowType" in queryInfo && queryInfo.windowType != windowType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("windowId" in queryInfo) {
|
||||
if (queryInfo.windowId == WindowManager.WINDOW_ID_CURRENT) {
|
||||
if (context.contentWindow != window) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (queryInfo.windowId != tab.windowId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("currentWindow" in queryInfo) {
|
||||
let eq = window == context.contentWindow;
|
||||
if (queryInfo.currentWindow != eq) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
let result = [];
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
let tabs = TabManager.getTabs(extension, window);
|
||||
for (let tab of tabs) {
|
||||
if (matches(window, tab)) {
|
||||
result.push(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
runSafe(context, callback, result);
|
||||
},
|
||||
|
||||
_execute: function(tabId, details, kind, callback) {
|
||||
let tab = tabId ? TabManager.getTab(tabId) : TabManager.activeTab;
|
||||
let mm = tab.linkedBrowser.messageManager;
|
||||
|
||||
let options = {js: [], css: []};
|
||||
if (details.code) {
|
||||
options[kind + 'Code'] = details.code;
|
||||
}
|
||||
if (details.file) {
|
||||
options[kind].push(extension.baseURI.resolve(details.file));
|
||||
}
|
||||
if (details.allFrames) {
|
||||
options.all_frames = details.allFrames;
|
||||
}
|
||||
if (details.matchAboutBlank) {
|
||||
options.match_about_blank = details.matchAboutBlank;
|
||||
}
|
||||
if (details.runAt) {
|
||||
options.run_at = details.runAt;
|
||||
}
|
||||
mm.sendAsyncMessage("Extension:Execute",
|
||||
{extensionId: extension.id, options});
|
||||
|
||||
// TODO: Call the callback with the result (which is what???).
|
||||
},
|
||||
|
||||
executeScript: function(...args) {
|
||||
if (args.length == 1) {
|
||||
self.tabs._execute(undefined, args[0], 'js', undefined);
|
||||
} else {
|
||||
self.tabs._execute(args[0], args[1], 'js', args[2]);
|
||||
}
|
||||
},
|
||||
|
||||
insertCss: function(tabId, details, callback) {
|
||||
if (args.length == 1) {
|
||||
self.tabs._execute(undefined, args[0], 'css', undefined);
|
||||
} else {
|
||||
self.tabs._execute(args[0], args[1], 'css', args[2]);
|
||||
}
|
||||
},
|
||||
|
||||
connect: function(tabId, connectInfo) {
|
||||
let tab = TabManager.getTab(tabId);
|
||||
let mm = tab.linkedBrowser.messageManager;
|
||||
|
||||
let name = connectInfo.name || "";
|
||||
let recipient = {extensionId: extension.id};
|
||||
if ("frameId" in connectInfo) {
|
||||
recipient.frameId = connectInfo.frameId;
|
||||
}
|
||||
return context.messenger.connect(mm, name, recipient);
|
||||
},
|
||||
|
||||
sendMessage: function(tabId, message, options, responseCallback) {
|
||||
let tab = TabManager.getTab(tabId);
|
||||
let mm = tab.linkedBrowser.messageManager;
|
||||
|
||||
let recipient = {extensionId: extension.id};
|
||||
if (options && "frameId" in options) {
|
||||
recipient.frameId = options.frameId;
|
||||
}
|
||||
return context.messenger.sendMessage(mm, message, recipient, responseCallback);
|
||||
},
|
||||
},
|
||||
};
|
||||
return self;
|
||||
});
|
|
@ -0,0 +1,324 @@
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
EventManager,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// This file provides some useful code for the |tabs| and |windows|
|
||||
// modules. All of the code is installed on |global|, which is a scope
|
||||
// shared among the different ext-*.js scripts.
|
||||
|
||||
// Manages mapping between XUL tabs and extension tab IDs.
|
||||
global.TabManager = {
|
||||
_tabs: new WeakMap(),
|
||||
_nextId: 1,
|
||||
|
||||
getId(tab) {
|
||||
if (this._tabs.has(tab)) {
|
||||
return this._tabs.get(tab);
|
||||
}
|
||||
let id = this._nextId++;
|
||||
this._tabs.set(tab, id);
|
||||
return id;
|
||||
},
|
||||
|
||||
getBrowserId(browser) {
|
||||
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
|
||||
// Some non-browser windows have gBrowser but not
|
||||
// getTabForBrowser!
|
||||
if (gBrowser && gBrowser.getTabForBrowser) {
|
||||
let tab = gBrowser.getTabForBrowser(browser);
|
||||
if (tab) {
|
||||
return this.getId(tab);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
|
||||
getTab(tabId) {
|
||||
// FIXME: Speed this up without leaking memory somehow.
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
if (!window.gBrowser) {
|
||||
continue;
|
||||
}
|
||||
for (let tab of window.gBrowser.tabs) {
|
||||
if (this.getId(tab) == tabId) {
|
||||
return tab;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
get activeTab() {
|
||||
let window = WindowManager.topWindow;
|
||||
if (window && window.gBrowser) {
|
||||
return window.gBrowser.selectedTab;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getStatus(tab) {
|
||||
return tab.getAttribute("busy") == "true" ? "loading" : "complete";
|
||||
},
|
||||
|
||||
convert(extension, tab) {
|
||||
let window = tab.ownerDocument.defaultView;
|
||||
let windowActive = window == WindowManager.topWindow;
|
||||
let result = {
|
||||
id: this.getId(tab),
|
||||
index: tab._tPos,
|
||||
windowId: WindowManager.getId(window),
|
||||
selected: tab.selected,
|
||||
highlighted: tab.selected,
|
||||
active: tab.selected,
|
||||
pinned: tab.pinned,
|
||||
status: this.getStatus(tab),
|
||||
incognito: PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser),
|
||||
width: tab.linkedBrowser.clientWidth,
|
||||
height: tab.linkedBrowser.clientHeight,
|
||||
};
|
||||
|
||||
if (extension.hasPermission("tabs")) {
|
||||
result.url = tab.linkedBrowser.currentURI.spec;
|
||||
if (tab.linkedBrowser.contentTitle) {
|
||||
result.title = tab.linkedBrowser.contentTitle;
|
||||
}
|
||||
let icon = window.gBrowser.getIcon(tab);
|
||||
if (icon) {
|
||||
result.favIconUrl = icon;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
getTabs(extension, window) {
|
||||
if (!window.gBrowser) {
|
||||
return [];
|
||||
}
|
||||
return [ for (tab of window.gBrowser.tabs) this.convert(extension, tab) ];
|
||||
},
|
||||
};
|
||||
|
||||
// Manages mapping between XUL windows and extension window IDs.
|
||||
global.WindowManager = {
|
||||
_windows: new WeakMap(),
|
||||
_nextId: 0,
|
||||
|
||||
WINDOW_ID_NONE: -1,
|
||||
WINDOW_ID_CURRENT: -2,
|
||||
|
||||
get topWindow() {
|
||||
return Services.wm.getMostRecentWindow("navigator:browser");
|
||||
},
|
||||
|
||||
windowType(window) {
|
||||
// TODO: Make this work.
|
||||
return "normal";
|
||||
},
|
||||
|
||||
getId(window) {
|
||||
if (this._windows.has(window)) {
|
||||
return this._windows.get(window);
|
||||
}
|
||||
let id = this._nextId++;
|
||||
this._windows.set(window, id);
|
||||
return id;
|
||||
},
|
||||
|
||||
getWindow(id) {
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
if (this.getId(window) == id) {
|
||||
return window;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
convert(extension, window, getInfo) {
|
||||
let result = {
|
||||
id: this.getId(window),
|
||||
focused: window == WindowManager.topWindow,
|
||||
top: window.screenY,
|
||||
left: window.screenX,
|
||||
width: window.outerWidth,
|
||||
height: window.outerHeight,
|
||||
incognito: PrivateBrowsingUtils.isWindowPrivate(window),
|
||||
|
||||
// We fudge on these next two.
|
||||
type: this.windowType(window),
|
||||
state: window.fullScreen ? "fullscreen" : "normal",
|
||||
};
|
||||
|
||||
if (getInfo && getInfo.populate) {
|
||||
results.tabs = TabManager.getTabs(extension, window);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
// Manages listeners for window opening and closing. A window is
|
||||
// considered open when the "load" event fires on it. A window is
|
||||
// closed when a "domwindowclosed" notification fires for it.
|
||||
global.WindowListManager = {
|
||||
_openListeners: new Set(),
|
||||
_closeListeners: new Set(),
|
||||
|
||||
addOpenListener(listener, fireOnExisting = true) {
|
||||
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
|
||||
Services.ww.registerNotification(this);
|
||||
}
|
||||
this._openListeners.add(listener);
|
||||
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
if (window.document.readyState != "complete") {
|
||||
window.addEventListener("load", this);
|
||||
} else if (fireOnExisting) {
|
||||
listener(window);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeOpenListener(listener) {
|
||||
this._openListeners.delete(listener);
|
||||
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
|
||||
Services.ww.unregisterNotification(this);
|
||||
}
|
||||
},
|
||||
|
||||
addCloseListener(listener) {
|
||||
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
|
||||
Services.ww.registerNotification(this);
|
||||
}
|
||||
this._closeListeners.add(listener);
|
||||
},
|
||||
|
||||
removeCloseListener(listener) {
|
||||
this._closeListeners.delete(listener);
|
||||
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
|
||||
Services.ww.unregisterNotification(this);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
let window = event.target.defaultView;
|
||||
window.removeEventListener("load", this.loadListener);
|
||||
if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let listener of this._openListeners) {
|
||||
listener(window);
|
||||
}
|
||||
},
|
||||
|
||||
queryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
|
||||
|
||||
observe(window, topic, data) {
|
||||
if (topic == "domwindowclosed") {
|
||||
if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
|
||||
return;
|
||||
}
|
||||
|
||||
window.removeEventListener("load", this);
|
||||
for (let listener of this._closeListeners) {
|
||||
listener(window);
|
||||
}
|
||||
} else {
|
||||
window.addEventListener("load", this);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Provides a facility to listen for DOM events across all XUL windows.
|
||||
global.AllWindowEvents = {
|
||||
_listeners: new Map(),
|
||||
|
||||
// If |type| is a normal event type, invoke |listener| each time
|
||||
// that event fires in any open window. If |type| is "progress", add
|
||||
// a web progress listener that covers all open windows.
|
||||
addListener(type, listener) {
|
||||
if (type == "domwindowopened") {
|
||||
return WindowListManager.addOpenListener(listener);
|
||||
} else if (type == "domwindowclosed") {
|
||||
return WindowListManager.addCloseListener(listener);
|
||||
}
|
||||
|
||||
let needOpenListener = this._listeners.size == 0;
|
||||
|
||||
if (!this._listeners.has(type)) {
|
||||
this._listeners.set(type, new Set());
|
||||
}
|
||||
let list = this._listeners.get(type);
|
||||
list.add(listener);
|
||||
|
||||
if (needOpenListener) {
|
||||
WindowListManager.addOpenListener(this.openListener);
|
||||
}
|
||||
},
|
||||
|
||||
removeListener(type, listener) {
|
||||
if (type == "domwindowopened") {
|
||||
return WindowListManager.removeOpenListener(listener);
|
||||
} else if (type == "domwindowclosed") {
|
||||
return WindowListManager.removeCloseListener(listener);
|
||||
}
|
||||
|
||||
let listeners = this._listeners.get(type);
|
||||
listeners.delete(listener);
|
||||
if (listeners.length == 0) {
|
||||
this._listeners.delete(type);
|
||||
if (this._listeners.size == 0) {
|
||||
WindowListManager.removeOpenListener(this.openListener);
|
||||
}
|
||||
}
|
||||
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
if (type == "progress") {
|
||||
window.gBrowser.removeTabsProgressListener(listener);
|
||||
} else {
|
||||
window.removeEventListener(type, listener);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Runs whenever the "load" event fires for a new window.
|
||||
openListener(window) {
|
||||
for (let [eventType, listeners] of AllWindowEvents._listeners) {
|
||||
for (let listener of listeners) {
|
||||
if (eventType == "progress") {
|
||||
window.gBrowser.addTabsProgressListener(listener);
|
||||
} else {
|
||||
window.addEventListener(eventType, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Subclass of EventManager where we just need to call
|
||||
// add/removeEventListener on each XUL window.
|
||||
global.WindowEventManager = function(context, name, event, listener)
|
||||
{
|
||||
EventManager.call(this, context, name, fire => {
|
||||
let listener2 = (...args) => listener(fire, ...args);
|
||||
AllWindowEvents.addListener(event, listener2);
|
||||
return () => {
|
||||
AllWindowEvents.removeListener(event, listener2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
WindowEventManager.prototype = Object.create(EventManager.prototype);
|
|
@ -0,0 +1,151 @@
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "NewTabURL",
|
||||
"resource:///modules/NewTabURL.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
EventManager,
|
||||
ignoreEvent,
|
||||
runSafe,
|
||||
} = ExtensionUtils;
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
windows: {
|
||||
WINDOW_ID_CURRENT: WindowManager.WINDOW_ID_CURRENT,
|
||||
WINDOW_ID_NONE: WindowManager.WINDOW_ID_NONE,
|
||||
|
||||
onCreated:
|
||||
new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
|
||||
fire(WindowManager.convert(extension, window));
|
||||
}).api(),
|
||||
|
||||
onRemoved:
|
||||
new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
|
||||
fire(WindowManager.getId(window));
|
||||
}).api(),
|
||||
|
||||
onFocusChanged: new EventManager(context, "windows.onFocusChanged", fire => {
|
||||
// FIXME: This will send multiple messages for a single focus change.
|
||||
let listener = event => {
|
||||
let window = WindowManager.topWindow;
|
||||
let windowId = window ? WindowManager.getId(window) : WindowManager.WINDOW_ID_NONE;
|
||||
fire(windowId);
|
||||
};
|
||||
AllWindowEvents.addListener("focus", listener);
|
||||
AllWindowEvents.addListener("blur", listener);
|
||||
return () => {
|
||||
AllWindowEvents.removeListener("focus", listener);
|
||||
AllWindowEvents.removeListener("blur", listener);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
get: function(windowId, getInfo, callback) {
|
||||
let window = WindowManager.getWindow(windowId);
|
||||
runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
|
||||
},
|
||||
|
||||
getCurrent: function(getInfo, callback) {
|
||||
let window = context.contentWindow;
|
||||
runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
|
||||
},
|
||||
|
||||
getLastFocused: function(...args) {
|
||||
let getInfo, callback;
|
||||
if (args.length == 1) {
|
||||
callback = args[0];
|
||||
} else {
|
||||
[getInfo, callback] = args;
|
||||
}
|
||||
let window = WindowManager.topWindow;
|
||||
runSafe(context, callback, WindowManager.convert(extension, window, getInfo));
|
||||
},
|
||||
|
||||
getAll: function(getAll, callback) {
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
let windows = [];
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
windows.push(WindowManager.convert(extension, window, getInfo));
|
||||
}
|
||||
runSafe(context, callback, windows);
|
||||
},
|
||||
|
||||
create: function(createData, callback) {
|
||||
function mkstr(s) {
|
||||
let result = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
|
||||
result.data = s;
|
||||
return result;
|
||||
}
|
||||
|
||||
let args = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
|
||||
if ("url" in createData) {
|
||||
if (Array.isArray(createData.url)) {
|
||||
let array = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
|
||||
for (let url of createData.url) {
|
||||
array.AppendElement(mkstr(url));
|
||||
}
|
||||
args.AppendElement(array);
|
||||
} else {
|
||||
args.AppendElement(mkstr(createData.url));
|
||||
}
|
||||
} else {
|
||||
args.AppendElement(mkstr(NewTabURL.get()));
|
||||
}
|
||||
|
||||
let extraFeatures = "";
|
||||
if ("incognito" in createData) {
|
||||
if (createData.incognito) {
|
||||
extraFeatures += ",private";
|
||||
} else {
|
||||
extraFeatures += ",non-private";
|
||||
}
|
||||
}
|
||||
|
||||
let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
|
||||
"chrome,dialog=no,all" + extraFeatures, args);
|
||||
|
||||
if ("left" in createData || "top" in createData) {
|
||||
let left = "left" in createData ? createData.left : window.screenX;
|
||||
let top = "top" in createData ? createData.top : window.screenY;
|
||||
window.moveTo(left, top);
|
||||
}
|
||||
if ("width" in createData || "height" in createData) {
|
||||
let width = "width" in createData ? createData.width : window.outerWidth;
|
||||
let height = "height" in createData ? createData.height : window.outerHeight;
|
||||
window.resizeTo(width, height);
|
||||
}
|
||||
|
||||
// TODO: focused, type, state
|
||||
|
||||
window.addEventListener("load", function listener() {
|
||||
window.removeEventListener("load", listener);
|
||||
if (callback) {
|
||||
runSafe(context, callback, WindowManager.convert(extension, window));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
update: function(windowId, updateInfo, callback) {
|
||||
let window = WindowManager.getWindow(windowId);
|
||||
if (updateInfo.focused) {
|
||||
Services.focus.activeWindow = window;
|
||||
}
|
||||
// TODO: All the other properties...
|
||||
runSafe(context, callback, WindowManager.convert(extension, window));
|
||||
},
|
||||
|
||||
remove: function(windowId, callback) {
|
||||
let window = WindowManager.getWindow(windowId);
|
||||
window.close();
|
||||
|
||||
let listener = () => {
|
||||
AllWindowEvents.removeListener("domwindowclosed", listener);
|
||||
if (callback) {
|
||||
runSafe(context, callback);
|
||||
}
|
||||
};
|
||||
AllWindowEvents.addListener("domwindowclosed", listener);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64" height="64" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<style>
|
||||
.style-puzzle-piece {
|
||||
fill: url('#gradient-linear-puzzle-piece');
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="gradient-linear-puzzle-piece" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#66cc52" stop-opacity="1"/>
|
||||
<stop offset="100%" stop-color="#60bf4c" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path class="style-puzzle-piece" d="M42,62c2.2,0,4-1.8,4-4l0-14.2c0,0,0.4-3.7,2.8-3.7c2.4,0,2.2,3.9,6.7,3.9c2.3,0,6.2-1.2,6.2-8.2 c0-7-3.9-7.9-6.2-7.9c-4.5,0-4.3,3.7-6.7,3.7c-2.4,0-2.8-3.8-2.8-3.8V22c0-2.2-1.8-4-4-4H31.5c0,0-3.4-0.6-3.4-3 c0-2.4,3.8-2.6,3.8-7.1c0-2.3-1.3-5.9-8.3-5.9s-8,3.6-8,5.9c0,4.5,3.4,4.7,3.4,7.1c0,2.4-3.4,3-3.4,3H6c-2.2,0-4,1.8-4,4l0,7.8 c0,0-0.4,6,4.4,6c3.1,0,3.2-4.1,7.3-4.1c2,0,4,1.9,4,6c0,4.2-2,6.3-4,6.3c-4,0-4.2-4.1-7.3-4.1c-4.8,0-4.4,5.8-4.4,5.8L2,58 c0,2.2,1.8,4,4,4H19c0,0,6.3,0.4,6.3-4.4c0-3.1-4-3.6-4-7.7c0-2,2.2-4.5,6.4-4.5c4.2,0,6.6,2.5,6.6,4.5c0,4-3.9,4.6-3.9,7.7 c0,4.9,6.3,4.4,6.3,4.4H42z"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.4 KiB |
|
@ -0,0 +1,11 @@
|
|||
# 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/.
|
||||
|
||||
browser.jar:
|
||||
content/browser/extension.svg (extension.svg)
|
||||
content/browser/ext-utils.js (ext-utils.js)
|
||||
content/browser/ext-contextMenus.js (ext-contextMenus.js)
|
||||
content/browser/ext-browserAction.js (ext-browserAction.js)
|
||||
content/browser/ext-tabs.js (ext-tabs.js)
|
||||
content/browser/ext-windows.js (ext-windows.js)
|
|
@ -0,0 +1,7 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env python
|
||||
# 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/.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import uuid
|
||||
import sys
|
||||
import os.path
|
||||
|
||||
parser = argparse.ArgumentParser(description='Create install.rdf from manifest.json')
|
||||
parser.add_argument('--locale')
|
||||
parser.add_argument('--profile')
|
||||
parser.add_argument('--uuid')
|
||||
parser.add_argument('dir')
|
||||
args = parser.parse_args()
|
||||
|
||||
manifestFile = os.path.join(args.dir, 'manifest.json')
|
||||
manifest = json.load(open(manifestFile))
|
||||
|
||||
locale = args.locale
|
||||
if not locale:
|
||||
locale = manifest.get('default_locale', 'en-US')
|
||||
|
||||
def process_locale(s):
|
||||
if s.startswith('__MSG_') and s.endswith('__'):
|
||||
tag = s[6:-2]
|
||||
path = os.path.join(args.dir, '_locales', locale, 'messages.json')
|
||||
data = json.load(open(path))
|
||||
return data[tag]['message']
|
||||
else:
|
||||
return s
|
||||
|
||||
id = args.uuid
|
||||
if not id:
|
||||
id = '{' + str(uuid.uuid4()) + '}'
|
||||
|
||||
name = process_locale(manifest['name'])
|
||||
desc = process_locale(manifest['description'])
|
||||
version = manifest['version']
|
||||
|
||||
installFile = open(os.path.join(args.dir, 'install.rdf'), 'w')
|
||||
print >>installFile, '<?xml version="1.0"?>'
|
||||
print >>installFile, '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"'
|
||||
print >>installFile, ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">'
|
||||
print >>installFile
|
||||
print >>installFile, ' <Description about="urn:mozilla:install-manifest">'
|
||||
print >>installFile, ' <em:id>{}</em:id>'.format(id)
|
||||
print >>installFile, ' <em:type>2</em:type>'
|
||||
print >>installFile, ' <em:name>{}</em:name>'.format(name)
|
||||
print >>installFile, ' <em:description>{}</em:description>'.format(desc)
|
||||
print >>installFile, ' <em:version>{}</em:version>'.format(version)
|
||||
print >>installFile, ' <em:bootstrap>true</em:bootstrap>'
|
||||
|
||||
print >>installFile, ' <em:targetApplication>'
|
||||
print >>installFile, ' <Description>'
|
||||
print >>installFile, ' <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>'
|
||||
print >>installFile, ' <em:minVersion>4.0</em:minVersion>'
|
||||
print >>installFile, ' <em:maxVersion>50.0</em:maxVersion>'
|
||||
print >>installFile, ' </Description>'
|
||||
print >>installFile, ' </em:targetApplication>'
|
||||
|
||||
print >>installFile, ' </Description>'
|
||||
print >>installFile, '</RDF>'
|
||||
installFile.close()
|
||||
|
||||
bootstrapPath = os.path.join(os.path.dirname(sys.argv[0]), 'bootstrap.js')
|
||||
data = open(bootstrapPath).read()
|
||||
boot = open(os.path.join(args.dir, 'bootstrap.js'), 'w')
|
||||
boot.write(data)
|
||||
boot.close()
|
||||
|
||||
if args.profile:
|
||||
os.system('mkdir -p {}/extensions'.format(args.profile))
|
||||
output = open(args.profile + '/extensions/' + id, 'w')
|
||||
print >>output, os.path.realpath(args.dir)
|
||||
output.close()
|
||||
else:
|
||||
dir = os.path.realpath(args.dir)
|
||||
if dir[-1] == os.sep:
|
||||
dir = dir[:-1]
|
||||
os.system('cd "{}"; zip ../"{}".xpi -r *'.format(args.dir, os.path.basename(dir)))
|
|
@ -9,6 +9,7 @@ DIRS += [
|
|||
'customizableui',
|
||||
'dirprovider',
|
||||
'downloads',
|
||||
'extensions',
|
||||
'feeds',
|
||||
'loop',
|
||||
'migration',
|
||||
|
|
|
@ -169,6 +169,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonWatcher",
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
|
||||
"resource://gre/modules/LightweightThemeManager.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
|
||||
"resource://gre/modules/ExtensionManagement.jsm");
|
||||
|
||||
const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
|
||||
const PREF_PLUGINS_UPDATEURL = "plugins.update.url";
|
||||
|
||||
|
@ -601,6 +604,12 @@ BrowserGlue.prototype = {
|
|||
os.addObserver(this, "xpi-signature-changed", false);
|
||||
os.addObserver(this, "autocomplete-did-enter-text", false);
|
||||
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
|
||||
|
||||
this._flashHangCount = 0;
|
||||
},
|
||||
|
||||
|
|
|
@ -59,6 +59,15 @@ this.E10SUtils = {
|
|||
mustLoadRemote = chromeReg.mustLoadURLRemotely(url);
|
||||
}
|
||||
|
||||
if (aURL.startsWith("moz-extension:")) {
|
||||
canLoadRemote = false;
|
||||
mustLoadRemote = false;
|
||||
}
|
||||
|
||||
if (aURL.startsWith("view-source:")) {
|
||||
return this.canLoadURIInProcess(aURL.substr("view-source:".length), aProcess);
|
||||
}
|
||||
|
||||
if (mustLoadRemote)
|
||||
return processIsRemote;
|
||||
|
||||
|
|
|
@ -1951,3 +1951,7 @@ chatbox {
|
|||
-moz-padding-end: 0 !important;
|
||||
-moz-margin-end: 0 !important;
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -3692,3 +3692,7 @@ window > chatbox {
|
|||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -2912,3 +2912,7 @@ chatbox {
|
|||
@media not all and (-moz-os-version: windows-xp) {
|
||||
%include browser-aero.css
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,578 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["Extension"];
|
||||
|
||||
/*
|
||||
* This file is the main entry point for extensions. When an extension
|
||||
* loads, its bootstrap.js file creates a Extension instance
|
||||
* and calls .startup() on it. It calls .shutdown() when the extension
|
||||
* unloads. Extension manages any extension-specific state in
|
||||
* the chrome process.
|
||||
*/
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/event-emitter.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
|
||||
"resource://gre/modules/Locale.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
|
||||
|
||||
// Register built-in parts of the API. Other parts may be registered
|
||||
// in browser/, mobile/, or b2g/.
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-alarms.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-backgroundPage.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-notifications.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-i18n.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-idle.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-runtime.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-extension.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-webNavigation.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-webRequest.js");
|
||||
ExtensionManagement.registerScript("chrome://extensions/content/ext-storage.js");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
MessageBroker,
|
||||
Messenger,
|
||||
injectAPI,
|
||||
} = ExtensionUtils;
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
let scriptScope = this;
|
||||
|
||||
// This object loads the ext-*.js scripts that define the extension API.
|
||||
let Management = {
|
||||
initialized: false,
|
||||
scopes: [],
|
||||
apis: [],
|
||||
emitter: new EventEmitter(),
|
||||
|
||||
// Loads all the ext-*.js scripts currently registered.
|
||||
lazyInit() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
for (let script of ExtensionManagement.getScripts()) {
|
||||
let scope = {extensions: this, global: scriptScope};
|
||||
Services.scriptloader.loadSubScript(script, scope, "UTF-8");
|
||||
|
||||
// Save the scope to avoid it being garbage collected.
|
||||
this.scopes.push(scope);
|
||||
}
|
||||
},
|
||||
|
||||
// Called by an ext-*.js script to register an API. The |api|
|
||||
// parameter should be an object of the form:
|
||||
// {
|
||||
// tabs: {
|
||||
// create: ...,
|
||||
// onCreated: ...
|
||||
// }
|
||||
// }
|
||||
// This registers tabs.create and tabs.onCreated as part of the API.
|
||||
registerAPI(api) {
|
||||
this.apis.push({api});
|
||||
},
|
||||
|
||||
// Same as above, but only register the API is the add-on has the
|
||||
// given permission.
|
||||
registerPrivilegedAPI(permission, api) {
|
||||
this.apis.push({api, permission});
|
||||
},
|
||||
|
||||
// Mash together into a single object all the APIs registered by the
|
||||
// functions above. Return the merged object.
|
||||
generateAPIs(extension, context) {
|
||||
let obj = {};
|
||||
|
||||
// Recursively copy properties from source to dest.
|
||||
function copy(dest, source) {
|
||||
for (let prop in source) {
|
||||
if (typeof(source[prop]) == "object") {
|
||||
if (!(prop in dest)) {
|
||||
dest[prop] = {};
|
||||
}
|
||||
copy(dest[prop], source[prop]);
|
||||
} else {
|
||||
dest[prop] = source[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let api of this.apis) {
|
||||
if (api.permission) {
|
||||
if (!extension.hasPermission(api.permission)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
api = api.api(extension, context);
|
||||
copy(obj, api);
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
// The ext-*.js scripts can ask to be notified for certain hooks.
|
||||
on(hook, callback) {
|
||||
this.emitter.on(hook, callback);
|
||||
},
|
||||
|
||||
// Ask to run all the callbacks that are registered for a given hook.
|
||||
emit(hook, ...args) {
|
||||
this.lazyInit();
|
||||
this.emitter.emit(hook, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
// A MessageBroker that's used to send and receive messages for
|
||||
// extension pages (which run in the chrome process).
|
||||
let globalBroker = new MessageBroker([Services.mm, Services.ppmm]);
|
||||
|
||||
// An extension page is an execution context for any extension content
|
||||
// that runs in the chrome process. It's used for background pages
|
||||
// (type="background"), popups (type="popup"), and any extension
|
||||
// content loaded into browser tabs (type="tab").
|
||||
//
|
||||
// |params| is an object with the following properties:
|
||||
// |type| is one of "background", "popup", or "tab".
|
||||
// |contentWindow| is the DOM window the content runs in.
|
||||
// |uri| is the URI of the content (optional).
|
||||
// |docShell| is the docshell the content runs in (optional).
|
||||
function ExtensionPage(extension, params)
|
||||
{
|
||||
let {type, contentWindow, uri, docShell} = params;
|
||||
this.extension = extension;
|
||||
this.type = type;
|
||||
this.contentWindow = contentWindow || null;
|
||||
this.uri = uri || extension.baseURI;
|
||||
this.onClose = new Set();
|
||||
|
||||
// This is the sender property passed to the Messenger for this
|
||||
// page. It can be augmented by the "page-open" hook.
|
||||
let sender = {id: extension.id};
|
||||
if (uri) {
|
||||
sender.url = uri.spec;
|
||||
}
|
||||
let delegate = {};
|
||||
Management.emit("page-load", this, params, sender, delegate);
|
||||
|
||||
let filter = {id: extension.id};
|
||||
this.messenger = new Messenger(this, globalBroker, sender, filter, delegate);
|
||||
|
||||
this.extension.views.add(this);
|
||||
}
|
||||
|
||||
ExtensionPage.prototype = {
|
||||
get cloneScope() {
|
||||
return this.contentWindow;
|
||||
},
|
||||
|
||||
callOnClose(obj) {
|
||||
this.onClose.add(obj);
|
||||
},
|
||||
|
||||
forgetOnClose(obj) {
|
||||
this.onClose.delete(obj);
|
||||
},
|
||||
|
||||
// Called when the extension shuts down.
|
||||
shutdown() {
|
||||
Management.emit("page-shutdown", this);
|
||||
this.unload();
|
||||
},
|
||||
|
||||
// This method is called when an extension page navigates away or
|
||||
// its tab is closed.
|
||||
unload() {
|
||||
Management.emit("page-unload", this);
|
||||
|
||||
this.extension.views.delete(this);
|
||||
|
||||
for (let obj of this.onClose) {
|
||||
obj.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Responsible for loading extension APIs into the right globals.
|
||||
let GlobalManager = {
|
||||
// Number of extensions currently enabled.
|
||||
count: 0,
|
||||
|
||||
// Map[docShell -> {extension, context}] where context is an ExtensionPage.
|
||||
docShells: new Map(),
|
||||
|
||||
// Map[extension ID -> Extension]. Determines which extension is
|
||||
// responsible for content under a particular extension ID.
|
||||
extensionMap: new Map(),
|
||||
|
||||
init(extension) {
|
||||
if (this.count == 0) {
|
||||
Services.obs.addObserver(this, "content-document-global-created", false);
|
||||
}
|
||||
this.count++;
|
||||
|
||||
this.extensionMap.set(extension.id, extension);
|
||||
},
|
||||
|
||||
uninit(extension) {
|
||||
this.count--;
|
||||
if (this.count == 0) {
|
||||
Services.obs.removeObserver(this, "content-document-global-created");
|
||||
}
|
||||
|
||||
for (let [docShell, data] of this.docShells) {
|
||||
if (extension == data.extension) {
|
||||
this.docShells.delete(docShell);
|
||||
}
|
||||
}
|
||||
|
||||
this.extensionMap.delete(extension.id);
|
||||
},
|
||||
|
||||
injectInDocShell(docShell, extension, context) {
|
||||
this.docShells.set(docShell, {extension, context});
|
||||
},
|
||||
|
||||
observe(contentWindow, topic, data) {
|
||||
function inject(extension, context) {
|
||||
let chromeObj = Cu.createObjectIn(contentWindow, {defineAs: "chrome"});
|
||||
let api = Management.generateAPIs(extension, context);
|
||||
injectAPI(api, chromeObj);
|
||||
}
|
||||
|
||||
let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.sameTypeRootTreeItem
|
||||
.QueryInterface(Ci.nsIDocShell);
|
||||
|
||||
if (this.docShells.has(docShell)) {
|
||||
let {extension, context} = this.docShells.get(docShell);
|
||||
inject(extension, context);
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't inject into sub-frames of a UI page.
|
||||
if (contentWindow != contentWindow.top) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the add-on associated with this document via the
|
||||
// principal's originAttributes. This value is computed by
|
||||
// extensionURIToAddonID, which ensures that we don't inject our
|
||||
// API into webAccessibleResources.
|
||||
let principal = contentWindow.document.nodePrincipal;
|
||||
let id = principal.originAttributes.addonId;
|
||||
if (!this.extensionMap.has(id)) {
|
||||
return;
|
||||
}
|
||||
let extension = this.extensionMap.get(id);
|
||||
let uri = contentWindow.document.documentURIObject;
|
||||
let context = new ExtensionPage(extension, {type: "tab", contentWindow, uri, docShell});
|
||||
inject(extension, context);
|
||||
|
||||
let eventHandler = docShell.chromeEventHandler;
|
||||
let listener = event => {
|
||||
eventHandler.removeEventListener("unload", listener);
|
||||
context.unload();
|
||||
};
|
||||
eventHandler.addEventListener("unload", listener, true);
|
||||
},
|
||||
};
|
||||
|
||||
// We create one instance of this class per extension. |addonData|
|
||||
// comes directly from bootstrap.js when initializing.
|
||||
function Extension(addonData)
|
||||
{
|
||||
let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
let uuid = uuidGenerator.generateUUID().number;
|
||||
uuid = uuid.substring(1, uuid.length - 1); // Strip of { and } off the UUID.
|
||||
this.uuid = uuid;
|
||||
|
||||
this.addonData = addonData;
|
||||
this.id = addonData.id;
|
||||
this.baseURI = Services.io.newURI("moz-extension://" + uuid, null, null);
|
||||
this.manifest = null;
|
||||
this.localeMessages = null;
|
||||
|
||||
this.views = new Set();
|
||||
|
||||
this.onStartup = null;
|
||||
|
||||
this.hasShutdown = false;
|
||||
this.onShutdown = new Set();
|
||||
|
||||
this.permissions = new Set();
|
||||
this.whiteListedHosts = null;
|
||||
this.webAccessibleResources = new Set();
|
||||
|
||||
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
|
||||
}
|
||||
|
||||
Extension.prototype = {
|
||||
// Representation of the extension to send to content
|
||||
// processes. This should include anything the content process might
|
||||
// need.
|
||||
serialize() {
|
||||
return {
|
||||
id: this.id,
|
||||
uuid: this.uuid,
|
||||
manifest: this.manifest,
|
||||
resourceURL: this.addonData.resourceURI.spec,
|
||||
baseURL: this.baseURI.spec,
|
||||
content_scripts: this.manifest.content_scripts || [],
|
||||
webAccessibleResources: this.webAccessibleResources,
|
||||
whiteListedHosts: this.whiteListedHosts.serialize(),
|
||||
};
|
||||
},
|
||||
|
||||
// https://developer.chrome.com/extensions/i18n
|
||||
localizeMessage(message, substitutions) {
|
||||
if (message in this.localeMessages) {
|
||||
let str = this.localeMessages[message].message;
|
||||
|
||||
if (!substitutions) {
|
||||
substitutions = [];
|
||||
}
|
||||
if (!Array.isArray(substitutions)) {
|
||||
substitutions = [substitutions];
|
||||
}
|
||||
|
||||
// https://developer.chrome.com/extensions/i18n-messages
|
||||
// |str| may contain substrings of the form $1 or $PLACEHOLDER$.
|
||||
// In the former case, we replace $n with substitutions[n - 1].
|
||||
// In the latter case, we consult the placeholders array.
|
||||
// The placeholder may itself use $n to refer to substitutions.
|
||||
let replacer = (matched, name) => {
|
||||
if (name.length == 1 && name[0] >= '1' && name[0] <= '9') {
|
||||
return substitutions[parseInt(name) - 1];
|
||||
} else {
|
||||
let content = this.localeMessages[message].placeholders[name].content;
|
||||
if (content[0] == '$') {
|
||||
return replacer(matched, content[1]);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
};
|
||||
return str.replace(/\$([A-Za-z_@]+)\$/, replacer)
|
||||
.replace(/\$([0-9]+)/, replacer)
|
||||
.replace(/\$\$/, "$");
|
||||
}
|
||||
|
||||
// Check for certain pre-defined messages.
|
||||
if (message == "@@extension_id") {
|
||||
return this.id;
|
||||
} else if (message == "@@ui_locale") {
|
||||
return Locale.getLocale();
|
||||
} else if (message == "@@bidi_dir") {
|
||||
return "ltr"; // FIXME
|
||||
}
|
||||
|
||||
Cu.reportError(`Unknown localization message ${message}`);
|
||||
return "??";
|
||||
},
|
||||
|
||||
localize(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
if (str.startsWith("__MSG_") && str.endsWith("__")) {
|
||||
let message = str.substring("__MSG_".length, str.length - "__".length);
|
||||
return this.localizeMessage(message);
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
readJSON(uri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
|
||||
if (!Components.isSuccessCode(status)) {
|
||||
reject(status);
|
||||
return;
|
||||
}
|
||||
let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
|
||||
try {
|
||||
resolve(JSON.parse(text));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
readManifest() {
|
||||
let manifestURI = Services.io.newURI("manifest.json", null, this.baseURI);
|
||||
return this.readJSON(manifestURI);
|
||||
},
|
||||
|
||||
readLocaleFile(locale) {
|
||||
let dir = locale.replace("-", "_");
|
||||
let url = `_locales/${dir}/messages.json`;
|
||||
let uri = Services.io.newURI(url, null, this.baseURI);
|
||||
return this.readJSON(uri);
|
||||
},
|
||||
|
||||
readLocaleMessages() {
|
||||
let locales = [];
|
||||
|
||||
// We need to base this off of this.addonData.resourceURI rather
|
||||
// than baseURI since baseURI is a moz-extension URI, which always
|
||||
// QIs to nsIFileURL.
|
||||
let uri = Services.io.newURI("_locales", null, this.addonData.resourceURI);
|
||||
if (uri instanceof Ci.nsIFileURL) {
|
||||
let file = uri.file;
|
||||
let enumerator;
|
||||
try {
|
||||
enumerator = file.directoryEntries;
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
|
||||
locales.push({
|
||||
name: file.leafName,
|
||||
locales: [file.leafName.replace("_", "-")]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (uri instanceof Ci.nsIJARURI && uri.JARFile instanceof Ci.nsIFileURL) {
|
||||
let file = uri.JARFile.file;
|
||||
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
|
||||
try {
|
||||
zipReader.open(file);
|
||||
let enumerator = zipReader.findEntries("_locales/*");
|
||||
while (enumerator.hasMore()) {
|
||||
let name = enumerator.getNext();
|
||||
let match = name.match(new RegExp("_locales\/([^/]*)"));
|
||||
if (match && match[1]) {
|
||||
locales.push({
|
||||
name: match[1],
|
||||
locales: [match[1].replace("_", "-")]
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
zipReader.close();
|
||||
}
|
||||
}
|
||||
|
||||
let locale = Locale.findClosestLocale(locales);
|
||||
if (locale) {
|
||||
return this.readLocaleFile(locale.name).catch(() => {});
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
runManifest(manifest) {
|
||||
let permissions = manifest.permissions || [];
|
||||
let webAccessibleResources = manifest.web_accessible_resources || [];
|
||||
|
||||
let whitelist = [];
|
||||
for (let perm of permissions) {
|
||||
if (perm.match(/:\/\//)) {
|
||||
whitelist.push(perm);
|
||||
} else {
|
||||
this.permissions.add(perm);
|
||||
}
|
||||
}
|
||||
this.whiteListedHosts = new MatchPattern(whitelist);
|
||||
|
||||
let resources = new Set();
|
||||
for (let url of webAccessibleResources) {
|
||||
resources.add(url);
|
||||
}
|
||||
this.webAccessibleResources = resources;
|
||||
|
||||
for (let directive in manifest) {
|
||||
Management.emit("manifest_" + directive, directive, this, manifest);
|
||||
}
|
||||
|
||||
let data = Services.ppmm.initialProcessData;
|
||||
if (!data["Extension:Extensions"]) {
|
||||
data["Extension:Extensions"] = [];
|
||||
}
|
||||
let serial = this.serialize();
|
||||
data["Extension:Extensions"].push(serial);
|
||||
Services.ppmm.broadcastAsyncMessage("Extension:Startup", serial);
|
||||
},
|
||||
|
||||
callOnClose(obj) {
|
||||
this.onShutdown.add(obj);
|
||||
},
|
||||
|
||||
forgetOnClose(obj) {
|
||||
this.onShutdown.delete(obj);
|
||||
},
|
||||
|
||||
startup() {
|
||||
GlobalManager.init(this);
|
||||
|
||||
return Promise.all([this.readManifest(), this.readLocaleMessages()]).then(([manifest, messages]) => {
|
||||
if (this.hasShutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.manifest = manifest;
|
||||
this.localeMessages = messages;
|
||||
|
||||
Management.emit("startup", this);
|
||||
|
||||
this.runManifest(manifest);
|
||||
}).catch(e => {
|
||||
dump(`Extension error: ${e} ${e.fileName}:${e.lineNumber}\n`);
|
||||
Cu.reportError(e);
|
||||
});
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
this.hasShutdown = true;
|
||||
if (!this.manifest) {
|
||||
return;
|
||||
}
|
||||
|
||||
GlobalManager.uninit(this);
|
||||
|
||||
for (let view of this.views) {
|
||||
view.shutdown();
|
||||
}
|
||||
|
||||
for (let obj of this.onShutdown) {
|
||||
obj.close();
|
||||
}
|
||||
|
||||
Management.emit("shutdown", this);
|
||||
|
||||
Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
|
||||
|
||||
ExtensionManagement.shutdownExtension(this.uuid);
|
||||
},
|
||||
|
||||
hasPermission(perm) {
|
||||
return this.permissions.has(perm);
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,521 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["ExtensionContent"];
|
||||
|
||||
/*
|
||||
* This file handles the content process side of extensions. It mainly
|
||||
* takes care of content script injection, content script APIs, and
|
||||
* messaging.
|
||||
*/
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
|
||||
"resource://gre/modules/ExtensionManagement.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
runSafeWithoutClone,
|
||||
MessageBroker,
|
||||
Messenger,
|
||||
ignoreEvent,
|
||||
injectAPI,
|
||||
} = ExtensionUtils;
|
||||
|
||||
function isWhenBeforeOrSame(when1, when2)
|
||||
{
|
||||
let table = {"document_start": 0,
|
||||
"document_end": 1,
|
||||
"document_idle": 2};
|
||||
return table[when1] <= table[when2];
|
||||
}
|
||||
|
||||
// This is the fairly simple API that we inject into content
|
||||
// scripts.
|
||||
let api = context => { return {
|
||||
runtime: {
|
||||
connect: function(extensionId, connectInfo) {
|
||||
let name = connectInfo && connectInfo.name || "";
|
||||
let recipient = extensionId ? {extensionId} : {extensionId: context.extensionId};
|
||||
return context.messenger.connect(context.messageManager, name, recipient);
|
||||
},
|
||||
|
||||
getManifest: function(context) {
|
||||
return context.extension.getManifest();
|
||||
},
|
||||
|
||||
getURL: function(path) {
|
||||
return context.extension.baseURI.resolve(url);
|
||||
},
|
||||
|
||||
onConnect: context.messenger.onConnect("runtime.onConnect"),
|
||||
|
||||
onMessage: context.messenger.onMessage("runtime.onMessage"),
|
||||
|
||||
sendMessage: function(...args) {
|
||||
let extensionId, message, options, responseCallback;
|
||||
if (args.length == 1) {
|
||||
message = args[0];
|
||||
} else if (args.length == 2) {
|
||||
[message, responseCallback] = args;
|
||||
} else {
|
||||
[extensionId, message, options, responseCallback] = args;
|
||||
}
|
||||
|
||||
let recipient = extensionId ? {extensionId} : {extensionId: context.extensionId};
|
||||
context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
|
||||
},
|
||||
},
|
||||
|
||||
extension: {
|
||||
getURL: function(path) {
|
||||
return context.extension.baseURI.resolve(url);
|
||||
},
|
||||
|
||||
inIncognitoContext: PrivateBrowsingUtils.isContentWindowPrivate(context.contentWindow),
|
||||
},
|
||||
}};
|
||||
|
||||
// Represents a content script.
|
||||
function Script(options)
|
||||
{
|
||||
this.options = options;
|
||||
this.run_at = this.options.run_at;
|
||||
this.js = this.options.js || [];
|
||||
this.css = this.options.css || [];
|
||||
|
||||
this.matches_ = new MatchPattern(this.options.matches);
|
||||
this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
|
||||
|
||||
// TODO: Support glob patterns.
|
||||
}
|
||||
|
||||
Script.prototype = {
|
||||
matches(window) {
|
||||
let uri = window.document.documentURIObject;
|
||||
if (!this.matches_.matches(uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.exclude_matches_.matches(uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.options.all_frames && window.top != window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: match_about_blank.
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
tryInject(extension, window, sandbox, shouldRun) {
|
||||
if (!this.matches(window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldRun("document_start")) {
|
||||
let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).
|
||||
getInterface(Ci.nsIDOMWindowUtils);
|
||||
|
||||
for (let url of this.css) {
|
||||
url = extension.baseURI.resolve(url);
|
||||
runSafeWithoutClone(winUtils.loadSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
|
||||
}
|
||||
|
||||
if (this.options.cssCode) {
|
||||
let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
|
||||
runSafeWithoutClone(winUtils.loadSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
|
||||
}
|
||||
}
|
||||
|
||||
let scheduled = this.run_at || "document_idle";
|
||||
if (shouldRun(scheduled)) {
|
||||
for (let url of this.js) {
|
||||
url = extension.baseURI.resolve(url);
|
||||
Services.scriptloader.loadSubScript(url, sandbox);
|
||||
}
|
||||
|
||||
if (this.options.jsCode) {
|
||||
Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getWindowMessageManager(contentWindow)
|
||||
{
|
||||
let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDocShell)
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor);
|
||||
try {
|
||||
return ir.getInterface(Ci.nsIContentFrameMessageManager);
|
||||
} catch (e) {
|
||||
// Some windows don't support this interface (hidden window).
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Scope in which extension content script code can run. It uses
|
||||
// Cu.Sandbox to run the code. There is a separate scope for each
|
||||
// frame.
|
||||
function ExtensionContext(extensionId, contentWindow)
|
||||
{
|
||||
this.extension = ExtensionManager.get(extensionId);
|
||||
this.extensionId = extensionId;
|
||||
this.contentWindow = contentWindow;
|
||||
|
||||
let utils = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
let outerWindowId = utils.outerWindowID;
|
||||
let frameId = contentWindow == contentWindow.top ? 0 : outerWindowId;
|
||||
this.frameId = frameId;
|
||||
|
||||
let mm = getWindowMessageManager(contentWindow);
|
||||
this.messageManager = mm;
|
||||
|
||||
let prin = [contentWindow];
|
||||
if (Services.scriptSecurityManager.isSystemPrincipal(contentWindow.document.nodePrincipal)) {
|
||||
// Make sure we don't hand out the system principal by accident.
|
||||
prin = Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal);
|
||||
}
|
||||
|
||||
this.sandbox = Cu.Sandbox(prin, {sandboxPrototype: contentWindow, wantXrays: true});
|
||||
|
||||
let delegate = {
|
||||
getSender(context, target, sender) {
|
||||
// Nothing to do here.
|
||||
}
|
||||
};
|
||||
|
||||
let url = contentWindow.location.href;
|
||||
let broker = ExtensionContent.getBroker(mm);
|
||||
this.messenger = new Messenger(this, broker, {id: extensionId, frameId, url},
|
||||
{id: extensionId, frameId}, delegate);
|
||||
|
||||
let chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "chrome"});
|
||||
injectAPI(api(this), chromeObj);
|
||||
|
||||
this.onClose = new Set();
|
||||
}
|
||||
|
||||
ExtensionContext.prototype = {
|
||||
get cloneScope() {
|
||||
return this.sandbox;
|
||||
},
|
||||
|
||||
execute(script, shouldRun) {
|
||||
script.tryInject(this.extension, this.contentWindow, this.sandbox, shouldRun);
|
||||
},
|
||||
|
||||
callOnClose(obj) {
|
||||
this.onClose.add(obj);
|
||||
},
|
||||
|
||||
forgetOnClose(obj) {
|
||||
this.onClose.delete(obj);
|
||||
},
|
||||
|
||||
close() {
|
||||
for (let obj of this.onClose) {
|
||||
obj.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Responsible for creating ExtensionContexts and injecting content
|
||||
// scripts into them when new documents are created.
|
||||
let DocumentManager = {
|
||||
extensionCount: 0,
|
||||
|
||||
// WeakMap[window -> Map[extensionId -> ExtensionContext]]
|
||||
windows: new WeakMap(),
|
||||
|
||||
init() {
|
||||
Services.obs.addObserver(this, "document-element-inserted", false);
|
||||
Services.obs.addObserver(this, "dom-window-destroyed", false);
|
||||
},
|
||||
|
||||
uninit() {
|
||||
Services.obs.removeObserver(this, "document-element-inserted");
|
||||
Services.obs.removeObserver(this, "dom-window-destroyed");
|
||||
},
|
||||
|
||||
getWindowState(contentWindow) {
|
||||
let readyState = contentWindow.document.readyState;
|
||||
if (readyState == "loading") {
|
||||
return "document_start";
|
||||
} else if (readyState == "interactive") {
|
||||
return "document_end";
|
||||
} else {
|
||||
return "document_idle";
|
||||
}
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
if (topic == "document-element-inserted") {
|
||||
let document = subject;
|
||||
let window = document && document.defaultView;
|
||||
if (!document || !document.location || !window) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we only load into frames that ExtensionContent.init
|
||||
// was called on (i.e., not frames for social or sidebars).
|
||||
let mm = getWindowMessageManager(window);
|
||||
if (!mm || !ExtensionContent.globals.has(mm)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.windows.delete(window);
|
||||
|
||||
this.trigger("document_start", window);
|
||||
window.addEventListener("DOMContentLoaded", this, true);
|
||||
window.addEventListener("load", this, true);
|
||||
} else if (topic == "dom-window-destroyed") {
|
||||
let window = subject;
|
||||
if (!this.windows.has(window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let extensions = this.windows.get(window);
|
||||
for (let [extensionId, context] of extensions) {
|
||||
context.close();
|
||||
}
|
||||
|
||||
this.windows.delete(window);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent: function(event) {
|
||||
let window = event.target.defaultView;
|
||||
window.removeEventListener(event.type, this, true);
|
||||
|
||||
// Need to check if we're still on the right page? Greasemonkey does this.
|
||||
|
||||
if (event.type == "DOMContentLoaded") {
|
||||
this.trigger("document_end", window);
|
||||
} else if (event.type == "load") {
|
||||
this.trigger("document_idle", window);
|
||||
}
|
||||
},
|
||||
|
||||
executeScript(global, extensionId, script) {
|
||||
let window = global.content;
|
||||
let extensions = this.windows.get(window);
|
||||
if (!extensions) {
|
||||
return;
|
||||
}
|
||||
let context = extensions.get(extensionId);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Somehow make sure we have the right permissions for this origin!
|
||||
// FIXME: Need to keep this around so that I will execute it later if we're not in the right state.
|
||||
context.execute(script, scheduled => scheduled == state);
|
||||
},
|
||||
|
||||
enumerateWindows: function*(docShell) {
|
||||
let window = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow)
|
||||
yield [window, this.getWindowState(window)];
|
||||
|
||||
for (let i = 0; i < docShell.childCount; i++) {
|
||||
let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
|
||||
yield* this.enumerateWindows(child);
|
||||
}
|
||||
},
|
||||
|
||||
getContext(extensionId, window) {
|
||||
if (!this.windows.has(window)) {
|
||||
this.windows.set(window, new Map());
|
||||
}
|
||||
let extensions = this.windows.get(window);
|
||||
if (!extensions.has(extensionId)) {
|
||||
let context = new ExtensionContext(extensionId, window);
|
||||
extensions.set(extensionId, context);
|
||||
}
|
||||
return extensions.get(extensionId);
|
||||
},
|
||||
|
||||
startupExtension(extensionId) {
|
||||
if (this.extensionCount == 0) {
|
||||
this.init();
|
||||
}
|
||||
this.extensionCount++;
|
||||
|
||||
let extension = ExtensionManager.get(extensionId);
|
||||
for (let global of ExtensionContent.globals.keys()) {
|
||||
for (let [window, state] of this.enumerateWindows(global.docShell)) {
|
||||
for (let script of extension.scripts) {
|
||||
if (script.matches(window)) {
|
||||
let context = this.getContext(extensionId, window);
|
||||
context.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
shutdownExtension(extensionId) {
|
||||
for (let global of ExtensionContent.globals.keys()) {
|
||||
for (let [window, state] of this.enumerateWindows(global.docShell)) {
|
||||
let extensions = this.windows.get(window);
|
||||
if (!extensions) {
|
||||
continue;
|
||||
}
|
||||
let context = extensions.get(extensionId);
|
||||
if (context) {
|
||||
context.close();
|
||||
extensions.delete(extensionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.extensionCount--;
|
||||
if (this.extensionCount == 0) {
|
||||
this.uninit();
|
||||
}
|
||||
},
|
||||
|
||||
trigger(when, window) {
|
||||
let state = this.getWindowState(window);
|
||||
for (let [extensionId, extension] of ExtensionManager.extensions) {
|
||||
for (let script of extension.scripts) {
|
||||
if (script.matches(window)) {
|
||||
let context = this.getContext(extensionId, window);
|
||||
context.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Represents a browser extension in the content process.
|
||||
function BrowserExtensionContent(data)
|
||||
{
|
||||
this.id = data.id;
|
||||
this.uuid = data.uuid;
|
||||
this.data = data;
|
||||
this.scripts = [ for (scriptData of data.content_scripts) new Script(scriptData) ];
|
||||
this.webAccessibleResources = data.webAccessibleResources;
|
||||
this.whiteListedHosts = data.whiteListedHosts;
|
||||
|
||||
this.manifest = data.manifest;
|
||||
this.baseURI = Services.io.newURI(data.baseURL, null, null);
|
||||
|
||||
let uri = Services.io.newURI(data.resourceURL, null, null);
|
||||
|
||||
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
|
||||
// Extension.jsm takes care of this in the parent.
|
||||
ExtensionManagement.startupExtension(this.uuid, uri, this);
|
||||
}
|
||||
};
|
||||
|
||||
BrowserExtensionContent.prototype = {
|
||||
shutdown() {
|
||||
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
|
||||
ExtensionManagement.shutdownExtension(this.uuid);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let ExtensionManager = {
|
||||
// Map[extensionId, BrowserExtensionContent]
|
||||
extensions: new Map(),
|
||||
|
||||
init() {
|
||||
Services.cpmm.addMessageListener("Extension:Startup", this);
|
||||
Services.cpmm.addMessageListener("Extension:Shutdown", this);
|
||||
|
||||
if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
|
||||
let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
|
||||
for (let data of extensions) {
|
||||
this.extensions.set(data.id, new BrowserExtensionContent(data));
|
||||
DocumentManager.startupExtension(data.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get(extensionId) {
|
||||
return this.extensions.get(extensionId);
|
||||
},
|
||||
|
||||
receiveMessage({name, data}) {
|
||||
let extension;
|
||||
switch (name) {
|
||||
case "Extension:Startup":
|
||||
extension = new BrowserExtensionContent(data);
|
||||
this.extensions.set(data.id, extension);
|
||||
DocumentManager.startupExtension(data.id);
|
||||
break;
|
||||
|
||||
case "Extension:Shutdown":
|
||||
extension = this.extensions.get(data.id);
|
||||
extension.shutdown();
|
||||
DocumentManager.shutdownExtension(data.id);
|
||||
this.extensions.delete(data.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ExtensionContent = {
|
||||
globals: new Map(),
|
||||
|
||||
init(global) {
|
||||
let broker = new MessageBroker([global]);
|
||||
this.globals.set(global, broker);
|
||||
|
||||
global.addMessageListener("Extension:Execute", this);
|
||||
|
||||
let windowId = global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
global.sendAsyncMessage("Extension:TopWindowID", {windowId});
|
||||
},
|
||||
|
||||
uninit(global) {
|
||||
this.globals.delete(global);
|
||||
|
||||
let windowId = global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId});
|
||||
},
|
||||
|
||||
getBroker(messageManager) {
|
||||
return this.globals.get(messageManager);
|
||||
},
|
||||
|
||||
receiveMessage({target, name, data}) {
|
||||
switch (name) {
|
||||
case "Extension:Execute":
|
||||
data.options.matches = "<all_urls>";
|
||||
let script = new Script(data.options);
|
||||
let {extensionId} = data;
|
||||
DocumentManager.executeScript(target, extensionId, script);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
ExtensionManager.init();
|
|
@ -0,0 +1,201 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["ExtensionManagement"];
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
/*
|
||||
* This file should be kept short and simple since it's loaded even
|
||||
* when no extensions are running.
|
||||
*/
|
||||
|
||||
// Keep track of frame IDs for content windows. Mostly we can just use
|
||||
// the outer window ID as the frame ID. However, the API specifies
|
||||
// that top-level windows have a frame ID of 0. So we need to keep
|
||||
// track of which windows are top-level. This code listens to messages
|
||||
// from ExtensionContent to do that.
|
||||
let Frames = {
|
||||
// Window IDs of top-level content windows.
|
||||
topWindowIds: new Set(),
|
||||
|
||||
init() {
|
||||
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
Services.mm.addMessageListener("Extension:TopWindowID", this);
|
||||
Services.mm.addMessageListener("Extension:RemoveTopWindowID", this);
|
||||
},
|
||||
|
||||
isTopWindowId(windowId) {
|
||||
return this.topWindowIds.has(windowId);
|
||||
},
|
||||
|
||||
// Convert an outer window ID to a frame ID. An outer window ID of 0
|
||||
// is invalid.
|
||||
getId(windowId) {
|
||||
if (this.isTopWindowId(windowId)) {
|
||||
return 0;
|
||||
} else if (windowId == 0) {
|
||||
return -1;
|
||||
} else {
|
||||
return windowId;
|
||||
}
|
||||
},
|
||||
|
||||
// Convert an outer window ID for a parent window to a frame
|
||||
// ID. Outer window IDs follow the same convention that
|
||||
// |window.top.parent === window.top|. The API works differently,
|
||||
// giving a frame ID of -1 for the the parent of a top-level
|
||||
// window. This function handles the conversion.
|
||||
getParentId(parentWindowId, windowId) {
|
||||
if (parentWindowId == windowId) {
|
||||
// We have a top-level window.
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Not a top-level window. Just return the ID as normal.
|
||||
return this.getId(parentWindowId);
|
||||
},
|
||||
|
||||
receiveMessage({name, data}) {
|
||||
switch (name) {
|
||||
case "Extension:TopWindowID":
|
||||
// FIXME: Need to handle the case where the content process
|
||||
// crashes. Right now we leak its top window IDs.
|
||||
this.topWindowIds.add(data.windowId);
|
||||
break;
|
||||
|
||||
case "Extension:RemoveTopWindowID":
|
||||
this.topWindowIds.delete(data.windowId);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
Frames.init();
|
||||
|
||||
// Manage the collection of ext-*.js scripts that define the extension API.
|
||||
let Scripts = {
|
||||
scripts: new Set(),
|
||||
|
||||
register(script) {
|
||||
this.scripts.add(script);
|
||||
},
|
||||
|
||||
getScripts() {
|
||||
return this.scripts;
|
||||
},
|
||||
};
|
||||
|
||||
// This object manages various platform-level issues related to
|
||||
// moz-extension:// URIs. It lives here so that it can be used in both
|
||||
// the parent and child processes.
|
||||
//
|
||||
// moz-extension URIs have the form moz-extension://uuid/path. Each
|
||||
// extension has its own UUID, unique to the machine it's installed
|
||||
// on. This is easier and more secure than using the extension ID,
|
||||
// since it makes it slightly harder to fingerprint for extensions if
|
||||
// each user uses different URIs for the extension.
|
||||
let Service = {
|
||||
initialized: false,
|
||||
|
||||
// Map[uuid -> extension].
|
||||
// extension can be an Extension (parent process) or BrowserExtensionContent (child process).
|
||||
uuidMap: new Map(),
|
||||
|
||||
init() {
|
||||
let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService);
|
||||
aps = aps.wrappedJSObject;
|
||||
this.aps = aps;
|
||||
aps.setExtensionURILoadCallback(this.extensionURILoadableByAnyone.bind(this));
|
||||
aps.setExtensionURIToAddonIdCallback(this.extensionURIToAddonID.bind(this));
|
||||
},
|
||||
|
||||
// Called when a new extension is loaded.
|
||||
startupExtension(uuid, uri, extension) {
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Create the moz-extension://uuid mapping.
|
||||
let handler = Services.io.getProtocolHandler("moz-extension");
|
||||
handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
|
||||
handler.setSubstitution(uuid, uri);
|
||||
|
||||
this.uuidMap.set(uuid, extension);
|
||||
this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
|
||||
},
|
||||
|
||||
// Called when an extension is unloaded.
|
||||
shutdownExtension(uuid) {
|
||||
let extension = this.uuidMap.get(uuid);
|
||||
this.uuidMap.delete(uuid);
|
||||
this.aps.setAddonLoadURICallback(extension.id, null);
|
||||
|
||||
let handler = Services.io.getProtocolHandler("moz-extension");
|
||||
handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
|
||||
handler.setSubstitution(uuid, null);
|
||||
},
|
||||
|
||||
// Return true if the given URI can be loaded from arbitrary web
|
||||
// content. The manifest.json |web_accessible_resources| directive
|
||||
// determines this.
|
||||
extensionURILoadableByAnyone(uri) {
|
||||
let uuid = uri.host;
|
||||
let extension = this.uuidMap.get(uuid);
|
||||
if (!extension) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let path = uri.path;
|
||||
if (path.length > 0 && path[0] == '/') {
|
||||
path = path.substr(1);
|
||||
}
|
||||
return extension.webAccessibleResources.has(path);
|
||||
},
|
||||
|
||||
// Checks whether a given extension can load this URI (typically via
|
||||
// an XML HTTP request). The manifest.json |permissions| directive
|
||||
// determines this.
|
||||
checkAddonMayLoad(extension, uri) {
|
||||
return extension.whiteListedHosts.matchesIgnoringPath(uri);
|
||||
},
|
||||
|
||||
// Finds the add-on ID associated with a given moz-extension:// URI.
|
||||
// This is used to set the addonId on the originAttributes for the
|
||||
// nsIPrincipal attached to the URI.
|
||||
extensionURIToAddonID(uri) {
|
||||
if (this.extensionURILoadableByAnyone(uri)) {
|
||||
// We don't want webAccessibleResources to be associated with
|
||||
// the add-on. That way they don't get any special privileges.
|
||||
return null;
|
||||
}
|
||||
|
||||
let uuid = uri.host;
|
||||
let extension = this.uuidMap.get(uuid);
|
||||
return extension ? extension.id : undefined;
|
||||
},
|
||||
};
|
||||
|
||||
let ExtensionManagement = {
|
||||
startupExtension: Service.startupExtension.bind(Service),
|
||||
shutdownExtension: Service.shutdownExtension.bind(Service),
|
||||
|
||||
registerScript: Scripts.register.bind(Scripts),
|
||||
getScripts: Scripts.getScripts.bind(Scripts),
|
||||
|
||||
getFrameId: Frames.getId.bind(Frames),
|
||||
getParentFrameId: Frames.getParentId.bind(Frames),
|
||||
};
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["ExtensionStorage"];
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm")
|
||||
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
|
||||
|
||||
let Path = OS.Path;
|
||||
let profileDir = OS.Constants.Path.profileDir;
|
||||
|
||||
let ExtensionStorage = {
|
||||
cache: new Map(),
|
||||
listeners: new Map(),
|
||||
|
||||
extensionDir: Path.join(profileDir, "browser-extension-data"),
|
||||
|
||||
getExtensionDir(extensionId) {
|
||||
return Path.join(this.extensionDir, extensionId);
|
||||
},
|
||||
|
||||
getStorageFile(extensionId) {
|
||||
return Path.join(this.extensionDir, extensionId, "storage.js");
|
||||
},
|
||||
|
||||
read(extensionId) {
|
||||
if (this.cache.has(extensionId)) {
|
||||
return this.cache.get(extensionId);
|
||||
}
|
||||
|
||||
let path = this.getStorageFile(extensionId);
|
||||
let decoder = new TextDecoder();
|
||||
let promise = OS.File.read(path);
|
||||
promise = promise.then(array => {
|
||||
return JSON.parse(decoder.decode(array));
|
||||
}).catch(() => {
|
||||
Cu.reportError("Unable to parse JSON data for extension storage.");
|
||||
return {};
|
||||
});
|
||||
this.cache.set(extensionId, promise);
|
||||
return promise;
|
||||
},
|
||||
|
||||
write(extensionId) {
|
||||
let promise = this.read(extensionId).then(extData => {
|
||||
let encoder = new TextEncoder();
|
||||
let array = encoder.encode(JSON.stringify(extData));
|
||||
let path = this.getStorageFile(extensionId);
|
||||
OS.File.makeDir(this.getExtensionDir(extensionId), {ignoreExisting: true, from: profileDir});
|
||||
let promise = OS.File.writeAtomic(path, array);
|
||||
return promise;
|
||||
}).catch(() => {
|
||||
// Make sure this promise is never rejected.
|
||||
Cu.reportError("Unable to write JSON data for extension storage.");
|
||||
});
|
||||
|
||||
AsyncShutdown.profileBeforeChange.addBlocker(
|
||||
"ExtensionStorage: Finish writing extension data",
|
||||
promise);
|
||||
|
||||
return promise.then(() => {
|
||||
AsyncShutdown.profileBeforeChange.removeBlocker(promise);
|
||||
});
|
||||
},
|
||||
|
||||
set(extensionId, items) {
|
||||
return this.read(extensionId).then(extData => {
|
||||
let changes = {};
|
||||
for (let prop in items) {
|
||||
changes[prop] = {oldValue: extData[prop], newValue: items[prop]};
|
||||
extData[prop] = items[prop];
|
||||
}
|
||||
|
||||
let listeners = this.listeners.get(extensionId);
|
||||
if (listeners) {
|
||||
for (let listener of listeners) {
|
||||
listener(changes);
|
||||
}
|
||||
}
|
||||
|
||||
return this.write(extensionId);
|
||||
});
|
||||
},
|
||||
|
||||
remove(extensionId, items) {
|
||||
return this.read(extensionId).then(extData => {
|
||||
let changes = {};
|
||||
for (let prop in items) {
|
||||
changes[prop] = {oldValue: extData[prop]};
|
||||
delete extData[prop];
|
||||
}
|
||||
|
||||
let listeners = this.listeners.get(extensionId);
|
||||
if (listeners) {
|
||||
for (let listener of listeners) {
|
||||
listener(changes);
|
||||
}
|
||||
}
|
||||
|
||||
return this.write(extensionId);
|
||||
});
|
||||
},
|
||||
|
||||
get(extensionId, keys) {
|
||||
return this.read(extensionId).then(extData => {
|
||||
let result = {};
|
||||
if (keys === null) {
|
||||
Object.assign(result, extData);
|
||||
} else if (typeof(keys) == "object") {
|
||||
for (let prop in keys) {
|
||||
if (prop in extData) {
|
||||
result[prop] = extData[prop];
|
||||
} else {
|
||||
result[prop] = keys[prop];
|
||||
}
|
||||
}
|
||||
} else if (typeof(keys) == "string") {
|
||||
result[prop] = extData[prop] || undefined;
|
||||
} else {
|
||||
for (let prop of keys) {
|
||||
result[prop] = extData[prop] || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
addOnChangedListener(extensionId, listener) {
|
||||
let listeners = this.listeners.get(extensionId) || new Set();
|
||||
listeners.add(listener);
|
||||
this.listeners.set(extensionId, listeners);
|
||||
},
|
||||
|
||||
removeOnChangedListener(extensionId, listener) {
|
||||
let listeners = this.listeners.get(extensionId);
|
||||
listeners.delete(listener);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,540 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["ExtensionUtils"];
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
// Run a function and report exceptions.
|
||||
function runSafeWithoutClone(f, ...args)
|
||||
{
|
||||
try {
|
||||
return f(...args);
|
||||
} catch (e) {
|
||||
dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n${e.stack}\n${Error().stack}`);
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Run a function, cloning arguments into context.cloneScope, and
|
||||
// report exceptions. |f| is expected to be in context.cloneScope.
|
||||
function runSafe(context, f, ...args)
|
||||
{
|
||||
try {
|
||||
args = Cu.cloneInto(args, context.cloneScope);
|
||||
} catch (e) {
|
||||
dump(`runSafe failure\n${context.cloneScope}\n${Error().stack}`);
|
||||
}
|
||||
return runSafeWithoutClone(f, ...args);
|
||||
}
|
||||
|
||||
// Similar to a WeakMap, but returns a particular default value for
|
||||
// |get| if a key is not present.
|
||||
function DefaultWeakMap(defaultValue)
|
||||
{
|
||||
this.defaultValue = defaultValue;
|
||||
this.weakmap = new WeakMap();
|
||||
}
|
||||
|
||||
DefaultWeakMap.prototype = {
|
||||
get(key) {
|
||||
if (this.weakmap.has(key)) {
|
||||
return this.weakmap.get(key);
|
||||
}
|
||||
return this.defaultValue;
|
||||
},
|
||||
|
||||
set(key, value) {
|
||||
if (key) {
|
||||
this.weakmap.set(key, value);
|
||||
} else {
|
||||
this.defaultValue = value;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// This is a generic class for managing event listeners. Example usage:
|
||||
//
|
||||
// new EventManager(context, "api.subAPI", fire => {
|
||||
// let listener = (...) => {
|
||||
// // Fire any listeners registered with addListener.
|
||||
// fire(arg1, arg2);
|
||||
// };
|
||||
// // Register the listener.
|
||||
// SomehowRegisterListener(listener);
|
||||
// return () => {
|
||||
// // Return a way to unregister the listener.
|
||||
// SomehowUnregisterListener(listener);
|
||||
// };
|
||||
// }).api()
|
||||
//
|
||||
// The result is an object with addListener, removeListener, and
|
||||
// hasListener methods. |context| is an add-on scope (either an
|
||||
// ExtensionPage in the chrome process or ExtensionContext in a
|
||||
// content process). |name| is for debugging. |register| is a function
|
||||
// to register the listener. |register| is only called once, event if
|
||||
// multiple listeners are registered. |register| should return an
|
||||
// unregister function that will unregister the listener.
|
||||
function EventManager(context, name, register)
|
||||
{
|
||||
this.context = context;
|
||||
this.name = name;
|
||||
this.register = register;
|
||||
this.unregister = null;
|
||||
this.callbacks = new Set();
|
||||
this.registered = false;
|
||||
}
|
||||
|
||||
EventManager.prototype = {
|
||||
addListener(callback) {
|
||||
if (!this.registered) {
|
||||
this.context.callOnClose(this);
|
||||
|
||||
let fireFunc = this.fire.bind(this);
|
||||
let fireWithoutClone = this.fireWithoutClone.bind(this);
|
||||
fireFunc.withoutClone = fireWithoutClone;
|
||||
this.unregister = this.register(fireFunc);
|
||||
}
|
||||
this.callbacks.add(callback);
|
||||
},
|
||||
|
||||
removeListener(callback) {
|
||||
if (!this.registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.callbacks.delete(callback);
|
||||
if (this.callbacks.length == 0) {
|
||||
this.unregister();
|
||||
|
||||
this.context.forgetOnClose(this);
|
||||
}
|
||||
},
|
||||
|
||||
hasListener(callback) {
|
||||
return this.callbacks.has(callback);
|
||||
},
|
||||
|
||||
fire(...args) {
|
||||
for (let callback of this.callbacks) {
|
||||
runSafe(this.context, callback, ...args);
|
||||
}
|
||||
},
|
||||
|
||||
fireWithoutClone(...args) {
|
||||
for (let callback of this.callbacks) {
|
||||
runSafeWithoutClone(callback, ...args);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.unregister();
|
||||
},
|
||||
|
||||
api() {
|
||||
return {
|
||||
addListener: callback => this.addListener(callback),
|
||||
removeListener: callback => this.removeListener(callback),
|
||||
hasListener: callback => this.hasListener(callback),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Similar to EventManager, but it doesn't try to consolidate event
|
||||
// notifications. Each addListener call causes us to register once. It
|
||||
// allows extra arguments to be passed to addListener.
|
||||
function SingletonEventManager(context, name, register)
|
||||
{
|
||||
this.context = context;
|
||||
this.name = name;
|
||||
this.register = register;
|
||||
this.unregister = new Map();
|
||||
context.callOnClose(this);
|
||||
}
|
||||
|
||||
SingletonEventManager.prototype = {
|
||||
addListener(callback, ...args) {
|
||||
let unregister = this.register(callback, ...args);
|
||||
this.unregister.set(callback, unregister);
|
||||
},
|
||||
|
||||
removeListener(callback) {
|
||||
if (!this.unregister.has(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let unregister = this.unregister.get(callback);
|
||||
this.unregister.delete(callback);
|
||||
this.unregister();
|
||||
},
|
||||
|
||||
hasListener(callback) {
|
||||
return this.unregister.has(callback);
|
||||
},
|
||||
|
||||
close() {
|
||||
for (let unregister of this.unregister.values()) {
|
||||
unregister();
|
||||
}
|
||||
},
|
||||
|
||||
api() {
|
||||
return {
|
||||
addListener: (...args) => this.addListener(...args),
|
||||
removeListener: (...args) => this.removeListener(...args),
|
||||
hasListener: (...args) => this.hasListener(...args),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Simple API for event listeners where events never fire.
|
||||
function ignoreEvent()
|
||||
{
|
||||
return {
|
||||
addListener: function(context, callback) {},
|
||||
removeListener: function(context, callback) {},
|
||||
hasListener: function(context, callback) {},
|
||||
};
|
||||
}
|
||||
|
||||
// Copy an API object from |source| into the scope |dest|.
|
||||
function injectAPI(source, dest)
|
||||
{
|
||||
for (let prop in source) {
|
||||
// Skip names prefixed with '_'.
|
||||
if (prop[0] == '_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = source[prop];
|
||||
if (typeof(value) == "function") {
|
||||
Cu.exportFunction(value, dest, {defineAs: prop});
|
||||
} else if (typeof(value) == "object") {
|
||||
let obj = Cu.createObjectIn(dest, {defineAs: prop});
|
||||
injectAPI(value, obj);
|
||||
} else {
|
||||
dest[prop] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Messaging primitives.
|
||||
*/
|
||||
|
||||
let nextBrokerId = 1;
|
||||
|
||||
let MESSAGES = [
|
||||
"Extension:Message",
|
||||
"Extension:Connect",
|
||||
];
|
||||
|
||||
// Receives messages from multiple message managers and directs them
|
||||
// to a set of listeners. On the child side: one broker per frame
|
||||
// script. On the parent side: one broker total, covering both the
|
||||
// global MM and the ppmm. Message must be tagged with a recipient,
|
||||
// which is an object with properties. Listeners can filter for
|
||||
// messages that have a certain value for a particular property in the
|
||||
// recipient. (If a message doesn't specify the given property, it's
|
||||
// considered a match.)
|
||||
function MessageBroker(messageManagers)
|
||||
{
|
||||
this.messageManagers = messageManagers;
|
||||
for (let mm of this.messageManagers) {
|
||||
for (let message of MESSAGES) {
|
||||
mm.addMessageListener(message, this);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners = {message: [], connect: []};
|
||||
}
|
||||
|
||||
MessageBroker.prototype = {
|
||||
uninit() {
|
||||
for (let mm of this.messageManagers) {
|
||||
for (let message of MESSAGES) {
|
||||
mm.removeMessageListener(message, this);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners = null;
|
||||
},
|
||||
|
||||
makeId() {
|
||||
return nextBrokerId++;
|
||||
},
|
||||
|
||||
addListener(type, listener, filter) {
|
||||
this.listeners[type].push({filter, listener});
|
||||
},
|
||||
|
||||
removeListener(type, listener) {
|
||||
let index = -1;
|
||||
for (let i = 0; i < this.listeners[type].length; i++) {
|
||||
if (this.listeners[type][i].listener == listener) {
|
||||
this.listeners[type].splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
runListeners(type, target, data) {
|
||||
let listeners = [];
|
||||
for (let {listener, filter} of this.listeners[type]) {
|
||||
let pass = true;
|
||||
for (let prop in filter) {
|
||||
if (prop in data.recipient && filter[prop] != data.recipient[prop]) {
|
||||
pass = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Save up the list of listeners to call in case they modify the
|
||||
// set of listeners.
|
||||
if (pass) {
|
||||
listeners.push(listener);
|
||||
}
|
||||
}
|
||||
|
||||
for (let listener of listeners) {
|
||||
listener(type, target, data.message, data.sender, data.recipient);
|
||||
}
|
||||
},
|
||||
|
||||
receiveMessage({name, data, target}) {
|
||||
switch (name) {
|
||||
case "Extension:Message":
|
||||
this.runListeners("message", target, data);
|
||||
break;
|
||||
|
||||
case "Extension:Connect":
|
||||
this.runListeners("connect", target, data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage(messageManager, type, message, sender, recipient) {
|
||||
let data = {message, sender, recipient};
|
||||
let names = {message: "Extension:Message", connect: "Extension:Connect"};
|
||||
messageManager.sendAsyncMessage(names[type], data);
|
||||
},
|
||||
};
|
||||
|
||||
// Abstraction for a Port object in the extension API. Each port has a unique ID.
|
||||
function Port(context, messageManager, name, id, sender)
|
||||
{
|
||||
this.context = context;
|
||||
this.messageManager = messageManager;
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.listenerName = `Extension:Port-${this.id}`;
|
||||
this.disconnectName = `Extension:Disconnect-${this.id}`;
|
||||
this.sender = sender;
|
||||
this.disconnected = false;
|
||||
}
|
||||
|
||||
Port.prototype = {
|
||||
api() {
|
||||
let portObj = Cu.createObjectIn(this.context.cloneScope);
|
||||
|
||||
// We want a close() notification when the window is destroyed.
|
||||
this.context.callOnClose(this);
|
||||
|
||||
let publicAPI = {
|
||||
name: this.name,
|
||||
disconnect: () => {
|
||||
this.disconnect();
|
||||
},
|
||||
postMessage: json => {
|
||||
if (this.disconnected) {
|
||||
throw "Attempt to postMessage on disconnected port";
|
||||
}
|
||||
this.messageManager.sendAsyncMessage(this.listenerName, json);
|
||||
},
|
||||
onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
|
||||
let listener = () => {
|
||||
if (!this.disconnected) {
|
||||
fire();
|
||||
}
|
||||
};
|
||||
|
||||
this.messageManager.addMessageListener(this.disconnectName, listener, true);
|
||||
return () => {
|
||||
this.messageManager.removeMessageListener(this.disconnectName, listener);
|
||||
};
|
||||
}).api(),
|
||||
onMessage: new EventManager(this.context, "Port.onMessage", fire => {
|
||||
let listener = ({data}) => {
|
||||
if (!this.disconnected) {
|
||||
fire(data);
|
||||
}
|
||||
};
|
||||
|
||||
this.messageManager.addMessageListener(this.listenerName, listener);
|
||||
return () => {
|
||||
this.messageManager.removeMessageListener(this.listenerName, listener);
|
||||
};
|
||||
}).api(),
|
||||
};
|
||||
|
||||
if (this.sender) {
|
||||
publicAPI.sender = this.sender;
|
||||
}
|
||||
|
||||
injectAPI(publicAPI, portObj);
|
||||
return portObj;
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
this.context.forgetOnClose(this);
|
||||
this.disconnect = true;
|
||||
this.messageManager.sendAsyncMessage(this.disconnectName);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.disconnect();
|
||||
},
|
||||
};
|
||||
|
||||
function getMessageManager(target)
|
||||
{
|
||||
if (target instanceof Ci.nsIDOMXULElement) {
|
||||
return target.messageManager;
|
||||
} else {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
// Each extension scope gets its own Messenger object. It handles the
|
||||
// basics of sendMessage, onMessage, connect, and onConnect.
|
||||
//
|
||||
// |context| is the extension scope.
|
||||
// |broker| is a MessageBroker used to receive and send messages.
|
||||
// |sender| is an object describing the sender (usually giving its extensionId, tabId, etc.)
|
||||
// |filter| is a recipient filter to apply to incoming messages from the broker.
|
||||
// |delegate| is an object that must implement a few methods:
|
||||
// getSender(context, messageManagerTarget, sender): returns a MessageSender
|
||||
// See https://developer.chrome.com/extensions/runtime#type-MessageSender.
|
||||
function Messenger(context, broker, sender, filter, delegate)
|
||||
{
|
||||
this.context = context;
|
||||
this.broker = broker;
|
||||
this.sender = sender;
|
||||
this.filter = filter;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
Messenger.prototype = {
|
||||
sendMessage(messageManager, msg, recipient, responseCallback) {
|
||||
let id = this.broker.makeId();
|
||||
let replyName = `Extension:Reply-${id}`;
|
||||
recipient.messageId = id;
|
||||
this.broker.sendMessage(messageManager, "message", msg, this.sender, recipient);
|
||||
|
||||
let onClose;
|
||||
let listener = ({data: response}) => {
|
||||
messageManager.removeMessageListener(replyName, listener);
|
||||
this.context.forgetOnClose(onClose);
|
||||
|
||||
if (response.gotData) {
|
||||
// TODO: Handle failure to connect to the extension?
|
||||
runSafe(this.context, responseCallback, response.data);
|
||||
}
|
||||
};
|
||||
onClose = {
|
||||
close() {
|
||||
messageManager.removeMessageListener(replyName, listener);
|
||||
}
|
||||
};
|
||||
if (responseCallback) {
|
||||
messageManager.addMessageListener(replyName, listener);
|
||||
this.context.callOnClose(onClose);
|
||||
}
|
||||
},
|
||||
|
||||
onMessage(name) {
|
||||
return new EventManager(this.context, name, fire => {
|
||||
let listener = (type, target, message, sender, recipient) => {
|
||||
message = Cu.cloneInto(message, this.context.cloneScope);
|
||||
if (this.delegate) {
|
||||
this.delegate.getSender(this.context, target, sender);
|
||||
}
|
||||
sender = Cu.cloneInto(sender, this.context.cloneScope);
|
||||
|
||||
let mm = getMessageManager(target);
|
||||
let replyName = `Extension:Reply-${recipient.messageId}`;
|
||||
|
||||
let valid = true, sent = false;
|
||||
let sendResponse = data => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
sent = true;
|
||||
mm.sendAsyncMessage(replyName, {data, gotData: true});
|
||||
};
|
||||
sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
|
||||
|
||||
let result = fire.withoutClone(message, sender, sendResponse);
|
||||
if (result !== true) {
|
||||
valid = false;
|
||||
}
|
||||
if (!sent) {
|
||||
mm.sendAsyncMessage(replyName, {gotData: false});
|
||||
}
|
||||
};
|
||||
|
||||
this.broker.addListener("message", listener, this.filter);
|
||||
return () => {
|
||||
this.broker.removeListener("message", listener);
|
||||
};
|
||||
}).api();
|
||||
},
|
||||
|
||||
connect(messageManager, name, recipient) {
|
||||
let portId = this.broker.makeId();
|
||||
let port = new Port(this.context, messageManager, name, portId, null);
|
||||
let msg = {name, portId};
|
||||
this.broker.sendMessage(messageManager, "connect", msg, this.sender, recipient);
|
||||
return port.api();
|
||||
},
|
||||
|
||||
onConnect(name) {
|
||||
return new EventManager(this.context, name, fire => {
|
||||
let listener = (type, target, message, sender, recipient) => {
|
||||
let {name, portId} = message;
|
||||
let mm = getMessageManager(target);
|
||||
if (this.delegate) {
|
||||
this.delegate.getSender(this.context, target, sender);
|
||||
}
|
||||
let port = new Port(this.context, mm, name, portId, sender);
|
||||
fire.withoutClone(port.api());
|
||||
};
|
||||
|
||||
this.broker.addListener("connect", listener, this.filter);
|
||||
return () => {
|
||||
this.broker.removeListener("connect", listener);
|
||||
};
|
||||
}).api();
|
||||
},
|
||||
};
|
||||
|
||||
let ExtensionUtils = {
|
||||
runSafe,
|
||||
DefaultWeakMap,
|
||||
EventManager,
|
||||
SingletonEventManager,
|
||||
ignoreEvent,
|
||||
injectAPI,
|
||||
MessageBroker,
|
||||
Messenger,
|
||||
};
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
EventManager,
|
||||
ignoreEvent,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// WeakMap[Extension -> Set[Alarm]]
|
||||
let alarmsMap = new WeakMap();
|
||||
|
||||
// WeakMap[Extension -> callback]
|
||||
let alarmCallbacksMap = new WeakMap();
|
||||
|
||||
// Manages an alarm created by the extension (alarms API).
|
||||
function Alarm(extension, name, alarmInfo)
|
||||
{
|
||||
this.extension = extension;
|
||||
this.name = name;
|
||||
this.when = alarmInfo.when;
|
||||
this.delayInMinutes = alarmInfo.delayInMinutes;
|
||||
this.periodInMinutes = alarmInfo.periodInMinutes;
|
||||
this.canceled = false;
|
||||
|
||||
let delay, scheduledTime;
|
||||
if (this.when) {
|
||||
scheduledTime = this.when;
|
||||
delay = this.when - Date.now();
|
||||
} else {
|
||||
delay = this.delayInMinutes * 60 * 1000;
|
||||
scheduledTime = Date.now() + delay;
|
||||
}
|
||||
|
||||
this.scheduledTime = scheduledTime;
|
||||
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
this.timer = timer;
|
||||
}
|
||||
|
||||
Alarm.prototype = {
|
||||
clear() {
|
||||
this.timer.cancel();
|
||||
alarmsMap.get(this.extension).delete(this);
|
||||
this.canceled = true;
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
if (alarmCallbacksMap.has(this.extension)) {
|
||||
alarmCallbacksMap.get(this.extension)(this);
|
||||
}
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.periodInMinutes) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
let delay = this.periodInMinutes * 60 * 1000;
|
||||
this.scheduledTime = Date.now() + delay;
|
||||
this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
},
|
||||
|
||||
get data() {
|
||||
return {
|
||||
name: this.name,
|
||||
scheduledTime: this.scheduledTime,
|
||||
periodInMinutes: this.periodInMinutes,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
extensions.on("startup", (type, extension) => {
|
||||
alarmsMap.set(extension, new Set());
|
||||
});
|
||||
|
||||
extensions.on("shutdown", (type, extension) => {
|
||||
for (let alarm of alarmsMap.get(extension)) {
|
||||
alarm.clear();
|
||||
}
|
||||
alarmsMap.delete(extension);
|
||||
});
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
alarms: {
|
||||
create: function(...args) {
|
||||
let name = "", alarmInfo;
|
||||
if (args.length == 1) {
|
||||
alarmInfo = args[0];
|
||||
} else {
|
||||
[name, alarmInfo] = args;
|
||||
}
|
||||
|
||||
let alarm = new Alarm(extension, name, alarmInfo);
|
||||
alarmsMap.get(extension).add(alarm);
|
||||
},
|
||||
|
||||
get: function(args) {
|
||||
let name = "", callback;
|
||||
if (args.length == 1) {
|
||||
callback = args[0];
|
||||
} else {
|
||||
[name, callback] = args;
|
||||
}
|
||||
|
||||
for (let alarm of alarmsMap.get(extension)) {
|
||||
if (alarm.name == name) {
|
||||
runSafe(context, callback, alarm.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getAll: function(callback) {
|
||||
let alarms = alarmsMap.get(extension);
|
||||
result = [ for (alarm of alarms) alarm.data ];
|
||||
runSafe(context, callback, result);
|
||||
},
|
||||
|
||||
clear: function(...args) {
|
||||
let name = "", callback;
|
||||
if (args.length == 1) {
|
||||
callback = args[0];
|
||||
} else {
|
||||
[name, callback] = args;
|
||||
}
|
||||
|
||||
let alarms = alarmsMap.get(extension);
|
||||
let cleared = false;
|
||||
for (let alarm of alarms) {
|
||||
if (alarm.name == name) {
|
||||
alarm.clear();
|
||||
cleared = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
runSafe(context, callback, cleared);
|
||||
}
|
||||
},
|
||||
|
||||
clearAll: function(callback) {
|
||||
let alarms = alarmsMap.get(extension);
|
||||
let cleared = false;
|
||||
for (let alarm of alarms) {
|
||||
alarm.clear();
|
||||
cleared = true;
|
||||
}
|
||||
if (callback) {
|
||||
runSafe(context, callback, cleared);
|
||||
}
|
||||
},
|
||||
|
||||
onAlarm: new EventManager(context, "alarms.onAlarm", fire => {
|
||||
let callback = alarm => {
|
||||
fire(alarm.data);
|
||||
};
|
||||
|
||||
alarmCallbacksMap.set(extension, callback);
|
||||
return () => {
|
||||
alarmCallbacksMap.delete(extension);
|
||||
};
|
||||
}).api(),
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
// WeakMap[Extension -> BackgroundPage]
|
||||
let backgroundPagesMap = new WeakMap();
|
||||
|
||||
// Responsible for the background_page section of the manifest.
|
||||
function BackgroundPage(options, extension)
|
||||
{
|
||||
this.extension = extension;
|
||||
this.scripts = options.scripts || [];
|
||||
this.page = options.page || null;
|
||||
this.contentWindow = null;
|
||||
this.webNav = null;
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
BackgroundPage.prototype = {
|
||||
build() {
|
||||
let webNav = Services.appShell.createWindowlessBrowser(false);
|
||||
this.webNav = webNav;
|
||||
|
||||
let principal = Services.scriptSecurityManager.createCodebasePrincipal(this.extension.baseURI,
|
||||
{addonId: this.extension.id});
|
||||
|
||||
let interfaceRequestor = webNav.QueryInterface(Ci.nsIInterfaceRequestor);
|
||||
let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
|
||||
|
||||
this.context = new ExtensionPage(this.extension, {type: "background", docShell});
|
||||
GlobalManager.injectInDocShell(docShell, this.extension, this.context);
|
||||
|
||||
docShell.createAboutBlankContentViewer(principal);
|
||||
|
||||
let window = webNav.document.defaultView;
|
||||
this.contentWindow = window;
|
||||
this.context.contentWindow = window;
|
||||
|
||||
let url;
|
||||
if (this.page) {
|
||||
url = this.extension.baseURI.resolve(this.page);
|
||||
} else {
|
||||
url = this.extension.baseURI.resolve("_blank.html");
|
||||
}
|
||||
webNav.loadURI(url, 0, null, null, null);
|
||||
|
||||
// TODO: Right now we run onStartup after the background page
|
||||
// finishes. See if this is what Chrome does.
|
||||
window.windowRoot.addEventListener("load", () => {
|
||||
if (this.scripts) {
|
||||
let doc = window.document;
|
||||
for (let script of this.scripts) {
|
||||
let url = this.extension.baseURI.resolve(script);
|
||||
let tag = doc.createElement("script");
|
||||
tag.setAttribute("src", url);
|
||||
tag.async = false;
|
||||
doc.body.appendChild(tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extension.onStartup) {
|
||||
this.extension.onStartup();
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
// Navigate away from the background page to invalidate any
|
||||
// setTimeouts or other callbacks.
|
||||
this.webNav.loadURI("about:blank", 0, null, null, null);
|
||||
this.webNav = null;
|
||||
},
|
||||
};
|
||||
|
||||
extensions.on("manifest_background", (type, directive, extension, manifest) => {
|
||||
let bgPage = new BackgroundPage(manifest.background, extension);
|
||||
bgPage.build();
|
||||
backgroundPagesMap.set(extension, bgPage);
|
||||
});
|
||||
|
||||
extensions.on("shutdown", (type, extension) => {
|
||||
if (backgroundPagesMap.has(extension)) {
|
||||
backgroundPagesMap.get(extension).shutdown();
|
||||
backgroundPagesMap.delete(extension);
|
||||
}
|
||||
});
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
extension: {
|
||||
getBackgroundPage: function() {
|
||||
return backgroundPagesMap.get(extension).contentWindow;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
extension: {
|
||||
getURL: function(url) {
|
||||
return extension.baseURI.resolve(url);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
i18n: {
|
||||
getMessage: function(messageName, substitutions) {
|
||||
return extension.localizeMessage(messageName, substitutions);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
extensions.registerPrivilegedAPI("idle", (extension, context) => {
|
||||
return {
|
||||
idle: {
|
||||
queryState: function(detectionIntervalInSeconds, callback) {
|
||||
runSafe(context, callback, "active");
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,140 @@
|
|||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
EventManager,
|
||||
ignoreEvent,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// WeakMap[Extension -> Set[Notification]]
|
||||
let notificationsMap = new WeakMap();
|
||||
|
||||
// WeakMap[Extension -> callback]
|
||||
let notificationCallbacksMap = new WeakMap();
|
||||
|
||||
// Manages a notification popup (notifications API) created by the extension.
|
||||
function Notification(extension, id, options)
|
||||
{
|
||||
this.extension = extension;
|
||||
this.id = id;
|
||||
this.options = options;
|
||||
|
||||
let imageURL;
|
||||
if (options.iconUrl) {
|
||||
imageURL = this.extension.baseURI.resolve(options.iconUrl);
|
||||
}
|
||||
|
||||
try {
|
||||
let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
|
||||
svc.showAlertNotification(imageURL,
|
||||
options.title,
|
||||
options.message,
|
||||
false, // textClickable
|
||||
this.id,
|
||||
this,
|
||||
this.id);
|
||||
} catch (e) {
|
||||
// This will fail if alerts aren't available on the system.
|
||||
}
|
||||
}
|
||||
|
||||
Notification.prototype = {
|
||||
clear() {
|
||||
try {
|
||||
let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
|
||||
svc.closeAlert(this.id);
|
||||
} catch (e) {
|
||||
// This will fail if the OS doesn't support this function.
|
||||
}
|
||||
notificationsMap.get(this.extension).delete(this);
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
if (topic != "alertfinished") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationCallbacksMap.has(this.extension)) {
|
||||
notificationCallbackMap.get(this.extension)(this);
|
||||
}
|
||||
|
||||
notificationsMap.get(this.extension).delete(this);
|
||||
},
|
||||
};
|
||||
|
||||
extensions.on("startup", (type, extension) => {
|
||||
notificationsMap.set(extension, new Set());
|
||||
});
|
||||
|
||||
extensions.on("shutdown", (type, extension) => {
|
||||
for (let notification of notificationsMap.get(extension)) {
|
||||
notification.clear();
|
||||
}
|
||||
notificationsMap.delete(extension);
|
||||
});
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
extensions.registerPrivilegedAPI("notifications", (extension, context) => {
|
||||
return {
|
||||
notifications: {
|
||||
create: function(...args) {
|
||||
let notificationId, options, callback;
|
||||
if (args.length == 1) {
|
||||
options = args[0];
|
||||
} else {
|
||||
[notificationId, options, callback] = args;
|
||||
}
|
||||
|
||||
if (!notificationId) {
|
||||
notificationId = nextId++;
|
||||
}
|
||||
|
||||
// FIXME: Lots of options still aren't supported, especially
|
||||
// buttons.
|
||||
let notification = new Notification(extension, notificationId, options);
|
||||
notificationsMap.get(extension).add(notification);
|
||||
|
||||
if (callback) {
|
||||
runSafe(context, callback, notificationId);
|
||||
}
|
||||
},
|
||||
|
||||
clear: function(notificationId, callback) {
|
||||
let notifications = notificationsMap.get(extension);
|
||||
let cleared = false;
|
||||
for (let notification of notifications) {
|
||||
if (notification.id == notificationId) {
|
||||
notification.clear();
|
||||
cleared = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
runSafe(context, callback, cleared);
|
||||
}
|
||||
},
|
||||
|
||||
getAll: function(callback) {
|
||||
let notifications = notificationsMap.get(extension);
|
||||
notifications = [ for (notification of notifications) notification.id ];
|
||||
runSafe(context, callback, notifications);
|
||||
},
|
||||
|
||||
onClosed: new EventManager(context, "notifications.onClosed", fire => {
|
||||
let listener = notification => {
|
||||
// FIXME: Support the byUser argument.
|
||||
fire(notification.id, true);
|
||||
};
|
||||
|
||||
notificationCallbackMap.set(extension, listener);
|
||||
return () => {
|
||||
notificationCallbackMap.delete(extension);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
// FIXME
|
||||
onButtonClicked: ignoreEvent(),
|
||||
onClicked: ignoreEvent(),
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
EventManager,
|
||||
ignoreEvent,
|
||||
} = ExtensionUtils;
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
runtime: {
|
||||
onStartup: new EventManager(context, "runtime.onStartup", fire => {
|
||||
extension.onStartup = fire;
|
||||
return () => {
|
||||
extension.onStartup = null;
|
||||
};
|
||||
}).api(),
|
||||
|
||||
onInstalled: ignoreEvent(),
|
||||
|
||||
onMessage: context.messenger.onMessage("runtime.onMessage"),
|
||||
|
||||
onConnect: context.messenger.onConnect("runtime.onConnect"),
|
||||
|
||||
sendMessage: function(...args) {
|
||||
let extensionId, message, options, responseCallback;
|
||||
if (args.length == 1) {
|
||||
message = args[0];
|
||||
} else if (args.length == 2) {
|
||||
[message, responseCallback] = args;
|
||||
} else {
|
||||
[extensionId, message, options, responseCallback] = args;
|
||||
}
|
||||
let recipient = {extensionId: extensionId ? extensionId : extension.id};
|
||||
return context.messenger.sendMessage(Services.cpmm, message, recipient, responseCallback);
|
||||
},
|
||||
|
||||
getManifest() {
|
||||
return Cu.cloneInto(extension.manifest, context.cloneScope);
|
||||
},
|
||||
|
||||
id: extension.id,
|
||||
|
||||
getURL: function(url) {
|
||||
return extension.baseURI.resolve(url);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
|
||||
"resource://gre/modules/ExtensionStorage.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
EventManager,
|
||||
ignoreEvent,
|
||||
runSafe,
|
||||
} = ExtensionUtils;
|
||||
|
||||
extensions.registerPrivilegedAPI("storage", (extension, context) => {
|
||||
return {
|
||||
storage: {
|
||||
local: {
|
||||
get: function(keys, callback) {
|
||||
ExtensionStorage.get(extension.id, keys).then(result => {
|
||||
runSafe(context, callback, result);
|
||||
});
|
||||
},
|
||||
set: function(items, callback) {
|
||||
ExtensionStorage.set(extension.id, items).then(() => {
|
||||
if (callback) {
|
||||
runSafe(context, callback);
|
||||
}
|
||||
});
|
||||
},
|
||||
remove: function(items, callback) {
|
||||
ExtensionStorage.remove(extension.id, items).then(() => {
|
||||
if (callback) {
|
||||
runSafe(context, callback);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
onChanged: new EventManager(context, "storage.local.onChanged", fire => {
|
||||
let listener = changes => {
|
||||
fire(changes, "local");
|
||||
};
|
||||
|
||||
ExtensionStorage.addOnChangedListener(extension.id, listener);
|
||||
return () => {
|
||||
ExtensionStorage.removeOnChangedListener(extension.id, listener);
|
||||
};
|
||||
}).api(),
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
|
||||
"resource://gre/modules/ExtensionManagement.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigation",
|
||||
"resource://gre/modules/WebNavigation.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
SingletonEventManager,
|
||||
ignoreEvent,
|
||||
runSafe,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// Similar to WebRequestEventManager but for WebNavigation.
|
||||
function WebNavigationEventManager(context, eventName)
|
||||
{
|
||||
let name = `webNavigation.${eventName}`;
|
||||
let register = callback => {
|
||||
let listener = data => {
|
||||
if (!data.browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tabId = TabManager.getBrowserId(data.browser);
|
||||
if (tabId == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data2 = {
|
||||
url: data.url,
|
||||
timeStamp: Date.now(),
|
||||
frameId: ExtensionManagement.getFrameId(data.windowId),
|
||||
parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
|
||||
};
|
||||
|
||||
// Fills in tabId typically.
|
||||
let result = {};
|
||||
extensions.emit("fill-browser-data", data.browser, data2, result);
|
||||
if (result.cancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
return runSafe(context, callback, data2);
|
||||
};
|
||||
|
||||
WebNavigation[eventName].addListener(listener);
|
||||
return () => {
|
||||
WebNavigation[eventName].removeListener(listener);
|
||||
};
|
||||
};
|
||||
|
||||
return SingletonEventManager.call(this, context, name, register);
|
||||
}
|
||||
|
||||
WebNavigationEventManager.prototype = Object.create(SingletonEventManager.prototype);
|
||||
|
||||
extensions.registerPrivilegedAPI("webNavigation", (extension, context) => {
|
||||
return {
|
||||
webNavigation: {
|
||||
onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
|
||||
onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
|
||||
onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
|
||||
onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
|
||||
onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
|
||||
onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
|
||||
onCreatedNavigationTarget: ignoreEvent(),
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
|
||||
"resource://gre/modules/WebRequest.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
let {
|
||||
SingletonEventManager,
|
||||
runSafe,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// EventManager-like class specifically for WebRequest. Inherits from
|
||||
// SingletonEventManager. Takes care of converting |details| parameter
|
||||
// when invoking listeners.
|
||||
function WebRequestEventManager(context, eventName)
|
||||
{
|
||||
let name = `webRequest.${eventName}`;
|
||||
let register = (callback, filter, info) => {
|
||||
let listener = data => {
|
||||
if (!data.browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tabId = TabManager.getBrowserId(data.browser);
|
||||
if (tabId == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data2 = {
|
||||
url: data.url,
|
||||
method: data.method,
|
||||
type: data.type,
|
||||
timeStamp: Date.now(),
|
||||
frameId: ExtensionManagement.getFrameId(data.windowId),
|
||||
parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
|
||||
};
|
||||
|
||||
// Fills in tabId typically.
|
||||
let result = {};
|
||||
extensions.emit("fill-browser-data", data.browser, data2, result);
|
||||
if (result.cancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
let optional = ["requestHeaders", "responseHeaders", "statusCode"];
|
||||
for (let opt of optional) {
|
||||
if (opt in data) {
|
||||
data2[opt] = data[opt];
|
||||
}
|
||||
}
|
||||
|
||||
return runSafe(context, callback, data2);
|
||||
};
|
||||
|
||||
let filter2 = {};
|
||||
filter2.urls = new MatchPattern(filter.urls);
|
||||
if (filter.types) {
|
||||
filter2.types = filter.types;
|
||||
}
|
||||
if (filter.tabId) {
|
||||
filter2.tabId = filter.tabId;
|
||||
}
|
||||
if (filter.windowId) {
|
||||
filter2.windowId = filter.windowId;
|
||||
}
|
||||
|
||||
let info2 = [];
|
||||
if (info) {
|
||||
for (let desc of info) {
|
||||
if (desc == "blocking" && !context.extension.hasPermission("webRequestBlocking")) {
|
||||
Cu.reportError("Using webRequest.addListener with the blocking option " +
|
||||
"requires the 'webRequestBlocking' permission.");
|
||||
} else {
|
||||
info2.push(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WebRequest[eventName].addListener(listener, filter2, info2);
|
||||
return () => {
|
||||
WebRequest[eventName].removeListener(listener);
|
||||
};
|
||||
};
|
||||
|
||||
return SingletonEventManager.call(this, context, name, register);
|
||||
}
|
||||
|
||||
WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype);
|
||||
|
||||
extensions.registerPrivilegedAPI("webRequest", (extension, context) => {
|
||||
return {
|
||||
webRequest: {
|
||||
onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(),
|
||||
onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
|
||||
onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
|
||||
onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(),
|
||||
onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
|
||||
onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
|
||||
handlerBehaviorChanged: function() {
|
||||
// TODO: Flush all caches.
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
# 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/.
|
||||
|
||||
toolkit.jar:
|
||||
% content extensions %content/extensions/
|
||||
content/extensions/ext-alarms.js (ext-alarms.js)
|
||||
content/extensions/ext-backgroundPage.js (ext-backgroundPage.js)
|
||||
content/extensions/ext-notifications.js (ext-notifications.js)
|
||||
content/extensions/ext-i18n.js (ext-i18n.js)
|
||||
content/extensions/ext-idle.js (ext-idle.js)
|
||||
content/extensions/ext-webRequest.js (ext-webRequest.js)
|
||||
content/extensions/ext-webNavigation.js (ext-webNavigation.js)
|
||||
content/extensions/ext-runtime.js (ext-runtime.js)
|
||||
content/extensions/ext-extension.js (ext-extension.js)
|
||||
content/extensions/ext-storage.js (ext-storage.js)
|
|
@ -0,0 +1,15 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'Extension.jsm',
|
||||
'ExtensionContent.jsm',
|
||||
'ExtensionManagement.jsm',
|
||||
'ExtensionStorage.jsm',
|
||||
'ExtensionUtils.jsm',
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
|
@ -22,6 +22,7 @@ DIRS += [
|
|||
'crashmonitor',
|
||||
'diskspacewatcher',
|
||||
'downloads',
|
||||
'extensions',
|
||||
'exthelper',
|
||||
'filepicker',
|
||||
'filewatcher',
|
||||
|
|
|
@ -104,7 +104,11 @@ AddonPolicyService.prototype = {
|
|||
* directly.
|
||||
*/
|
||||
setAddonLoadURICallback(aAddonId, aCallback) {
|
||||
this.mayLoadURICallbacks[aAddonId] = aCallback;
|
||||
if (aCallback) {
|
||||
this.mayLoadURICallbacks[aAddonId] = aCallback;
|
||||
} else {
|
||||
delete this.mayLoadURICallbacks[aAddonId];
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/* 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/. */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Locale"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
|
||||
const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
|
||||
const PREF_SELECTED_LOCALE = "general.useragent.locale";
|
||||
|
||||
this.Locale = {
|
||||
/**
|
||||
* Gets the currently selected locale for display.
|
||||
* @return the selected locale or "en-US" if none is selected
|
||||
*/
|
||||
getLocale() {
|
||||
if (Preferences.get(PREF_MATCH_OS_LOCALE, false))
|
||||
return Services.locale.getLocaleComponentForUserAgent();
|
||||
try {
|
||||
let locale = Preferences.get(PREF_SELECTED_LOCALE, null, Ci.nsIPrefLocalizedString);
|
||||
if (locale)
|
||||
return locale;
|
||||
}
|
||||
catch (e) {}
|
||||
return Preferences.get(PREF_SELECTED_LOCALE, "en-US");
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects the closest matching locale from a list of locales.
|
||||
*
|
||||
* @param aLocales
|
||||
* An array of locales
|
||||
* @return the best match for the currently selected locale
|
||||
*/
|
||||
findClosestLocale(aLocales) {
|
||||
let appLocale = this.getLocale();
|
||||
|
||||
// Holds the best matching localized resource
|
||||
var bestmatch = null;
|
||||
// The number of locale parts it matched with
|
||||
var bestmatchcount = 0;
|
||||
// The number of locale parts in the match
|
||||
var bestpartcount = 0;
|
||||
|
||||
var matchLocales = [appLocale.toLowerCase()];
|
||||
/* If the current locale is English then it will find a match if there is
|
||||
a valid match for en-US so no point searching that locale too. */
|
||||
if (matchLocales[0].substring(0, 3) != "en-")
|
||||
matchLocales.push("en-us");
|
||||
|
||||
for (let locale of matchLocales) {
|
||||
var lparts = locale.split("-");
|
||||
for (let localized of aLocales) {
|
||||
for (let found of localized.locales) {
|
||||
found = found.toLowerCase();
|
||||
// Exact match is returned immediately
|
||||
if (locale == found)
|
||||
return localized;
|
||||
|
||||
var fparts = found.split("-");
|
||||
/* If we have found a possible match and this one isn't any longer
|
||||
then we dont need to check further. */
|
||||
if (bestmatch && fparts.length < bestmatchcount)
|
||||
continue;
|
||||
|
||||
// Count the number of parts that match
|
||||
var maxmatchcount = Math.min(fparts.length, lparts.length);
|
||||
var matchcount = 0;
|
||||
while (matchcount < maxmatchcount &&
|
||||
fparts[matchcount] == lparts[matchcount])
|
||||
matchcount++;
|
||||
|
||||
/* If we matched more than the last best match or matched the same and
|
||||
this locale is less specific than the last best match. */
|
||||
if (matchcount > bestmatchcount ||
|
||||
(matchcount == bestmatchcount && fparts.length < bestpartcount)) {
|
||||
bestmatch = localized;
|
||||
bestmatchcount = matchcount;
|
||||
bestpartcount = fparts.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we found a valid match for this locale return it
|
||||
if (bestmatch)
|
||||
return bestmatch;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
|
@ -63,7 +63,7 @@ function SingleMatchPattern(pat)
|
|||
}
|
||||
|
||||
SingleMatchPattern.prototype = {
|
||||
matches(uri) {
|
||||
matches(uri, ignorePath = false) {
|
||||
if (this.scheme.indexOf(uri.scheme) == -1) {
|
||||
return false;
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ SingleMatchPattern.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.path.test(uri.path)) {
|
||||
if (!ignorePath && !this.path.test(uri.path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -114,6 +114,15 @@ MatchPattern.prototype = {
|
|||
return false;
|
||||
},
|
||||
|
||||
matchesIgnoringPath(uri) {
|
||||
for (let matcher of this.matchers) {
|
||||
if (matcher.matches(uri, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
serialize() {
|
||||
return this.pat;
|
||||
},
|
||||
|
|
|
@ -36,6 +36,7 @@ EXTRA_JS_MODULES += [
|
|||
'InlineSpellChecker.jsm',
|
||||
'InlineSpellCheckerContent.jsm',
|
||||
'LoadContextInfo.jsm',
|
||||
'Locale.jsm',
|
||||
'Log.jsm',
|
||||
'NewTabUtils.jsm',
|
||||
'ObjectUtils.jsm',
|
||||
|
|
|
@ -22,6 +22,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
|
|||
"resource://gre/modules/ChromeManifestParser.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
|
||||
"resource://gre/modules/LightweightThemeManager.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
|
||||
"resource://gre/modules/Locale.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils",
|
||||
|
@ -67,8 +69,6 @@ const PREF_INSTALL_CACHE = "extensions.installCache";
|
|||
const PREF_XPI_STATE = "extensions.xpiState";
|
||||
const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons";
|
||||
const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
|
||||
const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
|
||||
const PREF_SELECTED_LOCALE = "general.useragent.locale";
|
||||
const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
|
||||
const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending";
|
||||
const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin";
|
||||
|
@ -514,84 +514,6 @@ SafeInstallOperation.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the currently selected locale for display.
|
||||
* @return the selected locale or "en-US" if none is selected
|
||||
*/
|
||||
function getLocale() {
|
||||
if (Preferences.get(PREF_MATCH_OS_LOCALE, false))
|
||||
return Services.locale.getLocaleComponentForUserAgent();
|
||||
try {
|
||||
let locale = Preferences.get(PREF_SELECTED_LOCALE, null, Ci.nsIPrefLocalizedString);
|
||||
if (locale)
|
||||
return locale;
|
||||
}
|
||||
catch (e) {}
|
||||
return Preferences.get(PREF_SELECTED_LOCALE, "en-US");
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the closest matching locale from a list of locales.
|
||||
*
|
||||
* @param aLocales
|
||||
* An array of locales
|
||||
* @return the best match for the currently selected locale
|
||||
*/
|
||||
function findClosestLocale(aLocales) {
|
||||
let appLocale = getLocale();
|
||||
|
||||
// Holds the best matching localized resource
|
||||
var bestmatch = null;
|
||||
// The number of locale parts it matched with
|
||||
var bestmatchcount = 0;
|
||||
// The number of locale parts in the match
|
||||
var bestpartcount = 0;
|
||||
|
||||
var matchLocales = [appLocale.toLowerCase()];
|
||||
/* If the current locale is English then it will find a match if there is
|
||||
a valid match for en-US so no point searching that locale too. */
|
||||
if (matchLocales[0].substring(0, 3) != "en-")
|
||||
matchLocales.push("en-us");
|
||||
|
||||
for each (var locale in matchLocales) {
|
||||
var lparts = locale.split("-");
|
||||
for each (var localized in aLocales) {
|
||||
for each (let found in localized.locales) {
|
||||
found = found.toLowerCase();
|
||||
// Exact match is returned immediately
|
||||
if (locale == found)
|
||||
return localized;
|
||||
|
||||
var fparts = found.split("-");
|
||||
/* If we have found a possible match and this one isn't any longer
|
||||
then we dont need to check further. */
|
||||
if (bestmatch && fparts.length < bestmatchcount)
|
||||
continue;
|
||||
|
||||
// Count the number of parts that match
|
||||
var maxmatchcount = Math.min(fparts.length, lparts.length);
|
||||
var matchcount = 0;
|
||||
while (matchcount < maxmatchcount &&
|
||||
fparts[matchcount] == lparts[matchcount])
|
||||
matchcount++;
|
||||
|
||||
/* If we matched more than the last best match or matched the same and
|
||||
this locale is less specific than the last best match. */
|
||||
if (matchcount > bestmatchcount ||
|
||||
(matchcount == bestmatchcount && fparts.length < bestpartcount)) {
|
||||
bestmatch = localized;
|
||||
bestmatchcount = matchcount;
|
||||
bestpartcount = fparts.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we found a valid match for this locale return it
|
||||
if (bestmatch)
|
||||
return bestmatch;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the userDisabled and softDisabled properties of an add-on based on what
|
||||
* values those properties had for a previous instance of the add-on. The
|
||||
|
@ -6493,7 +6415,7 @@ AddonInternal.prototype = {
|
|||
get selectedLocale() {
|
||||
if (this._selectedLocale)
|
||||
return this._selectedLocale;
|
||||
let locale = findClosestLocale(this.locales);
|
||||
let locale = Locale.findClosestLocale(this.locales);
|
||||
this._selectedLocale = locale ? locale : this.defaultLocale;
|
||||
return this._selectedLocale;
|
||||
},
|
||||
|
|
Загрузка…
Ссылка в новой задаче