Bug 1175770 - New extension API (r=Mossop)

This commit is contained in:
Bill McCloskey 2015-06-03 15:34:44 -07:00
Родитель 1ad7580e1a
Коммит b1a00d7c72
40 изменённых файлов: 4303 добавлений и 84 удалений

Просмотреть файл

@ -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);
});

20
browser/components/extensions/bootstrap.js поставляемый Normal file
Просмотреть файл

@ -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;
},