Restore webext

This commit is contained in:
Fedor 2019-03-12 19:34:24 +03:00
parent 3e27fff9b4
commit db6eb94ebb
416 changed files with 81535 additions and 8 deletions

View File

@ -9,6 +9,9 @@ var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
#ifdef MOZ_WEBEXTENSIONS
Cu.import("resource://gre/modules/ExtensionContent.jsm");
#endif
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
@ -913,6 +916,14 @@ var UserContextIdNotifier = {
UserContextIdNotifier.init();
#ifdef MOZ_WEBEXTENSIONS
ExtensionContent.init(this);
addEventListener("unload", () => {
ExtensionContent.uninit(this);
RefreshBlocker.uninit();
});
#endif
addMessageListener("AllowScriptsToClose", () => {
content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)

View File

@ -56,6 +56,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
<field name="AppConstants" readonly="true">
(Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
</field>
#ifdef MOZ_WEBEXTENSIONS
<field name="ExtensionSearchHandler" readonly="true">
(Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
</field>
#endif
<constructor><![CDATA[
this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
@ -483,6 +488,15 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
actionDetails
);
break;
#ifdef MOZ_WEBEXTENSIONS
case "extension":
this.handleRevert();
// Give the extension control of handling the command.
let searchString = action.params.content;
let keyword = action.params.keyword;
this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
return;
#endif
}
} else {
// This is a fallback for add-ons and old testing code that directly
@ -1203,6 +1217,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
this._clearNoActions();
this.formatValue();
}
#ifdef MOZ_WEBEXTENSIONS
if (ExtensionSearchHandler.hasActiveInputSession()) {
ExtensionSearchHandler.handleInputCancelled();
}
#endif
]]></handler>
<handler event="dragstart" phase="capturing"><![CDATA[

View File

@ -95,7 +95,7 @@ browser.jar:
#endif
content/browser/browser-thumbnails.js (content/browser-thumbnails.js)
content/browser/browser-trackingprotection.js (content/browser-trackingprotection.js)
content/browser/tab-content.js (content/tab-content.js)
* content/browser/tab-content.js (content/tab-content.js)
content/browser/content.js (content/content.js)
content/browser/defaultthemes/1.footer.jpg (content/defaultthemes/1.footer.jpg)
content/browser/defaultthemes/1.header.jpg (content/defaultthemes/1.header.jpg)
@ -163,7 +163,7 @@ browser.jar:
content/browser/contentSearchUI.css (content/contentSearchUI.css)
content/browser/tabbrowser.css (content/tabbrowser.css)
content/browser/tabbrowser.xml (content/tabbrowser.xml)
content/browser/urlbarBindings.xml (content/urlbarBindings.xml)
* content/browser/urlbarBindings.xml (content/urlbarBindings.xml)
content/browser/utilityOverlay.js (content/utilityOverlay.js)
content/browser/usercontext.svg (content/usercontext.svg)
content/browser/web-panels.js (content/web-panels.js)

View File

@ -24,6 +24,9 @@ DIRS += [
'translation',
]
if CONFIG['MOZ_WEBEXTENSIONS']:
DIRS += ['webextensions']
DIRS += ['build']
XPIDL_SOURCES += [

View File

@ -0,0 +1,22 @@
"use strict";
module.exports = { // eslint-disable-line no-undef
"extends": "../../../toolkit/components/extensions/.eslintrc.js",
"globals": {
"AllWindowEvents": true,
"browserActionFor": true,
"currentWindow": true,
"EventEmitter": true,
"getCookieStoreIdForTab": true,
"IconDetails": true,
"makeWidgetId": true,
"pageActionFor": true,
"PanelPopup": true,
"TabContext": true,
"ViewPopup": true,
"WindowEventManager": true,
"WindowListManager": true,
"WindowManager": true,
},
};

View File

@ -0,0 +1,374 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
SingletonEventManager,
} = ExtensionUtils;
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
let listenerCount = 0;
function getTree(rootGuid, onlyChildren) {
function convert(node, parent) {
let treenode = {
id: node.guid,
title: node.title || "",
index: node.index,
dateAdded: node.dateAdded / 1000,
};
if (parent && node.guid != PlacesUtils.bookmarks.rootGuid) {
treenode.parentId = parent.guid;
}
if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
// This isn't quite correct. Recently Bookmarked ends up here ...
treenode.url = node.uri;
} else {
treenode.dateGroupModified = node.lastModified / 1000;
if (node.children && !onlyChildren) {
treenode.children = node.children.map(child => convert(child, node));
}
}
return treenode;
}
return PlacesUtils.promiseBookmarksTree(rootGuid, {
excludeItemsCallback: item => {
if (item.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
return true;
}
return item.annos &&
item.annos.find(a => a.name == PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
},
}).then(root => {
if (onlyChildren) {
let children = root.children || [];
return children.map(child => convert(child, root));
}
// It seems like the array always just contains the root node.
return [convert(root, null)];
}).catch(e => Promise.reject({message: e.message}));
}
function convert(result) {
let node = {
id: result.guid,
title: result.title || "",
index: result.index,
dateAdded: result.dateAdded.getTime(),
};
if (result.guid != PlacesUtils.bookmarks.rootGuid) {
node.parentId = result.parentGuid;
}
if (result.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
node.url = result.url.href; // Output is always URL object.
} else {
node.dateGroupModified = result.lastModified.getTime();
}
return node;
}
let observer = {
skipTags: true,
skipDescendantsOnItemRemoval: true,
onBeginUpdateBatch() {},
onEndUpdateBatch() {},
onItemAdded(id, parentId, index, itemType, uri, title, dateAdded, guid, parentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let bookmark = {
id: guid,
parentId: parentGuid,
index,
title,
dateAdded: dateAdded / 1000,
};
if (itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
bookmark.url = uri.spec;
} else {
bookmark.dateGroupModified = bookmark.dateAdded;
}
this.emit("created", bookmark);
},
onItemVisited() {},
onItemMoved(id, oldParentId, oldIndex, newParentId, newIndex, itemType, guid, oldParentGuid, newParentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let info = {
parentId: newParentGuid,
index: newIndex,
oldParentId: oldParentGuid,
oldIndex,
};
this.emit("moved", {guid, info});
},
onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let node = {
id: guid,
parentId: parentGuid,
index,
};
if (itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
node.url = uri.spec;
}
this.emit("removed", {guid, info: {parentId: parentGuid, index, node}});
},
onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid, oldVal, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let info = {};
if (prop == "title") {
info.title = val;
} else if (prop == "uri") {
info.url = val;
} else {
// Not defined yet.
return;
}
this.emit("changed", {guid, info});
},
};
EventEmitter.decorate(observer);
function decrementListeners() {
listenerCount -= 1;
if (!listenerCount) {
PlacesUtils.bookmarks.removeObserver(observer);
}
}
function incrementListeners() {
listenerCount++;
if (listenerCount == 1) {
PlacesUtils.bookmarks.addObserver(observer, false);
}
}
extensions.registerSchemaAPI("bookmarks", "addon_parent", context => {
return {
bookmarks: {
get: function(idOrIdList) {
let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];
return Task.spawn(function* () {
let bookmarks = [];
for (let id of list) {
let bookmark = yield PlacesUtils.bookmarks.fetch({guid: id});
if (!bookmark) {
throw new Error("Bookmark not found");
}
bookmarks.push(convert(bookmark));
}
return bookmarks;
}).catch(error => Promise.reject({message: error.message}));
},
getChildren: function(id) {
// TODO: We should optimize this.
return getTree(id, true);
},
getTree: function() {
return getTree(PlacesUtils.bookmarks.rootGuid, false);
},
getSubTree: function(id) {
return getTree(id, false);
},
search: function(query) {
return PlacesUtils.bookmarks.search(query).then(result => result.map(convert));
},
getRecent: function(numberOfItems) {
return PlacesUtils.bookmarks.getRecent(numberOfItems).then(result => result.map(convert));
},
create: function(bookmark) {
let info = {
title: bookmark.title || "",
};
// If url is NULL or missing, it will be a folder.
if (bookmark.url !== null) {
info.type = PlacesUtils.bookmarks.TYPE_BOOKMARK;
info.url = bookmark.url || "";
} else {
info.type = PlacesUtils.bookmarks.TYPE_FOLDER;
}
if (bookmark.index !== null) {
info.index = bookmark.index;
}
if (bookmark.parentId !== null) {
info.parentGuid = bookmark.parentId;
} else {
info.parentGuid = PlacesUtils.bookmarks.unfiledGuid;
}
try {
return PlacesUtils.bookmarks.insert(info).then(convert)
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
move: function(id, destination) {
let info = {
guid: id,
};
if (destination.parentId !== null) {
info.parentGuid = destination.parentId;
}
info.index = (destination.index === null) ?
PlacesUtils.bookmarks.DEFAULT_INDEX : destination.index;
try {
return PlacesUtils.bookmarks.update(info).then(convert)
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
update: function(id, changes) {
let info = {
guid: id,
};
if (changes.title !== null) {
info.title = changes.title;
}
if (changes.url !== null) {
info.url = changes.url;
}
try {
return PlacesUtils.bookmarks.update(info).then(convert)
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
remove: function(id) {
let info = {
guid: id,
};
// The API doesn't give you the old bookmark at the moment
try {
return PlacesUtils.bookmarks.remove(info, {preventRemovalOfNonEmptyFolders: true}).then(result => {})
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
removeTree: function(id) {
let info = {
guid: id,
};
try {
return PlacesUtils.bookmarks.remove(info).then(result => {})
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
onCreated: new SingletonEventManager(context, "bookmarks.onCreated", fire => {
let listener = (event, bookmark) => {
context.runSafe(fire, bookmark.id, bookmark);
};
observer.on("created", listener);
incrementListeners();
return () => {
observer.off("created", listener);
decrementListeners();
};
}).api(),
onRemoved: new SingletonEventManager(context, "bookmarks.onRemoved", fire => {
let listener = (event, data) => {
context.runSafe(fire, data.guid, data.info);
};
observer.on("removed", listener);
incrementListeners();
return () => {
observer.off("removed", listener);
decrementListeners();
};
}).api(),
onChanged: new SingletonEventManager(context, "bookmarks.onChanged", fire => {
let listener = (event, data) => {
context.runSafe(fire, data.guid, data.info);
};
observer.on("changed", listener);
incrementListeners();
return () => {
observer.off("changed", listener);
decrementListeners();
};
}).api(),
onMoved: new SingletonEventManager(context, "bookmarks.onMoved", fire => {
let listener = (event, data) => {
context.runSafe(fire, data.guid, data.info);
};
observer.on("moved", listener);
incrementListeners();
return () => {
observer.off("moved", listener);
decrementListeners();
};
}).api(),
},
};
});

View File

@ -0,0 +1,531 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
"@mozilla.org/inspector/dom-utils;1",
"inIDOMUtils");
Cu.import("resource://devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
var {
EventManager,
IconDetails,
} = ExtensionUtils;
const POPUP_PRELOAD_TIMEOUT_MS = 200;
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
function isAncestorOrSelf(target, node) {
for (; node; node = node.parentNode) {
if (node === target) {
return true;
}
}
return false;
}
// WeakMap[Extension -> BrowserAction]
var browserActionMap = new WeakMap();
// Responsible for the browser_action section of the manifest as well
// as the associated popup.
function BrowserAction(options, extension) {
this.extension = extension;
let widgetId = makeWidgetId(extension.id);
this.id = `${widgetId}-browser-action`;
this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
this.widget = null;
this.pendingPopup = null;
this.pendingPopupTimeout = null;
this.tabManager = TabManager.for(extension);
this.defaults = {
enabled: true,
title: options.default_title || extension.name,
badgeText: "",
badgeBackgroundColor: null,
icon: IconDetails.normalize({path: options.default_icon}, extension),
popup: options.default_popup || "",
};
this.browserStyle = options.browser_style || false;
if (options.browser_style === null) {
this.extension.logger.warn("Please specify whether you want browser_style " +
"or not in your browser_action options.");
}
this.tabContext = new TabContext(tab => Object.create(this.defaults),
extension);
EventEmitter.decorate(this);
}
BrowserAction.prototype = {
build() {
let widget = CustomizableUI.createWidget({
id: this.id,
viewId: this.viewId,
type: "view",
removable: true,
label: this.defaults.title || this.extension.name,
tooltiptext: this.defaults.title || "",
defaultArea: CustomizableUI.AREA_NAVBAR,
onBeforeCreated: document => {
let view = document.createElementNS(XUL_NS, "panelview");
view.id = this.viewId;
view.setAttribute("flex", "1");
document.getElementById("PanelUI-multiView").appendChild(view);
},
onDestroyed: document => {
let view = document.getElementById(this.viewId);
if (view) {
this.clearPopup();
CustomizableUI.hidePanelForNode(view);
view.remove();
}
},
onCreated: node => {
node.classList.add("badged-button");
node.classList.add("webextension-browser-action");
node.setAttribute("constrain-size", "true");
node.onmousedown = event => this.handleEvent(event);
this.updateButton(node, this.defaults);
},
onViewShowing: event => {
let document = event.target.ownerDocument;
let tabbrowser = document.defaultView.gBrowser;
let tab = tabbrowser.selectedTab;
let popupURL = this.getProperty(tab, "popup");
this.tabManager.addActiveTabPermission(tab);
// Popups are shown only if a popup URL is defined; otherwise
// a "click" event is dispatched. This is done for compatibility with the
// Google Chrome onClicked extension API.
if (popupURL) {
try {
let popup = this.getPopup(document.defaultView, popupURL);
event.detail.addBlocker(popup.attach(event.target));
} catch (e) {
Cu.reportError(e);
event.preventDefault();
}
} else {
// This isn't not a hack, but it seems to provide the correct behavior
// with the fewest complications.
event.preventDefault();
this.emit("click");
}
},
});
this.tabContext.on("tab-select", // eslint-disable-line mozilla/balanced-listeners
(evt, tab) => { this.updateWindow(tab.ownerGlobal); });
this.widget = widget;
},
/**
* Triggers this browser action for the given window, with the same effects as
* if it were clicked by a user.
*
* This has no effect if the browser action is disabled for, or not
* present in, the given window.
*/
triggerAction: Task.async(function* (window) {
let popup = ViewPopup.for(this.extension, window);
if (popup) {
popup.closePopup();
return;
}
let widget = this.widget.forWindow(window);
let tab = window.gBrowser.selectedTab;
if (!widget || !this.getProperty(tab, "enabled")) {
return;
}
// Popups are shown only if a popup URL is defined; otherwise
// a "click" event is dispatched. This is done for compatibility with the
// Google Chrome onClicked extension API.
if (this.getProperty(tab, "popup")) {
if (this.widget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
yield window.PanelUI.show();
}
let event = new window.CustomEvent("command", {bubbles: true, cancelable: true});
widget.node.dispatchEvent(event);
} else {
this.emit("click");
}
}),
handleEvent(event) {
let button = event.target;
let window = button.ownerDocument.defaultView;
switch (event.type) {
case "mousedown":
if (event.button == 0) {
// Begin pre-loading the browser for the popup, so it's more likely to
// be ready by the time we get a complete click.
let tab = window.gBrowser.selectedTab;
let popupURL = this.getProperty(tab, "popup");
let enabled = this.getProperty(tab, "enabled");
if (popupURL && enabled) {
// Add permission for the active tab so it will exist for the popup.
// Store the tab to revoke the permission during clearPopup.
if (!this.pendingPopup && !this.tabManager.hasActiveTabPermission(tab)) {
this.tabManager.addActiveTabPermission(tab);
this.tabToRevokeDuringClearPopup = tab;
}
this.pendingPopup = this.getPopup(window, popupURL);
window.addEventListener("mouseup", this, true);
} else {
this.clearPopup();
}
}
break;
case "mouseup":
if (event.button == 0) {
this.clearPopupTimeout();
// If we have a pending pre-loaded popup, cancel it after we've waited
// long enough that we can be relatively certain it won't be opening.
if (this.pendingPopup) {
let {node} = this.widget.forWindow(window);
if (isAncestorOrSelf(node, event.originalTarget)) {
this.pendingPopupTimeout = setTimeout(() => this.clearPopup(),
POPUP_PRELOAD_TIMEOUT_MS);
} else {
this.clearPopup();
}
}
}
break;
}
},
/**
* Returns a potentially pre-loaded popup for the given URL in the given
* window. If a matching pre-load popup already exists, returns that.
* Otherwise, initializes a new one.
*
* If a pre-load popup exists which does not match, it is destroyed before a
* new one is created.
*
* @param {Window} window
* The browser window in which to create the popup.
* @param {string} popupURL
* The URL to load into the popup.
* @returns {ViewPopup}
*/
getPopup(window, popupURL) {
this.clearPopupTimeout();
let {pendingPopup} = this;
this.pendingPopup = null;
if (pendingPopup) {
if (pendingPopup.window === window && pendingPopup.popupURL === popupURL) {
return pendingPopup;
}
pendingPopup.destroy();
}
let fixedWidth = this.widget.areaType == CustomizableUI.TYPE_MENU_PANEL;
return new ViewPopup(this.extension, window, popupURL, this.browserStyle, fixedWidth);
},
/**
* Clears any pending pre-loaded popup and related timeouts.
*/
clearPopup() {
this.clearPopupTimeout();
if (this.pendingPopup) {
if (this.tabToRevokeDuringClearPopup) {
this.tabManager.revokeActiveTabPermission(this.tabToRevokeDuringClearPopup);
this.tabToRevokeDuringClearPopup = null;
}
this.pendingPopup.destroy();
this.pendingPopup = null;
}
},
/**
* Clears any pending timeouts to clear stale, pre-loaded popups.
*/
clearPopupTimeout() {
if (this.pendingPopup) {
this.pendingPopup.window.removeEventListener("mouseup", this, true);
}
if (this.pendingPopupTimeout) {
clearTimeout(this.pendingPopupTimeout);
this.pendingPopupTimeout = null;
}
},
// Update the toolbar button |node| with the tab context data
// in |tabData|.
updateButton(node, tabData) {
let title = tabData.title || this.extension.name;
node.setAttribute("tooltiptext", title);
node.setAttribute("label", title);
if (tabData.badgeText) {
node.setAttribute("badge", tabData.badgeText);
} else {
node.removeAttribute("badge");
}
if (tabData.enabled) {
node.removeAttribute("disabled");
} else {
node.setAttribute("disabled", "true");
}
let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
"class", "toolbarbutton-badge");
if (badgeNode) {
let color = tabData.badgeBackgroundColor;
if (color) {
color = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`;
}
badgeNode.style.backgroundColor = color || "";
}
const LEGACY_CLASS = "toolbarbutton-legacy-addon";
node.classList.remove(LEGACY_CLASS);
let baseSize = 16;
let {icon, size} = IconDetails.getPreferredIcon(tabData.icon, this.extension, baseSize);
// If the best available icon size is not divisible by 16, check if we have
// an 18px icon to fall back to, and trim off the padding instead.
if (size % 16 && !icon.endsWith(".svg")) {
let result = IconDetails.getPreferredIcon(tabData.icon, this.extension, 18);
if (result.size % 18 == 0) {
baseSize = 18;
icon = result.icon;
node.classList.add(LEGACY_CLASS);
}
}
// These URLs should already be properly escaped, but make doubly sure CSS
// string escape characters are escaped here, since they could lead to a
// sandbox break.
let escape = str => str.replace(/[\\\s"]/g, encodeURIComponent);
let getIcon = size => escape(IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
node.setAttribute("style", `
--webextension-menupanel-image: url("${getIcon(32)}");
--webextension-menupanel-image-2x: url("${getIcon(64)}");
--webextension-toolbar-image: url("${escape(icon)}");
--webextension-toolbar-image-2x: url("${getIcon(baseSize * 2)}");
`);
},
// Update the toolbar button for a given window.
updateWindow(window) {
let widget = this.widget.forWindow(window);
if (widget) {
let tab = window.gBrowser.selectedTab;
this.updateButton(widget.node, this.tabContext.get(tab));
}
},
// 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.ownerGlobal);
}
} else {
for (let window of WindowListManager.browserWindows()) {
this.updateWindow(window);
}
}
},
// tab is allowed to be null.
// prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
setProperty(tab, prop, value) {
if (tab == null) {
this.defaults[prop] = value;
} else if (value != null) {
this.tabContext.get(tab)[prop] = value;
} else {
delete this.tabContext.get(tab)[prop];
}
this.updateOnChange(tab);
},
// tab is allowed to be null.
// prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
getProperty(tab, prop) {
if (tab == null) {
return this.defaults[prop];
}
return this.tabContext.get(tab)[prop];
},
shutdown() {
this.tabContext.shutdown();
CustomizableUI.destroyWidget(this.id);
},
};
BrowserAction.for = (extension) => {
return browserActionMap.get(extension);
};
global.browserActionFor = BrowserAction.for;
/* eslint-disable mozilla/balanced-listeners */
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);
}
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("browserAction", "addon_parent", context => {
let {extension} = context;
return {
browserAction: {
onClicked: new EventManager(context, "browserAction.onClicked", fire => {
let listener = () => {
let tab = TabManager.activeTab;
fire(TabManager.convert(extension, tab));
};
BrowserAction.for(extension).on("click", listener);
return () => {
BrowserAction.for(extension).off("click", listener);
};
}).api(),
enable: function(tabId) {
let tab = tabId !== null ? TabManager.getTab(tabId, context) : null;
BrowserAction.for(extension).setProperty(tab, "enabled", true);
},
disable: function(tabId) {
let tab = tabId !== null ? TabManager.getTab(tabId, context) : null;
BrowserAction.for(extension).setProperty(tab, "enabled", false);
},
setTitle: function(details) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
let title = details.title;
// Clear the tab-specific title when given a null string.
if (tab && title == "") {
title = null;
}
BrowserAction.for(extension).setProperty(tab, "title", title);
},
getTitle: function(details) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
let title = BrowserAction.for(extension).getProperty(tab, "title");
return Promise.resolve(title);
},
setIcon: function(details) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
let icon = IconDetails.normalize(details, extension, context);
BrowserAction.for(extension).setProperty(tab, "icon", icon);
},
setBadgeText: function(details) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
BrowserAction.for(extension).setProperty(tab, "badgeText", details.text);
},
getBadgeText: function(details) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
let text = BrowserAction.for(extension).getProperty(tab, "badgeText");
return Promise.resolve(text);
},
setPopup: function(details) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
// Note: Chrome resolves arguments to setIcon relative to the calling
// context, but resolves arguments to setPopup relative to the extension
// root.
// For internal consistency, we currently resolve both relative to the
// calling context.
let url = details.popup && context.uri.resolve(details.popup);
if (url && !context.checkLoadURL(url)) {
return Promise.reject({message: `Access denied for URL ${url}`});
}
BrowserAction.for(extension).setProperty(tab, "popup", url);
},
getPopup: function(details) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
let popup = BrowserAction.for(extension).getProperty(tab, "popup");
return Promise.resolve(popup);
},
setBadgeBackgroundColor: function(details) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
let color = details.color;
if (!Array.isArray(color)) {
let col = DOMUtils.colorToRGBA(color);
color = col && [col.r, col.g, col.b, Math.round(col.a * 255)];
}
BrowserAction.for(extension).setProperty(tab, "badgeBackgroundColor", color);
},
getBadgeBackgroundColor: function(details, callback) {
let tab = details.tabId !== null ? TabManager.getTab(details.tabId, context) : null;
let color = BrowserAction.for(extension).getProperty(tab, "badgeBackgroundColor");
return Promise.resolve(color || [0xd9, 0, 0, 255]);
},
},
};
});

View File

@ -0,0 +1,158 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
// If id is not specified for an item we use an integer.
// This ID need only be unique within a single addon. Since all addon code that
// can use this API runs in the same process, this local variable suffices.
var gNextMenuItemID = 0;
// Map[Extension -> Map[string or id, ContextMenusClickPropHandler]]
var gPropHandlers = new Map();
// The contextMenus API supports an "onclick" attribute in the create/update
// methods to register a callback. This class manages these onclick properties.
class ContextMenusClickPropHandler {
constructor(context) {
this.context = context;
// Map[string or integer -> callback]
this.onclickMap = new Map();
this.dispatchEvent = this.dispatchEvent.bind(this);
}
// A listener on contextMenus.onClicked that forwards the event to the only
// listener, if any.
dispatchEvent(info, tab) {
let onclick = this.onclickMap.get(info.menuItemId);
if (onclick) {
// No need for runSafe or anything because we are already being run inside
// an event handler -- the event is just being forwarded to the actual
// handler.
onclick(info, tab);
}
}
// Sets the `onclick` handler for the given menu item.
// The `onclick` function MUST be owned by `this.context`.
setListener(id, onclick) {
if (this.onclickMap.size === 0) {
this.context.childManager.getParentEvent("contextMenus.onClicked").addListener(this.dispatchEvent);
this.context.callOnClose(this);
}
this.onclickMap.set(id, onclick);
let propHandlerMap = gPropHandlers.get(this.context.extension);
if (!propHandlerMap) {
propHandlerMap = new Map();
} else {
// If the current callback was created in a different context, remove it
// from the other context.
let propHandler = propHandlerMap.get(id);
if (propHandler && propHandler !== this) {
propHandler.unsetListener(id);
}
}
propHandlerMap.set(id, this);
gPropHandlers.set(this.context.extension, propHandlerMap);
}
// Deletes the `onclick` handler for the given menu item.
// The `onclick` function MUST be owned by `this.context`.
unsetListener(id) {
if (!this.onclickMap.delete(id)) {
return;
}
if (this.onclickMap.size === 0) {
this.context.childManager.getParentEvent("contextMenus.onClicked").removeListener(this.dispatchEvent);
this.context.forgetOnClose(this);
}
let propHandlerMap = gPropHandlers.get(this.context.extension);
propHandlerMap.delete(id);
if (propHandlerMap.size === 0) {
gPropHandlers.delete(this.context.extension);
}
}
// Deletes the `onclick` handler for the given menu item, if any, regardless
// of the context where it was created.
unsetListenerFromAnyContext(id) {
let propHandlerMap = gPropHandlers.get(this.context.extension);
let propHandler = propHandlerMap && propHandlerMap.get(id);
if (propHandler) {
propHandler.unsetListener(id);
}
}
// Remove all `onclick` handlers of the extension.
deleteAllListenersFromExtension() {
let propHandlerMap = gPropHandlers.get(this.context.extension);
if (propHandlerMap) {
for (let [id, propHandler] of propHandlerMap) {
propHandler.unsetListener(id);
}
}
}
// Removes all `onclick` handlers from this context.
close() {
for (let id of this.onclickMap.keys()) {
this.unsetListener(id);
}
}
}
extensions.registerSchemaAPI("contextMenus", "addon_child", context => {
let onClickedProp = new ContextMenusClickPropHandler(context);
return {
contextMenus: {
create(createProperties, callback) {
if (createProperties.id === null) {
createProperties.id = ++gNextMenuItemID;
}
let {onclick} = createProperties;
delete createProperties.onclick;
context.childManager.callParentAsyncFunction("contextMenus.createInternal", [
createProperties,
]).then(() => {
if (onclick) {
onClickedProp.setListener(createProperties.id, onclick);
}
if (callback) {
callback();
}
});
return createProperties.id;
},
update(id, updateProperties) {
let {onclick} = updateProperties;
delete updateProperties.onclick;
return context.childManager.callParentAsyncFunction("contextMenus.update", [
id,
updateProperties,
]).then(() => {
if (onclick) {
onClickedProp.setListener(id, onclick);
} else if (onclick === null) {
onClickedProp.unsetListenerFromAnyContext(id);
}
// else onclick is not set so it should not be changed.
});
},
remove(id) {
onClickedProp.unsetListenerFromAnyContext(id);
return context.childManager.callParentAsyncFunction("contextMenus.remove", [
id,
]);
},
removeAll() {
onClickedProp.deleteAllListenersFromExtension();
return context.childManager.callParentAsyncFunction("contextMenus.removeAll", []);
},
},
};
});

View File

@ -0,0 +1,32 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
runSafeSyncWithoutClone,
SingletonEventManager,
} = ExtensionUtils;
extensions.registerSchemaAPI("omnibox", "addon_child", context => {
return {
omnibox: {
onInputChanged: new SingletonEventManager(context, "omnibox.onInputChanged", fire => {
let listener = (text, id) => {
runSafeSyncWithoutClone(fire, text, suggestions => {
// TODO: Switch to using callParentFunctionNoReturn once bug 1314903 is fixed.
context.childManager.callParentAsyncFunction("omnibox_internal.addSuggestions", [
id,
suggestions,
]);
});
};
context.childManager.getParentEvent("omnibox_internal.onInputChanged").addListener(listener);
return () => {
context.childManager.getParentEvent("omnibox_internal.onInputChanged").removeListener(listener);
};
}).api(),
},
};
});

View File

@ -0,0 +1,35 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
extensions.registerSchemaAPI("tabs", "addon_child", context => {
return {
tabs: {
connect: function(tabId, connectInfo) {
let name = "";
if (connectInfo && connectInfo.name !== null) {
name = connectInfo.name;
}
let recipient = {
extensionId: context.extension.id,
tabId,
};
if (connectInfo && connectInfo.frameId !== null) {
recipient.frameId = connectInfo.frameId;
}
return context.messenger.connect(context.messageManager, name, recipient);
},
sendMessage: function(tabId, message, options, responseCallback) {
let recipient = {
extensionId: context.extension.id,
tabId: tabId,
};
if (options && options.frameId !== null) {
recipient.frameId = options.frameId;
}
return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
},
},
};
});

View File

@ -0,0 +1,264 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
PlatformInfo,
} = ExtensionUtils;
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// WeakMap[Extension -> CommandList]
var commandsMap = new WeakMap();
function CommandList(manifest, extension) {
this.extension = extension;
this.id = makeWidgetId(extension.id);
this.windowOpenListener = null;
// Map[{String} commandName -> {Object} commandProperties]
this.commands = this.loadCommandsFromManifest(manifest);
// WeakMap[Window -> <xul:keyset>]
this.keysetsMap = new WeakMap();
this.register();
EventEmitter.decorate(this);
}
CommandList.prototype = {
/**
* Registers the commands to all open windows and to any which
* are later created.
*/
register() {
for (let window of WindowListManager.browserWindows()) {
this.registerKeysToDocument(window);
}
this.windowOpenListener = (window) => {
if (!this.keysetsMap.has(window)) {
this.registerKeysToDocument(window);
}
};
WindowListManager.addOpenListener(this.windowOpenListener);
},
/**
* Unregisters the commands from all open windows and stops commands
* from being registered to windows which are later created.
*/
unregister() {
for (let window of WindowListManager.browserWindows()) {
if (this.keysetsMap.has(window)) {
this.keysetsMap.get(window).remove();
}
}
WindowListManager.removeOpenListener(this.windowOpenListener);
},
/**
* Creates a Map from commands for each command in the manifest.commands object.
*
* @param {Object} manifest The manifest JSON object.
* @returns {Map<string, object>}
*/
loadCommandsFromManifest(manifest) {
let commands = new Map();
// For Windows, chrome.runtime expects 'win' while chrome.commands
// expects 'windows'. We can special case this for now.
let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
for (let [name, command] of Object.entries(manifest.commands)) {
let suggested_key = command.suggested_key || {};
let shortcut = suggested_key[os] || suggested_key.default;
shortcut = shortcut ? shortcut.replace(/\s+/g, "") : null;
commands.set(name, {
description: command.description,
shortcut,
});
}
return commands;
},
/**
* Registers the commands to a document.
* @param {ChromeWindow} window The XUL window to insert the Keyset.
*/
registerKeysToDocument(window) {
let doc = window.document;
let keyset = doc.createElementNS(XUL_NS, "keyset");
keyset.id = `ext-keyset-id-${this.id}`;
this.commands.forEach((command, name) => {
if (command.shortcut) {
let keyElement = this.buildKey(doc, name, command.shortcut);
keyset.appendChild(keyElement);
}
});
doc.documentElement.appendChild(keyset);
this.keysetsMap.set(window, keyset);
},
/**
* Builds a XUL Key element and attaches an onCommand listener which
* emits a command event with the provided name when fired.
*
* @param {Document} doc The XUL document.
* @param {string} name The name of the command.
* @param {string} shortcut The shortcut provided in the manifest.
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/key
*
* @returns {Document} The newly created Key element.
*/
buildKey(doc, name, shortcut) {
let keyElement = this.buildKeyFromShortcut(doc, shortcut);
// We need to have the attribute "oncommand" for the "command" listener to fire,
// and it is currently ignored when set to the empty string.
keyElement.setAttribute("oncommand", "//");
/* eslint-disable mozilla/balanced-listeners */
// We remove all references to the key elements when the extension is shutdown,
// therefore the listeners for these elements will be garbage collected.
keyElement.addEventListener("command", (event) => {
if (name == "_execute_page_action") {
let win = event.target.ownerDocument.defaultView;
pageActionFor(this.extension).triggerAction(win);
} else if (name == "_execute_browser_action") {
let win = event.target.ownerDocument.defaultView;
browserActionFor(this.extension).triggerAction(win);
} else {
TabManager.for(this.extension)
.addActiveTabPermission(TabManager.activeTab);
this.emit("command", name);
}
});
/* eslint-enable mozilla/balanced-listeners */
return keyElement;
},
/**
* Builds a XUL Key element from the provided shortcut.
*
* @param {Document} doc The XUL document.
* @param {string} shortcut The shortcut provided in the manifest.
*
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/key
* @returns {Document} The newly created Key element.
*/
buildKeyFromShortcut(doc, shortcut) {
let keyElement = doc.createElementNS(XUL_NS, "key");
let parts = shortcut.split("+");
// The key is always the last element.
let chromeKey = parts.pop();
// The modifiers are the remaining elements.
keyElement.setAttribute("modifiers", this.getModifiersAttribute(parts));
if (/^[A-Z]$/.test(chromeKey)) {
// We use the key attribute for all single digits and characters.
keyElement.setAttribute("key", chromeKey);
} else {
keyElement.setAttribute("keycode", this.getKeycodeAttribute(chromeKey));
keyElement.setAttribute("event", "keydown");
}
return keyElement;
},
/**
* Determines the corresponding XUL keycode from the given chrome key.
*
* For example:
*
* input | output
* ---------------------------------------
* "PageUP" | "VK_PAGE_UP"
* "Delete" | "VK_DELETE"
*
* @param {string} chromeKey The chrome key (e.g. "PageUp", "Space", ...)
* @returns {string} The constructed value for the Key's 'keycode' attribute.
*/
getKeycodeAttribute(chromeKey) {
if (/[0-9]/.test(chromeKey)) {
return `VK_${chromeKey}`;
}
return `VK${chromeKey.replace(/([A-Z])/g, "_$&").toUpperCase()}`;
},
/**
* Determines the corresponding XUL modifiers from the chrome modifiers.
*
* For example:
*
* input | output
* ---------------------------------------
* ["Ctrl", "Shift"] | "accel shift"
* ["MacCtrl"] | "control"
*
* @param {Array} chromeModifiers The array of chrome modifiers.
* @returns {string} The constructed value for the Key's 'modifiers' attribute.
*/
getModifiersAttribute(chromeModifiers) {
let modifiersMap = {
"Alt": "alt",
"Command": "accel",
"Ctrl": "accel",
"MacCtrl": "control",
"Shift": "shift",
};
return Array.from(chromeModifiers, modifier => {
return modifiersMap[modifier];
}).join(" ");
},
};
/* eslint-disable mozilla/balanced-listeners */
extensions.on("manifest_commands", (type, directive, extension, manifest) => {
commandsMap.set(extension, new CommandList(manifest, extension));
});
extensions.on("shutdown", (type, extension) => {
let commandsList = commandsMap.get(extension);
if (commandsList) {
commandsList.unregister();
commandsMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("commands", "addon_parent", context => {
let {extension} = context;
return {
commands: {
getAll() {
let commands = commandsMap.get(extension).commands;
return Promise.resolve(Array.from(commands, ([name, command]) => {
return ({
name,
description: command.description,
shortcut: command.shortcut,
});
}));
},
onCommand: new EventManager(context, "commands.onCommand", fire => {
let listener = (eventName, commandName) => {
fire(commandName);
};
commandsMap.get(extension).on("command", listener);
return () => {
commandsMap.get(extension).off("command", listener);
};
}).api(),
},
};
});

View File

@ -0,0 +1,537 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/MatchPattern.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var {
EventManager,
ExtensionError,
IconDetails,
} = ExtensionUtils;
// Map[Extension -> Map[ID -> MenuItem]]
// Note: we want to enumerate all the menu items so
// this cannot be a weak map.
var gContextMenuMap = new Map();
// Map[Extension -> MenuItem]
var gRootItems = new Map();
// If id is not specified for an item we use an integer.
var gNextMenuItemID = 0;
// Used to assign unique names to radio groups.
var gNextRadioGroupID = 0;
// The max length of a menu item's label.
var gMaxLabelLength = 64;
// When a new contextMenu is opened, this function is called and
// we populate the |xulMenu| with all the items from extensions
// to be displayed. We always clear all the items again when
// popuphidden fires.
var gMenuBuilder = {
build: function(contextData) {
let xulMenu = contextData.menu;
xulMenu.addEventListener("popuphidden", this);
this.xulMenu = xulMenu;
for (let [, root] of gRootItems) {
let rootElement = this.buildElementWithChildren(root, contextData);
if (!rootElement.firstChild || !rootElement.firstChild.childNodes.length) {
// If the root has no visible children, there is no reason to show
// the root menu item itself either.
continue;
}
rootElement.setAttribute("ext-type", "top-level-menu");
rootElement = this.removeTopLevelMenuIfNeeded(rootElement);
// Display the extension icon on the root element.
if (root.extension.manifest.icons) {
let parentWindow = contextData.menu.ownerGlobal;
let extension = root.extension;
let {icon} = IconDetails.getPreferredIcon(extension.manifest.icons, extension,
16 * parentWindow.devicePixelRatio);
// The extension icons in the manifest are not pre-resolved, since
// they're sometimes used by the add-on manager when the extension is
// not enabled, and its URLs are not resolvable.
let resolvedURL = root.extension.baseURI.resolve(icon);
if (rootElement.localName == "menu") {
rootElement.setAttribute("class", "menu-iconic");
} else if (rootElement.localName == "menuitem") {
rootElement.setAttribute("class", "menuitem-iconic");
}
rootElement.setAttribute("image", resolvedURL);
}
xulMenu.appendChild(rootElement);
this.itemsToCleanUp.add(rootElement);
}
},
buildElementWithChildren(item, contextData) {
let element = this.buildSingleElement(item, contextData);
let groupName;
for (let child of item.children) {
if (child.type == "radio" && !child.groupName) {
if (!groupName) {
groupName = `webext-radio-group-${gNextRadioGroupID++}`;
}
child.groupName = groupName;
} else {
groupName = null;
}
if (child.enabledForContext(contextData)) {
let childElement = this.buildElementWithChildren(child, contextData);
// Here element must be a menu element and its first child
// is a menupopup, we have to append its children to this
// menupopup.
element.firstChild.appendChild(childElement);
}
}
return element;
},
removeTopLevelMenuIfNeeded(element) {
// If there is only one visible top level element we don't need the
// root menu element for the extension.
let menuPopup = element.firstChild;
if (menuPopup && menuPopup.childNodes.length == 1) {
let onlyChild = menuPopup.firstChild;
onlyChild.remove();
return onlyChild;
}
return element;
},
buildSingleElement(item, contextData) {
let doc = contextData.menu.ownerDocument;
let element;
if (item.children.length > 0) {
element = this.createMenuElement(doc, item);
} else if (item.type == "separator") {
element = doc.createElement("menuseparator");
} else {
element = doc.createElement("menuitem");
}
return this.customizeElement(element, item, contextData);
},
createMenuElement(doc, item) {
let element = doc.createElement("menu");
// Menu elements need to have a menupopup child for its menu items.
let menupopup = doc.createElement("menupopup");
element.appendChild(menupopup);
return element;
},
customizeElement(element, item, contextData) {
let label = item.title;
if (label) {
if (contextData.isTextSelected && label.indexOf("%s") > -1) {
let selection = contextData.selectionText;
// The rendering engine will truncate the title if it's longer than 64 characters.
// But if it makes sense let's try truncate selection text only, to handle cases like
// 'look up "%s" in MyDictionary' more elegantly.
let maxSelectionLength = gMaxLabelLength - label.length + 2;
if (maxSelectionLength > 4) {
selection = selection.substring(0, maxSelectionLength - 3) + "...";
}
label = label.replace(/%s/g, selection);
}
element.setAttribute("label", label);
}
if (item.type == "checkbox") {
element.setAttribute("type", "checkbox");
if (item.checked) {
element.setAttribute("checked", "true");
}
} else if (item.type == "radio") {
element.setAttribute("type", "radio");
element.setAttribute("name", item.groupName);
if (item.checked) {
element.setAttribute("checked", "true");
}
}
if (!item.enabled) {
element.setAttribute("disabled", "true");
}
element.addEventListener("command", event => { // eslint-disable-line mozilla/balanced-listeners
if (event.target !== event.currentTarget) {
return;
}
const wasChecked = item.checked;
if (item.type == "checkbox") {
item.checked = !item.checked;
} else if (item.type == "radio") {
// Deselect all radio items in the current radio group.
for (let child of item.parent.children) {
if (child.type == "radio" && child.groupName == item.groupName) {
child.checked = false;
}
}
// Select the clicked radio item.
item.checked = true;
}
item.tabManager.addActiveTabPermission();
let tab = item.tabManager.convert(contextData.tab);
let info = item.getClickInfo(contextData, wasChecked);
item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
});
return element;
},
handleEvent: function(event) {
if (this.xulMenu != event.target || event.type != "popuphidden") {
return;
}
delete this.xulMenu;
let target = event.target;
target.removeEventListener("popuphidden", this);
for (let item of this.itemsToCleanUp) {
item.remove();
}
this.itemsToCleanUp.clear();
},
itemsToCleanUp: new Set(),
};
function contextMenuObserver(subject, topic, data) {
subject = subject.wrappedJSObject;
gMenuBuilder.build(subject);
}
function getContexts(contextData) {
let contexts = new Set(["all"]);
if (contextData.inFrame) {
contexts.add("frame");
}
if (contextData.isTextSelected) {
contexts.add("selection");
}
if (contextData.onLink) {
contexts.add("link");
}
if (contextData.onEditableArea) {
contexts.add("editable");
}
if (contextData.onImage) {
contexts.add("image");
}
if (contextData.onVideo) {
contexts.add("video");
}
if (contextData.onAudio) {
contexts.add("audio");
}
if (contexts.size == 1) {
contexts.add("page");
}
return contexts;
}
function MenuItem(extension, createProperties, isRoot = false) {
this.extension = extension;
this.children = [];
this.parent = null;
this.tabManager = TabManager.for(extension);
this.setDefaults();
this.setProps(createProperties);
if (!this.hasOwnProperty("_id")) {
this.id = gNextMenuItemID++;
}
// If the item is not the root and has no parent
// it must be a child of the root.
if (!isRoot && !this.parent) {
this.root.addChild(this);
}
}
MenuItem.prototype = {
setProps(createProperties) {
for (let propName in createProperties) {
if (createProperties[propName] === null) {
// Omitted optional argument.
continue;
}
this[propName] = createProperties[propName];
}
if (createProperties.documentUrlPatterns != null) {
this.documentUrlMatchPattern = new MatchPattern(this.documentUrlPatterns);
}
if (createProperties.targetUrlPatterns != null) {
this.targetUrlMatchPattern = new MatchPattern(this.targetUrlPatterns);
}
},
setDefaults() {
this.setProps({
type: "normal",
checked: false,
contexts: ["all"],
enabled: true,
});
},
set id(id) {
if (this.hasOwnProperty("_id")) {
throw new Error("Id of a MenuItem cannot be changed");
}
let isIdUsed = gContextMenuMap.get(this.extension).has(id);
if (isIdUsed) {
throw new Error("Id already exists");
}
this._id = id;
},
get id() {
return this._id;
},
ensureValidParentId(parentId) {
if (parentId === undefined) {
return;
}
let menuMap = gContextMenuMap.get(this.extension);
if (!menuMap.has(parentId)) {
throw new Error("Could not find any MenuItem with id: " + parentId);
}
for (let item = menuMap.get(parentId); item; item = item.parent) {
if (item === this) {
throw new ExtensionError("MenuItem cannot be an ancestor (or self) of its new parent.");
}
}
},
set parentId(parentId) {
this.ensureValidParentId(parentId);
if (this.parent) {
this.parent.detachChild(this);
}
if (parentId === undefined) {
this.root.addChild(this);
} else {
let menuMap = gContextMenuMap.get(this.extension);
menuMap.get(parentId).addChild(this);
}
},
get parentId() {
return this.parent ? this.parent.id : undefined;
},
addChild(child) {
if (child.parent) {
throw new Error("Child MenuItem already has a parent.");
}
this.children.push(child);
child.parent = this;
},
detachChild(child) {
let idx = this.children.indexOf(child);
if (idx < 0) {
throw new Error("Child MenuItem not found, it cannot be removed.");
}
this.children.splice(idx, 1);
child.parent = null;
},
get root() {
let extension = this.extension;
if (!gRootItems.has(extension)) {
let root = new MenuItem(extension,
{title: extension.name},
/* isRoot = */ true);
gRootItems.set(extension, root);
}
return gRootItems.get(extension);
},
remove() {
if (this.parent) {
this.parent.detachChild(this);
}
let children = this.children.slice(0);
for (let child of children) {
child.remove();
}
let menuMap = gContextMenuMap.get(this.extension);
menuMap.delete(this.id);
if (this.root == this) {
gRootItems.delete(this.extension);
}
},
getClickInfo(contextData, wasChecked) {
let mediaType;
if (contextData.onVideo) {
mediaType = "video";
}
if (contextData.onAudio) {
mediaType = "audio";
}
if (contextData.onImage) {
mediaType = "image";
}
let info = {
menuItemId: this.id,
editable: contextData.onEditableArea,
};
function setIfDefined(argName, value) {
if (value !== undefined) {
info[argName] = value;
}
}
setIfDefined("parentMenuItemId", this.parentId);
setIfDefined("mediaType", mediaType);
setIfDefined("linkUrl", contextData.linkUrl);
setIfDefined("srcUrl", contextData.srcUrl);
setIfDefined("pageUrl", contextData.pageUrl);
setIfDefined("frameUrl", contextData.frameUrl);
setIfDefined("selectionText", contextData.selectionText);
if ((this.type === "checkbox") || (this.type === "radio")) {
info.checked = this.checked;
info.wasChecked = wasChecked;
}
return info;
},
enabledForContext(contextData) {
let contexts = getContexts(contextData);
if (!this.contexts.some(n => contexts.has(n))) {
return false;
}
let docPattern = this.documentUrlMatchPattern;
let pageURI = Services.io.newURI(contextData.pageUrl, null, null);
if (docPattern && !docPattern.matches(pageURI)) {
return false;
}
let targetPattern = this.targetUrlMatchPattern;
if (targetPattern) {
let targetUrls = [];
if (contextData.onImage || contextData.onAudio || contextData.onVideo) {
// TODO: double check if srcUrl is always set when we need it
targetUrls.push(contextData.srcUrl);
}
if (contextData.onLink) {
targetUrls.push(contextData.linkUrl);
}
if (!targetUrls.some(targetUrl => targetPattern.matches(NetUtil.newURI(targetUrl)))) {
return false;
}
}
return true;
},
};
var gExtensionCount = 0;
/* eslint-disable mozilla/balanced-listeners */
extensions.on("startup", (type, extension) => {
gContextMenuMap.set(extension, new Map());
if (++gExtensionCount == 1) {
Services.obs.addObserver(contextMenuObserver,
"on-build-contextmenu",
false);
}
});
extensions.on("shutdown", (type, extension) => {
gContextMenuMap.delete(extension);
gRootItems.delete(extension);
if (--gExtensionCount == 0) {
Services.obs.removeObserver(contextMenuObserver,
"on-build-contextmenu");
}
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("contextMenus", "addon_parent", context => {
let {extension} = context;
return {
contextMenus: {
createInternal: function(createProperties) {
// Note that the id is required by the schema. If the addon did not set
// it, the implementation of contextMenus.create in the child should
// have added it.
let menuItem = new MenuItem(extension, createProperties);
gContextMenuMap.get(extension).set(menuItem.id, menuItem);
},
update: function(id, updateProperties) {
let menuItem = gContextMenuMap.get(extension).get(id);
if (menuItem) {
menuItem.setProps(updateProperties);
}
},
remove: function(id) {
let menuItem = gContextMenuMap.get(extension).get(id);
if (menuItem) {
menuItem.remove();
}
},
removeAll: function() {
let root = gRootItems.get(extension);
if (root) {
root.remove();
}
},
onClicked: new EventManager(context, "contextMenus.onClicked", fire => {
let listener = (event, info, tab) => {
fire(info, tab);
};
extension.on("webext-contextmenu-menuitem-click", listener);
return () => {
extension.off("webext-contextmenu-menuitem-click", listener);
};
}).api(),
},
};
});

View File

@ -0,0 +1,26 @@
"use strict";
/* eslint-disable mozilla/balanced-listeners */
extensions.on("uninstall", (msg, extension) => {
if (extension.uninstallURL) {
let browser = WindowManager.topWindow.gBrowser;
browser.addTab(extension.uninstallURL, {relatedToCurrent: true});
}
});
global.openOptionsPage = (extension) => {
let window = WindowManager.topWindow;
if (!window) {
return Promise.reject({message: "No browser window available"});
}
if (extension.manifest.options_ui.open_in_tab) {
window.switchToTabHavingURI(extension.manifest.options_ui.page, true);
return Promise.resolve();
}
let viewId = `addons://detail/${encodeURIComponent(extension.id)}/preferences`;
return window.BrowserOpenAddonsMgr(viewId);
};

View File

@ -0,0 +1,246 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
const {
normalizeTime,
SingletonEventManager,
} = ExtensionUtils;
let nsINavHistoryService = Ci.nsINavHistoryService;
const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
["link", nsINavHistoryService.TRANSITION_LINK],
["typed", nsINavHistoryService.TRANSITION_TYPED],
["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK],
["auto_subframe", nsINavHistoryService.TRANSITION_EMBED],
["manual_subframe", nsINavHistoryService.TRANSITION_FRAMED_LINK],
]);
let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map();
for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) {
TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition);
}
function getTransitionType(transition) {
// cannot set a default value for the transition argument as the framework sets it to null
transition = transition || "link";
let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition);
if (!transitionType) {
throw new Error(`|${transition}| is not a supported transition for history`);
}
return transitionType;
}
function getTransition(transitionType) {
return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link";
}
/*
* Converts a nsINavHistoryResultNode into a HistoryItem
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
*/
function convertNodeToHistoryItem(node) {
return {
id: node.pageGuid,
url: node.uri,
title: node.title,
lastVisitTime: PlacesUtils.toDate(node.time).getTime(),
visitCount: node.accessCount,
};
}
/*
* Converts a nsINavHistoryResultNode into a VisitItem
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
*/
function convertNodeToVisitItem(node) {
return {
id: node.pageGuid,
visitId: node.visitId,
visitTime: PlacesUtils.toDate(node.time).getTime(),
referringVisitId: node.fromVisitId,
transition: getTransition(node.visitType),
};
}
/*
* Converts a nsINavHistoryContainerResultNode into an array of objects
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryContainerResultNode
*/
function convertNavHistoryContainerResultNode(container, converter) {
let results = [];
container.containerOpen = true;
for (let i = 0; i < container.childCount; i++) {
let node = container.getChild(i);
results.push(converter(node));
}
container.containerOpen = false;
return results;
}
var _observer;
function getObserver() {
if (!_observer) {
_observer = {
onDeleteURI: function(uri, guid, reason) {
this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
},
onVisit: function(uri, visitId, time, sessionId, referringId, transitionType, guid, hidden, visitCount, typed) {
let data = {
id: guid,
url: uri.spec,
title: "",
lastVisitTime: time / 1000, // time from Places is microseconds,
visitCount,
typedCount: typed,
};
this.emit("visited", data);
},
onBeginUpdateBatch: function() {},
onEndUpdateBatch: function() {},
onTitleChanged: function() {},
onClearHistory: function() {
this.emit("visitRemoved", {allHistory: true, urls: []});
},
onPageChanged: function() {},
onFrecencyChanged: function() {},
onManyFrecenciesChanged: function() {},
onDeleteVisits: function(uri, time, guid, reason) {
this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
},
};
EventEmitter.decorate(_observer);
PlacesUtils.history.addObserver(_observer, false);
}
return _observer;
}
extensions.registerSchemaAPI("history", "addon_parent", context => {
return {
history: {
addUrl: function(details) {
let transition, date;
try {
transition = getTransitionType(details.transition);
} catch (error) {
return Promise.reject({message: error.message});
}
if (details.visitTime) {
date = normalizeTime(details.visitTime);
}
let pageInfo = {
title: details.title,
url: details.url,
visits: [
{
transition,
date,
},
],
};
try {
return PlacesUtils.history.insert(pageInfo).then(() => undefined);
} catch (error) {
return Promise.reject({message: error.message});
}
},
deleteAll: function() {
return PlacesUtils.history.clear();
},
deleteRange: function(filter) {
let newFilter = {
beginDate: normalizeTime(filter.startTime),
endDate: normalizeTime(filter.endTime),
};
// History.removeVisitsByFilter returns a boolean, but our API should return nothing
return PlacesUtils.history.removeVisitsByFilter(newFilter).then(() => undefined);
},
deleteUrl: function(details) {
let url = details.url;
// History.remove returns a boolean, but our API should return nothing
return PlacesUtils.history.remove(url).then(() => undefined);
},
search: function(query) {
let beginTime = (query.startTime == null) ?
PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000) :
PlacesUtils.toPRTime(normalizeTime(query.startTime));
let endTime = (query.endTime == null) ?
Number.MAX_VALUE :
PlacesUtils.toPRTime(normalizeTime(query.endTime));
if (beginTime > endTime) {
return Promise.reject({message: "The startTime cannot be after the endTime"});
}
let options = PlacesUtils.history.getNewQueryOptions();
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
options.maxResults = query.maxResults || 100;
let historyQuery = PlacesUtils.history.getNewQuery();
historyQuery.searchTerms = query.text;
historyQuery.beginTime = beginTime;
historyQuery.endTime = endTime;
let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToHistoryItem);
return Promise.resolve(results);
},
getVisits: function(details) {
let url = details.url;
if (!url) {
return Promise.reject({message: "A URL must be provided for getVisits"});
}
let options = PlacesUtils.history.getNewQueryOptions();
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
options.resultType = options.RESULTS_AS_VISIT;
let historyQuery = PlacesUtils.history.getNewQuery();
historyQuery.uri = NetUtil.newURI(url);
let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
return Promise.resolve(results);
},
onVisited: new SingletonEventManager(context, "history.onVisited", fire => {
let listener = (event, data) => {
context.runSafe(fire, data);
};
getObserver().on("visited", listener);
return () => {
getObserver().off("visited", listener);
};
}).api(),
onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => {
let listener = (event, data) => {
context.runSafe(fire, data);
};
getObserver().on("visitRemoved", listener);
return () => {
getObserver().off("visitRemoved", listener);
};
}).api(),
},
};
});

View File

@ -0,0 +1,104 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
"resource://gre/modules/ExtensionSearchHandler.jsm");
var {
SingletonEventManager,
} = ExtensionUtils;
// WeakMap[extension -> keyword]
let gKeywordMap = new WeakMap();
/* eslint-disable mozilla/balanced-listeners */
extensions.on("manifest_omnibox", (type, directive, extension, manifest) => {
let keyword = manifest.omnibox.keyword;
try {
// This will throw if the keyword is already registered.
ExtensionSearchHandler.registerKeyword(keyword, extension);
gKeywordMap.set(extension, keyword);
} catch (e) {
extension.manifestError(e.message);
}
});
extensions.on("shutdown", (type, extension) => {
let keyword = gKeywordMap.get(extension);
if (keyword) {
ExtensionSearchHandler.unregisterKeyword(keyword);
gKeywordMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("omnibox", "addon_parent", context => {
let {extension} = context;
return {
omnibox: {
setDefaultSuggestion(suggestion) {
let keyword = gKeywordMap.get(extension);
try {
// This will throw if the keyword failed to register.
ExtensionSearchHandler.setDefaultSuggestion(keyword, suggestion);
} catch (e) {
return Promise.reject(e.message);
}
},
onInputStarted: new SingletonEventManager(context, "omnibox.onInputStarted", fire => {
let listener = (eventName) => {
fire();
};
extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
return () => {
extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
};
}).api(),
onInputCancelled: new SingletonEventManager(context, "omnibox.onInputCancelled", fire => {
let listener = (eventName) => {
fire();
};
extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
return () => {
extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
};
}).api(),
onInputEntered: new SingletonEventManager(context, "omnibox.onInputEntered", fire => {
let listener = (eventName, text, disposition) => {
fire(text, disposition);
};
extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
return () => {
extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
};
}).api(),
},
omnibox_internal: {
addSuggestions(id, suggestions) {
let keyword = gKeywordMap.get(extension);
try {
ExtensionSearchHandler.addSuggestions(keyword, id, suggestions);
} catch (e) {
// Silently fail because the extension developer can not know for sure if the user
// has already invalidated the callback when asynchronously providing suggestions.
}
},
onInputChanged: new SingletonEventManager(context, "omnibox_internal.onInputChanged", fire => {
let listener = (eventName, text, id) => {
fire(text, id);
};
extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
return () => {
extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
};
}).api(),
},
};
});

View File

@ -0,0 +1,290 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
IconDetails,
} = ExtensionUtils;
// WeakMap[Extension -> PageAction]
var pageActionMap = new WeakMap();
// Handles URL bar icons, including the |page_action| manifest entry
// and associated API.
function PageAction(options, extension) {
this.extension = extension;
this.id = makeWidgetId(extension.id) + "-page-action";
this.tabManager = TabManager.for(extension);
this.defaults = {
show: false,
title: options.default_title || extension.name,
icon: IconDetails.normalize({path: options.default_icon}, extension),
popup: options.default_popup || "",
};
this.browserStyle = options.browser_style || false;
if (options.browser_style === null) {
this.extension.logger.warn("Please specify whether you want browser_style " +
"or not in your page_action options.");
}
this.tabContext = new TabContext(tab => Object.create(this.defaults),
extension);
this.tabContext.on("location-change", this.handleLocationChange.bind(this)); // eslint-disable-line mozilla/balanced-listeners
// WeakMap[ChromeWindow -> <xul:image>]
this.buttons = new WeakMap();
EventEmitter.decorate(this);
}
PageAction.prototype = {
// Returns the value of the property |prop| for the given tab, where
// |prop| is one of "show", "title", "icon", "popup".
getProperty(tab, prop) {
return this.tabContext.get(tab)[prop];
},
// Sets the value of the property |prop| for the given tab to the
// given value, symmetrically to |getProperty|.
//
// If |tab| is currently selected, updates the page action button to
// reflect the new value.
setProperty(tab, prop, value) {
if (value != null) {
this.tabContext.get(tab)[prop] = value;
} else {
delete this.tabContext.get(tab)[prop];
}
if (tab.selected) {
this.updateButton(tab.ownerGlobal);
}
},
// Updates the page action button in the given window to reflect the
// properties of the currently selected tab:
//
// Updates "tooltiptext" and "aria-label" to match "title" property.
// Updates "image" to match the "icon" property.
// Shows or hides the icon, based on the "show" property.
updateButton(window) {
let tabData = this.tabContext.get(window.gBrowser.selectedTab);
if (!(tabData.show || this.buttons.has(window))) {
// Don't bother creating a button for a window until it actually
// needs to be shown.
return;
}
let button = this.getButton(window);
if (tabData.show) {
// Update the title and icon only if the button is visible.
let title = tabData.title || this.extension.name;
button.setAttribute("tooltiptext", title);
button.setAttribute("aria-label", title);
// These URLs should already be properly escaped, but make doubly sure CSS
// string escape characters are escaped here, since they could lead to a
// sandbox break.
let escape = str => str.replace(/[\\\s"]/g, encodeURIComponent);
let getIcon = size => escape(IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
button.setAttribute("style", `
--webextension-urlbar-image: url("${getIcon(16)}");
--webextension-urlbar-image-2x: url("${getIcon(32)}");
`);
button.classList.add("webextension-page-action");
}
button.hidden = !tabData.show;
},
// Create an |image| node and add it to the |urlbar-icons|
// container in the given window.
addButton(window) {
let document = window.document;
let button = document.createElement("image");
button.id = this.id;
button.setAttribute("class", "urlbar-icon");
button.addEventListener("click", event => { // eslint-disable-line mozilla/balanced-listeners
if (event.button == 0) {
this.handleClick(window);
}
});
document.getElementById("urlbar-icons").appendChild(button);
return button;
},
// Returns the page action button for the given window, creating it if
// it doesn't already exist.
getButton(window) {
if (!this.buttons.has(window)) {
let button = this.addButton(window);
this.buttons.set(window, button);
}
return this.buttons.get(window);
},
/**
* Triggers this page action for the given window, with the same effects as
* if it were clicked by a user.
*
* This has no effect if the page action is hidden for the selected tab.
*
* @param {Window} window
*/
triggerAction(window) {
let pageAction = pageActionMap.get(this.extension);
if (pageAction.getProperty(window.gBrowser.selectedTab, "show")) {
pageAction.handleClick(window);
}
},
// Handles a click event on the page action button for the given
// window.
// If the page action has a |popup| property, a panel is opened to
// that URL. Otherwise, a "click" event is emitted, and dispatched to
// the any click listeners in the add-on.
handleClick(window) {
let tab = window.gBrowser.selectedTab;
let popupURL = this.tabContext.get(tab).popup;
this.tabManager.addActiveTabPermission(tab);
// If the widget has a popup URL defined, we open a popup, but do not
// dispatch a click event to the extension.
// If it has no popup URL defined, we dispatch a click event, but do not
// open a popup.
if (popupURL) {
new PanelPopup(this.extension, this.getButton(window), popupURL,
this.browserStyle);
} else {
this.emit("click", tab);
}
},
handleLocationChange(eventType, tab, fromBrowse) {
if (fromBrowse) {
this.tabContext.clear(tab);
}
this.updateButton(tab.ownerGlobal);
},
shutdown() {
this.tabContext.shutdown();
for (let window of WindowListManager.browserWindows()) {
if (this.buttons.has(window)) {
this.buttons.get(window).remove();
}
}
},
};
/* eslint-disable mozilla/balanced-listeners */
extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
let pageAction = new PageAction(manifest.page_action, extension);
pageActionMap.set(extension, pageAction);
});
extensions.on("shutdown", (type, extension) => {
if (pageActionMap.has(extension)) {
pageActionMap.get(extension).shutdown();
pageActionMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
PageAction.for = extension => {
return pageActionMap.get(extension);
};
global.pageActionFor = PageAction.for;
extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
let {extension} = context;
return {
pageAction: {
onClicked: new EventManager(context, "pageAction.onClicked", fire => {
let listener = (evt, tab) => {
fire(TabManager.convert(extension, tab));
};
let pageAction = PageAction.for(extension);
pageAction.on("click", listener);
return () => {
pageAction.off("click", listener);
};
}).api(),
show(tabId) {
let tab = TabManager.getTab(tabId, context);
PageAction.for(extension).setProperty(tab, "show", true);
},
hide(tabId) {
let tab = TabManager.getTab(tabId, context);
PageAction.for(extension).setProperty(tab, "show", false);
},
setTitle(details) {
let tab = TabManager.getTab(details.tabId, context);
// Clear the tab-specific title when given a null string.
PageAction.for(extension).setProperty(tab, "title", details.title || null);
},
getTitle(details) {
let tab = TabManager.getTab(details.tabId, context);
let title = PageAction.for(extension).getProperty(tab, "title");
return Promise.resolve(title);
},
setIcon(details) {
let tab = TabManager.getTab(details.tabId, context);
let icon = IconDetails.normalize(details, extension, context);
PageAction.for(extension).setProperty(tab, "icon", icon);
},
setPopup(details) {
let tab = TabManager.getTab(details.tabId, context);
// Note: Chrome resolves arguments to setIcon relative to the calling
// context, but resolves arguments to setPopup relative to the extension
// root.
// For internal consistency, we currently resolve both relative to the
// calling context.
let url = details.popup && context.uri.resolve(details.popup);
if (url && !context.checkLoadURL(url)) {
return Promise.reject({message: `Access denied for URL ${url}`});
}
PageAction.for(extension).setProperty(tab, "popup", url);
},
getPopup(details) {
let tab = TabManager.getTab(details.tabId, context);
let popup = PageAction.for(extension).getProperty(tab, "popup");
return Promise.resolve(popup);
},
},
};
});

View File

@ -0,0 +1,92 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
promiseObserved,
} = ExtensionUtils;
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
function getRecentlyClosed(maxResults, extension) {
let recentlyClosed = [];
// Get closed windows
let closedWindowData = SessionStore.getClosedWindowData(false);
for (let window of closedWindowData) {
recentlyClosed.push({
lastModified: window.closedAt,
window: WindowManager.convertFromSessionStoreClosedData(window, extension)});
}
// Get closed tabs
for (let window of WindowListManager.browserWindows()) {
let closedTabData = SessionStore.getClosedTabData(window, false);
for (let tab of closedTabData) {
recentlyClosed.push({
lastModified: tab.closedAt,
tab: TabManager.for(extension).convertFromSessionStoreClosedData(tab, window)});
}
}
// Sort windows and tabs
recentlyClosed.sort((a, b) => b.lastModified - a.lastModified);
return recentlyClosed.slice(0, maxResults);
}
function createSession(restored, extension, sessionId) {
if (!restored) {
return Promise.reject({message: `Could not restore object using sessionId ${sessionId}.`});
}
let sessionObj = {lastModified: Date.now()};
if (restored instanceof Ci.nsIDOMChromeWindow) {
return promiseObserved("sessionstore-single-window-restored", subject => subject == restored).then(() => {
sessionObj.window = WindowManager.convert(extension, restored, {populate: true});
return Promise.resolve([sessionObj]);
});
}
sessionObj.tab = TabManager.for(extension).convert(restored);
return Promise.resolve([sessionObj]);
}
extensions.registerSchemaAPI("sessions", "addon_parent", context => {
let {extension} = context;
return {
sessions: {
getRecentlyClosed: function(filter) {
let maxResults = filter.maxResults == undefined ? this.MAX_SESSION_RESULTS : filter.maxResults;
return Promise.resolve(getRecentlyClosed(maxResults, extension));
},
restore: function(sessionId) {
let session, closedId;
if (sessionId) {
closedId = sessionId;
session = SessionStore.undoCloseById(closedId);
} else if (SessionStore.lastClosedObjectType == "window") {
// If the most recently closed object is a window, just undo closing the most recent window.
session = SessionStore.undoCloseWindow(0);
} else {
// It is a tab, and we cannot call SessionStore.undoCloseTab without a window,
// so we must find the tab in which case we can just use its closedId.
let recentlyClosedTabs = [];
for (let window of WindowListManager.browserWindows()) {
let closedTabData = SessionStore.getClosedTabData(window, false);
for (let tab of closedTabData) {
recentlyClosedTabs.push(tab);
}
}
// Sort the tabs.
recentlyClosedTabs.sort((a, b) => b.closedAt - a.closedAt);
// Use the closedId of the most recently closed tab to restore it.
closedId = recentlyClosedTabs[0].closedId;
session = SessionStore.undoCloseById(closedId);
}
return createSession(session, extension, closedId);
},
},
};
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,231 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
"@mozilla.org/browser/aboutnewtab-service;1",
"nsIAboutNewTabService");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
promiseObserved,
} = ExtensionUtils;
function onXULFrameLoaderCreated({target}) {
target.messageManager.sendAsyncMessage("AllowScriptsToClose", {});
}
extensions.registerSchemaAPI("windows", "addon_parent", context => {
let {extension} = context;
return {
windows: {
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 => {
// Keep track of the last windowId used to fire an onFocusChanged event
let lastOnFocusChangedWindowId;
let listener = event => {
// Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
// event when switching focus between two Firefox windows.
Promise.resolve().then(() => {
let window = Services.focus.activeWindow;
let windowId = window ? WindowManager.getId(window) : WindowManager.WINDOW_ID_NONE;
if (windowId !== lastOnFocusChangedWindowId) {
fire(windowId);
lastOnFocusChangedWindowId = windowId;
}
});
};
AllWindowEvents.addListener("focus", listener);
AllWindowEvents.addListener("blur", listener);
return () => {
AllWindowEvents.removeListener("focus", listener);
AllWindowEvents.removeListener("blur", listener);
};
}).api(),
get: function(windowId, getInfo) {
let window = WindowManager.getWindow(windowId, context);
return Promise.resolve(WindowManager.convert(extension, window, getInfo));
},
getCurrent: function(getInfo) {
let window = currentWindow(context);
return Promise.resolve(WindowManager.convert(extension, window, getInfo));
},
getLastFocused: function(getInfo) {
let window = WindowManager.topWindow;
return Promise.resolve(WindowManager.convert(extension, window, getInfo));
},
getAll: function(getInfo) {
let windows = Array.from(WindowListManager.browserWindows(),
window => WindowManager.convert(extension, window, getInfo));
return Promise.resolve(windows);
},
create: function(createData) {
let needResize = (createData.left !== null || createData.top !== null ||
createData.width !== null || createData.height !== null);
if (needResize) {
if (createData.state !== null && createData.state != "normal") {
return Promise.reject({message: `"state": "${createData.state}" may not be combined with "left", "top", "width", or "height"`});
}
createData.state = "normal";
}
function mkstr(s) {
let result = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
result.data = s;
return result;
}
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
if (createData.tabId !== null) {
if (createData.url !== null) {
return Promise.reject({message: "`tabId` may not be used in conjunction with `url`"});
}
if (createData.allowScriptsToClose) {
return Promise.reject({message: "`tabId` may not be used in conjunction with `allowScriptsToClose`"});
}
let tab = TabManager.getTab(createData.tabId, context);
// Private browsing tabs can only be moved to private browsing
// windows.
let incognito = PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser);
if (createData.incognito !== null && createData.incognito != incognito) {
return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
}
createData.incognito = incognito;
args.appendElement(tab, /* weak = */ false);
} else if (createData.url !== null) {
if (Array.isArray(createData.url)) {
let array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
for (let url of createData.url) {
array.appendElement(mkstr(url), /* weak = */ false);
}
args.appendElement(array, /* weak = */ false);
} else {
args.appendElement(mkstr(createData.url), /* weak = */ false);
}
} else {
args.appendElement(mkstr(aboutNewTabService.newTabURL), /* weak = */ false);
}
let features = ["chrome"];
if (createData.type === null || createData.type == "normal") {
features.push("dialog=no", "all");
} else {
// All other types create "popup"-type windows by default.
features.push("dialog", "resizable", "minimizable", "centerscreen", "titlebar", "close");
}
if (createData.incognito !== null) {
if (createData.incognito) {
features.push("private");
} else {
features.push("non-private");
}
}
let {allowScriptsToClose, url} = createData;
if (allowScriptsToClose === null) {
allowScriptsToClose = typeof url === "string" && url.startsWith("moz-extension://");
}
let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
features.join(","), args);
WindowManager.updateGeometry(window, createData);
// TODO: focused, type
return new Promise(resolve => {
window.addEventListener("load", function listener() {
window.removeEventListener("load", listener);
if (["maximized", "normal"].includes(createData.state)) {
window.document.documentElement.setAttribute("sizemode", createData.state);
}
resolve(promiseObserved("browser-delayed-startup-finished", win => win == window));
});
}).then(() => {
// Some states only work after delayed-startup-finished
if (["minimized", "fullscreen", "docked"].includes(createData.state)) {
WindowManager.setState(window, createData.state);
}
if (allowScriptsToClose) {
for (let {linkedBrowser} of window.gBrowser.tabs) {
onXULFrameLoaderCreated({target: linkedBrowser});
linkedBrowser.addEventListener( // eslint-disable-line mozilla/balanced-listeners
"XULFrameLoaderCreated", onXULFrameLoaderCreated);
}
}
return WindowManager.convert(extension, window, {populate: true});
});
},
update: function(windowId, updateInfo) {
if (updateInfo.state !== null && updateInfo.state != "normal") {
if (updateInfo.left !== null || updateInfo.top !== null ||
updateInfo.width !== null || updateInfo.height !== null) {
return Promise.reject({message: `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`});
}
}
let window = WindowManager.getWindow(windowId, context);
if (updateInfo.focused) {
Services.focus.activeWindow = window;
}
if (updateInfo.state !== null) {
WindowManager.setState(window, updateInfo.state);
}
if (updateInfo.drawAttention) {
// Bug 1257497 - Firefox can't cancel attention actions.
window.getAttention();
}
WindowManager.updateGeometry(window, updateInfo);
// TODO: All the other properties, focused=false...
return Promise.resolve(WindowManager.convert(extension, window));
},
remove: function(windowId) {
let window = WindowManager.getWindow(windowId, context);
window.close();
return new Promise(resolve => {
let listener = () => {
AllWindowEvents.removeListener("domwindowclosed", listener);
resolve();
};
AllWindowEvents.addListener("domwindowclosed", listener);
});
},
},
};
});

View File

@ -0,0 +1,3 @@
body {
border-radius: 3.5px;
}

View File

@ -0,0 +1,11 @@
button,
select,
input[type="checkbox"] + label::before {
border-radius: 4px;
}
.panel-section-footer {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
overflow: hidden;
}

View File

@ -0,0 +1,5 @@
@media (-moz-os-version: windows-win7) {
body {
border-radius: 4px;
}
}

View File

@ -0,0 +1,572 @@
/* stylelint-disable property-no-vendor-prefix */
/* stylelint-disable property-no-vendor-prefix */
/* Base */
button,
select,
option,
input {
-moz-appearance: none;
}
/* Variables */
html,
body {
background: transparent;
box-sizing: border-box;
color: #222426;
cursor: default;
display: flex;
flex-direction: column;
font: caption;
margin: 0;
padding: 0;
-moz-user-select: none;
}
body * {
box-sizing: border-box;
text-align: start;
}
/* stylelint-disable property-no-vendor-prefix */
/* Buttons */
button,
select {
background-color: #fbfbfb;
border: 1px solid #b1b1b1;
box-shadow: 0 0 0 0 transparent;
font: caption;
height: 24px;
outline: 0 !important;
padding: 0 8px 0;
transition-duration: 250ms;
transition-property: box-shadow, border;
}
select {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg==);
background-position: calc(100% - 4px) center;
background-repeat: no-repeat;
padding-inline-end: 24px;
text-overflow: ellipsis;
}
label {
font: caption;
}
button::-moz-focus-inner {
border: 0;
outline: 0;
}
/* Dropdowns */
select {
background-color: #fbfbfb;
border: 1px solid #b1b1b1;
box-shadow: 0 0 0 0 transparent;
font: caption;
height: 24px;
outline: 0 !important;
padding: 0 8px 0;
transition-duration: 250ms;
transition-property: box-shadow, border;
}
select {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg==);
background-position: calc(100% - 4px) center;
background-repeat: no-repeat;
padding-inline-end: 24px;
text-overflow: ellipsis;
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #000;
}
select:-moz-focusring * {
color: #000;
text-shadow: none;
}
button.hover,
select.hover {
background-color: #ebebeb;
border: 1px solid #b1b1b1;
}
button.pressed,
select.pressed {
background-color: #d4d4d4;
border: 1px solid #858585;
}
button.disabled,
select.disabled {
color: #999;
opacity: .5;
}
button.focused,
select.focused {
border-color: #fff;
box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75);
}
button.default {
background-color: #0996f8;
border-color: #0670cc;
color: #fff;
}
button.default.hover {
background-color: #0670cc;
border-color: #005bab;
}
button.default.pressed {
background-color: #005bab;
border-color: #004480;
}
button.default.focused {
border-color: #fff;
}
/* Radio Buttons */
.radioItem {
margin-bottom: 6px;
text-align: left;
}
input[type="radio"] {
display: none;
}
input[type="radio"] + label {
-moz-user-select: none;
}
input[type="radio"] + label::before {
background-color: #fff;
background-position: center;
border: 1px solid #b1b1b1;
border-radius: 50%;
content: "";
display: inline-block;
height: 16px;
margin-right: 6px;
vertical-align: text-top;
width: 16px;
}
input[type="radio"]:hover + label::before,
.radioItem.hover input[type="radio"]:not(active) + label::before {
background-color: #fbfbfb;
border-color: #b1b1b1;
}
input[type="radio"]:hover:active + label::before,
.radioItem.pressed input[type="radio"]:not(active) + label::before {
background-color: #ebebeb;
border-color: #858585;
}
input[type="radio"]:checked + label::before {
background-color: #0996f8;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8Y2lyY2xlIGN4PSI4IiBjeT0iOCIgcj0iNCIgZmlsbD0iI2ZmZiIgLz4KPC9zdmc+Cg==);
border-color: #0670cc;
}
input[type="radio"]:checked:hover + label::before,
.radioItem.hover input[type="radio"]:checked:not(active) + label::before {
background-color: #0670cc;
border-color: #005bab;
}
input[type="radio"]:checked:hover:active + label::before,
.radioItem.pressed input[type="radio"]:checked:not(active) + label::before {
background-color: #005bab;
border-color: #004480;
}
.radioItem.disabled input[type="radio"] + label,
.radioItem.disabled input[type="radio"]:hover + label,
.radioItem.disabled input[type="radio"]:hover:active + label {
color: #999;
opacity: .5;
}
.radioItem.focused input[type="radio"] + label::before {
border-color: #0996f8;
box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75);
}
.radioItem.focused input[type="radio"]:checked + label::before {
border-color: #fff;
}
/* Checkboxes */
.checkboxItem {
margin-bottom: 6px;
text-align: left;
}
input[type="checkbox"] {
display: none;
}
input[type="checkbox"] + label {
-moz-user-select: none;
}
input[type="checkbox"] + label::before {
background-color: #fff;
background-position: center;
border: 1px solid #b1b1b1;
content: "";
display: inline-block;
height: 16px;
margin-right: 6px;
vertical-align: text-top;
width: 16px;
}
input[type="checkbox"]:hover + label::before,
.checkboxItem.hover input[type="checkbox"]:not(active) + label::before {
background-color: #fbfbfb;
border-color: #b1b1b1;
}
input[type="checkbox"]:hover:active + label::before,
.checkboxItem.pressed input[type="checkbox"]:not(active) + label::before {
background-color: #ebebeb;
border-color: #858585;
}
input[type="checkbox"]:checked + label::before {
background-color: #0996f8;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNNy43LDEyLjkgQzcuNCwxMy4zIDYuOCwxMy40IDYuNCwxMyBMMy4yLDkuOCBDMi44LDkuNCAyLjgsOC42IDMuMiw4LjIgQzMuNiw3LjggNC40LDcuOCA0LjgsOC4yIEw2LjksMTAuMyBMMTEuMSw0LjQgQzExLjUsMy45IDEyLjIsMy44IDEyLjcsNC4xIEMxMy4yLDQuNSAxMy4zLDUuMiAxMyw1LjcgTDcuNywxMi45IEw3LjcsMTIuOSBaIiBmaWxsPSIjZmZmIiAvPgo8L3N2Zz4K);
border-color: #0670cc;
}
input[type="checkbox"]:checked:hover + label::before,
.checkboxItem.hover input[type="checkbox"]:checked:not(active) + label::before {
background-color: #0670cc;
border-color: #005bab;
}
input[type="checkbox"]:checked:hover:active + label::before,
.checkboxItem.pressed input[type="checkbox"]:checked:not(active) + label::before {
background-color: #005bab;
border-color: #004480;
}
.checkboxItem.disabled input[type="checkbox"] + label,
.checkboxItem.disabled input[type="checkbox"]:hover + label,
.checkboxItem.disabled input[type="checkbox"]:hover:active + label {
color: #999;
opacity: .5;
}
.checkboxItem.focused input[type="checkbox"] + label::before {
border-color: #0996f8;
box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75);
}
.checkboxItem.focused input[type="checkbox"]:checked + label::before {
border-color: #fff;
}
/* Expander Button */
button.expander {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg==);
background-position: center;
background-repeat: no-repeat;
height: 24px;
padding: 0;
width: 24px;
}
/* Interactive States */
button:hover:not(.pressed):not(.disabled):not(.focused),
select:hover:not(.pressed):not(.disabled):not(.focused) {
background-color: #ebebeb;
border: 1px solid #b1b1b1;
}
button:hover:active:not(.hover):not(.disabled):not(.focused),
select:hover:active:not(.hover):not(.disabled):not(.focused) {
background-color: #d4d4d4;
border: 1px solid #858585;
}
button.default:hover:not(.pressed):not(.disabled):not(.focused) {
background-color: #0670cc;
border-color: #005bab;
}
button.default:hover:active:not(.hover):not(.disabled):not(.focused) {
background-color: #005bab;
border-color: #004480;
}
button:focus:not(.disabled) {
border-color: #fff !important;
box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75);
}
/* Fields */
input[type="text"],
textarea {
background-color: #fff;
border: 1px solid #b1b1b1;
box-shadow: 0 0 0 0 rgba(97, 181, 255, 0);
font: caption;
padding: 0 6px 0;
transition-duration: 250ms;
transition-property: box-shadow;
}
input[type="text"] {
height: 24px;
}
input[type="text"].hover,
textarea.hover {
border: 1px solid #858585;
}
input[type="text"].disabled,
textarea.disabled {
color: #999;
opacity: .5;
}
input[type="text"].focused,
textarea.focused {
border-color: #0996f8;
box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75);
}
/* Interactive States */
input[type="text"]:not(disabled):hover,
textarea:not(disabled):hover {
border: 1px solid #858585;
}
input[type="text"]:focus,
input[type="text"]:focus:hover,
textarea:focus,
textarea:focus:hover {
border-color: #0996f8;
box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75);
}
/* stylelint-disable property-no-vendor-prefix */
.panel-section {
display: flex;
flex-direction: row;
}
.panel-section-separator {
background-color: rgba(0, 0, 0, 0.15);
min-height: 1px;
}
/* Panel Section - Header */
.panel-section-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
padding: 16px;
}
.panel-section-header > .icon-section-header {
background-position: center center;
background-repeat: no-repeat;
height: 32px;
margin-right: 16px;
position: relative;
width: 32px;
}
.panel-section-header > .text-section-header {
align-self: center;
font-size: 1.385em;
font-weight: lighter;
}
/* Panel Section - List */
.panel-section-list {
flex-direction: column;
padding: 4px 0;
}
.panel-list-item {
align-items: center;
display: flex;
flex-direction: row;
height: 24px;
padding: 0 16px;
}
.panel-list-item:not(.disabled):hover {
background-color: rgba(0, 0, 0, 0.06);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.panel-list-item:not(.disabled):hover:active {
background-color: rgba(0, 0, 0, 0.1);
}
.panel-list-item.disabled {
color: #999;
}
.panel-list-item > .icon {
flex-grow: 0;
flex-shrink: 0;
}
.panel-list-item > .text {
flex-grow: 10;
}
.panel-list-item > .text-shortcut {
color: #808080;
font-family: "Lucida Grande", caption;
font-size: .847em;
justify-content: flex-end;
}
.panel-section-list .panel-section-separator {
margin: 4px 0;
}
/* Panel Section - Form Elements */
.panel-section-formElements {
display: flex;
flex-direction: column;
padding: 16px;
}
.panel-formElements-item {
align-items: center;
display: flex;
flex-direction: row;
margin-bottom: 12px;
}
.panel-formElements-item:last-child {
margin-bottom: 0;
}
.panel-formElements-item label {
flex-shrink: 0;
margin-right: 6px;
text-align: right;
}
.panel-formElements-item input[type="text"],
.panel-formElements-item select {
flex-grow: 1;
}
/* Panel Section - Footer */
.panel-section-footer {
background-color: rgba(0, 0, 0, 0.06);
border-top: 1px solid rgba(0, 0, 0, 0.15);
color: #1a1a1a;
display: flex;
flex-direction: row;
height: 41px;
margin-top: -1px;
padding: 0;
}
.panel-section-footer-button {
flex: 1 1 auto;
height: 100%;
margin: 0 -1px;
padding: 12px;
text-align: center;
}
.panel-section-footer-button > .text-shortcut {
color: #808080;
font-family: "Lucida Grande", caption;
font-size: .847em;
}
.panel-section-footer-button:hover {
background-color: rgba(0, 0, 0, 0.06);
}
.panel-section-footer-button:hover:active {
background-color: rgba(0, 0, 0, 0.1);
}
.panel-section-footer-button.default {
background-color: #0996f8;
box-shadow: 0 1px 0 #0670cc inset;
color: #fff;
}
.panel-section-footer-button.default:hover {
background-color: #0670cc;
box-shadow: 0 1px 0 #005bab inset;
}
.panel-section-footer-button.default:hover:active {
background-color: #005bab;
box-shadow: 0 1px 0 #004480 inset;
}
.panel-section-footer-separator {
background-color: rgba(0, 0, 0, 0.1);
width: 1px;
z-index: 99;
}
/* Panel Section - Tabs */
.panel-section-tabs {
color: #1a1a1a;
display: flex;
flex-direction: row;
height: 41px;
margin-bottom: -1px;
padding: 0;
}
.panel-section-tabs-button {
flex: 1 1 auto;
height: 100%;
margin: 0 -1px;
padding: 12px;
text-align: center;
}
.panel-section-tabs-button:hover {
background-color: rgba(0, 0, 0, 0.06);
}
.panel-section-tabs-button:hover:active {
background-color: rgba(0, 0, 0, 0.1);
}
.panel-section-tabs-button.selected {
box-shadow: 0 -1px 0 #0670cc inset, 0 -4px 0 #0996f8 inset;
color: #0996f8;
}
.panel-section-tabs-button.selected:hover {
color: #0670cc;
}
.panel-section-tabs-separator {
background-color: rgba(0, 0, 0, 0.1);
width: 1px;
z-index: 99;
}

View File

@ -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>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,31 @@
# scripts
category webextension-scripts bookmarks chrome://browser/content/ext-bookmarks.js
category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
category webextension-scripts commands chrome://browser/content/ext-commands.js
category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
category webextension-scripts history chrome://browser/content/ext-history.js
category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
category webextension-scripts sessions chrome://browser/content/ext-sessions.js
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
category webextension-scripts utils chrome://browser/content/ext-utils.js
category webextension-scripts windows chrome://browser/content/ext-windows.js
# scripts that must run in the same process as addon code.
category webextension-scripts-addon contextMenus chrome://browser/content/ext-c-contextMenus.js
category webextension-scripts-addon omnibox chrome://browser/content/ext-c-omnibox.js
category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
# schemas
category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
category webextension-schemas commands chrome://browser/content/schemas/commands.json
category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
category webextension-schemas history chrome://browser/content/schemas/history.json
category webextension-schemas omnibox chrome://browser/content/schemas/omnibox.json
category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
category webextension-schemas windows chrome://browser/content/schemas/windows.json

View File

@ -0,0 +1,29 @@
# 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.css
#ifdef XP_MACOSX
content/browser/extension-mac.css
content/browser/extension-mac-panel.css
#endif
#ifdef XP_WIN
content/browser/extension-win-panel.css
#endif
content/browser/extension.svg
content/browser/ext-bookmarks.js
content/browser/ext-browserAction.js
content/browser/ext-commands.js
content/browser/ext-contextMenus.js
content/browser/ext-desktop-runtime.js
content/browser/ext-history.js
content/browser/ext-omnibox.js
content/browser/ext-pageAction.js
content/browser/ext-sessions.js
content/browser/ext-tabs.js
content/browser/ext-utils.js
content/browser/ext-windows.js
content/browser/ext-c-contextMenus.js
content/browser/ext-c-omnibox.js
content/browser/ext-c-tabs.js

View File

@ -0,0 +1,14 @@
# -*- Mode: python; 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']
EXTRA_COMPONENTS += [
'extensions-browser.manifest',
]
DIRS += ['schemas']

View File

@ -0,0 +1,27 @@
// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,568 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": [
"bookmarks"
]
}]
}
]
},
{
"namespace": "bookmarks",
"description": "Use the <code>browser.bookmarks</code> API to create, organize, and otherwise manipulate bookmarks. Also see $(topic:override)[Override Pages], which you can use to create a custom Bookmark Manager page.",
"permissions": ["bookmarks"],
"types": [
{
"id": "BookmarkTreeNodeUnmodifiable",
"type": "string",
"enum": ["managed"],
"description": "Indicates the reason why this node is unmodifiable. The <var>managed</var> value indicates that this node was configured by the system administrator or by the custodian of a supervised user. Omitted if the node can be modified by the user and the extension (default)."
},
{
"id": "BookmarkTreeNode",
"type": "object",
"description": "A node (either a bookmark or a folder) in the bookmark tree. Child nodes are ordered within their parent folder.",
"properties": {
"id": {
"type": "string",
"description": "The unique identifier for the node. IDs are unique within the current profile, and they remain valid even after the browser is restarted."
},
"parentId": {
"type": "string",
"optional": true,
"description": "The <code>id</code> of the parent folder. Omitted for the root node."
},
"index": {
"type": "integer",
"optional": true,
"description": "The 0-based position of this node within its parent folder."
},
"url": {
"type": "string",
"optional": true,
"description": "The URL navigated to when a user clicks the bookmark. Omitted for folders."
},
"title": {
"type": "string",
"description": "The text displayed for the node."
},
"dateAdded": {
"type": "number",
"optional": true,
"description": "When this node was created, in milliseconds since the epoch (<code>new Date(dateAdded)</code>)."
},
"dateGroupModified": {
"type": "number",
"optional": true,
"description": "When the contents of this folder last changed, in milliseconds since the epoch."
},
"unmodifiable": {
"$ref": "BookmarkTreeNodeUnmodifiable",
"optional": true,
"description": "Indicates the reason why this node is unmodifiable. The <var>managed</var> value indicates that this node was configured by the system administrator or by the custodian of a supervised user. Omitted if the node can be modified by the user and the extension (default)."
},
"children": {
"type": "array",
"optional": true,
"items": { "$ref": "BookmarkTreeNode" },
"description": "An ordered list of children of this node."
}
}
},
{
"id": "CreateDetails",
"description": "Object passed to the create() function.",
"type": "object",
"properties": {
"parentId": {
"type": "string",
"optional": true,
"description": "Defaults to the Other Bookmarks folder."
},
"index": {
"type": "integer",
"minimum": 0,
"optional": true
},
"title": {
"type": "string",
"optional": true
},
"url": {
"type": "string",
"optional": true
}
}
}
],
"functions": [
{
"name": "get",
"type": "function",
"description": "Retrieves the specified BookmarkTreeNode(s).",
"async": "callback",
"parameters": [
{
"name": "idOrIdList",
"description": "A single string-valued id, or an array of string-valued ids",
"choices": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
}
]
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "results",
"type": "array",
"items": { "$ref": "BookmarkTreeNode" }
}
]
}
]
},
{
"name": "getChildren",
"type": "function",
"description": "Retrieves the children of the specified BookmarkTreeNode id.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "results",
"type": "array",
"items": { "$ref": "BookmarkTreeNode"}
}
]
}
]
},
{
"name": "getRecent",
"type": "function",
"description": "Retrieves the recently added bookmarks.",
"async": "callback",
"parameters": [
{
"type": "integer",
"minimum": 1,
"name": "numberOfItems",
"description": "The maximum number of items to return."
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "results",
"type": "array",
"items": { "$ref": "BookmarkTreeNode" }
}
]
}
]
},
{
"name": "getTree",
"type": "function",
"description": "Retrieves the entire Bookmarks hierarchy.",
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "results",
"type": "array",
"items": { "$ref": "BookmarkTreeNode" }
}
]
}
]
},
{
"name": "getSubTree",
"type": "function",
"description": "Retrieves part of the Bookmarks hierarchy, starting at the specified node.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "id",
"description": "The ID of the root of the subtree to retrieve."
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "results",
"type": "array",
"items": { "$ref": "BookmarkTreeNode" }
}
]
}
]
},
{
"name": "search",
"type": "function",
"description": "Searches for BookmarkTreeNodes matching the given query. Queries specified with an object produce BookmarkTreeNodes matching all specified properties.",
"async": "callback",
"parameters": [
{
"name": "query",
"description": "Either a string of words and quoted phrases that are matched against bookmark URLs and titles, or an object. If an object, the properties <code>query</code>, <code>url</code>, and <code>title</code> may be specified and bookmarks matching all specified properties will be produced.",
"choices": [
{
"type": "string",
"description": "A string of words and quoted phrases that are matched against bookmark URLs and titles."
},
{
"type": "object",
"description": "An object specifying properties and values to match when searching. Produces bookmarks matching all properties.",
"properties": {
"query": {
"type": "string",
"optional": true,
"description": "A string of words and quoted phrases that are matched against bookmark URLs and titles."
},
"url": {
"type": "string",
"format": "url",
"optional": true,
"description": "The URL of the bookmark; matches verbatim. Note that folders have no URL."
},
"title": {
"type": "string",
"optional": true,
"description": "The title of the bookmark; matches verbatim."
}
}
}
]
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "results",
"type": "array",
"items": { "$ref": "BookmarkTreeNode" }
}
]
}
]
},
{
"name": "create",
"type": "function",
"description": "Creates a bookmark or folder under the specified parentId. If url is NULL or missing, it will be a folder.",
"async": "callback",
"parameters": [
{
"$ref": "CreateDetails",
"name": "bookmark"
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"name": "result",
"$ref": "BookmarkTreeNode"
}
]
}
]
},
{
"name": "move",
"type": "function",
"description": "Moves the specified BookmarkTreeNode to the provided location.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "object",
"name": "destination",
"properties": {
"parentId": {
"type": "string",
"optional": true
},
"index": {
"type": "integer",
"minimum": 0,
"optional": true
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"name": "result",
"$ref": "BookmarkTreeNode"
}
]
}
]
},
{
"name": "update",
"type": "function",
"description": "Updates the properties of a bookmark or folder. Specify only the properties that you want to change; unspecified properties will be left unchanged. <b>Note:</b> Currently, only 'title' and 'url' are supported.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "object",
"name": "changes",
"properties": {
"title": {
"type": "string",
"optional": true
},
"url": {
"type": "string",
"optional": true
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"name": "result",
"$ref": "BookmarkTreeNode"
}
]
}
]
},
{
"name": "remove",
"type": "function",
"description": "Removes a bookmark or an empty bookmark folder.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "removeTree",
"type": "function",
"description": "Recursively removes a bookmark folder.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "import",
"unsupported": true,
"type": "function",
"description": "Imports bookmarks from an html bookmark file",
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "export",
"unsupported": true,
"type": "function",
"description": "Exports bookmarks to an html bookmark file",
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
}
],
"events": [
{
"name": "onCreated",
"type": "function",
"description": "Fired when a bookmark or folder is created.",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"$ref": "BookmarkTreeNode",
"name": "bookmark"
}
]
},
{
"name": "onRemoved",
"type": "function",
"description": "Fired when a bookmark or folder is removed. When a folder is removed recursively, a single notification is fired for the folder, and none for its contents.",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "object",
"name": "removeInfo",
"properties": {
"parentId": { "type": "string" },
"index": { "type": "integer" },
"node": { "$ref": "BookmarkTreeNode" }
}
}
]
},
{
"name": "onChanged",
"type": "function",
"description": "Fired when a bookmark or folder changes. <b>Note:</b> Currently, only title and url changes trigger this.",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "object",
"name": "changeInfo",
"properties": {
"title": { "type": "string" },
"url": {
"type": "string",
"optional": true
}
}
}
]
},
{
"name": "onMoved",
"type": "function",
"description": "Fired when a bookmark or folder is moved to a different parent folder.",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "object",
"name": "moveInfo",
"properties": {
"parentId": { "type": "string" },
"index": { "type": "integer" },
"oldParentId": { "type": "string" },
"oldIndex": { "type": "integer" }
}
}
]
},
{
"name": "onChildrenReordered",
"unsupported": true,
"type": "function",
"description": "Fired when the children of a folder have changed their order due to the order being sorted in the UI. This is not called as a result of a move().",
"parameters": [
{
"type": "string",
"name": "id"
},
{
"type": "object",
"name": "reorderInfo",
"properties": {
"childIds": {
"type": "array",
"items": { "type": "string" }
}
}
}
]
},
{
"name": "onImportBegan",
"unsupported": true,
"type": "function",
"description": "Fired when a bookmark import session is begun. Expensive observers should ignore onCreated updates until onImportEnded is fired. Observers should still handle other notifications immediately.",
"parameters": []
},
{
"name": "onImportEnded",
"unsupported": true,
"type": "function",
"description": "Fired when a bookmark import session is ended.",
"parameters": []
}
]
}
]

View File

@ -0,0 +1,430 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"browser_action": {
"type": "object",
"additionalProperties": { "$ref": "UnrecognizedProperty" },
"properties": {
"default_title": {
"type": "string",
"optional": true,
"preprocess": "localize"
},
"default_icon": {
"$ref": "IconPath",
"optional": true
},
"default_popup": {
"type": "string",
"format": "relativeUrl",
"optional": true,
"preprocess": "localize"
},
"browser_style": {
"type": "boolean",
"optional": true
}
},
"optional": true
}
}
}
]
},
{
"namespace": "browserAction",
"description": "Use browser actions to put icons in the main browser toolbar, to the right of the address bar. In addition to its icon, a browser action can also have a tooltip, a badge, and a popup.",
"permissions": ["manifest:browser_action"],
"types": [
{
"id": "ColorArray",
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 4,
"maxItems": 4
},
{
"id": "ImageDataType",
"type": "object",
"isInstanceOf": "ImageData",
"additionalProperties": { "type": "any" },
"postprocess": "convertImageDataToURL",
"description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
}
],
"functions": [
{
"name": "setTitle",
"type": "function",
"description": "Sets the title of the browser action. This shows up in the tooltip.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The string the browser action should display when moused over."
},
"tabId": {
"type": "integer",
"optional": true,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getTitle",
"type": "function",
"description": "Gets the title of the browser action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"description": "Specify the tab to get the title from. If no tab is specified, the non-tab-specific title is returned."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
},
{
"name": "setIcon",
"type": "function",
"description": "Sets the icon for the browser action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"imageData": {
"choices": [
{ "$ref": "ImageDataType" },
{
"type": "object",
"additionalProperties": {"$ref": "ImageDataType"}
}
],
"optional": true,
"description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'19': foo}'"
},
"path": {
"choices": [
{ "type": "string" },
{
"type": "object",
"additionalProperties": {"type": "string"}
}
],
"optional": true,
"description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
},
"tabId": {
"type": "integer",
"optional": true,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "setPopup",
"type": "function",
"description": "Sets the html document to be opened as a popup when the user clicks on the browser action's icon.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"minimum": 0,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
},
"popup": {
"type": "string",
"description": "The html file to show in a popup. If set to the empty string (''), no popup is shown."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getPopup",
"type": "function",
"description": "Gets the html document set as the popup for this browser action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"description": "Specify the tab to get the popup from. If no tab is specified, the non-tab-specific popup is returned."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
},
{
"name": "setBadgeText",
"type": "function",
"description": "Sets the badge text for the browser action. The badge is displayed on top of the icon.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Any number of characters can be passed, but only about four can fit in the space."
},
"tabId": {
"type": "integer",
"optional": true,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getBadgeText",
"type": "function",
"description": "Gets the badge text of the browser action. If no tab is specified, the non-tab-specific badge text is returned.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"description": "Specify the tab to get the badge text from. If no tab is specified, the non-tab-specific badge text is returned."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
},
{
"name": "setBadgeBackgroundColor",
"type": "function",
"description": "Sets the background color for the badge.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"color": {
"description": "An array of four integers in the range [0,255] that make up the RGBA color of the badge. For example, opaque red is <code>[255, 0, 0, 255]</code>. Can also be a string with a CSS value, with opaque red being <code>#FF0000</code> or <code>#F00</code>.",
"choices": [
{"type": "string"},
{"$ref": "ColorArray"}
]
},
"tabId": {
"type": "integer",
"optional": true,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getBadgeBackgroundColor",
"type": "function",
"description": "Gets the background color of the browser action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"description": "Specify the tab to get the badge background color from. If no tab is specified, the non-tab-specific badge background color is returned."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"$ref": "ColorArray"
}
]
}
]
},
{
"name": "enable",
"type": "function",
"description": "Enables the browser action for a tab. By default, browser actions are enabled.",
"async": "callback",
"parameters": [
{
"type": "integer",
"optional": true,
"name": "tabId",
"minimum": 0,
"description": "The id of the tab for which you want to modify the browser action."
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "disable",
"type": "function",
"description": "Disables the browser action for a tab.",
"async": "callback",
"parameters": [
{
"type": "integer",
"optional": true,
"name": "tabId",
"minimum": 0,
"description": "The id of the tab for which you want to modify the browser action."
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "openPopup",
"type": "function",
"description": "Opens the extension popup window in the active window but does not grant tab permissions.",
"unsupported": true,
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "popupView",
"type": "object",
"optional": true,
"description": "JavaScript 'window' object for the popup window if it was succesfully opened.",
"additionalProperties": { "type": "any" }
}
]
}
]
}
],
"events": [
{
"name": "onClicked",
"type": "function",
"description": "Fired when a browser action icon is clicked. This event will not fire if the browser action has a popup.",
"parameters": [
{
"name": "tab",
"$ref": "tabs.Tab"
}
]
}
]
}
]

View File

@ -0,0 +1,148 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"id": "KeyName",
"choices": [
{
"type": "string",
"pattern": "^\\s*(Alt|Ctrl|Command|MacCtrl)\\s*\\+\\s*(Shift\\s*\\+\\s*)?([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)\\s*$"
},
{
"type": "string",
"pattern": "^(MediaNextTrack|MediaPlayPause|MediaPrevTrack|MediaStop)$"
}
]
},
{
"$extend": "WebExtensionManifest",
"properties": {
"commands": {
"type": "object",
"optional": true,
"additionalProperties": {
"type": "object",
"additionalProperties": { "$ref": "UnrecognizedProperty" },
"properties": {
"suggested_key": {
"type": "object",
"optional": true,
"properties": {
"default": {
"$ref": "KeyName",
"optional": true
},
"mac": {
"$ref": "KeyName",
"optional": true
},
"linux": {
"$ref": "KeyName",
"optional": true
},
"windows": {
"$ref": "KeyName",
"optional": true
},
"chromeos": {
"type": "string",
"optional": true
},
"android": {
"type": "string",
"optional": true
},
"ios": {
"type": "string",
"optional": true
},
"additionalProperties": {
"type": "string",
"deprecated": "Unknown platform name",
"optional": true
}
}
},
"description": {
"type": "string",
"optional": true
}
}
}
}
}
}
]
},
{
"namespace": "commands",
"description": "Use the commands API to add keyboard shortcuts that trigger actions in your extension, for example, an action to open the browser action or send a command to the xtension.",
"permissions": ["manifest:commands"],
"types": [
{
"id": "Command",
"type": "object",
"properties": {
"name": {
"type": "string",
"optional": true,
"description": "The name of the Extension Command"
},
"description": {
"type": "string",
"optional": true,
"description": "The Extension Command description"
},
"shortcut": {
"type": "string",
"optional": true,
"description": "The shortcut active for this command, or blank if not active."
}
}
}
],
"events": [
{
"name": "onCommand",
"description": "Fired when a registered command is activated using a keyboard shortcut.",
"type": "function",
"parameters": [
{
"name": "command",
"type": "string"
}
]
}
],
"functions": [
{
"name": "getAll",
"type": "function",
"async": "callback",
"description": "Returns all the registered extension commands for this extension and their shortcut (if active).",
"parameters": [
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"name": "commands",
"type": "array",
"items": {
"$ref": "Command"
}
}
],
"description": "Called to return the registered commands."
}
]
}
]
}
]

View File

@ -0,0 +1,424 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": [
"contextMenus"
]
}]
}
]
},
{
"namespace": "contextMenus",
"description": "Use the <code>browser.contextMenus</code> API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
"permissions": ["contextMenus"],
"properties": {
"ACTION_MENU_TOP_LEVEL_LIMIT": {
"value": 6,
"description": "The maximum number of top level extension items that can be added to an extension action context menu. Any items beyond this limit will be ignored."
}
},
"types": [
{
"id": "ContextType",
"type": "string",
"enum": ["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio", "launcher", "browser_action", "page_action"],
"description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'launcher'. The 'launcher' context is only supported by apps and is used to add menu items to the context menu that appears when clicking on the app icon in the launcher/taskbar/dock/etc. Different platforms might put limitations on what is actually supported in a launcher context menu."
},
{
"id": "ItemType",
"type": "string",
"enum": ["normal", "checkbox", "radio", "separator"],
"description": "The type of menu item."
},
{
"id": "OnClickData",
"type": "object",
"description": "Information sent when a context menu item is clicked.",
"properties": {
"menuItemId": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"description": "The ID of the menu item that was clicked."
},
"parentMenuItemId": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"optional": true,
"description": "The parent ID, if any, for the item clicked."
},
"mediaType": {
"type": "string",
"optional": true,
"description": "One of 'image', 'video', or 'audio' if the context menu was activated on one of these types of elements."
},
"linkUrl": {
"type": "string",
"optional": true,
"description": "If the element is a link, the URL it points to."
},
"srcUrl": {
"type": "string",
"optional": true,
"description": "Will be present for elements with a 'src' URL."
},
"pageUrl": {
"type": "string",
"optional": true,
"description": "The URL of the page where the menu item was clicked. This property is not set if the click occured in a context where there is no current page, such as in a launcher context menu."
},
"frameUrl": {
"type": "string",
"optional": true,
"description": " The URL of the frame of the element where the context menu was clicked, if it was in a frame."
},
"selectionText": {
"type": "string",
"optional": true,
"description": "The text for the context selection, if any."
},
"editable": {
"type": "boolean",
"description": "A flag indicating whether the element is editable (text input, textarea, etc.)."
},
"wasChecked": {
"type": "boolean",
"optional": true,
"description": "A flag indicating the state of a checkbox or radio item before it was clicked."
},
"checked": {
"type": "boolean",
"optional": true,
"description": "A flag indicating the state of a checkbox or radio item after it is clicked."
}
}
}
],
"functions": [
{
"name": "create",
"type": "function",
"description": "Creates a new context menu item. Note that if an error occurs during creation, you may not find out until the creation callback fires (the details will be in $(ref:runtime.lastError)).",
"returns": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"description": "The ID of the newly created item."
},
"parameters": [
{
"type": "object",
"name": "createProperties",
"properties": {
"type": {
"$ref": "ItemType",
"optional": true,
"description": "The type of menu item. Defaults to 'normal' if not specified."
},
"id": {
"type": "string",
"optional": true,
"description": "The unique ID to assign to this item. Mandatory for event pages. Cannot be the same as another ID for this extension."
},
"title": {
"type": "string",
"optional": true,
"description": "The text to be displayed in the item; this is <em>required</em> unless <code>type</code> is 'separator'. When the context is 'selection', you can use <code>%s</code> within the string to show the selected text. For example, if this parameter's value is \"Translate '%s' to Pig Latin\" and the user selects the word \"cool\", the context menu item for the selection is \"Translate 'cool' to Pig Latin\"."
},
"checked": {
"type": "boolean",
"optional": true,
"description": "The initial state of a checkbox or radio item: true for selected and false for unselected. Only one radio item can be selected at a time in a given group of radio items."
},
"contexts": {
"type": "array",
"items": {
"$ref": "ContextType"
},
"minItems": 1,
"optional": true,
"description": "List of contexts this menu item will appear in. Defaults to ['page'] if not specified."
},
"onclick": {
"type": "function",
"optional": true,
"description": "A function that will be called back when the menu item is clicked. Event pages cannot use this; instead, they should register a listener for $(ref:contextMenus.onClicked).",
"parameters": [
{
"name": "info",
"$ref": "contextMenusInternal.OnClickData",
"description": "Information about the item clicked and the context where the click happened."
},
{
"name": "tab",
"$ref": "tabs.Tab",
"description": "The details of the tab where the click took place. Note: this parameter only present for extensions."
}
]
},
"parentId": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"optional": true,
"description": "The ID of a parent menu item; this makes the item a child of a previously added item."
},
"documentUrlPatterns": {
"type": "array",
"items": {"type": "string"},
"optional": true,
"description": "Lets you restrict the item to apply only to documents whose URL matches one of the given patterns. (This applies to frames as well.) For details on the format of a pattern, see $(topic:match_patterns)[Match Patterns]."
},
"targetUrlPatterns": {
"type": "array",
"items": {"type": "string"},
"optional": true,
"description": "Similar to documentUrlPatterns, but lets you filter based on the src attribute of img/audio/video tags and the href of anchor tags."
},
"enabled": {
"type": "boolean",
"optional": true,
"description": "Whether this context menu item is enabled or disabled. Defaults to true."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"description": "Called when the item has been created in the browser. If there were any problems creating the item, details will be available in $(ref:runtime.lastError).",
"parameters": []
}
]
},
{
"name": "createInternal",
"type": "function",
"allowedContexts": ["addon_parent_only"],
"async": "callback",
"description": "Identical to contextMenus.create, except: the 'id' field is required and allows an integer, 'onclick' is not allowed, and the method is async (and the return value is not a menu item ID).",
"parameters": [
{
"type": "object",
"name": "createProperties",
"properties": {
"type": {
"$ref": "ItemType",
"optional": true
},
"id": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
]
},
"title": {
"type": "string",
"optional": true
},
"checked": {
"type": "boolean",
"optional": true
},
"contexts": {
"type": "array",
"items": {
"$ref": "ContextType"
},
"minItems": 1,
"optional": true
},
"parentId": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"optional": true
},
"documentUrlPatterns": {
"type": "array",
"items": {"type": "string"},
"optional": true
},
"targetUrlPatterns": {
"type": "array",
"items": {"type": "string"},
"optional": true
},
"enabled": {
"type": "boolean",
"optional": true
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "update",
"type": "function",
"description": "Updates a previously created context menu item.",
"async": "callback",
"parameters": [
{
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"name": "id",
"description": "The ID of the item to update."
},
{
"type": "object",
"name": "updateProperties",
"description": "The properties to update. Accepts the same values as the create function.",
"properties": {
"type": {
"$ref": "ItemType",
"optional": true
},
"title": {
"type": "string",
"optional": true
},
"checked": {
"type": "boolean",
"optional": true
},
"contexts": {
"type": "array",
"items": {
"$ref": "ContextType"
},
"minItems": 1,
"optional": true
},
"onclick": {
"type": "function",
"optional": "omit-key-if-missing",
"parameters": [
{
"name": "info",
"$ref": "contextMenusInternal.OnClickData"
},
{
"name": "tab",
"$ref": "tabs.Tab",
"description": "The details of the tab where the click took place. Note: this parameter only present for extensions."
}
]
},
"parentId": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"optional": true,
"description": "Note: You cannot change an item to be a child of one of its own descendants."
},
"documentUrlPatterns": {
"type": "array",
"items": {"type": "string"},
"optional": true
},
"targetUrlPatterns": {
"type": "array",
"items": {"type": "string"},
"optional": true
},
"enabled": {
"type": "boolean",
"optional": true
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [],
"description": "Called when the context menu has been updated."
}
]
},
{
"name": "remove",
"type": "function",
"description": "Removes a context menu item.",
"async": "callback",
"parameters": [
{
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"name": "menuItemId",
"description": "The ID of the context menu item to remove."
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [],
"description": "Called when the context menu has been removed."
}
]
},
{
"name": "removeAll",
"type": "function",
"description": "Removes all context menu items added by this extension.",
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [],
"description": "Called when removal is complete."
}
]
}
],
"events": [
{
"name": "onClicked",
"type": "function",
"description": "Fired when a context menu item is clicked.",
"parameters": [
{
"name": "info",
"$ref": "OnClickData",
"description": "Information about the item clicked and the context where the click happened."
},
{
"name": "tab",
"$ref": "tabs.Tab",
"description": "The details of the tab where the click took place. If the click did not take place in a tab, this parameter will be missing.",
"optional": true
}
]
}
]
}
]

View File

@ -0,0 +1,78 @@
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "contextMenusInternal",
"description": "Use the <code>browser.contextMenus</code> API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
"types": [
{
"id": "OnClickData",
"type": "object",
"description": "Information sent when a context menu item is clicked.",
"properties": {
"menuItemId": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"description": "The ID of the menu item that was clicked."
},
"parentMenuItemId": {
"choices": [
{ "type": "integer" },
{ "type": "string" }
],
"optional": true,
"description": "The parent ID, if any, for the item clicked."
},
"mediaType": {
"type": "string",
"optional": true,
"description": "One of 'image', 'video', or 'audio' if the context menu was activated on one of these types of elements."
},
"linkUrl": {
"type": "string",
"optional": true,
"description": "If the element is a link, the URL it points to."
},
"srcUrl": {
"type": "string",
"optional": true,
"description": "Will be present for elements with a 'src' URL."
},
"pageUrl": {
"type": "string",
"optional": true,
"description": "The URL of the page where the menu item was clicked. This property is not set if the click occured in a context where there is no current page, such as in a launcher context menu."
},
"frameUrl": {
"type": "string",
"optional": true,
"description": " The URL of the frame of the element where the context menu was clicked, if it was in a frame."
},
"selectionText": {
"type": "string",
"optional": true,
"description": "The text for the context selection, if any."
},
"editable": {
"type": "boolean",
"description": "A flag indicating whether the element is editable (text input, textarea, etc.)."
},
"wasChecked": {
"type": "boolean",
"optional": true,
"description": "A flag indicating the state of a checkbox or radio item before it was clicked."
},
"checked": {
"type": "boolean",
"optional": true,
"description": "A flag indicating the state of a checkbox or radio item after it is clicked."
}
}
}
]
}
]

View File

@ -0,0 +1,316 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": [
"history"
]
}]
}
]
},
{
"namespace": "history",
"description": "Use the <code>browser.history</code> API to interact with the browser's record of visited pages. You can add, remove, and query for URLs in the browser's history. To override the history page with your own version, see $(topic:override)[Override Pages].",
"permissions": ["history"],
"types": [
{
"id": "TransitionType",
"type": "string",
"enum": ["link", "typed", "auto_bookmark", "auto_subframe", "manual_subframe", "generated", "auto_toplevel", "form_submit", "reload", "keyword", "keyword_generated"],
"description": "The $(topic:transition-types)[transition type] for this visit from its referrer."
},
{
"id": "HistoryItem",
"type": "object",
"description": "An object encapsulating one result of a history query.",
"properties": {
"id": {
"type": "string",
"description": "The unique identifier for the item."
},
"url": {
"type": "string",
"optional": true,
"description": "The URL navigated to by a user."
},
"title": {
"type": "string",
"optional": true,
"description": "The title of the page when it was last loaded."
},
"lastVisitTime": {
"type": "number",
"optional": true,
"description": "When this page was last loaded, represented in milliseconds since the epoch."
},
"visitCount": {
"type": "integer",
"optional": true,
"description": "The number of times the user has navigated to this page."
},
"typedCount": {
"type": "integer",
"optional": true,
"description": "The number of times the user has navigated to this page by typing in the address."
}
}
},
{
"id": "VisitItem",
"type": "object",
"description": "An object encapsulating one visit to a URL.",
"properties": {
"id": {
"type": "string",
"description": "The unique identifier for the item."
},
"visitId": {
"type": "string",
"description": "The unique identifier for this visit."
},
"visitTime": {
"type": "number",
"optional": true,
"description": "When this visit occurred, represented in milliseconds since the epoch."
},
"referringVisitId": {
"type": "string",
"description": "The visit ID of the referrer."
},
"transition": {
"$ref": "TransitionType",
"description": "The $(topic:transition-types)[transition type] for this visit from its referrer."
}
}
}
],
"functions": [
{
"name": "search",
"type": "function",
"description": "Searches the history for the last visit time of each page matching the query.",
"async": "callback",
"parameters": [
{
"name": "query",
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "A free-text query to the history service. Leave empty to retrieve all pages."
},
"startTime": {
"$ref": "extensionTypes.Date",
"optional": true,
"description": "Limit results to those visited after this date. If not specified, this defaults to 24 hours in the past."
},
"endTime": {
"$ref": "extensionTypes.Date",
"optional": true,
"description": "Limit results to those visited before this date."
},
"maxResults": {
"type": "integer",
"optional": true,
"minimum": 1,
"description": "The maximum number of results to retrieve. Defaults to 100."
}
}
},
{
"name": "callback",
"type": "function",
"parameters": [
{
"name": "results",
"type": "array",
"items": {
"$ref": "HistoryItem"
}
}
]
}
]
},
{
"name": "getVisits",
"type": "function",
"description": "Retrieves information about visits to a URL.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL for which to retrieve visit information. It must be in the format as returned from a call to history.search."
}
}
},
{
"name": "callback",
"type": "function",
"parameters": [
{
"name": "results",
"type": "array",
"items": {
"$ref": "VisitItem"
}
}
]
}
]
},
{
"name": "addUrl",
"type": "function",
"description": "Adds a URL to the history with a default visitTime of the current time and a default $(topic:transition-types)[transition type] of \"link\".",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to add. Must be a valid URL that can be added to history."
},
"title": {
"type": "string",
"optional": true,
"description": "The title of the page."
},
"transition": {
"$ref": "TransitionType",
"optional": true,
"description": "The $(topic:transition-types)[transition type] for this visit from its referrer."
},
"visitTime": {
"$ref": "extensionTypes.Date",
"optional": true,
"description": "The date when this visit occurred."
}
}
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": []
}
]
},
{
"name": "deleteUrl",
"type": "function",
"description": "Removes all occurrences of the given URL from the history.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to remove."
}
}
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": []
}
]
},
{
"name": "deleteRange",
"type": "function",
"description": "Removes all items within the specified date range from the history. Pages will not be removed from the history unless all visits fall within the range.",
"async": "callback",
"parameters": [
{
"name": "range",
"type": "object",
"properties": {
"startTime": {
"$ref": "extensionTypes.Date",
"description": "Items added to history after this date."
},
"endTime": {
"$ref": "extensionTypes.Date",
"description": "Items added to history before this date."
}
}
},
{
"name": "callback",
"type": "function",
"parameters": []
}
]
},
{
"name": "deleteAll",
"type": "function",
"description": "Deletes all items from the history.",
"async": "callback",
"parameters": [
{
"name": "callback",
"type": "function",
"parameters": []
}
]
}
],
"events": [
{
"name": "onVisited",
"type": "function",
"description": "Fired when a URL is visited, providing the HistoryItem data for that URL. This event fires before the page has loaded.",
"parameters": [
{
"name": "result",
"$ref": "HistoryItem"
}
]
},
{
"name": "onVisitRemoved",
"type": "function",
"description": "Fired when one or more URLs are removed from the history service. When all visits have been removed the URL is purged from history.",
"parameters": [
{
"name": "removed",
"type": "object",
"properties": {
"allHistory": {
"type": "boolean",
"description": "True if all history was removed. If true, then urls will be empty."
},
"urls": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
]
}
]
}
]

View File

@ -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/.
browser.jar:
content/browser/schemas/bookmarks.json
content/browser/schemas/browser_action.json
content/browser/schemas/commands.json
content/browser/schemas/context_menus.json
content/browser/schemas/context_menus_internal.json
content/browser/schemas/history.json
content/browser/schemas/omnibox.json
content/browser/schemas/page_action.json
content/browser/schemas/sessions.json
content/browser/schemas/tabs.json
content/browser/schemas/windows.json

View File

@ -0,0 +1,7 @@
# -*- Mode: python; 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']

View File

@ -0,0 +1,248 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"omnibox": {
"type": "object",
"additionalProperties": { "$ref": "UnrecognizedProperty" },
"properties": {
"keyword": {
"type": "string",
"pattern": "^[^?\\s:]([^\\s:]*[^/\\s:])?$"
}
},
"optional": true
}
}
}
]
},
{
"namespace": "omnibox",
"description": "The omnibox API allows you to register a keyword with Firefox's address bar.",
"permissions": ["manifest:omnibox"],
"types": [
{
"id": "DescriptionStyleType",
"type": "string",
"description": "The style type.",
"enum": ["url", "match", "dim"]
},
{
"id": "OnInputEnteredDisposition",
"type": "string",
"enum": ["currentTab", "newForegroundTab", "newBackgroundTab"],
"description": "The window disposition for the omnibox query. This is the recommended context to display results. For example, if the omnibox command is to navigate to a certain URL, a disposition of 'newForegroundTab' means the navigation should take place in a new selected tab."
},
{
"id": "SuggestResult",
"type": "object",
"description": "A suggest result.",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "The text that is put into the URL bar, and that is sent to the extension when the user chooses this entry."
},
"description": {
"type": "string",
"minLength": 1,
"description": "The text that is displayed in the URL dropdown. Can contain XML-style markup for styling. The supported tags are 'url' (for a literal URL), 'match' (for highlighting text that matched what the user's query), and 'dim' (for dim helper text). The styles can be nested, eg. <dim><match>dimmed match</match></dim>. You must escape the five predefined entities to display them as text: stackoverflow.com/a/1091953/89484 "
},
"descriptionStyles": {
"optional": true,
"unsupported": true,
"type": "array",
"description": "An array of style ranges for the description, as provided by the extension.",
"items": {
"type": "object",
"description": "The style ranges for the description, as provided by the extension.",
"properties": {
"offset": { "type": "integer" },
"type": { "description": "The style type", "$ref": "DescriptionStyleType"},
"length": { "type": "integer", "optional": true }
}
}
},
"descriptionStylesRaw": {
"optional": true,
"unsupported": true,
"type": "array",
"description": "An array of style ranges for the description, as provided by ToValue().",
"items": {
"type": "object",
"description": "The style ranges for the description, as provided by ToValue().",
"properties": {
"offset": { "type": "integer" },
"type": { "type": "integer" }
}
}
}
}
},
{
"id": "DefaultSuggestResult",
"type": "object",
"description": "A suggest result.",
"properties": {
"description": {
"type": "string",
"minLength": 1,
"description": "The text that is displayed in the URL dropdown."
},
"descriptionStyles": {
"optional": true,
"unsupported": true,
"type": "array",
"description": "An array of style ranges for the description, as provided by the extension.",
"items": {
"type": "object",
"description": "The style ranges for the description, as provided by the extension.",
"properties": {
"offset": { "type": "integer" },
"type": { "description": "The style type", "$ref": "DescriptionStyleType"},
"length": { "type": "integer", "optional": true }
}
}
},
"descriptionStylesRaw": {
"optional": true,
"unsupported": true,
"type": "array",
"description": "An array of style ranges for the description, as provided by ToValue().",
"items": {
"type": "object",
"description": "The style ranges for the description, as provided by ToValue().",
"properties": {
"offset": { "type": "integer" },
"type": { "type": "integer" }
}
}
}
}
}
],
"functions": [
{
"name": "setDefaultSuggestion",
"type": "function",
"description": "Sets the description and styling for the default suggestion. The default suggestion is the text that is displayed in the first suggestion row underneath the URL bar.",
"parameters": [
{
"name": "suggestion",
"$ref": "DefaultSuggestResult",
"description": "A partial SuggestResult object, without the 'content' parameter."
}
]
}
],
"events": [
{
"name": "onInputStarted",
"type": "function",
"description": "User has started a keyword input session by typing the extension's keyword. This is guaranteed to be sent exactly once per input session, and before any onInputChanged events.",
"parameters": []
},
{
"name": "onInputChanged",
"type": "function",
"description": "User has changed what is typed into the omnibox.",
"parameters": [
{
"type": "string",
"name": "text"
},
{
"name": "suggest",
"type": "function",
"description": "A callback passed to the onInputChanged event used for sending suggestions back to the browser.",
"parameters": [
{
"name": "suggestResults",
"type": "array",
"description": "Array of suggest results",
"items": {
"$ref": "SuggestResult"
}
}
]
}
]
},
{
"name": "onInputEntered",
"type": "function",
"description": "User has accepted what is typed into the omnibox.",
"parameters": [
{
"type": "string",
"name": "text"
},
{
"name": "disposition",
"$ref": "OnInputEnteredDisposition"
}
]
},
{
"name": "onInputCancelled",
"type": "function",
"description": "User has ended the keyword input session without accepting the input.",
"parameters": []
}
]
},
{
"namespace": "omnibox_internal",
"description": "The internal namespace used by the omnibox API.",
"defaultContexts": ["addon_parent_only"],
"functions": [
{
"name": "addSuggestions",
"type": "function",
"async": "callback",
"description": "Internal function used by omnibox.onInputChanged for adding search suggestions",
"parameters": [
{
"name": "id",
"type": "integer",
"description": "The ID of the callback received by onInputChangedInternal"
},
{
"name": "suggestResults",
"type": "array",
"description": "Array of suggest results",
"items": {
"$ref": "omnibox.SuggestResult"
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
}
],
"events": [
{
"name": "onInputChanged",
"type": "function",
"description": "Identical to omnibox.onInputChanged except no 'suggest' callback is provided.",
"parameters": [
{
"type": "string",
"name": "text"
}
]
}
]
}
]

View File

@ -0,0 +1,235 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"page_action": {
"type": "object",
"additionalProperties": { "$ref": "UnrecognizedProperty" },
"properties": {
"default_title": {
"type": "string",
"optional": true,
"preprocess": "localize"
},
"default_icon": {
"$ref": "IconPath",
"optional": true
},
"default_popup": {
"type": "string",
"format": "relativeUrl",
"optional": true,
"preprocess": "localize"
},
"browser_style": {
"type": "boolean",
"optional": true
}
},
"optional": true
}
}
}
]
},
{
"namespace": "pageAction",
"description": "Use the <code>browser.pageAction</code> API to put icons inside the address bar. Page actions represent actions that can be taken on the current page, but that aren't applicable to all pages.",
"permissions": ["manifest:page_action"],
"types": [
{
"id": "ImageDataType",
"type": "object",
"isInstanceOf": "ImageData",
"additionalProperties": { "type": "any" },
"postprocess": "convertImageDataToURL",
"description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
}
],
"functions": [
{
"name": "show",
"type": "function",
"async": "callback",
"description": "Shows the page action. The page action is shown whenever the tab is selected.",
"parameters": [
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "hide",
"type": "function",
"async": "callback",
"description": "Hides the page action.",
"parameters": [
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "setTitle",
"type": "function",
"description": "Sets the title of the page action. This is displayed in a tooltip over the page action.",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
"title": {"type": "string", "description": "The tooltip string."}
}
}
]
},
{
"name": "getTitle",
"type": "function",
"description": "Gets the title of the page action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"description": "Specify the tab to get the title from."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
},
{
"name": "setIcon",
"type": "function",
"description": "Sets the icon for the page action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
"imageData": {
"choices": [
{ "$ref": "ImageDataType" },
{
"type": "object",
"additionalProperties": {"$ref": "ImageDataType"}
}
],
"optional": true,
"description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'19': foo}'"
},
"path": {
"choices": [
{ "type": "string" },
{
"type": "object",
"additionalProperties": {"type": "string"}
}
],
"optional": true,
"description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "setPopup",
"type": "function",
"async": true,
"description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
"popup": {
"type": "string",
"description": "The html file to show in a popup. If set to the empty string (''), no popup is shown."
}
}
}
]
},
{
"name": "getPopup",
"type": "function",
"description": "Gets the html document set as the popup for this page action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"description": "Specify the tab to get the popup from."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
}
],
"events": [
{
"name": "onClicked",
"type": "function",
"description": "Fired when a page action icon is clicked. This event will not fire if the page action has a popup.",
"parameters": [
{
"name": "tab",
"$ref": "tabs.Tab"
}
]
}
]
}
]

View File

@ -0,0 +1,146 @@
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": [
"sessions"
]
}]
}
]
},
{
"namespace": "sessions",
"description": "Use the <code>chrome.sessions</code> API to query and restore tabs and windows from a browsing session.",
"permissions": ["sessions"],
"types": [
{
"id": "Filter",
"type": "object",
"properties": {
"maxResults": {
"type": "integer",
"minimum": 0,
"maximum": 25,
"optional": true,
"description": "The maximum number of entries to be fetched in the requested list. Omit this parameter to fetch the maximum number of entries ($(ref:sessions.MAX_SESSION_RESULTS))."
}
}
},
{
"id": "Session",
"type": "object",
"properties": {
"lastModified": {"type": "integer", "description": "The time when the window or tab was closed or modified, represented in milliseconds since the epoch."},
"tab": {"$ref": "tabs.Tab", "optional": true, "description": "The $(ref:tabs.Tab), if this entry describes a tab. Either this or $(ref:sessions.Session.window) will be set."},
"window": {"$ref": "windows.Window", "optional": true, "description": "The $(ref:windows.Window), if this entry describes a window. Either this or $(ref:sessions.Session.tab) will be set."}
}
},
{
"id": "Device",
"type": "object",
"properties": {
"info": {"type": "string"},
"deviceName": {"type": "string", "description": "The name of the foreign device."},
"sessions": {"type": "array", "items": {"$ref": "Session"}, "description": "A list of open window sessions for the foreign device, sorted from most recently to least recently modified session."}
}
}
],
"functions": [
{
"name": "getRecentlyClosed",
"type": "function",
"description": "Gets the list of recently closed tabs and/or windows.",
"async": "callback",
"parameters": [
{
"$ref": "Filter",
"name": "filter",
"optional": true,
"default": {}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "sessions", "type": "array", "items": { "$ref": "Session" }, "description": "The list of closed entries in reverse order that they were closed (the most recently closed tab or window will be at index <code>0</code>). The entries may contain either tabs or windows."
}
]
}
]
},
{
"name": "getDevices",
"unsupported": true,
"type": "function",
"description": "Retrieves all devices with synced sessions.",
"async": "callback",
"parameters": [
{
"$ref": "Filter",
"name": "filter",
"optional": true
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "devices", "type": "array", "items": { "$ref": "Device" }, "description": "The list of $(ref:sessions.Device) objects for each synced session, sorted in order from device with most recently modified session to device with least recently modified session. $(ref:tabs.Tab) objects are sorted by recency in the $(ref:windows.Window) of the $(ref:sessions.Session) objects."
}
]
}
]
},
{
"name": "restore",
"type": "function",
"description": "Reopens a $(ref:windows.Window) or $(ref:tabs.Tab), with an optional callback to run when the entry has been restored.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "sessionId",
"optional": true,
"description": "The $(ref:windows.Window.sessionId), or $(ref:tabs.Tab.sessionId) to restore. If this parameter is not specified, the most recently closed session is restored."
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"$ref": "Session",
"name": "restoredSession",
"description": "A $(ref:sessions.Session) containing the restored $(ref:windows.Window) or $(ref:tabs.Tab) object."
}
]
}
]
}
],
"events": [
{
"name": "onChanged",
"unsupported": true,
"description": "Fired when recently closed tabs and/or windows are changed. This event does not monitor synced sessions changes.",
"type": "function"
}
],
"properties": {
"MAX_SESSION_RESULTS": {
"value": 25,
"description": "The maximum number of $(ref:sessions.Session) that will be included in a requested list."
}
}
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,508 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "windows",
"description": "Use the <code>browser.windows</code> API to interact with browser windows. You can use this API to create, modify, and rearrange windows in the browser.",
"types": [
{
"id": "WindowType",
"type": "string",
"description": "The type of browser window this is. Under some circumstances a Window may not be assigned type property, for example when querying closed windows from the $(ref:sessions) API.",
"enum": ["normal", "popup", "panel", "app", "devtools"]
},
{
"id": "WindowState",
"type": "string",
"description": "The state of this browser window. Under some circumstances a Window may not be assigned state property, for example when querying closed windows from the $(ref:sessions) API.",
"enum": ["normal", "minimized", "maximized", "fullscreen", "docked"]
},
{
"id": "Window",
"type": "object",
"properties": {
"id": {
"type": "integer",
"optional": true,
"minimum": 0,
"description": "The ID of the window. Window IDs are unique within a browser session. Under some circumstances a Window may not be assigned an ID, for example when querying windows using the $(ref:sessions) API, in which case a session ID may be present."
},
"focused": {
"type": "boolean",
"description": "Whether the window is currently the focused window."
},
"top": {
"type": "integer",
"optional": true,
"description": "The offset of the window from the top edge of the screen in pixels. Under some circumstances a Window may not be assigned top property, for example when querying closed windows from the $(ref:sessions) API."
},
"left": {
"type": "integer",
"optional": true,
"description": "The offset of the window from the left edge of the screen in pixels. Under some circumstances a Window may not be assigned left property, for example when querying closed windows from the $(ref:sessions) API."
},
"width": {
"type": "integer",
"optional": true,
"description": "The width of the window, including the frame, in pixels. Under some circumstances a Window may not be assigned width property, for example when querying closed windows from the $(ref:sessions) API."
},
"height": {
"type": "integer",
"optional": true,
"description": "The height of the window, including the frame, in pixels. Under some circumstances a Window may not be assigned height property, for example when querying closed windows from the $(ref:sessions) API."
},
"tabs": {
"type": "array",
"items": { "$ref": "tabs.Tab" },
"optional": true,
"description": "Array of $(ref:tabs.Tab) objects representing the current tabs in the window."
},
"incognito": {
"type": "boolean",
"description": "Whether the window is incognito."
},
"type": {
"$ref": "WindowType",
"optional": true,
"description": "The type of browser window this is."
},
"state": {
"$ref": "WindowState",
"optional": true,
"description": "The state of this browser window."
},
"alwaysOnTop": {
"type": "boolean",
"description": "Whether the window is set to be always on top."
},
"sessionId": {
"type": "string",
"optional": true,
"description": "The session ID used to uniquely identify a Window obtained from the $(ref:sessions) API."
}
}
},
{
"id": "CreateType",
"type": "string",
"description": "Specifies what type of browser window to create. The 'panel' and 'detached_panel' types create a popup unless the '--enable-panels' flag is set.",
"enum": ["normal", "popup", "panel", "detached_panel"]
}
],
"properties": {
"WINDOW_ID_NONE": {
"value": -1,
"description": "The windowId value that represents the absence of a browser window."
},
"WINDOW_ID_CURRENT": {
"value": -2,
"description": "The windowId value that represents the $(topic:current-window)[current window]."
}
},
"functions": [
{
"name": "get",
"type": "function",
"description": "Gets details about a window.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": -2
},
{
"type": "object",
"name": "getInfo",
"optional": true,
"description": "",
"properties": {
"populate": {
"type": "boolean",
"optional": true,
"description": "If true, the $(ref:windows.Window) object will have a <var>tabs</var> property that contains a list of the $(ref:tabs.Tab) objects. The <code>Tab</code> objects only contain the <code>url</code>, <code>title</code> and <code>favIconUrl</code> properties if the extension's manifest file includes the <code>\"tabs\"</code> permission."
},
"windowTypes": {
"type": "array",
"items": {
"$ref": "WindowType"
},
"optional": true,
"description": "If set, the $(ref:windows.Window) returned will be filtered based on its type. If unset the default filter is set to <code>['app', 'normal', 'panel', 'popup']</code>, with <code>'app'</code> and <code>'panel'</code> window types limited to the extension's own windows."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "window",
"$ref": "Window"
}
]
}
]
},
{
"name": "getCurrent",
"type": "function",
"description": "Gets the $(topic:current-window)[current window].",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "getInfo",
"optional": true,
"description": "",
"properties": {
"populate": {
"type": "boolean",
"optional": true,
"description": "If true, the $(ref:windows.Window) object will have a <var>tabs</var> property that contains a list of the $(ref:tabs.Tab) objects. The <code>Tab</code> objects only contain the <code>url</code>, <code>title</code> and <code>favIconUrl</code> properties if the extension's manifest file includes the <code>\"tabs\"</code> permission."
},
"windowTypes": {
"type": "array",
"items": { "$ref": "WindowType" },
"optional": true,
"description": "If set, the $(ref:windows.Window) returned will be filtered based on its type. If unset the default filter is set to <code>['app', 'normal', 'panel', 'popup']</code>, with <code>'app'</code> and <code>'panel'</code> window types limited to the extension's own windows."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "window",
"$ref": "Window"
}
]
}
]
},
{
"name": "getLastFocused",
"type": "function",
"description": "Gets the window that was most recently focused &mdash; typically the window 'on top'.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "getInfo",
"optional": true,
"description": "",
"properties": {
"populate": {
"type": "boolean",
"optional": true,
"description": "If true, the $(ref:windows.Window) object will have a <var>tabs</var> property that contains a list of the $(ref:tabs.Tab) objects. The <code>Tab</code> objects only contain the <code>url</code>, <code>title</code> and <code>favIconUrl</code> properties if the extension's manifest file includes the <code>\"tabs\"</code> permission."
},
"windowTypes": {
"type": "array",
"items": { "$ref": "WindowType" },
"optional": true,
"description": "If set, the $(ref:windows.Window) returned will be filtered based on its type. If unset the default filter is set to <code>['app', 'normal', 'panel', 'popup']</code>, with <code>'app'</code> and <code>'panel'</code> window types limited to the extension's own windows."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "window",
"$ref": "Window"
}
]
}
]
},
{
"name": "getAll",
"type": "function",
"description": "Gets all windows.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "getInfo",
"optional": true,
"description": "",
"properties": {
"populate": {
"type": "boolean",
"optional": true,
"description": "If true, each $(ref:windows.Window) object will have a <var>tabs</var> property that contains a list of the $(ref:tabs.Tab) objects for that window. The <code>Tab</code> objects only contain the <code>url</code>, <code>title</code> and <code>favIconUrl</code> properties if the extension's manifest file includes the <code>\"tabs\"</code> permission."
},
"windowTypes": {
"type": "array",
"items": { "$ref": "WindowType" },
"optional": true,
"description": "If set, the $(ref:windows.Window) returned will be filtered based on its type. If unset the default filter is set to <code>['app', 'normal', 'panel', 'popup']</code>, with <code>'app'</code> and <code>'panel'</code> window types limited to the extension's own windows."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "windows",
"type": "array",
"items": { "$ref": "Window" }
}
]
}
]
},
{
"name": "create",
"type": "function",
"description": "Creates (opens) a new browser with any optional sizing, position or default URL provided.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "createData",
"optional": true,
"default": {},
"properties": {
"url": {
"description": "A URL or array of URLs to open as tabs in the window. Fully-qualified URLs must include a scheme (i.e. 'http://www.google.com', not 'www.google.com'). Relative URLs will be relative to the current page within the extension. Defaults to the New Tab Page.",
"optional": true,
"choices": [
{ "type": "string", "format": "relativeUrl" },
{
"type": "array",
"items": { "type": "string", "format": "relativeUrl" }
}
]
},
"tabId": {
"type": "integer",
"minimum": 0,
"optional": true,
"description": "The id of the tab for which you want to adopt to the new window."
},
"left": {
"type": "integer",
"optional": true,
"description": "The number of pixels to position the new window from the left edge of the screen. If not specified, the new window is offset naturally from the last focused window. This value is ignored for panels."
},
"top": {
"type": "integer",
"optional": true,
"description": "The number of pixels to position the new window from the top edge of the screen. If not specified, the new window is offset naturally from the last focused window. This value is ignored for panels."
},
"width": {
"type": "integer",
"minimum": 0,
"optional": true,
"description": "The width in pixels of the new window, including the frame. If not specified defaults to a natural width."
},
"height": {
"type": "integer",
"minimum": 0,
"optional": true,
"description": "The height in pixels of the new window, including the frame. If not specified defaults to a natural height."
},
"focused": {
"unsupported": true,
"type": "boolean",
"optional": true,
"description": "If true, opens an active window. If false, opens an inactive window."
},
"incognito": {
"type": "boolean",
"optional": true,
"description": "Whether the new window should be an incognito window."
},
"type": {
"$ref": "CreateType",
"optional": true,
"description": "Specifies what type of browser window to create. The 'panel' and 'detached_panel' types create a popup unless the '--enable-panels' flag is set."
},
"state": {
"$ref": "WindowState",
"optional": true,
"description": "The initial state of the window. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined with 'left', 'top', 'width' or 'height'."
},
"allowScriptsToClose": {
"type": "boolean",
"optional": true,
"description": "Allow scripts to close the window."
}
},
"optional": true
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"name": "window",
"$ref": "Window",
"description": "Contains details about the created window.",
"optional": true
}
]
}
]
},
{
"name": "update",
"type": "function",
"description": "Updates the properties of a window. Specify only the properties that you want to change; unspecified properties will be left unchanged.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": -2
},
{
"type": "object",
"name": "updateInfo",
"properties": {
"left": {
"type": "integer",
"optional": true,
"description": "The offset from the left edge of the screen to move the window to in pixels. This value is ignored for panels."
},
"top": {
"type": "integer",
"optional": true,
"description": "The offset from the top edge of the screen to move the window to in pixels. This value is ignored for panels."
},
"width": {
"type": "integer",
"minimum": 0,
"optional": true,
"description": "The width to resize the window to in pixels. This value is ignored for panels."
},
"height": {
"type": "integer",
"minimum": 0,
"optional": true,
"description": "The height to resize the window to in pixels. This value is ignored for panels."
},
"focused": {
"type": "boolean",
"optional": true,
"description": "If true, brings the window to the front. If false, brings the next window in the z-order to the front."
},
"drawAttention": {
"type": "boolean",
"optional": true,
"description": "If true, causes the window to be displayed in a manner that draws the user's attention to the window, without changing the focused window. The effect lasts until the user changes focus to the window. This option has no effect if the window already has focus. Set to false to cancel a previous draw attention request."
},
"state": {
"$ref": "WindowState",
"optional": true,
"description": "The new state of the window. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined with 'left', 'top', 'width' or 'height'."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"name": "window",
"$ref": "Window"
}
]
}
]
},
{
"name": "remove",
"type": "function",
"description": "Removes (closes) a window, and all the tabs inside it.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": 0
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
}
],
"events": [
{
"name": "onCreated",
"type": "function",
"description": "Fired when a window is created.",
"filters": [
{
"name": "windowTypes",
"type": "array",
"items": { "$ref": "WindowType" },
"description": "Conditions that the window's type being created must satisfy. By default it will satisfy <code>['app', 'normal', 'panel', 'popup']</code>, with <code>'app'</code> and <code>'panel'</code> window types limited to the extension's own windows."
}
],
"parameters": [
{
"$ref": "Window",
"name": "window",
"description": "Details of the window that was created."
}
]
},
{
"name": "onRemoved",
"type": "function",
"description": "Fired when a window is removed (closed).",
"filters": [
{
"name": "windowTypes",
"type": "array",
"items": { "$ref": "WindowType" },
"description": "Conditions that the window's type being removed must satisfy. By default it will satisfy <code>['app', 'normal', 'panel', 'popup']</code>, with <code>'app'</code> and <code>'panel'</code> window types limited to the extension's own windows."
}
],
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": 0,
"description": "ID of the removed window."
}
]
},
{
"name": "onFocusChanged",
"type": "function",
"description": "Fired when the currently focused window changes. Will be $(ref:windows.WINDOW_ID_NONE) if all browser windows have lost focus. Note: On some Linux window managers, WINDOW_ID_NONE will always be sent immediately preceding a switch from one browser window to another.",
"filters": [
{
"name": "windowTypes",
"type": "array",
"items": { "$ref": "WindowType" },
"description": "Conditions that the window's type being removed must satisfy. By default it will satisfy <code>['app', 'normal', 'panel', 'popup']</code>, with <code>'app'</code> and <code>'panel'</code> window types limited to the extension's own windows."
}
],
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": -1,
"description": "ID of the newly focused window."
}
]
}
]
}
]

View File

@ -16,3 +16,16 @@ AC_SUBST(MC_BASILISK)
dnl Optional parts of the build.
dnl ========================================================
dnl = Disable WebExtensions
dnl ========================================================
MOZ_ARG_DISABLE_BOOL(webextensions,
[ --disable-webextensions Disable WebExtensions],
MOZ_WEBEXTENSIONS=,
MOZ_WEBEXTENSIONS=1)
if test -n "$MOZ_WEBEXTENSIONS"; then
AC_DEFINE(MOZ_WEBEXTENSIONS)
fi
AC_SUBST(MOZ_WEBEXTENSIONS)

View File

@ -51,6 +51,7 @@ MOZ_APP_STATIC_INI=1
MOZ_WEBGL_CONFORMANT=1
MOZ_JSDOWNLOADS=1
MOZ_WEBRTC=1
MOZ_WEBEXTENSIONS=1
MOZ_DEVTOOLS=1
MOZ_SERVICES_COMMON=1
MOZ_SERVICES_SYNC=1

View File

@ -397,9 +397,15 @@
@RESPATH@/components/addonManager.js
@RESPATH@/components/amContentHandler.js
@RESPATH@/components/amInstallTrigger.js
#ifdef MOZ_WEBEXTENSIONS
@RESPATH@/components/amWebAPI.js
#endif
@RESPATH@/components/amWebInstallListener.js
@RESPATH@/components/nsBlocklistService.js
@RESPATH@/components/blocklist.manifest
#ifdef MOZ_WEBEXTENSIONS
@RESPATH@/components/nsBlocklistServiceContent.js
#endif
#ifdef MOZ_UPDATER
@RESPATH@/components/nsUpdateService.manifest
@RESPATH@/components/nsUpdateService.js
@ -548,6 +554,12 @@
@RESPATH@/components/TestInterfaceJSMaplike.js
#endif
#ifdef MOZ_WEBEXTENSIONS
; [Extensions]
@RESPATH@/components/extensions-toolkit.manifest
@RESPATH@/browser/components/extensions-browser.manifest
#endif
; Modules
@RESPATH@/browser/modules/*
@RESPATH@/modules/*

View File

@ -67,6 +67,6 @@ DevToolsModules(
'worker.js',
)
FINAL_TARGET_FILES.chrome.devtools.modules.devtools.server.actors += [
FINAL_TARGET_PP_FILES.chrome.devtools.modules.devtools.server.actors += [
'webbrowser.js',
]

View File

@ -30,6 +30,9 @@ loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker
loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "devtools/server/actors/worker", true);
loader.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true);
loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
#ifdef MOZ_WEBEXTENSIONS
loader.lazyImporter(this, "ExtensionContent", "resource://gre/modules/ExtensionContent.jsm");
#endif
// Assumptions on events module:
// events needs to be dispatched synchronously,
@ -981,6 +984,21 @@ TabActor.prototype = {
return null;
},
#ifdef MOZ_WEBEXTENSIONS
/**
* Getter for the WebExtensions ContentScript globals related to the
* current tab content's DOM window.
*/
get webextensionsContentScriptGlobals() {
// Ignore xpcshell runtime which spawn TabActors without a window.
if (this.window) {
return ExtensionContent.getContentScriptGlobalsForWindow(this.window);
}
return [];
},
#endif
/**
* Getter for the list of all content DOM windows in this tabActor
* @return {Array}

View File

@ -0,0 +1,12 @@
[ Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
Constructor(DOMString type, AddonEventInit eventInitDict)]
interface AddonEvent : Event {
readonly attribute DOMString id;
readonly attribute boolean needsRestart;
};
dictionary AddonEventInit : EventInit {
required DOMString id;
required boolean needsRestart;
};

View File

@ -0,0 +1,91 @@
/* 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/.
*/
/* We need a JSImplementation but cannot get one without a contract ID.
Since Addon and AddonInstall are only ever created from JS they don't need
real contract IDs. */
[ChromeOnly, JSImplementation="dummy"]
interface Addon {
// The add-on's ID.
readonly attribute DOMString id;
// The add-on's version.
readonly attribute DOMString version;
// The add-on's type (extension, theme, etc.).
readonly attribute DOMString type;
// The add-on's name in the current locale.
readonly attribute DOMString name;
// The add-on's description in the current locale.
readonly attribute DOMString description;
// If the user has enabled this add-on, note that it still may not be running
// depending on whether enabling requires a restart or if the add-on is
// incompatible in some way.
readonly attribute boolean isEnabled;
// If the add-on is currently active in the browser.
readonly attribute boolean isActive;
// If the add-on may be uninstalled
readonly attribute boolean canUninstall;
Promise<boolean> uninstall();
Promise<void> setEnabled(boolean value);
};
[ChromeOnly, JSImplementation="dummy"]
interface AddonInstall : EventTarget {
// One of the STATE_* symbols from AddonManager.jsm
readonly attribute DOMString state;
// One of the ERROR_* symbols from AddonManager.jsm, or null
readonly attribute DOMString? error;
// How many bytes have been downloaded
readonly attribute long long progress;
// How many total bytes will need to be downloaded or -1 if unknown
readonly attribute long long maxProgress;
Promise<void> install();
Promise<void> cancel();
};
dictionary addonInstallOptions {
required DOMString url;
// If a non-empty string is passed for "hash", it is used to verify the
// checksum of the downloaded XPI before installing. If is omitted or if
// it is null or empty string, no checksum verification is performed.
DOMString? hash = null;
};
[HeaderFile="mozilla/AddonManagerWebAPI.h",
Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
NavigatorProperty="mozAddonManager",
JSImplementation="@mozilla.org/addon-web-api/manager;1"]
interface AddonManager : EventTarget {
/**
* Gets information about an add-on
*
* @param id
* The ID of the add-on to test for.
* @return A promise. It will resolve to an Addon if the add-on is installed.
*/
Promise<Addon> getAddonByID(DOMString id);
/**
* Creates an AddonInstall object for a given URL.
*
* @param options
* Only one supported option: 'url', the URL of the addon to install.
* @return A promise that resolves to an instance of AddonInstall.
*/
Promise<AddonInstall> createInstall(optional addonInstallOptions options);
/* Hooks for managing event listeners */
[ChromeOnly]
void eventListenerWasAdded(DOMString type);
[ChromeOnly]
void eventListenerWasRemoved(DOMString type);
};
[ChromeOnly,Exposed=System,HeaderFile="mozilla/AddonManagerWebAPI.h"]
interface AddonManagerPermissions {
static boolean isHostPermitted(DOMString host);
};

View File

@ -591,6 +591,9 @@ WEBIDL_FILES = [
'XULElement.webidl',
]
if CONFIG['MOZ_WEBEXTENSIONS']:
WEBIDL_FILES += ['AddonManager.webidl']
if CONFIG['MOZ_AUDIO_CHANNEL_MANAGER']:
WEBIDL_FILES += [
'AudioChannelManager.webidl',
@ -719,6 +722,9 @@ GENERATED_EVENTS_WEBIDL_FILES = [
'WebGLContextEvent.webidl',
]
if CONFIG['MOZ_WEBEXTENSIONS']:
GENERATED_EVENTS_WEBIDL_FILES += ['AddonEvent.webidl']
if CONFIG['MOZ_WEBRTC']:
GENERATED_EVENTS_WEBIDL_FILES += [
'RTCDataChannelEvent.webidl',

View File

@ -1,5 +1,7 @@
component {66354bc9-7ed1-4692-ae1d-8da97d6b205e} nsBlocklistService.js process=main
contract @mozilla.org/extensions/blocklist;1 {66354bc9-7ed1-4692-ae1d-8da97d6b205e} process=main
category profile-after-change nsBlocklistService @mozilla.org/extensions/blocklist;1 process=main
component {e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d} nsBlocklistServiceContent.js process=content
contract @mozilla.org/extensions/blocklist;1 {e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d} process=content
category update-timer nsBlocklistService @mozilla.org/extensions/blocklist;1,getService,blocklist-background-update-timer,extensions.blocklist.interval,86400

View File

@ -6,6 +6,7 @@
EXTRA_COMPONENTS += [
'blocklist.manifest',
'nsBlocklistServiceContent.js',
]
EXTRA_PP_COMPONENTS += [

View File

@ -24,8 +24,13 @@ try {
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
#ifdef MOZ_WEBEXTENSIONS
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
"resource://gre/modules/UpdateUtils.jsm");
#else
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
"resource://gre/modules/UpdateChannel.jsm");
#endif
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
@ -566,7 +571,11 @@ Blocklist.prototype = {
dsURI = dsURI.replace(/%BUILD_TARGET%/g, gApp.OS + "_" + gABI);
dsURI = dsURI.replace(/%OS_VERSION%/g, gOSVersion);
dsURI = dsURI.replace(/%LOCALE%/g, getLocale());
#ifdef MOZ_WEBEXTENSIONS
dsURI = dsURI.replace(/%CHANNEL%/g, UpdateUtils.UpdateChannel);
#else
dsURI = dsURI.replace(/%CHANNEL%/g, UpdateChannel.get());
#endif
dsURI = dsURI.replace(/%PLATFORM_VERSION%/g, gApp.platformVersion);
dsURI = dsURI.replace(/%DISTRIBUTION%/g,
getDistributionPrefValue(PREF_APP_DISTRIBUTION));

View File

@ -0,0 +1,113 @@
/* 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 Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
const kMissingAPIMessage = "Unsupported blocklist call in the child process."
/*
* A lightweight blocklist proxy for the content process that traps plugin
* related blocklist checks and forwards them to the parent. This interface is
* primarily designed to insure overlays work.. it does not control plugin
* or addon loading.
*/
function Blocklist() {
this.init();
}
Blocklist.prototype = {
classID: Components.ID("{e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsIBlocklistService]),
init: function() {
Services.cpmm.addMessageListener("Blocklist:blocklistInvalidated", this);
Services.obs.addObserver(this, "xpcom-shutdown", false);
},
uninit: function() {
Services.cpmm.removeMessageListener("Blocklist:blocklistInvalidated", this);
Services.obs.removeObserver(this, "xpcom-shutdown", false);
},
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "xpcom-shutdown":
this.uninit();
break;
}
},
// Message manager message handlers
receiveMessage: function(aMsg) {
switch (aMsg.name) {
case "Blocklist:blocklistInvalidated":
Services.obs.notifyObservers(null, "blocklist-updated", null);
Services.cpmm.sendAsyncMessage("Blocklist:content-blocklist-updated");
break;
default:
throw new Error("Unknown blocklist message received from content: " + aMsg.name);
}
},
/*
* A helper that queries key data from a plugin or addon object
* and generates a simple data wrapper suitable for ipc. We hand
* these directly to the nsBlockListService in the parent which
* doesn't query for much.. allowing us to get away with this.
*/
flattenObject: function(aTag) {
// Based on debugging the nsBlocklistService, these are the props the
// parent side will check on our objects.
let props = ["name", "description", "filename", "version"];
let dataWrapper = {};
for (let prop of props) {
dataWrapper[prop] = aTag[prop];
}
return dataWrapper;
},
// We support the addon methods here for completeness, but content currently
// only calls getPluginBlocklistState.
isAddonBlocklisted: function(aAddon, aAppVersion, aToolkitVersion) {
return true;
},
getAddonBlocklistState: function(aAddon, aAppVersion, aToolkitVersion) {
return Components.interfaces.nsIBlocklistService.STATE_BLOCKED;
},
// There are a few callers in layout that rely on this.
getPluginBlocklistState: function(aPluginTag, aAppVersion, aToolkitVersion) {
return Services.cpmm.sendSyncMessage("Blocklist:getPluginBlocklistState", {
addonData: this.flattenObject(aPluginTag),
appVersion: aAppVersion,
toolkitVersion: aToolkitVersion
})[0];
},
getAddonBlocklistURL: function(aAddon, aAppVersion, aToolkitVersion) {
throw new Error(kMissingAPIMessage);
},
getPluginBlocklistURL: function(aPluginTag) {
throw new Error(kMissingAPIMessage);
},
getPluginInfoURL: function(aPluginTag) {
throw new Error(kMissingAPIMessage);
}
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Blocklist]);

View File

@ -39,6 +39,9 @@
#include "nsBrowserStatusFilter.h"
#include "mozilla/FinalizationWitnessService.h"
#include "mozilla/NativeOSFileInternals.h"
#ifdef MOZ_WEBEXTENSIONS
#include "mozilla/AddonContentPolicy.h"
#endif
#include "mozilla/AddonPathService.h"
#if defined(XP_WIN)
@ -129,6 +132,9 @@ NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(FinalizationWitnessService, Init)
NS_GENERIC_FACTORY_CONSTRUCTOR(NativeOSFileInternalsService)
NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NativeFileWatcherService, Init)
#ifdef MOZ_WEBEXTENSIONS
NS_GENERIC_FACTORY_CONSTRUCTOR(AddonContentPolicy)
#endif
NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonPathService, AddonPathService::GetInstance)
NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
@ -165,6 +171,9 @@ NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID);
#endif
NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID);
NS_DEFINE_NAMED_CID(NATIVE_OSFILE_INTERNALS_SERVICE_CID);
#ifdef MOZ_WEBEXTENSIONS
NS_DEFINE_NAMED_CID(NS_ADDONCONTENTPOLICY_CID);
#endif
NS_DEFINE_NAMED_CID(NS_ADDON_PATH_SERVICE_CID);
NS_DEFINE_NAMED_CID(NATIVE_FILEWATCHER_SERVICE_CID);
@ -202,6 +211,9 @@ static const Module::CIDEntry kToolkitCIDs[] = {
#endif
{ &kFINALIZATIONWITNESSSERVICE_CID, false, nullptr, FinalizationWitnessServiceConstructor },
{ &kNATIVE_OSFILE_INTERNALS_SERVICE_CID, false, nullptr, NativeOSFileInternalsServiceConstructor },
#ifdef MOZ_WEBEXTENSIONS
{ &kNS_ADDONCONTENTPOLICY_CID, false, nullptr, AddonContentPolicyConstructor },
#endif
{ &kNS_ADDON_PATH_SERVICE_CID, false, nullptr, AddonPathServiceConstructor },
{ &kNATIVE_FILEWATCHER_SERVICE_CID, false, nullptr, NativeFileWatcherServiceConstructor },
{ nullptr }
@ -241,12 +253,18 @@ static const Module::ContractIDEntry kToolkitContracts[] = {
#endif
{ FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID },
{ NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID, &kNATIVE_OSFILE_INTERNALS_SERVICE_CID },
#ifdef MOZ_WEBEXTENSIONS
{ NS_ADDONCONTENTPOLICY_CONTRACTID, &kNS_ADDONCONTENTPOLICY_CID },
#endif
{ NS_ADDONPATHSERVICE_CONTRACTID, &kNS_ADDON_PATH_SERVICE_CID },
{ NATIVE_FILEWATCHER_SERVICE_CONTRACTID, &kNATIVE_FILEWATCHER_SERVICE_CID },
{ nullptr }
};
static const mozilla::Module::CategoryEntry kToolkitCategories[] = {
#ifdef MOZ_WEBEXTENSIONS
{ "content-policy", NS_ADDONCONTENTPOLICY_CONTRACTID, NS_ADDONCONTENTPOLICY_CONTRACTID },
#endif
{ nullptr }
};

View File

@ -66,6 +66,9 @@ DIRS += [
'xulstore'
]
if CONFIG['MOZ_WEBEXTENSIONS']:
DIRS += ['webextensions']
DIRS += ['mozintl']
if not CONFIG['MOZ_FENNEC']:

View File

@ -0,0 +1,494 @@
"use strict";
module.exports = { // eslint-disable-line no-undef
"extends": "../../.eslintrc.js",
"parserOptions": {
"ecmaVersion": 8,
},
"globals": {
"Cc": true,
"Ci": true,
"Components": true,
"Cr": true,
"Cu": true,
"dump": true,
"TextDecoder": false,
"TextEncoder": false,
// Specific to WebExtensions:
"Extension": true,
"ExtensionManagement": true,
"extensions": true,
"global": true,
"NetUtil": true,
"openOptionsPage": true,
"require": false,
"runSafe": true,
"runSafeSync": true,
"runSafeSyncWithoutClone": true,
"Services": true,
"TabManager": true,
"WindowListManager": true,
"XPCOMUtils": true,
},
"rules": {
// Rules from the mozilla plugin
"mozilla/balanced-listeners": "error",
"mozilla/no-aArgs": "error",
"mozilla/no-cpows-in-tests": "warn",
"mozilla/var-only-at-top-level": "warn",
"valid-jsdoc": ["error", {
"prefer": {
"return": "returns",
},
"preferType": {
"Boolean": "boolean",
"Number": "number",
"String": "string",
"bool": "boolean",
},
"requireParamDescription": false,
"requireReturn": false,
"requireReturnDescription": false,
}],
// Braces only needed for multi-line arrow function blocks
// "arrow-body-style": ["error", "as-needed"],
// Require spacing around =>
"arrow-spacing": "error",
// Always require spacing around a single line block
"block-spacing": "warn",
// Forbid spaces inside the square brackets of array literals.
"array-bracket-spacing": ["error", "never"],
// Forbid spaces inside the curly brackets of object literals.
"object-curly-spacing": ["error", "never"],
// No space padding in parentheses
"space-in-parens": ["error", "never"],
// Enforce one true brace style (opening brace on the same line) and avoid
// start and end braces on the same line.
"brace-style": ["error", "1tbs", {"allowSingleLine": true}],
// No space before always a space after a comma
"comma-spacing": ["error", {"before": false, "after": true}],
// Commas at the end of the line not the start
"comma-style": "error",
// Don't require spaces around computed properties
"computed-property-spacing": ["error", "never"],
// Functions are not required to consistently return something or nothing
"consistent-return": "off",
// Require braces around blocks that start a new line
"curly": ["error", "all"],
// Always require a trailing EOL
"eol-last": "error",
// Require function* name()
"generator-star-spacing": ["error", {"before": false, "after": true}],
// Two space indent
"indent": ["error", 2, {"SwitchCase": 1}],
// Space after colon not before in property declarations
"key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "minimum"}],
// Require spaces before and after finally, catch, etc.
"keyword-spacing": "error",
// Unix linebreaks
"linebreak-style": ["error", "unix"],
// Always require parenthesis for new calls
"new-parens": "error",
// Use [] instead of Array()
"no-array-constructor": "error",
// No duplicate arguments in function declarations
"no-dupe-args": "error",
// No duplicate keys in object declarations
"no-dupe-keys": "error",
// No duplicate cases in switch statements
"no-duplicate-case": "error",
// If an if block ends with a return no need for an else block
// "no-else-return": "error",
// Disallow empty statements. This will report an error for:
// try { something(); } catch (e) {}
// but will not report it for:
// try { something(); } catch (e) { /* Silencing the error because ...*/ }
// which is a valid use case.
"no-empty": "error",
// No empty character classes in regex
"no-empty-character-class": "error",
// Disallow empty destructuring
"no-empty-pattern": "error",
// No assiging to exception variable
"no-ex-assign": "error",
// No using !! where casting to boolean is already happening
"no-extra-boolean-cast": "warn",
// No double semicolon
"no-extra-semi": "error",
// No overwriting defined functions
"no-func-assign": "error",
// No invalid regular expresions
"no-invalid-regexp": "error",
// No odd whitespace characters
"no-irregular-whitespace": "error",
// No single if block inside an else block
"no-lonely-if": "warn",
// No mixing spaces and tabs in indent
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
// Disallow use of multiple spaces (sometimes used to align const values,
// array or object items, etc.). It's hard to maintain and doesn't add that
// much benefit.
"no-multi-spaces": "warn",
// No reassigning native JS objects
"no-native-reassign": "error",
// Nested ternary statements are confusing
"no-nested-ternary": "error",
// Use {} instead of new Object()
"no-new-object": "error",
// No Math() or JSON()
"no-obj-calls": "error",
// No octal literals
"no-octal": "error",
// No redeclaring variables
"no-redeclare": "error",
// No unnecessary comparisons
"no-self-compare": "error",
// No spaces between function name and parentheses
"no-spaced-func": "warn",
// No trailing whitespace
"no-trailing-spaces": "error",
// Error on newline where a semicolon is needed
"no-unexpected-multiline": "error",
// No unreachable statements
"no-unreachable": "error",
// No expressions where a statement is expected
"no-unused-expressions": "error",
// No declaring variables that are never used
"no-unused-vars": ["error", {"args": "none", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}],
// No using variables before defined
"no-use-before-define": "error",
// No using with
"no-with": "error",
// Always require semicolon at end of statement
"semi": ["error", "always"],
// Require space before blocks
"space-before-blocks": "error",
// Never use spaces before function parentheses
"space-before-function-paren": ["error", {"anonymous": "never", "named": "never"}],
// Require spaces around operators, except for a|0.
"space-infix-ops": ["error", {"int32Hint": true}],
// ++ and -- should not need spacing
"space-unary-ops": ["warn", {"nonwords": false, "words": true, "overrides": {"typeof": false}}],
// No comparisons to NaN
"use-isnan": "error",
// Only check typeof against valid results
"valid-typeof": "error",
// Disallow using variables outside the blocks they are defined (especially
// since only let and const are used, see "no-var").
"block-scoped-var": "error",
// Allow trailing commas for easy list extension. Having them does not
// impair readability, but also not required either.
"comma-dangle": ["error", "always-multiline"],
// Warn about cyclomatic complexity in functions.
"complexity": "warn",
// Don't warn for inconsistent naming when capturing this (not so important
// with auto-binding fat arrow functions).
// "consistent-this": ["error", "self"],
// Don't require a default case in switch statements. Avoid being forced to
// add a bogus default when you know all possible cases are handled.
"default-case": "off",
// Enforce dots on the next line with property name.
"dot-location": ["error", "property"],
// Encourage the use of dot notation whenever possible.
"dot-notation": "error",
// Allow using == instead of ===, in the interest of landing something since
// the devtools codebase is split on convention here.
"eqeqeq": "off",
// Don't require function expressions to have a name.
// This makes the code more verbose and hard to read. Our engine already
// does a fantastic job assigning a name to the function, which includes
// the enclosing function name, and worst case you have a line number that
// you can just look up.
"func-names": "off",
// Allow use of function declarations and expressions.
"func-style": "off",
// Don't enforce the maximum depth that blocks can be nested. The complexity
// rule is a better rule to check this.
"max-depth": "off",
// Maximum length of a line.
// Disabled because we exceed this in too many places.
"max-len": [0, 80],
// Maximum depth callbacks can be nested.
"max-nested-callbacks": ["error", 4],
// Don't limit the number of parameters that can be used in a function.
"max-params": "off",
// Don't limit the maximum number of statement allowed in a function. We
// already have the complexity rule that's a better measurement.
"max-statements": "off",
// Don't require a capital letter for constructors, only check if all new
// operators are followed by a capital letter. Don't warn when capitalized
// functions are used without the new operator.
"new-cap": ["off", {"capIsNew": false}],
// Allow use of bitwise operators.
"no-bitwise": "off",
// Disallow use of arguments.caller or arguments.callee.
"no-caller": "error",
// Disallow the catch clause parameter name being the same as a variable in
// the outer scope, to avoid confusion.
"no-catch-shadow": "off",
// Disallow assignment in conditional expressions.
"no-cond-assign": "error",
// Disallow using the console API.
"no-console": "error",
// Allow using constant expressions in conditions like while (true)
"no-constant-condition": "off",
// Allow use of the continue statement.
"no-continue": "off",
// Disallow control characters in regular expressions.
"no-control-regex": "error",
// Disallow use of debugger.
"no-debugger": "error",
// Disallow deletion of variables (deleting properties is fine).
"no-delete-var": "error",
// Allow division operators explicitly at beginning of regular expression.
"no-div-regex": "off",
// Disallow use of eval(). We have other APIs to evaluate code in content.
"no-eval": "error",
// Disallow adding to native types
"no-extend-native": "error",
// Disallow unnecessary function binding.
"no-extra-bind": "error",
// Allow unnecessary parentheses, as they may make the code more readable.
"no-extra-parens": "off",
// Disallow fallthrough of case statements, except if there is a comment.
"no-fallthrough": "error",
// Allow the use of leading or trailing decimal points in numeric literals.
"no-floating-decimal": "off",
// Allow comments inline after code.
"no-inline-comments": "off",
// Disallow use of labels for anything other then loops and switches.
"no-labels": ["error", {"allowLoop": true}],
// Disallow use of multiline strings (use template strings instead).
"no-multi-str": "warn",
// Disallow multiple empty lines.
"no-multiple-empty-lines": [1, {"max": 2}],
// Allow reassignment of function parameters.
"no-param-reassign": "off",
// Allow string concatenation with __dirname and __filename (not a node env).
"no-path-concat": "off",
// Allow use of unary operators, ++ and --.
"no-plusplus": "off",
// Allow using process.env (not a node environment).
"no-process-env": "off",
// Allow using process.exit (not a node environment).
"no-process-exit": "off",
// Disallow usage of __proto__ property.
"no-proto": "error",
// Disallow multiple spaces in a regular expression literal.
"no-regex-spaces": "error",
// Allow reserved words being used as object literal keys.
"no-reserved-keys": "off",
// Don't restrict usage of specified node modules (not a node environment).
"no-restricted-modules": "off",
// Disallow use of assignment in return statement. It is preferable for a
// single line of code to have only one easily predictable effect.
"no-return-assign": "error",
// Don't warn about declaration of variables already declared in the outer scope.
"no-shadow": "off",
// Disallow shadowing of names such as arguments.
"no-shadow-restricted-names": "error",
// Allow use of synchronous methods (not a node environment).
"no-sync": "off",
// Allow the use of ternary operators.
"no-ternary": "off",
// Disallow throwing literals (eg. throw "error" instead of
// throw new Error("error")).
"no-throw-literal": "error",
// Disallow use of undeclared variables unless mentioned in a /* global */
// block. Note that globals from head.js are automatically imported in tests
// by the import-headjs-globals rule form the mozilla eslint plugin.
"no-undef": "error",
// Allow dangling underscores in identifiers (for privates).
"no-underscore-dangle": "off",
// Allow use of undefined variable.
"no-undefined": "off",
// Disallow the use of Boolean literals in conditional expressions.
"no-unneeded-ternary": "error",
// We use var-only-at-top-level instead of no-var as we allow top level
// vars.
"no-var": "off",
// Allow using TODO/FIXME comments.
"no-warning-comments": "off",
// Don't require method and property shorthand syntax for object literals.
// We use this in the code a lot, but not consistently, and this seems more
// like something to check at code review time.
"object-shorthand": "off",
// Allow more than one variable declaration per function.
"one-var": "off",
// Disallow padding within blocks.
"padded-blocks": ["warn", "never"],
// Don't require quotes around object literal property names.
"quote-props": "off",
// Double quotes should be used.
"quotes": ["warn", "double", {"avoidEscape": true, "allowTemplateLiterals": true}],
// Require use of the second argument for parseInt().
"radix": "error",
// Enforce spacing after semicolons.
"semi-spacing": ["error", {"before": false, "after": true}],
// Don't require to sort variables within the same declaration block.
// Anyway, one-var is disabled.
"sort-vars": "off",
// Require a space immediately following the // in a line comment.
"spaced-comment": ["error", "always"],
// Require "use strict" to be defined globally in the script.
"strict": ["error", "global"],
// Allow vars to be declared anywhere in the scope.
"vars-on-top": "off",
// Don't require immediate function invocation to be wrapped in parentheses.
"wrap-iife": "off",
// Don't require regex literals to be wrapped in parentheses (which
// supposedly prevent them from being mistaken for division operators).
"wrap-regex": "off",
// Disallow Yoda conditions (where literal value comes first).
"yoda": "error",
// disallow use of eval()-like methods
"no-implied-eval": "error",
// Disallow function or variable declarations in nested blocks
"no-inner-declarations": "error",
// Disallow usage of __iterator__ property
"no-iterator": "error",
// Disallow labels that share a name with a variable
"no-label-var": "error",
// Disallow creating new instances of String, Number, and Boolean
"no-new-wrappers": "error",
},
};

View File

@ -0,0 +1,902 @@
/* 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";
this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"];
/* globals Extension ExtensionData */
/*
* 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.importGlobalProperties(["TextEncoder"]);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
"resource://gre/modules/ExtensionAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon",
"resource://testing-common/ExtensionTestCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
"resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
"resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "require",
"resource://devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/ExtensionContent.jsm");
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
Cu.import("resource://gre/modules/ExtensionParent.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
var {
GlobalManager,
ParentAPIManager,
apiManager: Management,
} = ExtensionParent;
const {
EventEmitter,
LocaleData,
getUniqueId,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
const LOGGER_ID_BASE = "addons.webextension.";
const UUID_MAP_PREF = "extensions.webextensions.uuids";
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
const COMMENT_REGEXP = new RegExp(String.raw`
^
(
(?:
[^"\n] |
" (?:[^"\\\n] | \\.)* "
)*?
)
//.*
`.replace(/\s+/g, ""), "gm");
// All moz-extension URIs use a machine-specific UUID rather than the
// extension's own ID in the host component. This makes it more
// difficult for web pages to detect whether a user has a given add-on
// installed (by trying to load a moz-extension URI referring to a
// web_accessible_resource from the extension). UUIDMap.get()
// returns the UUID for a given add-on ID.
var UUIDMap = {
_read() {
let pref = Preferences.get(UUID_MAP_PREF, "{}");
try {
return JSON.parse(pref);
} catch (e) {
Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
return {};
}
},
_write(map) {
Preferences.set(UUID_MAP_PREF, JSON.stringify(map));
},
get(id, create = true) {
let map = this._read();
if (id in map) {
return map[id];
}
let uuid = null;
if (create) {
uuid = uuidGen.generateUUID().number;
uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
map[id] = uuid;
this._write(map);
}
return uuid;
},
remove(id) {
let map = this._read();
delete map[id];
this._write(map);
},
};
// This is the old interface that UUIDMap replaced, to be removed when
// the references listed in bug 1291399 are updated.
/* exported getExtensionUUID */
function getExtensionUUID(id) {
return UUIDMap.get(id, true);
}
// For extensions that have called setUninstallURL(), send an event
// so the browser can display the URL.
var UninstallObserver = {
initialized: false,
init() {
if (!this.initialized) {
AddonManager.addAddonListener(this);
XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false);
XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false);
this.initialized = true;
}
},
onUninstalling(addon) {
let extension = GlobalManager.extensionMap.get(addon.id);
if (extension) {
// Let any other interested listeners respond
// (e.g., display the uninstall URL)
Management.emit("uninstall", extension);
}
},
onUninstalled(addon) {
let uuid = UUIDMap.get(addon.id, false);
if (!uuid) {
return;
}
if (!this.leaveStorage) {
// Clear browser.local.storage
ExtensionStorage.clear(addon.id);
// Clear any IndexedDB storage created by the extension
let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`);
let principal = Services.scriptSecurityManager.createCodebasePrincipal(
baseURI, {addonId: addon.id}
);
Services.qms.clearStoragesForPrincipal(principal);
// Clear localStorage created by the extension
let attrs = JSON.stringify({addonId: addon.id});
Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs);
}
if (!this.leaveUuid) {
// Clear the entry in the UUID map
UUIDMap.remove(addon.id);
}
},
};
UninstallObserver.init();
// Represents the data contained in an extension, contained either
// in a directory or a zip file, which may or may not be installed.
// This class implements the functionality of the Extension class,
// primarily related to manifest parsing and localization, which is
// useful prior to extension installation or initialization.
//
// No functionality of this class is guaranteed to work before
// |readManifest| has been called, and completed.
this.ExtensionData = class {
constructor(rootURI) {
this.rootURI = rootURI;
this.manifest = null;
this.id = null;
this.uuid = null;
this.localeData = null;
this._promiseLocales = null;
this.apiNames = new Set();
this.dependencies = new Set();
this.permissions = new Set();
this.errors = [];
}
get builtinMessages() {
return null;
}
get logger() {
let id = this.id || "<unknown>";
return Log.repository.getLogger(LOGGER_ID_BASE + id);
}
// Report an error about the extension's manifest file.
manifestError(message) {
this.packagingError(`Reading manifest: ${message}`);
}
// Report an error about the extension's general packaging.
packagingError(message) {
this.errors.push(message);
this.logger.error(`Loading extension '${this.id}': ${message}`);
}
/**
* Returns the moz-extension: URL for the given path within this
* extension.
*
* Must not be called unless either the `id` or `uuid` property has
* already been set.
*
* @param {string} path The path portion of the URL.
* @returns {string}
*/
getURL(path = "") {
if (!(this.id || this.uuid)) {
throw new Error("getURL may not be called before an `id` or `uuid` has been set");
}
if (!this.uuid) {
this.uuid = UUIDMap.get(this.id);
}
return `moz-extension://${this.uuid}/${path}`;
}
readDirectory(path) {
return Task.spawn(function* () {
if (this.rootURI instanceof Ci.nsIFileURL) {
let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
let iter = new OS.File.DirectoryIterator(fullPath);
let results = [];
try {
yield iter.forEach(entry => {
results.push(entry);
});
} catch (e) {
// Always return a list, even if the directory does not exist (or is
// not a directory) for symmetry with the ZipReader behavior.
}
iter.close();
return results;
}
// FIXME: We need a way to do this without main thread IO.
let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file;
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
zipReader.open(file);
try {
let results = [];
// Normalize the directory path.
path = `${uri.JAREntry}/${path}`;
path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
// Escape pattern metacharacters.
let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
let enumerator = zipReader.findEntries(pattern + "*");
while (enumerator.hasMore()) {
let name = enumerator.getNext();
if (!name.startsWith(path)) {
throw new Error("Unexpected ZipReader entry");
}
// The enumerator returns the full path of all entries.
// Trim off the leading path, and filter out entries from
// subdirectories.
name = name.slice(path.length);
if (name && !/\/./.test(name)) {
results.push({
name: name.replace("/", ""),
isDir: name.endsWith("/"),
});
}
}
return results;
} finally {
zipReader.close();
}
}.bind(this));
}
readJSON(path) {
return new Promise((resolve, reject) => {
let uri = this.rootURI.resolve(`./${path}`);
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
if (!Components.isSuccessCode(status)) {
// Convert status code to a string
let e = Components.Exception("", status);
reject(new Error(`Error while loading '${uri}' (${e.name})`));
return;
}
try {
let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(),
{charset: "utf-8"});
text = text.replace(COMMENT_REGEXP, "$1");
resolve(JSON.parse(text));
} catch (e) {
reject(e);
}
});
});
}
// Reads the extension's |manifest.json| file, and stores its
// parsed contents in |this.manifest|.
readManifest() {
return Promise.all([
this.readJSON("manifest.json"),
Management.lazyInit(),
]).then(([manifest]) => {
this.manifest = manifest;
this.rawManifest = manifest;
if (manifest && manifest.default_locale) {
return this.initLocale();
}
}).then(() => {
let context = {
url: this.baseURI && this.baseURI.spec,
principal: this.principal,
logError: error => {
this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
},
preprocessors: {},
};
if (this.localeData) {
context.preprocessors.localize = (value, context) => this.localize(value);
}
let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
if (normalized.error) {
this.manifestError(normalized.error);
} else {
this.manifest = normalized.value;
}
try {
// Do not override the add-on id that has been already assigned.
if (!this.id && this.manifest.applications.gecko.id) {
this.id = this.manifest.applications.gecko.id;
}
} catch (e) {
// Errors are handled by the type checks above.
}
let permissions = this.manifest.permissions || [];
let whitelist = [];
for (let perm of permissions) {
this.permissions.add(perm);
let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
if (!match) {
whitelist.push(perm);
} else if (match[1] == "experiments" && match[2]) {
this.apiNames.add(match[2]);
}
}
this.whiteListedHosts = new MatchPattern(whitelist);
for (let api of this.apiNames) {
this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
}
return this.manifest;
});
}
localizeMessage(...args) {
return this.localeData.localizeMessage(...args);
}
localize(...args) {
return this.localeData.localize(...args);
}
// If a "default_locale" is specified in that manifest, returns it
// as a Gecko-compatible locale string. Otherwise, returns null.
get defaultLocale() {
if (this.manifest.default_locale != null) {
return this.normalizeLocaleCode(this.manifest.default_locale);
}
return null;
}
// Normalizes a Chrome-compatible locale code to the appropriate
// Gecko-compatible variant. Currently, this means simply
// replacing underscores with hyphens.
normalizeLocaleCode(locale) {
return String.replace(locale, /_/g, "-");
}
// Reads the locale file for the given Gecko-compatible locale code, and
// stores its parsed contents in |this.localeMessages.get(locale)|.
readLocaleFile(locale) {
return Task.spawn(function* () {
let locales = yield this.promiseLocales();
let dir = locales.get(locale) || locale;
let file = `_locales/${dir}/messages.json`;
try {
let messages = yield this.readJSON(file);
return this.localeData.addLocale(locale, messages, this);
} catch (e) {
this.packagingError(`Loading locale file ${file}: ${e}`);
return new Map();
}
}.bind(this));
}
// Reads the list of locales available in the extension, and returns a
// Promise which resolves to a Map upon completion.
// Each map key is a Gecko-compatible locale code, and each value is the
// "_locales" subdirectory containing that locale:
//
// Map(gecko-locale-code -> locale-directory-name)
promiseLocales() {
if (!this._promiseLocales) {
this._promiseLocales = Task.spawn(function* () {
let locales = new Map();
let entries = yield this.readDirectory("_locales");
for (let file of entries) {
if (file.isDir) {
let locale = this.normalizeLocaleCode(file.name);
locales.set(locale, file.name);
}
}
this.localeData = new LocaleData({
defaultLocale: this.defaultLocale,
locales,
builtinMessages: this.builtinMessages,
});
return locales;
}.bind(this));
}
return this._promiseLocales;
}
// Reads the locale messages for all locales, and returns a promise which
// resolves to a Map of locale messages upon completion. Each key in the map
// is a Gecko-compatible locale code, and each value is a locale data object
// as returned by |readLocaleFile|.
initAllLocales() {
return Task.spawn(function* () {
let locales = yield this.promiseLocales();
yield Promise.all(Array.from(locales.keys(),
locale => this.readLocaleFile(locale)));
let defaultLocale = this.defaultLocale;
if (defaultLocale) {
if (!locales.has(defaultLocale)) {
this.manifestError('Value for "default_locale" property must correspond to ' +
'a directory in "_locales/". Not found: ' +
JSON.stringify(`_locales/${this.manifest.default_locale}/`));
}
} else if (locales.size) {
this.manifestError('The "default_locale" property is required when a ' +
'"_locales/" directory is present.');
}
return this.localeData.messages;
}.bind(this));
}
// Reads the locale file for the given Gecko-compatible locale code, or the
// default locale if no locale code is given, and sets it as the currently
// selected locale on success.
//
// Pre-loads the default locale for fallback message processing, regardless
// of the locale specified.
//
// If no locales are unavailable, resolves to |null|.
initLocale(locale = this.defaultLocale) {
return Task.spawn(function* () {
if (locale == null) {
return null;
}
let promises = [this.readLocaleFile(locale)];
let {defaultLocale} = this;
if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
promises.push(this.readLocaleFile(defaultLocale));
}
let results = yield Promise.all(promises);
this.localeData.selectedLocale = locale;
return results[0];
}.bind(this));
}
};
let _browserUpdated = false;
const PROXIED_EVENTS = new Set(["test-harness-message"]);
// We create one instance of this class per extension. |addonData|
// comes directly from bootstrap.js when initializing.
this.Extension = class extends ExtensionData {
constructor(addonData, startupReason) {
super(addonData.resourceURI);
this.uuid = UUIDMap.get(addonData.id);
this.instanceId = getUniqueId();
this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
if (addonData.cleanupFile) {
Services.obs.addObserver(this, "xpcom-shutdown", false);
this.cleanupFile = addonData.cleanupFile || null;
delete addonData.cleanupFile;
}
this.addonData = addonData;
this.startupReason = startupReason;
this.id = addonData.id;
this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL);
this.principal = this.createPrincipal();
this.onStartup = null;
this.hasShutdown = false;
this.onShutdown = new Set();
this.uninstallURL = null;
this.apis = [];
this.whiteListedHosts = null;
this.webAccessibleResources = null;
this.emitter = new EventEmitter();
}
static set browserUpdated(updated) {
_browserUpdated = updated;
}
static get browserUpdated() {
return _browserUpdated;
}
static generateXPI(data) {
return ExtensionTestCommon.generateXPI(data);
}
static generateZipFile(files, baseName = "generated-extension.xpi") {
return ExtensionTestCommon.generateZipFile(files, baseName);
}
static generate(data) {
return ExtensionTestCommon.generate(data);
}
on(hook, f) {
return this.emitter.on(hook, f);
}
off(hook, f) {
return this.emitter.off(hook, f);
}
emit(event, ...args) {
if (PROXIED_EVENTS.has(event)) {
Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
}
return this.emitter.emit(event, ...args);
}
receiveMessage({name, data}) {
if (name === this.MESSAGE_EMIT_EVENT) {
this.emitter.emit(data.event, ...data.args);
}
}
testMessage(...args) {
this.emit("test-harness-message", ...args);
}
createPrincipal(uri = this.baseURI) {
return Services.scriptSecurityManager.createCodebasePrincipal(
uri, {addonId: this.id});
}
// Checks that the given URL is a child of our baseURI.
isExtensionURL(url) {
let uri = Services.io.newURI(url, null, null);
let common = this.baseURI.getCommonBaseSpec(uri);
return common == this.baseURI.spec;
}
readManifest() {
return super.readManifest().then(manifest => {
if (AppConstants.RELEASE_OR_BETA) {
return manifest;
}
// Load Experiments APIs that this extension depends on.
return Promise.all(
Array.from(this.apiNames, api => ExtensionAPIs.load(api))
).then(apis => {
for (let API of apis) {
this.apis.push(new API(this));
}
return manifest;
});
});
}
// 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,
instanceId: this.instanceId,
manifest: this.manifest,
resourceURL: this.addonData.resourceURI.spec,
baseURL: this.baseURI.spec,
content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
webAccessibleResources: this.webAccessibleResources.serialize(),
whiteListedHosts: this.whiteListedHosts.serialize(),
localeData: this.localeData.serialize(),
permissions: this.permissions,
principal: this.principal,
};
}
broadcast(msg, data) {
return new Promise(resolve => {
let count = Services.ppmm.childCount;
Services.ppmm.addMessageListener(msg + "Complete", function listener() {
count--;
if (count == 0) {
Services.ppmm.removeMessageListener(msg + "Complete", listener);
resolve();
}
});
Services.ppmm.broadcastAsyncMessage(msg, data);
});
}
runManifest(manifest) {
// Strip leading slashes from web_accessible_resources.
let strippedWebAccessibleResources = [];
if (manifest.web_accessible_resources) {
strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, ""));
}
this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
let promises = [];
for (let directive in manifest) {
if (manifest[directive] !== null) {
promises.push(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);
return this.broadcast("Extension:Startup", serial).then(() => {
return Promise.all(promises);
});
}
callOnClose(obj) {
this.onShutdown.add(obj);
}
forgetOnClose(obj) {
this.onShutdown.delete(obj);
}
get builtinMessages() {
return new Map([
["@@extension_id", this.uuid],
]);
}
// Reads the locale file for the given Gecko-compatible locale code, or if
// no locale is given, the available locale closest to the UI locale.
// Sets the currently selected locale on success.
initLocale(locale = undefined) {
// Ugh.
let super_ = super.initLocale.bind(this);
return Task.spawn(function* () {
if (locale === undefined) {
let locales = yield this.promiseLocales();
let localeList = Array.from(locales.keys(), locale => {
return {name: locale, locales: [locale]};
});
let match = Locale.findClosestLocale(localeList);
locale = match ? match.name : this.defaultLocale;
}
return super_(locale);
}.bind(this));
}
startup() {
let started = false;
return this.readManifest().then(() => {
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
started = true;
if (!this.hasShutdown) {
return this.initLocale();
}
}).then(() => {
if (this.errors.length) {
return Promise.reject({errors: this.errors});
}
if (this.hasShutdown) {
return;
}
GlobalManager.init(this);
// The "startup" Management event sent on the extension instance itself
// is emitted just before the Management "startup" event,
// and it is used to run code that needs to be executed before
// any of the "startup" listeners.
this.emit("startup", this);
Management.emit("startup", this);
return this.runManifest(this.manifest);
}).then(() => {
Management.emit("ready", this);
}).catch(e => {
dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
Cu.reportError(e);
if (started) {
ExtensionManagement.shutdownExtension(this.uuid);
}
this.cleanupGeneratedFile();
throw e;
});
}
cleanupGeneratedFile() {
if (!this.cleanupFile) {
return;
}
let file = this.cleanupFile;
this.cleanupFile = null;
Services.obs.removeObserver(this, "xpcom-shutdown");
this.broadcast("Extension:FlushJarCache", {path: file.path}).then(() => {
// We can't delete this file until everyone using it has
// closed it (because Windows is dumb). So we wait for all the
// child processes (including the parent) to flush their JAR
// caches. These caches may keep the file open.
file.remove(false);
});
}
shutdown() {
this.hasShutdown = true;
Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
if (!this.manifest) {
ExtensionManagement.shutdownExtension(this.uuid);
this.cleanupGeneratedFile();
return;
}
GlobalManager.uninit(this);
for (let obj of this.onShutdown) {
obj.close();
}
for (let api of this.apis) {
api.destroy();
}
ParentAPIManager.shutdownExtension(this.id);
Management.emit("shutdown", this);
Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
MessageChannel.abortResponses({extensionId: this.id});
ExtensionManagement.shutdownExtension(this.uuid);
this.cleanupGeneratedFile();
}
observe(subject, topic, data) {
if (topic == "xpcom-shutdown") {
this.cleanupGeneratedFile();
}
}
hasPermission(perm) {
let match = /^manifest:(.*)/.exec(perm);
if (match) {
return this.manifest[match[1]] != null;
}
return this.permissions.has(perm);
}
get name() {
return this.manifest.name;
}
};

View File

@ -0,0 +1,81 @@
/* 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";
this.EXPORTED_SYMBOLS = ["ExtensionAPI", "ExtensionAPIs"];
/* exported ExtensionAPIs */
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
const global = this;
class ExtensionAPI {
constructor(extension) {
this.extension = extension;
}
destroy() {
}
getAPI(context) {
throw new Error("Not Implemented");
}
}
var ExtensionAPIs = {
apis: ExtensionManagement.APIs.apis,
load(apiName) {
let api = this.apis.get(apiName);
if (api.loadPromise) {
return api.loadPromise;
}
let {script, schema} = api;
let addonId = `${apiName}@experiments.addons.mozilla.org`;
api.sandbox = Cu.Sandbox(global, {
wantXrays: false,
sandboxName: script,
addonId,
metadata: {addonID: addonId},
});
api.sandbox.ExtensionAPI = ExtensionAPI;
Services.scriptloader.loadSubScript(script, api.sandbox, "UTF-8");
api.loadPromise = Schemas.load(schema).then(() => {
return Cu.evalInSandbox("API", api.sandbox);
});
return api.loadPromise;
},
unload(apiName) {
let api = this.apis.get(apiName);
let {schema} = api;
Schemas.unload(schema);
Cu.nukeSandbox(api.sandbox);
api.sandbox = null;
api.loadPromise = null;
},
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,679 @@
/* 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";
/**
* This module contains utilities and base classes for logic which is
* common between the parent and child process, and in particular
* between ExtensionParent.jsm and ExtensionChild.jsm.
*/
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
/* exported ExtensionCommon */
this.EXPORTED_SYMBOLS = ["ExtensionCommon"];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventEmitter,
ExtensionError,
SpreadArgs,
getConsole,
getInnerWindowID,
getUniqueId,
runSafeSync,
runSafeSyncWithoutClone,
instanceOf,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", getConsole);
class BaseContext {
constructor(envType, extension) {
this.envType = envType;
this.onClose = new Set();
this.checkedLastError = false;
this._lastError = null;
this.contextId = getUniqueId();
this.unloaded = false;
this.extension = extension;
this.jsonSandbox = null;
this.active = true;
this.incognito = null;
this.messageManager = null;
this.docShell = null;
this.contentWindow = null;
this.innerWindowID = 0;
}
setContentWindow(contentWindow) {
let {document} = contentWindow;
let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
this.innerWindowID = getInnerWindowID(contentWindow);
this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
if (this.incognito == null) {
this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
}
MessageChannel.setupMessageManagers([this.messageManager]);
let onPageShow = event => {
if (!event || event.target === document) {
this.docShell = docShell;
this.contentWindow = contentWindow;
this.active = true;
}
};
let onPageHide = event => {
if (!event || event.target === document) {
// Put this off until the next tick.
Promise.resolve().then(() => {
this.docShell = null;
this.contentWindow = null;
this.active = false;
});
}
};
onPageShow();
contentWindow.addEventListener("pagehide", onPageHide, true);
contentWindow.addEventListener("pageshow", onPageShow, true);
this.callOnClose({
close: () => {
onPageHide();
if (this.active) {
contentWindow.removeEventListener("pagehide", onPageHide, true);
contentWindow.removeEventListener("pageshow", onPageShow, true);
}
},
});
}
get cloneScope() {
throw new Error("Not implemented");
}
get principal() {
throw new Error("Not implemented");
}
runSafe(...args) {
if (this.unloaded) {
Cu.reportError("context.runSafe called after context unloaded");
} else if (!this.active) {
Cu.reportError("context.runSafe called while context is inactive");
} else {
return runSafeSync(this, ...args);
}
}
runSafeWithoutClone(...args) {
if (this.unloaded) {
Cu.reportError("context.runSafeWithoutClone called after context unloaded");
} else if (!this.active) {
Cu.reportError("context.runSafeWithoutClone called while context is inactive");
} else {
return runSafeSyncWithoutClone(...args);
}
}
checkLoadURL(url, options = {}) {
let ssm = Services.scriptSecurityManager;
let flags = ssm.STANDARD;
if (!options.allowScript) {
flags |= ssm.DISALLOW_SCRIPT;
}
if (!options.allowInheritsPrincipal) {
flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
}
if (options.dontReportErrors) {
flags |= ssm.DONT_REPORT_ERRORS;
}
try {
ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags);
} catch (e) {
return false;
}
return true;
}
/**
* Safely call JSON.stringify() on an object that comes from an
* extension.
*
* @param {array<any>} args Arguments for JSON.stringify()
* @returns {string} The stringified representation of obj
*/
jsonStringify(...args) {
if (!this.jsonSandbox) {
this.jsonSandbox = Cu.Sandbox(this.principal, {
sameZoneAs: this.cloneScope,
wantXrays: false,
});
}
return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
}
callOnClose(obj) {
this.onClose.add(obj);
}
forgetOnClose(obj) {
this.onClose.delete(obj);
}
/**
* A wrapper around MessageChannel.sendMessage which adds the extension ID
* to the recipient object, and ensures replies are not processed after the
* context has been unloaded.
*
* @param {nsIMessageManager} target
* @param {string} messageName
* @param {object} data
* @param {object} [options]
* @param {object} [options.sender]
* @param {object} [options.recipient]
*
* @returns {Promise}
*/
sendMessage(target, messageName, data, options = {}) {
options.recipient = Object.assign({extensionId: this.extension.id}, options.recipient);
options.sender = options.sender || {};
options.sender.extensionId = this.extension.id;
options.sender.contextId = this.contextId;
return MessageChannel.sendMessage(target, messageName, data, options);
}
get lastError() {
this.checkedLastError = true;
return this._lastError;
}
set lastError(val) {
this.checkedLastError = false;
this._lastError = val;
}
/**
* Normalizes the given error object for use by the target scope. If
* the target is an error object which belongs to that scope, it is
* returned as-is. If it is an ordinary object with a `message`
* property, it is converted into an error belonging to the target
* scope. If it is an Error object which does *not* belong to the
* clone scope, it is reported, and converted to an unexpected
* exception error.
*
* @param {Error|object} error
* @returns {Error}
*/
normalizeError(error) {
if (error instanceof this.cloneScope.Error) {
return error;
}
let message;
if (instanceOf(error, "Object") || error instanceof ExtensionError) {
message = error.message;
} else if (typeof error == "object" &&
this.principal.subsumes(Cu.getObjectPrincipal(error))) {
message = error.message;
} else {
Cu.reportError(error);
}
message = message || "An unexpected error occurred";
return new this.cloneScope.Error(message);
}
/**
* Sets the value of `.lastError` to `error`, calls the given
* callback, and reports an error if the value has not been checked
* when the callback returns.
*
* @param {object} error An object with a `message` property. May
* optionally be an `Error` object belonging to the target scope.
* @param {function} callback The callback to call.
* @returns {*} The return value of callback.
*/
withLastError(error, callback) {
this.lastError = this.normalizeError(error);
try {
return callback();
} finally {
if (!this.checkedLastError) {
Cu.reportError(`Unchecked lastError value: ${this.lastError}`);
}
this.lastError = null;
}
}
/**
* Wraps the given promise so it can be safely returned to extension
* code in this context.
*
* If `callback` is provided, however, it is used as a completion
* function for the promise, and no promise is returned. In this case,
* the callback is called when the promise resolves or rejects. In the
* latter case, `lastError` is set to the rejection value, and the
* callback function must check `browser.runtime.lastError` or
* `extension.runtime.lastError` in order to prevent it being reported
* to the console.
*
* @param {Promise} promise The promise with which to wrap the
* callback. May resolve to a `SpreadArgs` instance, in which case
* each element will be used as a separate argument.
*
* Unless the promise object belongs to the cloneScope global, its
* resolution value is cloned into cloneScope prior to calling the
* `callback` function or resolving the wrapped promise.
*
* @param {function} [callback] The callback function to wrap
*
* @returns {Promise|undefined} If callback is null, a promise object
* belonging to the target scope. Otherwise, undefined.
*/
wrapPromise(promise, callback = null) {
let runSafe = this.runSafe.bind(this);
if (promise instanceof this.cloneScope.Promise) {
runSafe = this.runSafeWithoutClone.bind(this);
}
if (callback) {
promise.then(
args => {
if (this.unloaded) {
dump(`Promise resolved after context unloaded\n`);
} else if (!this.active) {
dump(`Promise resolved while context is inactive\n`);
} else if (args instanceof SpreadArgs) {
runSafe(callback, ...args);
} else {
runSafe(callback, args);
}
},
error => {
this.withLastError(error, () => {
if (this.unloaded) {
dump(`Promise rejected after context unloaded\n`);
} else if (!this.active) {
dump(`Promise rejected while context is inactive\n`);
} else {
this.runSafeWithoutClone(callback);
}
});
});
} else {
return new this.cloneScope.Promise((resolve, reject) => {
promise.then(
value => {
if (this.unloaded) {
dump(`Promise resolved after context unloaded\n`);
} else if (!this.active) {
dump(`Promise resolved while context is inactive\n`);
} else if (value instanceof SpreadArgs) {
runSafe(resolve, value.length == 1 ? value[0] : value);
} else {
runSafe(resolve, value);
}
},
value => {
if (this.unloaded) {
dump(`Promise rejected after context unloaded: ${value && value.message}\n`);
} else if (!this.active) {
dump(`Promise rejected while context is inactive: ${value && value.message}\n`);
} else {
this.runSafeWithoutClone(reject, this.normalizeError(value));
}
});
});
}
}
unload() {
this.unloaded = true;
MessageChannel.abortResponses({
extensionId: this.extension.id,
contextId: this.contextId,
});
for (let obj of this.onClose) {
obj.close();
}
}
/**
* A simple proxy for unload(), for use with callOnClose().
*/
close() {
this.unload();
}
}
/**
* An object that runs the implementation of a schema API. Instantiations of
* this interfaces are used by Schemas.jsm.
*
* @interface
*/
class SchemaAPIInterface {
/**
* Calls this as a function that returns its return value.
*
* @abstract
* @param {Array} args The parameters for the function.
* @returns {*} The return value of the invoked function.
*/
callFunction(args) {
throw new Error("Not implemented");
}
/**
* Calls this as a function and ignores its return value.
*
* @abstract
* @param {Array} args The parameters for the function.
*/
callFunctionNoReturn(args) {
throw new Error("Not implemented");
}
/**
* Calls this as a function that completes asynchronously.
*
* @abstract
* @param {Array} args The parameters for the function.
* @param {function(*)} [callback] The callback to be called when the function
* completes.
* @returns {Promise|undefined} Must be void if `callback` is set, and a
* promise otherwise. The promise is resolved when the function completes.
*/
callAsyncFunction(args, callback) {
throw new Error("Not implemented");
}
/**
* Retrieves the value of this as a property.
*
* @abstract
* @returns {*} The value of the property.
*/
getProperty() {
throw new Error("Not implemented");
}
/**
* Assigns the value to this as property.
*
* @abstract
* @param {string} value The new value of the property.
*/
setProperty(value) {
throw new Error("Not implemented");
}
/**
* Registers a `listener` to this as an event.
*
* @abstract
* @param {function} listener The callback to be called when the event fires.
* @param {Array} args Extra parameters for EventManager.addListener.
* @see EventManager.addListener
*/
addListener(listener, args) {
throw new Error("Not implemented");
}
/**
* Checks whether `listener` is listening to this as an event.
*
* @abstract
* @param {function} listener The event listener.
* @returns {boolean} Whether `listener` is registered with this as an event.
* @see EventManager.hasListener
*/
hasListener(listener) {
throw new Error("Not implemented");
}
/**
* Unregisters `listener` from this as an event.
*
* @abstract
* @param {function} listener The event listener.
* @see EventManager.removeListener
*/
removeListener(listener) {
throw new Error("Not implemented");
}
}
/**
* An object that runs a locally implemented API.
*/
class LocalAPIImplementation extends SchemaAPIInterface {
/**
* Constructs an implementation of the `name` method or property of `pathObj`.
*
* @param {object} pathObj The object containing the member with name `name`.
* @param {string} name The name of the implemented member.
* @param {BaseContext} context The context in which the schema is injected.
*/
constructor(pathObj, name, context) {
super();
this.pathObj = pathObj;
this.name = name;
this.context = context;
}
callFunction(args) {
return this.pathObj[this.name](...args);
}
callFunctionNoReturn(args) {
this.pathObj[this.name](...args);
}
callAsyncFunction(args, callback) {
let promise;
try {
promise = this.pathObj[this.name](...args) || Promise.resolve();
} catch (e) {
promise = Promise.reject(e);
}
return this.context.wrapPromise(promise, callback);
}
getProperty() {
return this.pathObj[this.name];
}
setProperty(value) {
this.pathObj[this.name] = value;
}
addListener(listener, args) {
try {
this.pathObj[this.name].addListener.call(null, listener, ...args);
} catch (e) {
throw this.context.normalizeError(e);
}
}
hasListener(listener) {
return this.pathObj[this.name].hasListener.call(null, listener);
}
removeListener(listener) {
this.pathObj[this.name].removeListener.call(null, listener);
}
}
/**
* This object loads the ext-*.js scripts that define the extension API.
*
* This class instance is shared with the scripts that it loads, so that the
* ext-*.js scripts and the instantiator can communicate with each other.
*/
class SchemaAPIManager extends EventEmitter {
/**
* @param {string} processType
* "main" - The main, one and only chrome browser process.
* "addon" - An addon process.
* "content" - A content process.
*/
constructor(processType) {
super();
this.processType = processType;
this.global = this._createExtGlobal();
this._scriptScopes = [];
this._schemaApis = {
addon_parent: [],
addon_child: [],
content_parent: [],
content_child: [],
};
}
/**
* Create a global object that is used as the shared global for all ext-*.js
* scripts that are loaded via `loadScript`.
*
* @returns {object} A sandbox that is used as the global by `loadScript`.
*/
_createExtGlobal() {
let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
wantXrays: false,
sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
});
Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
XPCOMUtils.defineLazyGetter(global, "console", getConsole);
XPCOMUtils.defineLazyModuleGetter(global, "require",
"resource://devtools/shared/Loader.jsm");
return global;
}
/**
* Load an ext-*.js script. The script runs in its own scope, if it wishes to
* share state with another script it can assign to the `global` variable. If
* it wishes to communicate with this API manager, use `extensions`.
*
* @param {string} scriptUrl The URL of the ext-*.js script.
*/
loadScript(scriptUrl) {
// Create the object in the context of the sandbox so that the script runs
// in the sandbox's context instead of here.
let scope = Cu.createObjectIn(this.global);
Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8");
// Save the scope to avoid it being garbage collected.
this._scriptScopes.push(scope);
}
/**
* Called by an ext-*.js script to register an API.
*
* @param {string} namespace The API namespace.
* Intended to match the namespace of the generated API, but not used at
* the moment - see bugzil.la/1295774.
* @param {string} envType Restricts the API to contexts that run in the
* given environment. Must be one of the following:
* - "addon_parent" - addon APIs that runs in the main process.
* - "addon_child" - addon APIs that runs in an addon process.
* - "content_parent" - content script APIs that runs in the main process.
* - "content_child" - content script APIs that runs in a content process.
* @param {function(BaseContext)} getAPI A function that returns an object
* that will be merged with |chrome| and |browser|. The next example adds
* the create, update and remove methods to the tabs API.
*
* registerSchemaAPI("tabs", "addon_parent", (context) => ({
* tabs: { create, update },
* }));
* registerSchemaAPI("tabs", "addon_parent", (context) => ({
* tabs: { remove },
* }));
*/
registerSchemaAPI(namespace, envType, getAPI) {
this._schemaApis[envType].push({namespace, getAPI});
}
/**
* Exports all registered scripts to `obj`.
*
* @param {BaseContext} context The context for which the API bindings are
* generated.
* @param {object} obj The destination of the API.
*/
generateAPIs(context, obj) {
let apis = this._schemaApis[context.envType];
if (!apis) {
Cu.reportError(`No APIs have been registered for ${context.envType}`);
return;
}
SchemaAPIManager.generateAPIs(context, apis, obj);
}
/**
* Mash together all the APIs from `apis` into `obj`.
*
* @param {BaseContext} context The context for which the API bindings are
* generated.
* @param {Array} apis A list of objects, see `registerSchemaAPI`.
* @param {object} obj The destination of the API.
*/
static generateAPIs(context, apis, obj) {
// Recursively copy properties from source to dest.
function copy(dest, source) {
for (let prop in source) {
let desc = Object.getOwnPropertyDescriptor(source, prop);
if (typeof(desc.value) == "object") {
if (!(prop in dest)) {
dest[prop] = {};
}
copy(dest[prop], source[prop]);
} else {
Object.defineProperty(dest, prop, desc);
}
}
}
for (let api of apis) {
if (Schemas.checkPermissions(api.namespace, context.extension)) {
api = api.getAPI(context);
copy(obj, api);
}
}
}
}
const ExtensionCommon = {
BaseContext,
LocalAPIImplementation,
SchemaAPIInterface,
SchemaAPIManager,
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,321 @@
/* 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";
this.EXPORTED_SYMBOLS = ["ExtensionManagement"];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
XPCOMUtils.defineLazyGetter(this, "UUIDMap", () => {
let {UUIDMap} = Cu.import("resource://gre/modules/Extension.jsm", {});
return UUIDMap;
});
/*
* 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.
var 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, true);
},
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;
}
if (windowId == 0) {
return -1;
}
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();
var APIs = {
apis: new Map(),
register(namespace, schema, script) {
if (this.apis.has(namespace)) {
throw new Error(`API namespace already exists: ${namespace}`);
}
this.apis.set(namespace, {schema, script});
},
unregister(namespace) {
if (!this.apis.has(namespace)) {
throw new Error(`API namespace does not exist: ${namespace}`);
}
this.apis.delete(namespace);
},
};
function getURLForExtension(id, path = "") {
let uuid = UUIDMap.get(id, false);
if (!uuid) {
Cu.reportError(`Called getURLForExtension on unmapped extension ${id}`);
return null;
}
return `moz-extension://${uuid}/${path}`;
}
// 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.
var 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.setAddonHasPermissionCallback(extension.id, extension.hasPermission.bind(extension));
this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension));
this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy);
this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.bind(this, extension));
},
// Called when an extension is unloaded.
shutdownExtension(uuid) {
let extension = this.uuidMap.get(uuid);
this.uuidMap.delete(uuid);
this.aps.setAddonHasPermissionCallback(extension.id, null);
this.aps.setAddonLoadURICallback(extension.id, null);
this.aps.setAddonLocalizeCallback(extension.id, null);
this.aps.setAddonCSP(extension.id, null);
this.aps.setBackgroundPageUrlCallback(uuid, 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 || !extension.webAccessibleResources) {
return false;
}
let path = uri.QueryInterface(Ci.nsIURL).filePath;
if (path.length > 0 && path[0] == "/") {
path = path.substr(1);
}
return extension.webAccessibleResources.matches(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);
},
generateBackgroundPageUrl(extension) {
let background_scripts = extension.manifest.background &&
extension.manifest.background.scripts;
if (!background_scripts) {
return;
}
let html = "<!DOCTYPE html>\n<body>\n";
for (let script of background_scripts) {
script = script.replace(/"/g, "&quot;");
html += `<script src="${script}"></script>\n`;
}
html += "</body>\n</html>\n";
return "data:text/html;charset=utf-8," + encodeURIComponent(html);
},
// 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) {
let uuid = uri.host;
let extension = this.uuidMap.get(uuid);
return extension ? extension.id : undefined;
},
};
// API Levels Helpers
// 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 or remote web pages.
function getAddonIdForWindow(window) {
return Cu.getObjectPrincipal(window).originAttributes.addonId;
}
const API_LEVELS = Object.freeze({
NO_PRIVILEGES: 0,
CONTENTSCRIPT_PRIVILEGES: 1,
FULL_PRIVILEGES: 2,
});
// Finds the API Level ("FULL_PRIVILEGES", "CONTENTSCRIPT_PRIVILEGES", "NO_PRIVILEGES")
// with a given a window object.
function getAPILevelForWindow(window, addonId) {
const {NO_PRIVILEGES, CONTENTSCRIPT_PRIVILEGES, FULL_PRIVILEGES} = API_LEVELS;
// Non WebExtension URLs and WebExtension URLs from a different extension
// has no access to APIs.
if (!addonId || getAddonIdForWindow(window) != addonId) {
return NO_PRIVILEGES;
}
// Extension pages running in the content process always defaults to
// "content script API level privileges".
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
return CONTENTSCRIPT_PRIVILEGES;
}
let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
// Handling of ExtensionPages running inside sub-frames.
if (docShell.sameTypeParent) {
let parentWindow = docShell.sameTypeParent.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
// The option page iframe embedded in the about:addons tab should have
// full API level privileges. (see Bug 1256282 for rationale)
let parentDocument = parentWindow.document;
let parentIsSystemPrincipal = Services.scriptSecurityManager
.isSystemPrincipal(parentDocument.nodePrincipal);
if (parentDocument.location.href == "about:addons" && parentIsSystemPrincipal) {
return FULL_PRIVILEGES;
}
// The addon iframes embedded in a addon page from with the same addonId
// should have the same privileges of the sameTypeParent.
// (see Bug 1258347 for rationale)
let parentSameAddonPrivileges = getAPILevelForWindow(parentWindow, addonId);
if (parentSameAddonPrivileges > NO_PRIVILEGES) {
return parentSameAddonPrivileges;
}
// In all the other cases, WebExtension URLs loaded into sub-frame UI
// will have "content script API level privileges".
// (see Bug 1214658 for rationale)
return CONTENTSCRIPT_PRIVILEGES;
}
// WebExtension URLs loaded into top frames UI could have full API level privileges.
return FULL_PRIVILEGES;
}
this.ExtensionManagement = {
startupExtension: Service.startupExtension.bind(Service),
shutdownExtension: Service.shutdownExtension.bind(Service),
registerAPI: APIs.register.bind(APIs),
unregisterAPI: APIs.unregister.bind(APIs),
getFrameId: Frames.getId.bind(Frames),
getParentFrameId: Frames.getParentId.bind(Frames),
getURLForExtension,
// exported API Level Helpers
getAddonIdForWindow,
getAPILevelForWindow,
API_LEVELS,
APIs,
};

View File

@ -0,0 +1,551 @@
/* 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";
/**
* This module contains code for managing APIs that need to run in the
* parent process, and handles the parent side of operations that need
* to be proxied from ExtensionChild.jsm.
*/
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
/* exported ExtensionParent */
this.EXPORTED_SYMBOLS = ["ExtensionParent"];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
"resource://gre/modules/NativeMessaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
BaseContext,
SchemaAPIManager,
} = ExtensionCommon;
var {
MessageManagerProxy,
SpreadArgs,
defineLazyGetter,
findPathInObject,
} = ExtensionUtils;
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
let schemaURLs = new Set();
if (!AppConstants.RELEASE_OR_BETA) {
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
}
let GlobalManager;
let ParentAPIManager;
let ProxyMessenger;
// This object loads the ext-*.js scripts that define the extension API.
let apiManager = new class extends SchemaAPIManager {
constructor() {
super("main");
this.initialized = null;
}
// Loads all the ext-*.js scripts currently registered.
lazyInit() {
if (this.initialized) {
return this.initialized;
}
// Load order matters here. The base manifest defines types which are
// extended by other schemas, so needs to be loaded first.
let promise = Schemas.load(BASE_SCHEMA).then(() => {
let promises = [];
for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
promises.push(Schemas.load(url));
}
for (let url of schemaURLs) {
promises.push(Schemas.load(url));
}
return Promise.all(promises);
});
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
this.loadScript(value);
}
this.initialized = promise;
return this.initialized;
}
registerSchemaAPI(namespace, envType, getAPI) {
if (envType == "addon_parent" || envType == "content_parent") {
super.registerSchemaAPI(namespace, envType, getAPI);
}
}
}();
// Subscribes to messages related to the extension messaging API and forwards it
// to the relevant message manager. The "sender" field for the `onMessage` and
// `onConnect` events are updated if needed.
ProxyMessenger = {
_initialized: false,
init() {
if (this._initialized) {
return;
}
this._initialized = true;
// TODO(robwu): When addons move to a separate process, we should use the
// parent process manager(s) of the addon process(es) instead of the
// in-process one.
let pipmm = Services.ppmm.getChildAt(0);
// Listen on the global frame message manager because content scripts send
// and receive extension messages via their frame.
// Listen on the parent process message manager because `runtime.connect`
// and `runtime.sendMessage` requests must be delivered to all frames in an
// addon process (by the API contract).
// And legacy addons are not associated with a frame, so that is another
// reason for having a parent process manager here.
let messageManagers = [Services.mm, pipmm];
MessageChannel.addListener(messageManagers, "Extension:Connect", this);
MessageChannel.addListener(messageManagers, "Extension:Message", this);
MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
},
receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
if (recipient.toNativeApp) {
let {childId, toNativeApp} = recipient;
if (messageName == "Extension:Message") {
let context = ParentAPIManager.getContextById(childId);
return new NativeApp(context, toNativeApp).sendMessage(data);
}
if (messageName == "Extension:Connect") {
let context = ParentAPIManager.getContextById(childId);
NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
return true;
}
// "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
// native messages are handled by NativeApp.
return;
}
let extension = GlobalManager.extensionMap.get(sender.extensionId);
let receiverMM = this._getMessageManagerForRecipient(recipient);
if (!extension || !receiverMM) {
return Promise.reject({
result: MessageChannel.RESULT_NO_HANDLER,
message: "No matching message handler for the given recipient.",
});
}
if ((messageName == "Extension:Message" ||
messageName == "Extension:Connect") &&
apiManager.global.tabGetSender) {
// From ext-tabs.js, undefined on Android.
apiManager.global.tabGetSender(extension, target, sender);
}
return MessageChannel.sendMessage(receiverMM, messageName, data, {
sender,
recipient,
responseType,
});
},
/**
* @param {object} recipient An object that was passed to
* `MessageChannel.sendMessage`.
* @returns {object|null} The message manager matching the recipient if found.
*/
_getMessageManagerForRecipient(recipient) {
let {extensionId, tabId} = recipient;
// tabs.sendMessage / tabs.connect
if (tabId) {
// `tabId` being set implies that the tabs API is supported, so we don't
// need to check whether `TabManager` exists.
let tab = apiManager.global.TabManager.getTab(tabId, null, null);
return tab && tab.linkedBrowser.messageManager;
}
// runtime.sendMessage / runtime.connect
if (extensionId) {
// TODO(robwu): map the extensionId to the addon parent process's message
// manager when they run in a separate process.
return Services.ppmm.getChildAt(0);
}
return null;
},
};
// Responsible for loading extension APIs into the right globals.
GlobalManager = {
// Map[extension ID -> Extension]. Determines which extension is
// responsible for content under a particular extension ID.
extensionMap: new Map(),
initialized: false,
init(extension) {
if (this.extensionMap.size == 0) {
ProxyMessenger.init();
apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
this.initialized = true;
}
this.extensionMap.set(extension.id, extension);
},
uninit(extension) {
this.extensionMap.delete(extension.id);
if (this.extensionMap.size == 0 && this.initialized) {
apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
this.initialized = false;
}
},
_onExtensionBrowser(type, browser) {
browser.messageManager.loadFrameScript(`data:,
Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
ExtensionContent.init(this);
addEventListener("unload", function() {
ExtensionContent.uninit(this);
});
`, false);
},
getExtension(extensionId) {
return this.extensionMap.get(extensionId);
},
injectInObject(context, isChromeCompat, dest) {
apiManager.generateAPIs(context, dest);
SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
},
};
/**
* The proxied parent side of a context in ExtensionChild.jsm, for the
* parent side of a proxied API.
*/
class ProxyContextParent extends BaseContext {
constructor(envType, extension, params, xulBrowser, principal) {
super(envType, extension);
this.uri = NetUtil.newURI(params.url);
this.incognito = params.incognito;
// This message manager is used by ParentAPIManager to send messages and to
// close the ProxyContext if the underlying message manager closes. This
// message manager object may change when `xulBrowser` swaps docshells, e.g.
// when a tab is moved to a different window.
this.messageManagerProxy = new MessageManagerProxy(xulBrowser);
Object.defineProperty(this, "principal", {
value: principal, enumerable: true, configurable: true,
});
this.listenerProxies = new Map();
apiManager.emit("proxy-context-load", this);
}
get cloneScope() {
return this.sandbox;
}
get xulBrowser() {
return this.messageManagerProxy.eventTarget;
}
get parentMessageManager() {
return this.messageManagerProxy.messageManager;
}
shutdown() {
this.unload();
}
unload() {
if (this.unloaded) {
return;
}
this.messageManagerProxy.dispose();
super.unload();
apiManager.emit("proxy-context-unload", this);
}
}
defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
let obj = {};
GlobalManager.injectInObject(this, false, obj);
return obj;
});
defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() {
return Cu.Sandbox(this.principal);
});
/**
* The parent side of proxied API context for extension content script
* running in ExtensionContent.jsm.
*/
class ContentScriptContextParent extends ProxyContextParent {
}
/**
* The parent side of proxied API context for extension page, such as a
* background script, a tab page, or a popup, running in
* ExtensionChild.jsm.
*/
class ExtensionPageContextParent extends ProxyContextParent {
constructor(envType, extension, params, xulBrowser) {
super(envType, extension, params, xulBrowser, extension.principal);
this.viewType = params.viewType;
}
// The window that contains this context. This may change due to moving tabs.
get xulWindow() {
return this.xulBrowser.ownerGlobal;
}
get windowId() {
if (!apiManager.global.WindowManager || this.viewType == "background") {
return;
}
// viewType popup or tab:
return apiManager.global.WindowManager.getId(this.xulWindow);
}
get tabId() {
if (!apiManager.global.TabManager) {
return; // Not yet supported on Android.
}
let {gBrowser} = this.xulBrowser.ownerGlobal;
let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
return tab && apiManager.global.TabManager.getId(tab);
}
onBrowserChange(browser) {
super.onBrowserChange(browser);
this.xulBrowser = browser;
}
shutdown() {
apiManager.emit("page-shutdown", this);
super.shutdown();
}
}
ParentAPIManager = {
proxyContexts: new Map(),
init() {
Services.obs.addObserver(this, "message-manager-close", false);
Services.mm.addMessageListener("API:CreateProxyContext", this);
Services.mm.addMessageListener("API:CloseProxyContext", this, true);
Services.mm.addMessageListener("API:Call", this);
Services.mm.addMessageListener("API:AddListener", this);
Services.mm.addMessageListener("API:RemoveListener", this);
},
observe(subject, topic, data) {
if (topic === "message-manager-close") {
let mm = subject;
for (let [childId, context] of this.proxyContexts) {
if (context.parentMessageManager === mm) {
this.closeProxyContext(childId);
}
}
}
},
shutdownExtension(extensionId) {
for (let [childId, context] of this.proxyContexts) {
if (context.extension.id == extensionId) {
context.shutdown();
this.proxyContexts.delete(childId);
}
}
},
receiveMessage({name, data, target}) {
switch (name) {
case "API:CreateProxyContext":
this.createProxyContext(data, target);
break;
case "API:CloseProxyContext":
this.closeProxyContext(data.childId);
break;
case "API:Call":
this.call(data, target);
break;
case "API:AddListener":
this.addListener(data, target);
break;
case "API:RemoveListener":
this.removeListener(data);
break;
}
},
createProxyContext(data, target) {
let {envType, extensionId, childId, principal} = data;
if (this.proxyContexts.has(childId)) {
throw new Error("A WebExtension context with the given ID already exists!");
}
let extension = GlobalManager.getExtension(extensionId);
if (!extension) {
throw new Error(`No WebExtension found with ID ${extensionId}`);
}
let context;
if (envType == "addon_parent") {
// Privileged addon contexts can only be loaded in documents whose main
// frame is also the same addon.
if (principal.URI.prePath !== extension.baseURI.prePath ||
!target.contentPrincipal.subsumes(principal)) {
throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
}
context = new ExtensionPageContextParent(envType, extension, data, target);
} else if (envType == "content_parent") {
context = new ContentScriptContextParent(envType, extension, data, target, principal);
} else {
throw new Error(`Invalid WebExtension context envType: ${envType}`);
}
this.proxyContexts.set(childId, context);
},
closeProxyContext(childId) {
let context = this.proxyContexts.get(childId);
if (context) {
context.unload();
this.proxyContexts.delete(childId);
}
},
call(data, target) {
let context = this.getContextById(data.childId);
if (context.parentMessageManager !== target.messageManager) {
throw new Error("Got message on unexpected message manager");
}
let reply = result => {
if (!context.parentMessageManager) {
Cu.reportError("Cannot send function call result: other side closed connection");
return;
}
context.parentMessageManager.sendAsyncMessage(
"API:CallResult",
Object.assign({
childId: data.childId,
callId: data.callId,
}, result));
};
try {
let args = Cu.cloneInto(data.args, context.sandbox);
let result = findPathInObject(context.apiObj, data.path)(...args);
if (data.callId) {
result = result || Promise.resolve();
result.then(result => {
result = result instanceof SpreadArgs ? [...result] : [result];
reply({result});
}, error => {
error = context.normalizeError(error);
reply({error: {message: error.message}});
});
}
} catch (e) {
if (data.callId) {
let error = context.normalizeError(e);
reply({error: {message: error.message}});
} else {
Cu.reportError(e);
}
}
},
addListener(data, target) {
let context = this.getContextById(data.childId);
if (context.parentMessageManager !== target.messageManager) {
throw new Error("Got message on unexpected message manager");
}
let {childId} = data;
function listener(...listenerArgs) {
return context.sendMessage(
context.parentMessageManager,
"API:RunListener",
{
childId,
listenerId: data.listenerId,
path: data.path,
args: listenerArgs,
},
{
recipient: {childId},
});
}
context.listenerProxies.set(data.listenerId, listener);
let args = Cu.cloneInto(data.args, context.sandbox);
findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
},
removeListener(data) {
let context = this.getContextById(data.childId);
let listener = context.listenerProxies.get(data.listenerId);
findPathInObject(context.apiObj, data.path).removeListener(listener);
},
getContextById(childId) {
let context = this.proxyContexts.get(childId);
if (!context) {
let error = new Error("WebExtension context not found!");
Cu.reportError(error);
throw error;
}
return context;
},
};
ParentAPIManager.init();
const ExtensionParent = {
GlobalManager,
ParentAPIManager,
apiManager,
};

View File

@ -0,0 +1,241 @@
/* 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";
this.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");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
function jsonReplacer(key, value) {
switch (typeof(value)) {
// Serialize primitive types as-is.
case "string":
case "number":
case "boolean":
return value;
case "object":
if (value === null) {
return value;
}
switch (Cu.getClassName(value, true)) {
// Serialize arrays and ordinary objects as-is.
case "Array":
case "Object":
return value;
// Serialize Date objects and regular expressions as their
// string representations.
case "Date":
case "RegExp":
return String(value);
}
break;
}
if (!key) {
// If this is the root object, and we can't serialize it, serialize
// the value to an empty object.
return {};
}
// Everything else, omit entirely.
return undefined;
}
this.ExtensionStorage = {
cache: new Map(),
listeners: new Map(),
/**
* Sanitizes the given value, and returns a JSON-compatible
* representation of it, based on the privileges of the given global.
*
* @param {value} value
* The value to sanitize.
* @param {Context} context
* The extension context in which to sanitize the value
* @returns {value}
* The sanitized value.
*/
sanitize(value, context) {
let json = context.jsonStringify(value, jsonReplacer);
return JSON.parse(json);
},
getExtensionDir(extensionId) {
return OS.Path.join(this.extensionDir, extensionId);
},
getStorageFile(extensionId) {
return OS.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((error) => {
if (!error.becauseNoSuchFile) {
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: OS.Constants.Path.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, context) {
return this.read(extensionId).then(extData => {
let changes = {};
for (let prop in items) {
let item = this.sanitize(items[prop], context);
changes[prop] = {oldValue: extData[prop], newValue: item};
extData[prop] = item;
}
this.notifyListeners(extensionId, changes);
return this.write(extensionId);
});
},
remove(extensionId, items) {
return this.read(extensionId).then(extData => {
let changes = {};
for (let prop of [].concat(items)) {
changes[prop] = {oldValue: extData[prop]};
delete extData[prop];
}
this.notifyListeners(extensionId, changes);
return this.write(extensionId);
});
},
clear(extensionId) {
return this.read(extensionId).then(extData => {
let changes = {};
for (let prop of Object.keys(extData)) {
changes[prop] = {oldValue: extData[prop]};
delete extData[prop];
}
this.notifyListeners(extensionId, 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" && !Array.isArray(keys)) {
for (let prop in keys) {
if (prop in extData) {
result[prop] = extData[prop];
} else {
result[prop] = keys[prop];
}
}
} else {
for (let prop of [].concat(keys)) {
if (prop in extData) {
result[prop] = extData[prop];
}
}
}
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);
},
notifyListeners(extensionId, changes) {
let listeners = this.listeners.get(extensionId);
if (listeners) {
for (let listener of listeners) {
listener(changes);
}
}
},
init() {
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
return;
}
Services.obs.addObserver(this, "extension-invalidate-storage-cache", false);
Services.obs.addObserver(this, "xpcom-shutdown", false);
},
observe(subject, topic, data) {
if (topic == "xpcom-shutdown") {
Services.obs.removeObserver(this, "extension-invalidate-storage-cache");
Services.obs.removeObserver(this, "xpcom-shutdown");
} else if (topic == "extension-invalidate-storage-cache") {
this.cache.clear();
}
},
};
XPCOMUtils.defineLazyGetter(ExtensionStorage, "extensionDir",
() => OS.Path.join(OS.Constants.Path.profileDir, "browser-extension-data"));
ExtensionStorage.init();

View File

@ -0,0 +1,343 @@
/* 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";
/**
* This module contains extension testing helper logic which is common
* between all test suites.
*/
/* exported ExtensionTestCommon, MockExtension */
this.EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.importGlobalProperties(["TextEncoder"]);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
"resource://gre/modules/ExtensionParent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyGetter(this, "apiManager",
() => ExtensionParent.apiManager);
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const {
flushJarCache,
instanceOf,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
/**
* A skeleton Extension-like object, used for testing, which installs an
* add-on via the add-on manager when startup() is called, and
* uninstalles it on shutdown().
*
* @param {string} id
* @param {nsIFile} file
* @param {nsIURI} rootURI
* @param {string} installType
*/
class MockExtension {
constructor(file, rootURI, installType) {
this.id = null;
this.file = file;
this.rootURI = rootURI;
this.installType = installType;
this.addon = null;
let promiseEvent = eventName => new Promise(resolve => {
let onstartup = (msg, extension) => {
if (this.addon && extension.id == this.addon.id) {
apiManager.off(eventName, onstartup);
this.id = extension.id;
this._extension = extension;
resolve(extension);
}
};
apiManager.on(eventName, onstartup);
});
this._extension = null;
this._extensionPromise = promiseEvent("startup");
this._readyPromise = promiseEvent("ready");
}
testMessage(...args) {
return this._extension.testMessage(...args);
}
on(...args) {
this._extensionPromise.then(extension => {
extension.on(...args);
});
}
off(...args) {
this._extensionPromise.then(extension => {
extension.off(...args);
});
}
startup() {
if (this.installType == "temporary") {
return AddonManager.installTemporaryAddon(this.file).then(addon => {
this.addon = addon;
return this._readyPromise;
});
} else if (this.installType == "permanent") {
return new Promise((resolve, reject) => {
AddonManager.getInstallForFile(this.file, install => {
let listener = {
onInstallFailed: reject,
onInstallEnded: (install, newAddon) => {
this.addon = newAddon;
resolve(this._readyPromise);
},
};
install.addListener(listener);
install.install();
});
});
}
throw new Error("installType must be one of: temporary, permanent");
}
shutdown() {
this.addon.uninstall();
return this.cleanupGeneratedFile();
}
cleanupGeneratedFile() {
flushJarCache(this.file);
return OS.File.remove(this.file.path);
}
}
class ExtensionTestCommon {
/**
* This code is designed to make it easy to test a WebExtension
* without creating a bunch of files. Everything is contained in a
* single JSON blob.
*
* Properties:
* "background": "<JS code>"
* A script to be loaded as the background script.
* The "background" section of the "manifest" property is overwritten
* if this is provided.
* "manifest": {...}
* Contents of manifest.json
* "files": {"filename1": "contents1", ...}
* Data to be included as files. Can be referenced from the manifest.
* If a manifest file is provided here, it takes precedence over
* a generated one. Always use "/" as a directory separator.
* Directories should appear here only implicitly (as a prefix
* to file names)
*
* To make things easier, the value of "background" and "files"[] can
* be a function, which is converted to source that is run.
*
* The generated extension is stored in the system temporary directory,
* and an nsIFile object pointing to it is returned.
*
* @param {object} data
* @returns {nsIFile}
*/
static generateXPI(data) {
let manifest = data.manifest;
if (!manifest) {
manifest = {};
}
let files = data.files;
if (!files) {
files = {};
}
function provide(obj, keys, value, override = false) {
if (keys.length == 1) {
if (!(keys[0] in obj) || override) {
obj[keys[0]] = value;
}
} else {
if (!(keys[0] in obj)) {
obj[keys[0]] = {};
}
provide(obj[keys[0]], keys.slice(1), value, override);
}
}
provide(manifest, ["name"], "Generated extension");
provide(manifest, ["manifest_version"], 2);
provide(manifest, ["version"], "1.0");
if (data.background) {
let bgScript = uuidGen.generateUUID().number + ".js";
provide(manifest, ["background", "scripts"], [bgScript], true);
files[bgScript] = data.background;
}
provide(files, ["manifest.json"], manifest);
if (data.embedded) {
// Package this as a webextension embedded inside a legacy
// extension.
let xpiFiles = {
"install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest"
em:id="${manifest.applications.gecko.id}"
em:name="${manifest.name}"
em:type="2"
em:version="${manifest.version}"
em:description=""
em:hasEmbeddedWebExtension="true"
em:bootstrap="true">
<!-- Firefox -->
<em:targetApplication>
<Description
em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
em:minVersion="51.0a1"
em:maxVersion="*"/>
</em:targetApplication>
</Description>
</RDF>
`,
"bootstrap.js": `
function install() {}
function uninstall() {}
function shutdown() {}
function startup(data) {
data.webExtension.startup();
}
`,
};
for (let [path, data] of Object.entries(files)) {
xpiFiles[`webextension/${path}`] = data;
}
files = xpiFiles;
}
return this.generateZipFile(files);
}
static generateZipFile(files, baseName = "generated-extension.xpi") {
let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
let zipW = new ZipWriter();
let file = FileUtils.getFile("TmpD", [baseName]);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
const MODE_WRONLY = 0x02;
const MODE_TRUNCATE = 0x20;
zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
// Needs to be in microseconds for some reason.
let time = Date.now() * 1000;
function generateFile(filename) {
let components = filename.split("/");
let path = "";
for (let component of components.slice(0, -1)) {
path += component + "/";
if (!zipW.hasEntry(path)) {
zipW.addEntryDirectory(path, time, false);
}
}
}
for (let filename in files) {
let script = files[filename];
if (typeof(script) == "function") {
script = "(" + script.toString() + ")()";
} else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
script = JSON.stringify(script);
}
if (!instanceOf(script, "ArrayBuffer")) {
script = new TextEncoder("utf-8").encode(script).buffer;
}
let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
stream.setData(script, 0, script.byteLength);
generateFile(filename);
zipW.addEntryStream(filename, time, 0, stream, false);
}
zipW.close();
return file;
}
/**
* Generates a new extension using |Extension.generateXPI|, and initializes a
* new |Extension| instance which will execute it.
*
* @param {object} data
* @returns {Extension}
*/
static generate(data) {
let file = this.generateXPI(data);
flushJarCache(file);
Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
let fileURI = Services.io.newFileURI(file);
let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
// This may be "temporary" or "permanent".
if (data.useAddonManager) {
return new MockExtension(file, jarURI, data.useAddonManager);
}
let id;
if (data.manifest) {
if (data.manifest.applications && data.manifest.applications.gecko) {
id = data.manifest.applications.gecko.id;
} else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
id = data.manifest.browser_specific_settings.gecko.id;
}
}
if (!id) {
id = uuidGen.generateUUID().number;
}
return new Extension({
id,
resourceURI: jarURI,
cleanupFile: file,
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,306 @@
/* 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";
this.EXPORTED_SYMBOLS = ["ExtensionTestUtils"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Components.utils.import("resource://gre/modules/Task.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "Management", () => {
const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
return Management;
});
/* exported ExtensionTestUtils */
let BASE_MANIFEST = Object.freeze({
"applications": Object.freeze({
"gecko": Object.freeze({
"id": "test@web.ext",
}),
}),
"manifest_version": 2,
"name": "name",
"version": "0",
});
class ExtensionWrapper {
constructor(extension, testScope) {
this.extension = extension;
this.testScope = testScope;
this.state = "uninitialized";
this.testResolve = null;
this.testDone = new Promise(resolve => { this.testResolve = resolve; });
this.messageHandler = new Map();
this.messageAwaiter = new Map();
this.messageQueue = new Set();
this.attachListeners();
this.testScope.do_register_cleanup(() => {
if (this.messageQueue.size) {
let names = Array.from(this.messageQueue, ([msg]) => msg);
this.testScope.equal(JSON.stringify(names), "[]", "message queue is empty");
}
if (this.messageAwaiter.size) {
let names = Array.from(this.messageAwaiter.keys());
this.testScope.equal(JSON.stringify(names), "[]", "no tasks awaiting on messages");
}
});
this.testScope.do_register_cleanup(() => {
if (this.state == "pending" || this.state == "running") {
this.testScope.equal(this.state, "unloaded", "Extension left running at test shutdown");
return this.unload();
} else if (extension.state == "unloading") {
this.testScope.equal(this.state, "unloaded", "Extension not fully unloaded at test shutdown");
}
});
this.testScope.do_print(`Extension loaded`);
}
attachListeners() {
/* eslint-disable mozilla/balanced-listeners */
this.extension.on("test-eq", (kind, pass, msg, expected, actual) => {
this.testScope.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`);
});
this.extension.on("test-log", (kind, pass, msg) => {
this.testScope.do_print(msg);
});
this.extension.on("test-result", (kind, pass, msg) => {
this.testScope.ok(pass, msg);
});
this.extension.on("test-done", (kind, pass, msg, expected, actual) => {
this.testScope.ok(pass, msg);
this.testResolve(msg);
});
this.extension.on("test-message", (kind, msg, ...args) => {
let handler = this.messageHandler.get(msg);
if (handler) {
handler(...args);
} else {
this.messageQueue.add([msg, ...args]);
this.checkMessages();
}
});
/* eslint-enable mozilla/balanced-listeners */
}
startup() {
if (this.state != "uninitialized") {
throw new Error("Extension already started");
}
this.state = "pending";
return this.extension.startup().then(
result => {
this.state = "running";
return result;
},
error => {
this.state = "failed";
return Promise.reject(error);
});
}
unload() {
if (this.state != "running") {
throw new Error("Extension not running");
}
this.state = "unloading";
this.extension.shutdown();
this.state = "unloaded";
return Promise.resolve();
}
/*
* This method marks the extension unloading without actually calling
* shutdown, since shutting down a MockExtension causes it to be uninstalled.
*
* Normally you shouldn't need to use this unless you need to test something
* that requires a restart, such as updates.
*/
markUnloaded() {
if (this.state != "running") {
throw new Error("Extension not running");
}
this.state = "unloaded";
return Promise.resolve();
}
sendMessage(...args) {
this.extension.testMessage(...args);
}
awaitFinish(msg) {
return this.testDone.then(actual => {
if (msg) {
this.testScope.equal(actual, msg, "test result correct");
}
return actual;
});
}
checkMessages() {
for (let message of this.messageQueue) {
let [msg, ...args] = message;
let listener = this.messageAwaiter.get(msg);
if (listener) {
this.messageQueue.delete(message);
this.messageAwaiter.delete(msg);
listener.resolve(...args);
return;
}
}
}
checkDuplicateListeners(msg) {
if (this.messageHandler.has(msg) || this.messageAwaiter.has(msg)) {
throw new Error("only one message handler allowed");
}
}
awaitMessage(msg) {
return new Promise(resolve => {
this.checkDuplicateListeners(msg);
this.messageAwaiter.set(msg, {resolve});
this.checkMessages();
});
}
onMessage(msg, callback) {
this.checkDuplicateListeners(msg);
this.messageHandler.set(msg, callback);
}
}
var ExtensionTestUtils = {
BASE_MANIFEST,
normalizeManifest: Task.async(function* (manifest, baseManifest = BASE_MANIFEST) {
yield Management.lazyInit();
let errors = [];
let context = {
url: null,
logError: error => {
errors.push(error);
},
preprocessors: {},
};
manifest = Object.assign({}, baseManifest, manifest);
let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
normalized.errors = errors;
return normalized;
}),
currentScope: null,
profileDir: null,
init(scope) {
this.currentScope = scope;
this.profileDir = scope.do_get_profile();
// We need to load at least one frame script into every message
// manager to ensure that the scriptable wrapper for its global gets
// created before we try to access it externally. If we don't, we
// fail sanity checks on debug builds the first time we try to
// create a wrapper, because we should never have a global without a
// cached wrapper.
Services.mm.loadFrameScript("data:text/javascript,//", true);
let tmpD = this.profileDir.clone();
tmpD.append("tmp");
tmpD.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
let dirProvider = {
getFile(prop, persistent) {
persistent.value = false;
if (prop == "TmpD") {
return tmpD.clone();
}
return null;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
};
Services.dirsvc.registerProvider(dirProvider);
scope.do_register_cleanup(() => {
tmpD.remove(true);
Services.dirsvc.unregisterProvider(dirProvider);
this.currentScope = null;
});
},
addonManagerStarted: false,
mockAppInfo() {
const {updateAppInfo} = Cu.import("resource://testing-common/AppInfo.jsm", {});
updateAppInfo({
ID: "xpcshell@tests.mozilla.org",
name: "XPCShell",
version: "48",
platformVersion: "48",
});
},
startAddonManager() {
if (this.addonManagerStarted) {
return;
}
this.addonManagerStarted = true;
this.mockAppInfo();
let manager = Cc["@mozilla.org/addons/integration;1"].getService(Ci.nsIObserver)
.QueryInterface(Ci.nsITimerCallback);
manager.observe(null, "addons-startup", null);
},
loadExtension(data) {
let extension = Extension.generate(data);
return new ExtensionWrapper(extension, this.currentScope);
},
};

View File

@ -0,0 +1,250 @@
/* 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";
this.EXPORTED_SYMBOLS = ["LegacyExtensionsUtils"];
/* exported LegacyExtensionsUtils, LegacyExtensionContext */
/**
* This file exports helpers for Legacy Extensions that want to embed a webextensions
* and exchange messages with the embedded WebExtension.
*/
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
var {
BaseContext,
} = ExtensionCommon;
var {
Messenger,
} = ExtensionChild;
/**
* Instances created from this class provide to a legacy extension
* a simple API to exchange messages with a webextension.
*/
var LegacyExtensionContext = class extends BaseContext {
/**
* Create a new LegacyExtensionContext given a target Extension instance.
*
* @param {Extension} targetExtension
* The webextension instance associated with this context. This will be the
* instance of the newly created embedded webextension when this class is
* used through the EmbeddedWebExtensionsUtils.
*/
constructor(targetExtension) {
super("legacy_extension", targetExtension);
// Legacy Extensions (xul overlays, bootstrap restartless and Addon SDK)
// runs with a systemPrincipal.
let addonPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
Object.defineProperty(
this, "principal",
{value: addonPrincipal, enumerable: true, configurable: true}
);
let cloneScope = Cu.Sandbox(this.principal, {});
Cu.setSandboxMetadata(cloneScope, {addonId: targetExtension.id});
Object.defineProperty(
this, "cloneScope",
{value: cloneScope, enumerable: true, configurable: true, writable: true}
);
let sender = {id: targetExtension.id};
let filter = {extensionId: targetExtension.id};
// Legacy addons live in the main process. Messages from other addons are
// Messages from WebExtensions are sent to the main process and forwarded via
// the parent process manager to the legacy extension.
this.messenger = new Messenger(this, [Services.cpmm], sender, filter);
this.api = {
browser: {
runtime: {
onConnect: this.messenger.onConnect("runtime.onConnect"),
onMessage: this.messenger.onMessage("runtime.onMessage"),
},
},
};
}
/**
* This method is called when the extension shuts down or is unloaded,
* and it nukes the cloneScope sandbox, if any.
*/
unload() {
if (this.unloaded) {
throw new Error("Error trying to unload LegacyExtensionContext twice.");
}
super.unload();
Cu.nukeSandbox(this.cloneScope);
this.cloneScope = null;
}
};
var EmbeddedExtensionManager;
/**
* Instances of this class are used internally by the exported EmbeddedWebExtensionsUtils
* to manage the embedded webextension instance and the related LegacyExtensionContext
* instance used to exchange messages with it.
*/
class EmbeddedExtension {
/**
* Create a new EmbeddedExtension given the add-on id and the base resource URI of the
* container add-on (the webextension resources will be loaded from the "webextension/"
* subdir of the base resource URI for the legacy extension add-on).
*
* @param {Object} containerAddonParams
* An object with the following properties:
* @param {string} containerAddonParams.id
* The Add-on id of the Legacy Extension which will contain the embedded webextension.
* @param {nsIURI} containerAddonParams.resourceURI
* The nsIURI of the Legacy Extension container add-on.
*/
constructor({id, resourceURI}) {
this.addonId = id;
this.resourceURI = resourceURI;
// Setup status flag.
this.started = false;
}
/**
* Start the embedded webextension.
*
* @returns {Promise<LegacyContextAPI>} A promise which resolve to the API exposed to the
* legacy context.
*/
startup() {
if (this.started) {
return Promise.reject(new Error("This embedded extension has already been started"));
}
// Setup the startup promise.
this.startupPromise = new Promise((resolve, reject) => {
let embeddedExtensionURI = Services.io.newURI("webextension/", null, this.resourceURI);
// This is the instance of the WebExtension embedded in the hybrid add-on.
this.extension = new Extension({
id: this.addonId,
resourceURI: embeddedExtensionURI,
});
// This callback is register to the "startup" event, emitted by the Extension instance
// after the extension manifest.json has been loaded without any errors, but before
// starting any of the defined contexts (which give the legacy part a chance to subscribe
// runtime.onMessage/onConnect listener before the background page has been loaded).
const onBeforeStarted = () => {
this.extension.off("startup", onBeforeStarted);
// Resolve the startup promise and reset the startupError.
this.started = true;
this.startupPromise = null;
// Create the legacy extension context, the legacy container addon
// needs to use it before the embedded webextension startup,
// because it is supposed to be used during the legacy container startup
// to subscribe its message listeners (which are supposed to be able to
// receive any message that the embedded part can try to send to it
// during its startup).
this.context = new LegacyExtensionContext(this.extension);
// Destroy the LegacyExtensionContext cloneScope when
// the embedded webextensions is unloaded.
this.extension.callOnClose({
close: () => {
this.context.unload();
},
});
// resolve startupPromise to execute any pending shutdown that has been
// chained to it.
resolve(this.context.api);
};
this.extension.on("startup", onBeforeStarted);
// Run ambedded extension startup and catch any error during embedded extension
// startup.
this.extension.startup().catch((err) => {
this.started = false;
this.startupPromise = null;
this.extension.off("startup", onBeforeStarted);
reject(err);
});
});
return this.startupPromise;
}
/**
* Shuts down the embedded webextension.
*
* @returns {Promise<void>} a promise that is resolved when the shutdown has been done
*/
shutdown() {
EmbeddedExtensionManager.untrackEmbeddedExtension(this);
// If there is a pending startup, wait to be completed and then shutdown.
if (this.startupPromise) {
return this.startupPromise.then(() => {
this.extension.shutdown();
});
}
// Run shutdown now if the embedded webextension has been correctly started
if (this.extension && this.started && !this.extension.hasShutdown) {
this.extension.shutdown();
}
return Promise.resolve();
}
}
// Keep track on the created EmbeddedExtension instances and destroy
// them when their container addon is going to be disabled or uninstalled.
EmbeddedExtensionManager = {
// Map of the existent EmbeddedExtensions instances by addon id.
embeddedExtensionsByAddonId: new Map(),
untrackEmbeddedExtension(embeddedExtensionInstance) {
// Remove this instance from the tracked embedded extensions
let id = embeddedExtensionInstance.addonId;
if (this.embeddedExtensionsByAddonId.get(id) == embeddedExtensionInstance) {
this.embeddedExtensionsByAddonId.delete(id);
}
},
getEmbeddedExtensionFor({id, resourceURI}) {
let embeddedExtension = this.embeddedExtensionsByAddonId.get(id);
if (!embeddedExtension) {
embeddedExtension = new EmbeddedExtension({id, resourceURI});
// Keep track of the embedded extension instance.
this.embeddedExtensionsByAddonId.set(id, embeddedExtension);
}
return embeddedExtension;
},
};
this.LegacyExtensionsUtils = {
getEmbeddedExtensionFor: (addon) => {
return EmbeddedExtensionManager.getEmbeddedExtensionFor(addon);
},
};

View File

@ -0,0 +1,797 @@
/* 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";
/**
* This module provides wrappers around standard message managers to
* simplify bidirectional communication. It currently allows a caller to
* send a message to a single listener, and receive a reply. If there
* are no matching listeners, or the message manager disconnects before
* a reply is received, the caller is returned an error.
*
* The listener end may specify filters for the messages it wishes to
* receive, and the sender end likewise may specify recipient tags to
* match the filters.
*
* The message handler on the listener side may return its response
* value directly, or may return a promise, the resolution or rejection
* of which will be returned instead. The sender end likewise receives a
* promise which resolves or rejects to the listener's response.
*
*
* A basic setup works something like this:
*
* A content script adds a message listener to its global
* nsIContentFrameMessageManager, with an appropriate set of filters:
*
* {
* init(messageManager, window, extensionID) {
* this.window = window;
*
* MessageChannel.addListener(
* messageManager, "ContentScript:TouchContent",
* this);
*
* this.messageFilterStrict = {
* innerWindowID: getInnerWindowID(window),
* extensionID: extensionID,
* };
*
* this.messageFilterPermissive = {
* outerWindowID: getOuterWindowID(window),
* };
* },
*
* receiveMessage({ target, messageName, sender, recipient, data }) {
* if (messageName == "ContentScript:TouchContent") {
* return new Promise(resolve => {
* this.touchWindow(data.touchWith, result => {
* resolve({ touchResult: result });
* });
* });
* }
* },
* };
*
* A script in the parent process sends a message to the content process
* via a tab message manager, including recipient tags to match its
* filter, and an optional sender tag to identify itself:
*
* let data = { touchWith: "pencil" };
* let sender = { extensionID, contextID };
* let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID };
*
* MessageChannel.sendMessage(
* tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
* data, {recipient, sender}
* ).then(result => {
* alert(result.touchResult);
* });
*
* Since the lifetimes of message senders and receivers may not always
* match, either side of the message channel may cancel pending
* responses which match its sender or recipient tags.
*
* For the above client, this might be done from an
* inner-window-destroyed observer, when its target scope is destroyed:
*
* observe(subject, topic, data) {
* if (topic == "inner-window-destroyed") {
* let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
*
* MessageChannel.abortResponses({ innerWindowID });
* }
* },
*
* From the parent, it may be done when its context is being destroyed:
*
* onDestroy() {
* MessageChannel.abortResponses({
* extensionID: this.extensionID,
* contextID: this.contextID,
* });
* },
*
*/
this.EXPORTED_SYMBOLS = ["MessageChannel"];
/* globals MessageChannel */
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, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyGetter(this, "MessageManagerProxy",
() => ExtensionUtils.MessageManagerProxy);
/**
* Handles the mapping and dispatching of messages to their registered
* handlers. There is one broker per message manager and class of
* messages. Each class of messages is mapped to one native message
* name, e.g., "MessageChannel:Message", and is dispatched to handlers
* based on an internal message name, e.g., "Extension:ExecuteScript".
*/
class FilteringMessageManager {
/**
* @param {string} messageName
* The name of the native message this broker listens for.
* @param {function} callback
* A function which is called for each message after it has been
* mapped to its handler. The function receives two arguments:
*
* result:
* An object containing either a `handler` or an `error` property.
* If no error occurs, `handler` will be a matching handler that
* was registered by `addHandler`. Otherwise, the `error` property
* will contain an object describing the error.
*
* data:
* An object describing the message, as defined in
* `MessageChannel.addListener`.
* @param {nsIMessageListenerManager} messageManager
*/
constructor(messageName, callback, messageManager) {
this.messageName = messageName;
this.callback = callback;
this.messageManager = messageManager;
this.messageManager.addMessageListener(this.messageName, this, true);
this.handlers = new Map();
}
/**
* Receives a message from our message manager, maps it to a handler, and
* passes the result to our message callback.
*/
receiveMessage({data, target}) {
let handlers = Array.from(this.getHandlers(data.messageName, data.sender, data.recipient));
data.target = target;
this.callback(handlers, data);
}
/**
* Iterates over all handlers for the given message name. If `recipient`
* is provided, only iterates over handlers whose filters match it.
*
* @param {string|number} messageName
* The message for which to return handlers.
* @param {object} sender
* The sender data on which to filter handlers.
* @param {object} recipient
* The recipient data on which to filter handlers.
*/
* getHandlers(messageName, sender, recipient) {
let handlers = this.handlers.get(messageName) || new Set();
for (let handler of handlers) {
if (MessageChannel.matchesFilter(handler.messageFilterStrict || {}, recipient) &&
MessageChannel.matchesFilter(handler.messageFilterPermissive || {}, recipient, false) &&
(!handler.filterMessage || handler.filterMessage(sender, recipient))) {
yield handler;
}
}
}
/**
* Registers a handler for the given message.
*
* @param {string} messageName
* The internal message name for which to register the handler.
* @param {object} handler
* An opaque handler object. The object may have a
* `messageFilterStrict` and/or a `messageFilterPermissive`
* property and/or a `filterMessage` method on which to filter messages.
*
* Final dispatching is handled by the message callback passed to
* the constructor.
*/
addHandler(messageName, handler) {
if (!this.handlers.has(messageName)) {
this.handlers.set(messageName, new Set());
}
this.handlers.get(messageName).add(handler);
}
/**
* Unregisters a handler for the given message.
*
* @param {string} messageName
* The internal message name for which to unregister the handler.
* @param {object} handler
* The handler object to unregister.
*/
removeHandler(messageName, handler) {
this.handlers.get(messageName).delete(handler);
}
}
/**
* Manages mappings of message managers to their corresponding message
* brokers. Brokers are lazily created for each message manager the
* first time they are accessed. In the case of content frame message
* managers, they are also automatically destroyed when the frame
* unload event fires.
*/
class FilteringMessageManagerMap extends Map {
// Unfortunately, we can't use a WeakMap for this, because message
// managers do not support preserved wrappers.
/**
* @param {string} messageName
* The native message name passed to `FilteringMessageManager` constructors.
* @param {function} callback
* The message callback function passed to
* `FilteringMessageManager` constructors.
*/
constructor(messageName, callback) {
super();
this.messageName = messageName;
this.callback = callback;
}
/**
* Returns, and possibly creates, a message broker for the given
* message manager.
*
* @param {nsIMessageListenerManager} target
* The message manager for which to return a broker.
*
* @returns {FilteringMessageManager}
*/
get(target) {
if (this.has(target)) {
return super.get(target);
}
let broker = new FilteringMessageManager(this.messageName, this.callback, target);
this.set(target, broker);
if (target instanceof Ci.nsIDOMEventTarget) {
let onUnload = event => {
target.removeEventListener("unload", onUnload);
this.delete(target);
};
target.addEventListener("unload", onUnload);
}
return broker;
}
}
const MESSAGE_MESSAGE = "MessageChannel:Message";
const MESSAGE_RESPONSE = "MessageChannel:Response";
this.MessageChannel = {
init() {
Services.obs.addObserver(this, "message-manager-close", false);
Services.obs.addObserver(this, "message-manager-disconnect", false);
this.messageManagers = new FilteringMessageManagerMap(
MESSAGE_MESSAGE, this._handleMessage.bind(this));
this.responseManagers = new FilteringMessageManagerMap(
MESSAGE_RESPONSE, this._handleResponse.bind(this));
/**
* Contains a list of pending responses, either waiting to be
* received or waiting to be sent. @see _addPendingResponse
*/
this.pendingResponses = new Set();
},
RESULT_SUCCESS: 0,
RESULT_DISCONNECTED: 1,
RESULT_NO_HANDLER: 2,
RESULT_MULTIPLE_HANDLERS: 3,
RESULT_ERROR: 4,
RESULT_NO_RESPONSE: 5,
REASON_DISCONNECTED: {
result: this.RESULT_DISCONNECTED,
message: "Message manager disconnected",
},
/**
* Specifies that only a single listener matching the specified
* recipient tag may be listening for the given message, at the other
* end of the target message manager.
*
* If no matching listeners exist, a RESULT_NO_HANDLER error will be
* returned. If multiple matching listeners exist, a
* RESULT_MULTIPLE_HANDLERS error will be returned.
*/
RESPONSE_SINGLE: 0,
/**
* If multiple message managers matching the specified recipient tag
* are listening for a message, all listeners are notified, but only
* the first response or error is returned.
*
* Only handlers which return a value other than `undefined` are
* considered to have responded. Returning a Promise which evaluates
* to `undefined` is interpreted as an explicit response.
*
* If no matching listeners exist, a RESULT_NO_HANDLER error will be
* returned. If no listeners return a response, a RESULT_NO_RESPONSE
* error will be returned.
*/
RESPONSE_FIRST: 1,
/**
* If multiple message managers matching the specified recipient tag
* are listening for a message, all listeners are notified, and all
* responses are returned as an array, once all listeners have
* replied.
*/
RESPONSE_ALL: 2,
/**
* Fire-and-forget: The sender of this message does not expect a reply.
*/
RESPONSE_NONE: 3,
/**
* Initializes message handlers for the given message managers if needed.
*
* @param {Array<nsIMessageListenerManager>} messageManagers
*/
setupMessageManagers(messageManagers) {
for (let mm of messageManagers) {
// This call initializes a FilteringMessageManager for |mm| if needed.
// The FilteringMessageManager must be created to make sure that senders
// of messages that expect a reply, such as MessageChannel:Message, do
// actually receive a default reply even if there are no explicit message
// handlers.
this.messageManagers.get(mm);
}
},
/**
* Returns true if the properties of the `data` object match those in
* the `filter` object. Matching is done on a strict equality basis,
* and the behavior varies depending on the value of the `strict`
* parameter.
*
* @param {object} filter
* The filter object to match against.
* @param {object} data
* The data object being matched.
* @param {boolean} [strict=false]
* If true, all properties in the `filter` object have a
* corresponding property in `data` with the same value. If
* false, properties present in both objects must have the same
* value.
* @returns {boolean} True if the objects match.
*/
matchesFilter(filter, data, strict = true) {
if (strict) {
return Object.keys(filter).every(key => {
return key in data && data[key] === filter[key];
});
}
return Object.keys(filter).every(key => {
return !(key in data) || data[key] === filter[key];
});
},
/**
* Adds a message listener to the given message manager.
*
* @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets
* The message managers on which to listen.
* @param {string|number} messageName
* The name of the message to listen for.
* @param {MessageReceiver} handler
* The handler to dispatch to. Must be an object with the following
* properties:
*
* receiveMessage:
* A method which is called for each message received by the
* listener. The method takes one argument, an object, with the
* following properties:
*
* messageName:
* The internal message name, as passed to `sendMessage`.
*
* target:
* The message manager which received this message.
*
* channelId:
* The internal ID of the transaction, used to map responses to
* the original sender.
*
* sender:
* An object describing the sender, as passed to `sendMessage`.
*
* recipient:
* An object describing the recipient, as passed to
* `sendMessage`.
*
* data:
* The contents of the message, as passed to `sendMessage`.
*
* The method may return any structured-clone-compatible
* object, which will be returned as a response to the message
* sender. It may also instead return a `Promise`, the
* resolution or rejection value of which will likewise be
* returned to the message sender.
*
* messageFilterStrict:
* An object containing arbitrary properties on which to filter
* received messages. Messages will only be dispatched to this
* object if the `recipient` object passed to `sendMessage`
* matches this filter, as determined by `matchesFilter` with
* `strict=true`.
*
* messageFilterPermissive:
* An object containing arbitrary properties on which to filter
* received messages. Messages will only be dispatched to this
* object if the `recipient` object passed to `sendMessage`
* matches this filter, as determined by `matchesFilter` with
* `strict=false`.
*
* filterMessage:
* An optional function that prevents the handler from handling a
* message by returning `false`. See `getHandlers` for the parameters.
*/
addListener(targets, messageName, handler) {
for (let target of [].concat(targets)) {
this.messageManagers.get(target).addHandler(messageName, handler);
}
},
/**
* Removes a message listener from the given message manager.
*
* @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets
* The message managers on which to stop listening.
* @param {string|number} messageName
* The name of the message to stop listening for.
* @param {MessageReceiver} handler
* The handler to stop dispatching to.
*/
removeListener(targets, messageName, handler) {
for (let target of [].concat(targets)) {
if (this.messageManagers.has(target)) {
this.messageManagers.get(target).removeHandler(messageName, handler);
}
}
},
/**
* Sends a message via the given message manager. Returns a promise which
* resolves or rejects with the return value of the message receiver.
*
* The promise also rejects if there is no matching listener, or the other
* side of the message manager disconnects before the response is received.
*
* @param {nsIMessageSender} target
* The message manager on which to send the message.
* @param {string} messageName
* The name of the message to send, as passed to `addListener`.
* @param {object} data
* A structured-clone-compatible object to send to the message
* recipient.
* @param {object} [options]
* An object containing any of the following properties:
* @param {object} [options.recipient]
* A structured-clone-compatible object to identify the message
* recipient. The object must match the `messageFilterStrict` and
* `messageFilterPermissive` filters defined by recipients in order
* for the message to be received.
* @param {object} [options.sender]
* A structured-clone-compatible object to identify the message
* sender. This object may also be used to avoid delivering the
* message to the sender, and as a filter to prematurely
* abort responses when the sender is being destroyed.
* @see `abortResponses`.
* @param {integer} [options.responseType=RESPONSE_SINGLE]
* Specifies the type of response expected. See the `RESPONSE_*`
* contents for details.
* @returns {Promise}
*/
sendMessage(target, messageName, data, options = {}) {
let sender = options.sender || {};
let recipient = options.recipient || {};
let responseType = options.responseType || this.RESPONSE_SINGLE;
let channelId = ExtensionUtils.getUniqueId();
let message = {messageName, channelId, sender, recipient, data, responseType};
if (responseType == this.RESPONSE_NONE) {
try {
target.sendAsyncMessage(MESSAGE_MESSAGE, message);
} catch (e) {
// Caller is not expecting a reply, so dump the error to the console.
Cu.reportError(e);
return Promise.reject(e);
}
return Promise.resolve(); // Not expecting any reply.
}
let deferred = PromiseUtils.defer();
deferred.sender = recipient;
deferred.messageManager = target;
this._addPendingResponse(deferred);
// The channel ID is used as the message name when routing responses.
// Add a message listener to the response broker, and remove it once
// we've gotten (or canceled) a response.
let broker = this.responseManagers.get(target);
broker.addHandler(channelId, deferred);
let cleanup = () => {
broker.removeHandler(channelId, deferred);
};
deferred.promise.then(cleanup, cleanup);
try {
target.sendAsyncMessage(MESSAGE_MESSAGE, message);
} catch (e) {
deferred.reject(e);
}
return deferred.promise;
},
_callHandlers(handlers, data) {
let responseType = data.responseType;
// At least one handler is required for all response types but
// RESPONSE_ALL.
if (handlers.length == 0 && responseType != this.RESPONSE_ALL) {
return Promise.reject({result: MessageChannel.RESULT_NO_HANDLER,
message: "No matching message handler"});
}
if (responseType == this.RESPONSE_SINGLE) {
if (handlers.length > 1) {
return Promise.reject({result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
message: `Multiple matching handlers for ${data.messageName}`});
}
// Note: We use `new Promise` rather than `Promise.resolve` here
// so that errors from the handler are trapped and converted into
// rejected promises.
return new Promise(resolve => {
resolve(handlers[0].receiveMessage(data));
});
}
let responses = handlers.map(handler => {
try {
return handler.receiveMessage(data);
} catch (e) {
return Promise.reject(e);
}
});
responses = responses.filter(response => response !== undefined);
switch (responseType) {
case this.RESPONSE_FIRST:
if (responses.length == 0) {
return Promise.reject({result: MessageChannel.RESULT_NO_RESPONSE,
message: "No handler returned a response"});
}
return Promise.race(responses);
case this.RESPONSE_ALL:
return Promise.all(responses);
}
return Promise.reject({message: "Invalid response type"});
},
/**
* Handles dispatching message callbacks from the message brokers to their
* appropriate `MessageReceivers`, and routing the responses back to the
* original senders.
*
* Each handler object is a `MessageReceiver` object as passed to
* `addListener`.
*
* @param {Array<MessageHandler>} handlers
* @param {object} data
* @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
*/
_handleMessage(handlers, data) {
if (data.responseType == this.RESPONSE_NONE) {
handlers.forEach(handler => {
// The sender expects no reply, so dump any errors to the console.
new Promise(resolve => {
resolve(handler.receiveMessage(data));
}).catch(e => {
Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e);
});
});
// Note: Unhandled messages are silently dropped.
return;
}
let target = new MessageManagerProxy(data.target);
let deferred = {
sender: data.sender,
messageManager: target,
};
deferred.promise = new Promise((resolve, reject) => {
deferred.reject = reject;
this._callHandlers(handlers, data).then(resolve, reject);
}).then(
value => {
let response = {
result: this.RESULT_SUCCESS,
messageName: data.channelId,
recipient: {},
value,
};
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
},
error => {
let response = {
result: this.RESULT_ERROR,
messageName: data.channelId,
recipient: {},
error: {},
};
if (error && typeof(error) == "object") {
if (error.result) {
response.result = error.result;
}
// Error objects are not structured-clonable, so just copy
// over the important properties.
for (let key of ["fileName", "filename", "lineNumber",
"columnNumber", "message", "stack", "result"]) {
if (key in error) {
response.error[key] = error[key];
}
}
}
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
}).catch(e => {
Cu.reportError(e);
}).then(() => {
target.dispose();
});
this._addPendingResponse(deferred);
},
/**
* Handles message callbacks from the response brokers.
*
* Each handler object is a deferred object created by `sendMessage`, and
* should be resolved or rejected based on the contents of the response.
*
* @param {Array<MessageHandler>} handlers
* @param {object} data
* @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
*/
_handleResponse(handlers, data) {
// If we have an error at this point, we have handler to report it to,
// so just log it.
if (handlers.length == 0) {
Cu.reportError(`No matching message response handler for ${data.messageName}`);
} else if (handlers.length > 1) {
Cu.reportError(`Multiple matching response handlers for ${data.messageName}`);
} else if (data.result === this.RESULT_SUCCESS) {
handlers[0].resolve(data.value);
} else {
handlers[0].reject(data.error);
}
},
/**
* Adds a pending response to the the `pendingResponses` list.
*
* The response object must be a deferred promise with the following
* properties:
*
* promise:
* The promise object which resolves or rejects when the response
* is no longer pending.
*
* reject:
* A function which, when called, causes the `promise` object to be
* rejected.
*
* sender:
* A sender object, as passed to `sendMessage.
*
* messageManager:
* The message manager the response will be sent or received on.
*
* When the promise resolves or rejects, it will be removed from the
* list.
*
* These values are used to clear pending responses when execution
* contexts are destroyed.
*
* @param {Deferred} deferred
*/
_addPendingResponse(deferred) {
let cleanup = () => {
this.pendingResponses.delete(deferred);
};
this.pendingResponses.add(deferred);
deferred.promise.then(cleanup, cleanup);
},
/**
* Aborts any pending message responses to senders matching the given
* filter.
*
* @param {object} sender
* The object on which to filter senders, as determined by
* `matchesFilter`.
* @param {object} [reason]
* An optional object describing the reason the response was aborted.
* Will be passed to the promise rejection handler of all aborted
* responses.
*/
abortResponses(sender, reason = this.REASON_DISCONNECTED) {
for (let response of this.pendingResponses) {
if (this.matchesFilter(sender, response.sender)) {
response.reject(reason);
}
}
},
/**
* Aborts any pending message responses to the broker for the given
* message manager.
*
* @param {nsIMessageListenerManager} target
* The message manager for which to abort brokers.
* @param {object} reason
* An object describing the reason the responses were aborted.
* Will be passed to the promise rejection handler of all aborted
* responses.
*/
abortMessageManager(target, reason) {
for (let response of this.pendingResponses) {
if (MessageManagerProxy.matches(response.messageManager, target)) {
response.reject(reason);
}
}
},
observe(subject, topic, data) {
switch (topic) {
case "message-manager-close":
case "message-manager-disconnect":
try {
if (this.responseManagers.has(subject)) {
this.abortMessageManager(subject, this.REASON_DISCONNECTED);
}
} finally {
this.responseManagers.delete(subject);
this.messageManagers.delete(subject);
}
break;
}
},
};
MessageChannel.init();

View File

@ -0,0 +1,443 @@
/* 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";
this.EXPORTED_SYMBOLS = ["HostManifestManager", "NativeApp"];
/* globals NativeApp */
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
"resource://gre/modules/ExtensionChild.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Subprocess",
"resource://gre/modules/Subprocess.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
"resource://gre/modules/WindowsRegistry.jsm");
const HOST_MANIFEST_SCHEMA = "chrome://extensions/content/schemas/native_host_manifest.json";
const VALID_APPLICATION = /^\w+(\.\w+)*$/;
// For a graceful shutdown (i.e., when the extension is unloaded or when it
// explicitly calls disconnect() on a native port), how long we give the native
// application to exit before we start trying to kill it. (in milliseconds)
const GRACEFUL_SHUTDOWN_TIME = 3000;
// Hard limits on maximum message size that can be read/written
// These are defined in the native messaging documentation, note that
// the write limit is imposed by the "wire protocol" in which message
// boundaries are defined by preceding each message with its length as
// 4-byte unsigned integer so this is the largest value that can be
// represented. Good luck generating a serialized message that large,
// the practical write limit is likely to be dictated by available memory.
const MAX_READ = 1024 * 1024;
const MAX_WRITE = 0xffffffff;
// Preferences that can lower the message size limits above,
// used for testing the limits.
const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
const REGPATH = "Software\\Mozilla\\NativeMessagingHosts";
this.HostManifestManager = {
_initializePromise: null,
_lookup: null,
init() {
if (!this._initializePromise) {
let platform = AppConstants.platform;
if (platform == "win") {
this._lookup = this._winLookup;
} else if (platform == "macosx" || platform == "linux") {
let dirs = [
Services.dirsvc.get("XREUserNativeMessaging", Ci.nsIFile).path,
Services.dirsvc.get("XRESysNativeMessaging", Ci.nsIFile).path,
];
this._lookup = (application, context) => this._tryPaths(application, dirs, context);
} else {
throw new Error(`Native messaging is not supported on ${AppConstants.platform}`);
}
this._initializePromise = Schemas.load(HOST_MANIFEST_SCHEMA);
}
return this._initializePromise;
},
_winLookup(application, context) {
const REGISTRY = Ci.nsIWindowsRegKey;
let regPath = `${REGPATH}\\${application}`;
let path = WindowsRegistry.readRegKey(REGISTRY.ROOT_KEY_CURRENT_USER,
regPath, "", REGISTRY.WOW64_64);
if (!path) {
path = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
regPath, "", REGISTRY.WOW64_64);
}
if (!path) {
return null;
}
return this._tryPath(path, application, context)
.then(manifest => manifest ? {path, manifest} : null);
},
_tryPath(path, application, context) {
return Promise.resolve()
.then(() => OS.File.read(path, {encoding: "utf-8"}))
.then(data => {
let manifest;
try {
manifest = JSON.parse(data);
} catch (ex) {
let msg = `Error parsing native host manifest ${path}: ${ex.message}`;
Cu.reportError(msg);
return null;
}
let normalized = Schemas.normalize(manifest, "manifest.NativeHostManifest", context);
if (normalized.error) {
Cu.reportError(normalized.error);
return null;
}
manifest = normalized.value;
if (manifest.name != application) {
let msg = `Native host manifest ${path} has name property ${manifest.name} (expected ${application})`;
Cu.reportError(msg);
return null;
}
return normalized.value;
}).catch(ex => {
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
return null;
}
throw ex;
});
},
_tryPaths: Task.async(function* (application, dirs, context) {
for (let dir of dirs) {
let path = OS.Path.join(dir, `${application}.json`);
let manifest = yield this._tryPath(path, application, context);
if (manifest) {
return {path, manifest};
}
}
return null;
}),
/**
* Search for a valid native host manifest for the given application name.
* The directories searched and rules for manifest validation are all
* detailed in the native messaging documentation.
*
* @param {string} application The name of the applciation to search for.
* @param {object} context A context object as expected by Schemas.normalize.
* @returns {object} The contents of the validated manifest, or null if
* no valid manifest can be found for this application.
*/
lookupApplication(application, context) {
if (!VALID_APPLICATION.test(application)) {
throw new Error(`Invalid application "${application}"`);
}
return this.init().then(() => this._lookup(application, context));
},
};
this.NativeApp = class extends EventEmitter {
/**
* @param {BaseContext} context The context that initiated the native app.
* @param {string} application The identifier of the native app.
*/
constructor(context, application) {
super();
this.context = context;
this.name = application;
// We want a close() notification when the window is destroyed.
this.context.callOnClose(this);
this.proc = null;
this.readPromise = null;
this.sendQueue = [];
this.writePromise = null;
this.sentDisconnect = false;
this.startupPromise = HostManifestManager.lookupApplication(application, context)
.then(hostInfo => {
// Put the two errors together to not leak information about whether a native
// application is installed to addons that do not have the right permission.
if (!hostInfo || !hostInfo.manifest.allowed_extensions.includes(context.extension.id)) {
throw new context.cloneScope.Error(`This extension does not have permission to use native application ${application} (or the application is not installed)`);
}
let command = hostInfo.manifest.path;
if (AppConstants.platform == "win") {
// OS.Path.join() ignores anything before the last absolute path
// it sees, so if command is already absolute, it remains unchanged
// here. If it is relative, we get the proper absolute path here.
command = OS.Path.join(OS.Path.dirname(hostInfo.path), command);
}
let subprocessOpts = {
command: command,
arguments: [hostInfo.path],
workdir: OS.Path.dirname(command),
stderr: "pipe",
};
return Subprocess.call(subprocessOpts);
}).then(proc => {
this.startupPromise = null;
this.proc = proc;
this._startRead();
this._startWrite();
this._startStderrRead();
}).catch(err => {
this.startupPromise = null;
Cu.reportError(err instanceof Error ? err : err.message);
this._cleanup(err);
});
}
/**
* Open a connection to a native messaging host.
*
* @param {BaseContext} context The context associated with the port.
* @param {nsIMessageSender} messageManager The message manager used to send
* and receive messages from the port's creator.
* @param {string} portId A unique internal ID that identifies the port.
* @param {object} sender The object describing the creator of the connection
* request.
* @param {string} application The name of the native messaging host.
*/
static onConnectNative(context, messageManager, portId, sender, application) {
let app = new NativeApp(context, application);
let port = new ExtensionChild.Port(context, messageManager, [Services.mm], "", portId, sender, sender);
app.once("disconnect", (what, err) => port.disconnect(err));
/* eslint-disable mozilla/balanced-listeners */
app.on("message", (what, msg) => port.postMessage(msg));
/* eslint-enable mozilla/balanced-listeners */
port.registerOnMessage(msg => app.send(msg));
port.registerOnDisconnect(msg => app.close());
}
/**
* @param {BaseContext} context The scope from where `message` originates.
* @param {*} message A message from the extension, meant for a native app.
* @returns {ArrayBuffer} An ArrayBuffer that can be sent to the native app.
*/
static encodeMessage(context, message) {
message = context.jsonStringify(message);
let buffer = new TextEncoder().encode(message).buffer;
if (buffer.byteLength > NativeApp.maxWrite) {
throw new context.cloneScope.Error("Write too big");
}
return buffer;
}
// A port is definitely "alive" if this.proc is non-null. But we have
// to provide a live port object immediately when connecting so we also
// need to consider a port alive if proc is null but the startupPromise
// is still pending.
get _isDisconnected() {
return (!this.proc && !this.startupPromise);
}
_startRead() {
if (this.readPromise) {
throw new Error("Entered _startRead() while readPromise is non-null");
}
this.readPromise = this.proc.stdout.readUint32()
.then(len => {
if (len > NativeApp.maxRead) {
throw new this.context.cloneScope.Error(`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${NativeApp.maxRead} bytes.`);
}
return this.proc.stdout.readJSON(len);
}).then(msg => {
this.emit("message", msg);
this.readPromise = null;
this._startRead();
}).catch(err => {
if (err.errorCode != Subprocess.ERROR_END_OF_FILE) {
Cu.reportError(err instanceof Error ? err : err.message);
}
this._cleanup(err);
});
}
_startWrite() {
if (this.sendQueue.length == 0) {
return;
}
if (this.writePromise) {
throw new Error("Entered _startWrite() while writePromise is non-null");
}
let buffer = this.sendQueue.shift();
let uintArray = Uint32Array.of(buffer.byteLength);
this.writePromise = Promise.all([
this.proc.stdin.write(uintArray.buffer),
this.proc.stdin.write(buffer),
]).then(() => {
this.writePromise = null;
this._startWrite();
}).catch(err => {
Cu.reportError(err.message);
this._cleanup(err);
});
}
_startStderrRead() {
let proc = this.proc;
let app = this.name;
Task.spawn(function* () {
let partial = "";
while (true) {
let data = yield proc.stderr.readString();
if (data.length == 0) {
// We have hit EOF, just stop reading
if (partial) {
Services.console.logStringMessage(`stderr output from native app ${app}: ${partial}`);
}
break;
}
let lines = data.split(/\r?\n/);
lines[0] = partial + lines[0];
partial = lines.pop();
for (let line of lines) {
Services.console.logStringMessage(`stderr output from native app ${app}: ${line}`);
}
}
});
}
send(msg) {
if (this._isDisconnected) {
throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
}
if (Cu.getClassName(msg, true) != "ArrayBuffer") {
// This error cannot be triggered by extensions; it indicates an error in
// our implementation.
throw new Error("The message to the native messaging host is not an ArrayBuffer");
}
let buffer = msg;
if (buffer.byteLength > NativeApp.maxWrite) {
throw new this.context.cloneScope.Error("Write too big");
}
this.sendQueue.push(buffer);
if (!this.startupPromise && !this.writePromise) {
this._startWrite();
}
}
// Shut down the native application and also signal to the extension
// that the connect has been disconnected.
_cleanup(err) {
this.context.forgetOnClose(this);
let doCleanup = () => {
// Set a timer to kill the process gracefully after one timeout
// interval and kill it forcefully after two intervals.
let timer = setTimeout(() => {
this.proc.kill(GRACEFUL_SHUTDOWN_TIME);
}, GRACEFUL_SHUTDOWN_TIME);
let promise = Promise.all([
this.proc.stdin.close()
.catch(err => {
if (err.errorCode != Subprocess.ERROR_END_OF_FILE) {
throw err;
}
}),
this.proc.wait(),
]).then(() => {
this.proc = null;
clearTimeout(timer);
});
AsyncShutdown.profileBeforeChange.addBlocker(
`Native Messaging: Wait for application ${this.name} to exit`,
promise);
promise.then(() => {
AsyncShutdown.profileBeforeChange.removeBlocker(promise);
});
return promise;
};
if (this.proc) {
doCleanup();
} else if (this.startupPromise) {
this.startupPromise.then(doCleanup);
}
if (!this.sentDisconnect) {
this.sentDisconnect = true;
if (err && err.errorCode == Subprocess.ERROR_END_OF_FILE) {
err = null;
}
this.emit("disconnect", err);
}
}
// Called from Context when the extension is shut down.
close() {
this._cleanup();
}
sendMessage(msg) {
let responsePromise = new Promise((resolve, reject) => {
this.once("message", (what, msg) => { resolve(msg); });
this.once("disconnect", (what, err) => { reject(err); });
});
let result = this.startupPromise.then(() => {
this.send(msg);
return responsePromise;
});
result.then(() => {
this._cleanup();
}, () => {
// Prevent the response promise from being reported as an
// unchecked rejection if the startup promise fails.
responsePromise.catch(() => {});
this._cleanup();
});
return result;
}
};
XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxRead", PREF_MAX_READ, MAX_READ);
XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxWrite", PREF_MAX_WRITE, MAX_WRITE);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,155 @@
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
} = ExtensionUtils;
// WeakMap[Extension -> Map[name -> Alarm]]
var alarmsMap = new WeakMap();
// WeakMap[Extension -> Set[callback]]
var 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 {
if (!this.delayInMinutes) {
this.delayInMinutes = this.periodInMinutes;
}
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.name);
this.canceled = true;
},
observe(subject, topic, data) {
if (this.canceled) {
return;
}
for (let callback of alarmCallbacksMap.get(this.extension)) {
callback(this);
}
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,
};
},
};
/* eslint-disable mozilla/balanced-listeners */
extensions.on("startup", (type, extension) => {
alarmsMap.set(extension, new Map());
alarmCallbacksMap.set(extension, new Set());
});
extensions.on("shutdown", (type, extension) => {
if (alarmsMap.has(extension)) {
for (let alarm of alarmsMap.get(extension).values()) {
alarm.clear();
}
alarmsMap.delete(extension);
alarmCallbacksMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("alarms", "addon_parent", context => {
let {extension} = context;
return {
alarms: {
create: function(name, alarmInfo) {
name = name || "";
let alarms = alarmsMap.get(extension);
if (alarms.has(name)) {
alarms.get(name).clear();
}
let alarm = new Alarm(extension, name, alarmInfo);
alarms.set(alarm.name, alarm);
},
get: function(name) {
name = name || "";
let alarms = alarmsMap.get(extension);
if (alarms.has(name)) {
return Promise.resolve(alarms.get(name).data);
}
return Promise.resolve();
},
getAll: function() {
let result = Array.from(alarmsMap.get(extension).values(), alarm => alarm.data);
return Promise.resolve(result);
},
clear: function(name) {
name = name || "";
let alarms = alarmsMap.get(extension);
if (alarms.has(name)) {
alarms.get(name).clear();
return Promise.resolve(true);
}
return Promise.resolve(false);
},
clearAll: function() {
let cleared = false;
for (let alarm of alarmsMap.get(extension).values()) {
alarm.clear();
cleared = true;
}
return Promise.resolve(cleared);
},
onAlarm: new EventManager(context, "alarms.onAlarm", fire => {
let callback = alarm => {
fire(alarm.data);
};
alarmCallbacksMap.get(extension).add(callback);
return () => {
alarmCallbacksMap.get(extension).delete(callback);
};
}).api(),
},
};
});

View File

@ -0,0 +1,147 @@
"use strict";
var {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
promiseDocumentLoaded,
promiseObserved,
} = ExtensionUtils;
const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI(
`<?xml version="1.0"?>
<window id="documentElement"/>`);
// WeakMap[Extension -> BackgroundPage]
var backgroundPagesMap = new WeakMap();
// Responsible for the background_page section of the manifest.
function BackgroundPage(options, extension) {
this.extension = extension;
this.page = options.page || null;
this.isGenerated = !!options.scripts;
this.windowlessBrowser = null;
this.webNav = null;
}
BackgroundPage.prototype = {
build: Task.async(function* () {
let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
this.windowlessBrowser = windowlessBrowser;
let url;
if (this.page) {
url = this.extension.baseURI.resolve(this.page);
} else if (this.isGenerated) {
url = this.extension.baseURI.resolve("_generated_background_page.html");
}
if (!this.extension.isExtensionURL(url)) {
this.extension.manifestError("Background page must be a file within the extension");
url = this.extension.baseURI.resolve("_blank.html");
}
let system = Services.scriptSecurityManager.getSystemPrincipal();
// The windowless browser is a thin wrapper around a docShell that keeps
// its related resources alive. It implements nsIWebNavigation and
// forwards its methods to the underlying docShell, but cannot act as a
// docShell itself. Calling `getInterface(nsIDocShell)` gives us the
// underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us
// access to the webNav methods that are already available on the
// windowless browser, but contrary to appearances, they are not the same
// object.
let chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIWebNavigation);
if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
let attrs = chromeShell.getOriginAttributes();
attrs.privateBrowsingId = 1;
chromeShell.setOriginAttributes(attrs);
}
chromeShell.useGlobalHistory = false;
chromeShell.createAboutBlankContentViewer(system);
chromeShell.loadURI(XUL_URL, 0, null, null, null);
yield promiseObserved("chrome-document-global-created",
win => win.document == chromeShell.document);
let chromeDoc = yield promiseDocumentLoaded(chromeShell.document);
let browser = chromeDoc.createElement("browser");
browser.setAttribute("type", "content");
browser.setAttribute("disableglobalhistory", "true");
chromeDoc.documentElement.appendChild(browser);
extensions.emit("extension-browser-inserted", browser);
browser.messageManager.sendAsyncMessage("Extension:InitExtensionView", {
viewType: "background",
url,
});
yield new Promise(resolve => {
browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad() {
browser.messageManager.removeMessageListener("Extension:ExtensionViewLoaded", onLoad);
resolve();
});
});
// TODO(robwu): This is not webext-oop compatible.
this.webNav = browser.docShell.QueryInterface(Ci.nsIWebNavigation);
let window = this.webNav.document.defaultView;
// Set the add-on's main debugger global, for use in the debugger
// console.
if (this.extension.addonData.instanceID) {
AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
.then(addon => addon.setDebugGlobal(window));
}
this.extension.emit("startup");
}),
shutdown() {
if (this.extension.addonData.instanceID) {
AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
.then(addon => addon.setDebugGlobal(null));
}
// Navigate away from the background page to invalidate any
// setTimeouts or other callbacks.
if (this.webNav) {
this.webNav.loadURI("about:blank", 0, null, null, null);
this.webNav = null;
}
this.windowlessBrowser.loadURI("about:blank", 0, null, null, null);
this.windowlessBrowser.close();
this.windowlessBrowser = null;
},
};
/* eslint-disable mozilla/balanced-listeners */
extensions.on("manifest_background", (type, directive, extension, manifest) => {
let bgPage = new BackgroundPage(manifest.background, extension);
backgroundPagesMap.set(extension, bgPage);
return bgPage.build();
});
extensions.on("shutdown", (type, extension) => {
if (backgroundPagesMap.has(extension)) {
backgroundPagesMap.get(extension).shutdown();
backgroundPagesMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */

View File

@ -0,0 +1,217 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "require",
"resource://devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyGetter(this, "colorUtils", () => {
return require("devtools/shared/css/color").colorUtils;
});
const {
stylesheetMap,
} = ExtensionUtils;
/* globals addMessageListener, content, docShell, sendAsyncMessage */
// Minimum time between two resizes.
const RESIZE_TIMEOUT = 100;
const BrowserListener = {
init({allowScriptsToClose, fixedWidth, maxHeight, maxWidth, stylesheets}) {
this.fixedWidth = fixedWidth;
this.stylesheets = stylesheets || [];
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
this.oldBackground = null;
if (allowScriptsToClose) {
content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.allowScriptsToClose();
}
addEventListener("DOMWindowCreated", this, true);
addEventListener("load", this, true);
addEventListener("DOMContentLoaded", this, true);
addEventListener("DOMWindowClose", this, true);
addEventListener("MozScrolledAreaChanged", this, true);
},
destroy() {
removeEventListener("DOMWindowCreated", this, true);
removeEventListener("load", this, true);
removeEventListener("DOMContentLoaded", this, true);
removeEventListener("DOMWindowClose", this, true);
removeEventListener("MozScrolledAreaChanged", this, true);
},
receiveMessage({name, data}) {
if (name === "Extension:InitBrowser") {
this.init(data);
}
},
handleEvent(event) {
switch (event.type) {
case "DOMWindowCreated":
if (event.target === content.document) {
let winUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
for (let url of this.stylesheets) {
winUtils.addSheet(stylesheetMap.get(url), winUtils.AGENT_SHEET);
}
}
break;
case "DOMWindowClose":
if (event.target === content) {
event.preventDefault();
sendAsyncMessage("Extension:DOMWindowClose");
}
break;
case "DOMContentLoaded":
if (event.target === content.document) {
sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href});
this.handleDOMChange(true);
}
break;
case "load":
if (event.target.contentWindow === content) {
// For about:addons inline <browsers>, we currently receive a load
// event on the <browser> element, but no load or DOMContentLoaded
// events from the content window.
sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href});
} else if (event.target !== content.document) {
break;
}
// We use a capturing listener, so we get this event earlier than any
// load listeners in the content page. Resizing after a timeout ensures
// that we calculate the size after the entire event cycle has completed
// (unless someone spins the event loop, anyway), and hopefully after
// the content has made any modifications.
Promise.resolve().then(() => {
this.handleDOMChange(true);
});
// Mutation observer to make sure the panel shrinks when the content does.
new content.MutationObserver(this.handleDOMChange.bind(this)).observe(
content.document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
});
break;
case "MozScrolledAreaChanged":
this.handleDOMChange();
break;
}
},
// Resizes the browser to match the preferred size of the content (debounced).
handleDOMChange(ignoreThrottling = false) {
if (ignoreThrottling && this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
this.resizeTimeout = null;
}
if (this.resizeTimeout == null) {
this.resizeTimeout = setTimeout(() => {
try {
if (content) {
this._handleDOMChange("delayed");
}
} finally {
this.resizeTimeout = null;
}
}, RESIZE_TIMEOUT);
this._handleDOMChange();
}
},
_handleDOMChange(detail) {
let doc = content.document;
let body = doc.body;
if (!body || doc.compatMode === "BackCompat") {
// In quirks mode, the root element is used as the scroll frame, and the
// body lies about its scroll geometry, and returns the values for the
// root instead.
body = doc.documentElement;
}
let result;
if (this.fixedWidth) {
// If we're in a fixed-width area (namely a slide-in subview of the main
// menu panel), we need to calculate the view height based on the
// preferred height of the content document's root scrollable element at the
// current width, rather than the complete preferred dimensions of the
// content window.
// Compensate for any offsets (margin, padding, ...) between the scroll
// area of the body and the outer height of the document.
let getHeight = elem => elem.getBoundingClientRect(elem).height;
let bodyPadding = getHeight(doc.documentElement) - getHeight(body);
let height = Math.ceil(body.scrollHeight + bodyPadding);
result = {height, detail};
} else {
let background = doc.defaultView.getComputedStyle(body).backgroundColor;
let bgColor = colorUtils.colorToRGBA(background);
if (bgColor.a !== 1) {
// Ignore non-opaque backgrounds.
background = null;
}
if (background !== this.oldBackground) {
sendAsyncMessage("Extension:BrowserBackgroundChanged", {background});
}
this.oldBackground = background;
// Adjust the size of the browser based on its content's preferred size.
let {contentViewer} = docShell;
let ratio = content.devicePixelRatio;
let w = {}, h = {};
contentViewer.getContentSizeConstrained(this.maxWidth * ratio,
this.maxHeight * ratio,
w, h);
let width = Math.ceil(w.value / ratio);
let height = Math.ceil(h.value / ratio);
result = {width, height, detail};
}
sendAsyncMessage("Extension:BrowserResized", result);
},
};
addMessageListener("Extension:InitBrowser", BrowserListener);

View File

@ -0,0 +1,45 @@
"use strict";
global.initializeBackgroundPage = (contentWindow) => {
// Override the `alert()` method inside background windows;
// we alias it to console.log().
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1203394
let alertDisplayedWarning = false;
let alertOverwrite = text => {
if (!alertDisplayedWarning) {
require("devtools/client/framework/devtools-browser");
let {HUDService} = require("devtools/client/webconsole/hudservice");
HUDService.openBrowserConsoleOrFocus();
contentWindow.console.warn("alert() is not supported in background windows; please use console.log instead.");
alertDisplayedWarning = true;
}
contentWindow.console.log(text);
};
Cu.exportFunction(alertOverwrite, contentWindow, {defineAs: "alert"});
};
extensions.registerSchemaAPI("extension", "addon_child", context => {
function getBackgroundPage() {
for (let view of context.extension.views) {
if (view.viewType == "background" && context.principal.subsumes(view.principal)) {
return view.contentWindow;
}
}
return null;
}
return {
extension: {
getBackgroundPage,
},
runtime: {
getBackgroundPage() {
return context.cloneScope.Promise.resolve(getBackgroundPage());
},
},
};
});

View File

@ -0,0 +1,57 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
function extensionApiFactory(context) {
return {
extension: {
getURL(url) {
return context.extension.baseURI.resolve(url);
},
get lastError() {
return context.lastError;
},
get inIncognitoContext() {
return context.incognito;
},
},
};
}
extensions.registerSchemaAPI("extension", "addon_child", extensionApiFactory);
extensions.registerSchemaAPI("extension", "content_child", extensionApiFactory);
extensions.registerSchemaAPI("extension", "addon_child", context => {
return {
extension: {
getViews: function(fetchProperties) {
let result = Cu.cloneInto([], context.cloneScope);
for (let view of context.extension.views) {
if (!view.active) {
continue;
}
if (!context.principal.subsumes(view.principal)) {
continue;
}
if (fetchProperties !== null) {
if (fetchProperties.type !== null && view.viewType != fetchProperties.type) {
continue;
}
if (fetchProperties.windowId !== null && view.windowId != fetchProperties.windowId) {
continue;
}
}
result.push(view.contentWindow);
}
return result;
},
},
};
});

View File

@ -0,0 +1,96 @@
"use strict";
function runtimeApiFactory(context) {
let {extension} = context;
return {
runtime: {
onConnect: context.messenger.onConnect("runtime.onConnect"),
onMessage: context.messenger.onMessage("runtime.onMessage"),
onConnectExternal: context.messenger.onConnectExternal("runtime.onConnectExternal"),
onMessageExternal: context.messenger.onMessageExternal("runtime.onMessageExternal"),
connect: function(extensionId, connectInfo) {
let name = connectInfo !== null && connectInfo.name || "";
extensionId = extensionId || extension.id;
let recipient = {extensionId};
return context.messenger.connect(context.messageManager, name, recipient);
},
sendMessage: function(...args) {
let options; // eslint-disable-line no-unused-vars
let extensionId, message, responseCallback;
if (typeof args[args.length - 1] == "function") {
responseCallback = args.pop();
}
if (!args.length) {
return Promise.reject({message: "runtime.sendMessage's message argument is missing"});
} else if (args.length == 1) {
message = args[0];
} else if (args.length == 2) {
if (typeof args[0] == "string" && args[0]) {
[extensionId, message] = args;
} else {
[message, options] = args;
}
} else if (args.length == 3) {
[extensionId, message, options] = args;
} else if (args.length == 4 && !responseCallback) {
return Promise.reject({message: "runtime.sendMessage's last argument is not a function"});
} else {
return Promise.reject({message: "runtime.sendMessage received too many arguments"});
}
if (extensionId != null && typeof extensionId != "string") {
return Promise.reject({message: "runtime.sendMessage's extensionId argument is invalid"});
}
if (options != null && typeof options != "object") {
return Promise.reject({message: "runtime.sendMessage's options argument is invalid"});
}
extensionId = extensionId || extension.id;
let recipient = {extensionId};
return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
},
connectNative(application) {
let recipient = {
childId: context.childManager.id,
toNativeApp: application,
};
return context.messenger.connectNative(context.messageManager, "", recipient);
},
sendNativeMessage(application, message) {
let recipient = {
childId: context.childManager.id,
toNativeApp: application,
};
return context.messenger.sendNativeMessage(context.messageManager, message, recipient);
},
get lastError() {
return context.lastError;
},
getManifest() {
return Cu.cloneInto(extension.manifest, context.cloneScope);
},
id: extension.id,
getURL: function(url) {
return extension.baseURI.resolve(url);
},
},
};
}
extensions.registerSchemaAPI("runtime", "addon_child", runtimeApiFactory);
extensions.registerSchemaAPI("runtime", "content_child", runtimeApiFactory);

View File

@ -0,0 +1,62 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
Cu.import("resource://gre/modules/Services.jsm");
function storageApiFactory(context) {
function sanitize(items) {
// The schema validator already takes care of arrays (which are only allowed
// to contain strings). Strings and null are safe values.
if (typeof items != "object" || items === null || Array.isArray(items)) {
return items;
}
// If we got here, then `items` is an object generated by `ObjectType`'s
// `normalize` method from Schemas.jsm. The object returned by `normalize`
// lives in this compartment, while the values live in compartment of
// `context.contentWindow`. The `sanitize` method runs with the principal
// of `context`, so we cannot just use `ExtensionStorage.sanitize` because
// it is not allowed to access properties of `items`.
// So we enumerate all properties and sanitize each value individually.
let sanitized = {};
for (let [key, value] of Object.entries(items)) {
sanitized[key] = ExtensionStorage.sanitize(value, context);
}
return sanitized;
}
return {
storage: {
local: {
get: function(keys) {
keys = sanitize(keys);
return context.childManager.callParentAsyncFunction("storage.local.get", [
keys,
]);
},
set: function(items) {
items = sanitize(items);
return context.childManager.callParentAsyncFunction("storage.local.set", [
items,
]);
},
},
sync: {
get: function(keys) {
keys = sanitize(keys);
return context.childManager.callParentAsyncFunction("storage.sync.get", [
keys,
]);
},
set: function(items) {
items = sanitize(items);
return context.childManager.callParentAsyncFunction("storage.sync.set", [
items,
]);
},
},
},
};
}
extensions.registerSchemaAPI("storage", "addon_child", storageApiFactory);
extensions.registerSchemaAPI("storage", "content_child", storageApiFactory);

View File

@ -0,0 +1,188 @@
"use strict";
Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
var {
SingletonEventManager,
} = ExtensionUtils;
/**
* Checks whether the given error matches the given expectations.
*
* @param {*} error
* The error to check.
* @param {string|RegExp|function|null} expectedError
* The expectation to check against. If this parameter is:
*
* - a string, the error message must exactly equal the string.
* - a regular expression, it must match the error message.
* - a function, it is called with the error object and its
* return value is returned.
* - null, the function always returns true.
* @param {BaseContext} context
*
* @returns {boolean}
* True if the error matches the expected error.
*/
function errorMatches(error, expectedError, context) {
if (expectedError === null) {
return true;
}
if (typeof expectedError === "function") {
return context.runSafeWithoutClone(expectedError, error);
}
if (typeof error !== "object" || error == null ||
typeof error.message !== "string") {
return false;
}
if (typeof expectedError === "string") {
return error.message === expectedError;
}
try {
return expectedError.test(error.message);
} catch (e) {
Cu.reportError(e);
}
return false;
}
/**
* Calls .toSource() on the given value, but handles null, undefined,
* and errors.
*
* @param {*} value
* @returns {string}
*/
function toSource(value) {
if (value === null) {
return "null";
}
if (value === undefined) {
return "undefined";
}
if (typeof value === "string") {
return JSON.stringify(value);
}
try {
return String(value.toSource());
} catch (e) {
return "<unknown>";
}
}
function makeTestAPI(context) {
const {extension} = context;
function getStack() {
return new context.cloneScope.Error().stack.replace(/^/gm, " ");
}
function assertTrue(value, msg) {
extension.emit("test-result", Boolean(value), String(msg), getStack());
}
return {
test: {
sendMessage(...args) {
extension.emit("test-message", ...args);
},
notifyPass(msg) {
extension.emit("test-done", true, msg, getStack());
},
notifyFail(msg) {
extension.emit("test-done", false, msg, getStack());
},
log(msg) {
extension.emit("test-log", true, msg, getStack());
},
fail(msg) {
assertTrue(false, msg);
},
succeed(msg) {
assertTrue(true, msg);
},
assertTrue(value, msg) {
assertTrue(value, msg);
},
assertFalse(value, msg) {
assertTrue(!value, msg);
},
assertEq(expected, actual, msg) {
let equal = expected === actual;
expected = String(expected);
actual = String(actual);
if (!equal && expected === actual) {
actual += " (different)";
}
extension.emit("test-eq", equal, String(msg), expected, actual, getStack());
},
assertRejects(promise, expectedError, msg) {
// Wrap in a native promise for consistency.
promise = Promise.resolve(promise);
if (msg) {
msg = `: ${msg}`;
}
return promise.then(result => {
assertTrue(false, `Promise resolved, expected rejection${msg}`);
}, error => {
let errorMessage = toSource(error && error.message);
assertTrue(errorMatches(error, expectedError, context),
`Promise rejected, expecting rejection to match ${toSource(expectedError)}, ` +
`got ${errorMessage}${msg}`);
});
},
assertThrows(func, expectedError, msg) {
if (msg) {
msg = `: ${msg}`;
}
try {
func();
assertTrue(false, `Function did not throw, expected error${msg}`);
} catch (error) {
let errorMessage = toSource(error && error.message);
assertTrue(errorMatches(error, expectedError, context),
`Function threw, expecting error to match ${toSource(expectedError)}` +
`got ${errorMessage}${msg}`);
}
},
onMessage: new SingletonEventManager(context, "test.onMessage", fire => {
let handler = (event, ...args) => {
context.runSafe(fire, ...args);
};
extension.on("test-harness-message", handler);
return () => {
extension.off("test-harness-message", handler);
};
}).api(),
},
};
}
extensions.registerSchemaAPI("test", "addon_child", makeTestAPI);
extensions.registerSchemaAPI("test", "content_child", makeTestAPI);

View File

@ -0,0 +1,484 @@
"use strict";
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
"resource://gre/modules/ContextualIdentityService.jsm");
var {
EventManager,
} = ExtensionUtils;
var DEFAULT_STORE = "firefox-default";
var PRIVATE_STORE = "firefox-private";
var CONTAINER_STORE = "firefox-container-";
global.getCookieStoreIdForTab = function(data, tab) {
if (data.incognito) {
return PRIVATE_STORE;
}
if (tab.userContextId) {
return CONTAINER_STORE + tab.userContextId;
}
return DEFAULT_STORE;
};
global.isPrivateCookieStoreId = function(storeId) {
return storeId == PRIVATE_STORE;
};
global.isDefaultCookieStoreId = function(storeId) {
return storeId == DEFAULT_STORE;
};
global.isContainerCookieStoreId = function(storeId) {
return storeId !== null && storeId.startsWith(CONTAINER_STORE);
};
global.getContainerForCookieStoreId = function(storeId) {
if (!global.isContainerCookieStoreId(storeId)) {
return null;
}
let containerId = storeId.substring(CONTAINER_STORE.length);
if (ContextualIdentityService.getIdentityFromId(containerId)) {
return parseInt(containerId, 10);
}
return null;
};
global.isValidCookieStoreId = function(storeId) {
return global.isDefaultCookieStoreId(storeId) ||
global.isPrivateCookieStoreId(storeId) ||
global.isContainerCookieStoreId(storeId);
};
function convert({cookie, isPrivate}) {
let result = {
name: cookie.name,
value: cookie.value,
domain: cookie.host,
hostOnly: !cookie.isDomain,
path: cookie.path,
secure: cookie.isSecure,
httpOnly: cookie.isHttpOnly,
session: cookie.isSession,
};
if (!cookie.isSession) {
result.expirationDate = cookie.expiry;
}
if (cookie.originAttributes.userContextId) {
result.storeId = CONTAINER_STORE + cookie.originAttributes.userContextId;
} else if (cookie.originAttributes.privateBrowsingId || isPrivate) {
result.storeId = PRIVATE_STORE;
} else {
result.storeId = DEFAULT_STORE;
}
return result;
}
function isSubdomain(otherDomain, baseDomain) {
return otherDomain == baseDomain || otherDomain.endsWith("." + baseDomain);
}
// Checks that the given extension has permission to set the given cookie for
// the given URI.
function checkSetCookiePermissions(extension, uri, cookie) {
// Permission checks:
//
// - If the extension does not have permissions for the specified
// URL, it cannot set cookies for it.
//
// - If the specified URL could not set the given cookie, neither can
// the extension.
//
// Ideally, we would just have the cookie service make the latter
// determination, but that turns out to be quite complicated. At the
// moment, it requires constructing a cookie string and creating a
// dummy channel, both of which can be problematic. It also triggers
// a whole set of additional permission and preference checks, which
// may or may not be desirable.
//
// So instead, we do a similar set of checks here. Exactly what
// cookies a given URL should be able to set is not well-documented,
// and is not standardized in any standard that anyone actually
// follows. So instead, we follow the rules used by the cookie
// service.
//
// See source/netwerk/cookie/nsCookieService.cpp, in particular
// CheckDomain() and SetCookieInternal().
if (uri.scheme != "http" && uri.scheme != "https") {
return false;
}
if (!extension.whiteListedHosts.matchesIgnoringPath(uri)) {
return false;
}
if (!cookie.host) {
// If no explicit host is specified, this becomes a host-only cookie.
cookie.host = uri.host;
return true;
}
// A leading "." is not expected, but is tolerated if it's not the only
// character in the host. If there is one, start by stripping it off. We'll
// add a new one on success.
if (cookie.host.length > 1) {
cookie.host = cookie.host.replace(/^\./, "");
}
cookie.host = cookie.host.toLowerCase();
if (cookie.host != uri.host) {
// Not an exact match, so check for a valid subdomain.
let baseDomain;
try {
baseDomain = Services.eTLD.getBaseDomain(uri);
} catch (e) {
if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
// The cookie service uses these to determine whether the domain
// requires an exact match. We already know we don't have an exact
// match, so return false. In all other cases, re-raise the error.
return false;
}
throw e;
}
// The cookie domain must be a subdomain of the base domain. This prevents
// us from setting cookies for domains like ".co.uk".
// The domain of the requesting URL must likewise be a subdomain of the
// cookie domain. This prevents us from setting cookies for entirely
// unrelated domains.
if (!isSubdomain(cookie.host, baseDomain) ||
!isSubdomain(uri.host, cookie.host)) {
return false;
}
// RFC2109 suggests that we may only add cookies for sub-domains 1-level
// below us, but enforcing that would break the web, so we don't.
}
// An explicit domain was passed, so add a leading "." to make this a
// domain cookie.
cookie.host = "." + cookie.host;
// We don't do any significant checking of path permissions. RFC2109
// suggests we only allow sites to add cookies for sub-paths, similar to
// same origin policy enforcement, but no-one implements this.
return true;
}
function* query(detailsIn, props, context) {
// Different callers want to filter on different properties. |props|
// tells us which ones they're interested in.
let details = {};
props.forEach(property => {
if (detailsIn[property] !== null) {
details[property] = detailsIn[property];
}
});
if ("domain" in details) {
details.domain = details.domain.toLowerCase().replace(/^\./, "");
}
let userContextId = 0;
let isPrivate = context.incognito;
if (details.storeId) {
if (!global.isValidCookieStoreId(details.storeId)) {
return;
}
if (global.isDefaultCookieStoreId(details.storeId)) {
isPrivate = false;
} else if (global.isPrivateCookieStoreId(details.storeId)) {
isPrivate = true;
} else if (global.isContainerCookieStoreId(details.storeId)) {
isPrivate = false;
userContextId = global.getContainerForCookieStoreId(details.storeId);
if (!userContextId) {
return;
}
}
}
let storeId = DEFAULT_STORE;
if (isPrivate) {
storeId = PRIVATE_STORE;
} else if ("storeId" in details) {
storeId = details.storeId;
}
// We can use getCookiesFromHost for faster searching.
let enumerator;
let uri;
if ("url" in details) {
try {
uri = NetUtil.newURI(details.url).QueryInterface(Ci.nsIURL);
Services.cookies.usePrivateMode(isPrivate, () => {
enumerator = Services.cookies.getCookiesFromHost(uri.host, {userContextId});
});
} catch (ex) {
// This often happens for about: URLs
return;
}
} else if ("domain" in details) {
Services.cookies.usePrivateMode(isPrivate, () => {
enumerator = Services.cookies.getCookiesFromHost(details.domain, {userContextId});
});
} else {
Services.cookies.usePrivateMode(isPrivate, () => {
enumerator = Services.cookies.enumerator;
});
}
// Based on nsCookieService::GetCookieStringInternal
function matches(cookie) {
function domainMatches(host) {
return cookie.rawHost == host || (cookie.isDomain && host.endsWith(cookie.host));
}
function pathMatches(path) {
let cookiePath = cookie.path.replace(/\/$/, "");
if (!path.startsWith(cookiePath)) {
return false;
}
// path == cookiePath, but without the redundant string compare.
if (path.length == cookiePath.length) {
return true;
}
// URL path is a substring of the cookie path, so it matches if, and
// only if, the next character is a path delimiter.
let pathDelimiters = ["/", "?", "#", ";"];
return pathDelimiters.includes(path[cookiePath.length]);
}
// "Restricts the retrieved cookies to those that would match the given URL."
if (uri) {
if (!domainMatches(uri.host)) {
return false;
}
if (cookie.isSecure && uri.scheme != "https") {
return false;
}
if (!pathMatches(uri.path)) {
return false;
}
}
if ("name" in details && details.name != cookie.name) {
return false;
}
if (userContextId != cookie.originAttributes.userContextId) {
return false;
}
// "Restricts the retrieved cookies to those whose domains match or are subdomains of this one."
if ("domain" in details && !isSubdomain(cookie.rawHost, details.domain)) {
return false;
}
// "Restricts the retrieved cookies to those whose path exactly matches this string.""
if ("path" in details && details.path != cookie.path) {
return false;
}
if ("secure" in details && details.secure != cookie.isSecure) {
return false;
}
if ("session" in details && details.session != cookie.isSession) {
return false;
}
// Check that the extension has permissions for this host.
if (!context.extension.whiteListedHosts.matchesCookie(cookie)) {
return false;
}
return true;
}
while (enumerator.hasMoreElements()) {
let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
if (matches(cookie)) {
yield {cookie, isPrivate, storeId};
}
}
}
extensions.registerSchemaAPI("cookies", "addon_parent", context => {
let {extension} = context;
let self = {
cookies: {
get: function(details) {
// FIXME: We don't sort by length of path and creation time.
for (let cookie of query(details, ["url", "name", "storeId"], context)) {
return Promise.resolve(convert(cookie));
}
// Found no match.
return Promise.resolve(null);
},
getAll: function(details) {
let allowed = ["url", "name", "domain", "path", "secure", "session", "storeId"];
let result = Array.from(query(details, allowed, context), convert);
return Promise.resolve(result);
},
set: function(details) {
let uri = NetUtil.newURI(details.url).QueryInterface(Ci.nsIURL);
let path;
if (details.path !== null) {
path = details.path;
} else {
// This interface essentially emulates the behavior of the
// Set-Cookie header. In the case of an omitted path, the cookie
// service uses the directory path of the requesting URL, ignoring
// any filename or query parameters.
path = uri.directory;
}
let name = details.name !== null ? details.name : "";
let value = details.value !== null ? details.value : "";
let secure = details.secure !== null ? details.secure : false;
let httpOnly = details.httpOnly !== null ? details.httpOnly : false;
let isSession = details.expirationDate === null;
let expiry = isSession ? Number.MAX_SAFE_INTEGER : details.expirationDate;
let isPrivate = context.incognito;
let userContextId = 0;
if (global.isDefaultCookieStoreId(details.storeId)) {
isPrivate = false;
} else if (global.isPrivateCookieStoreId(details.storeId)) {
isPrivate = true;
} else if (global.isContainerCookieStoreId(details.storeId)) {
let containerId = global.getContainerForCookieStoreId(details.storeId);
if (containerId === null) {
return Promise.reject({message: `Illegal storeId: ${details.storeId}`});
}
isPrivate = false;
userContextId = containerId;
} else if (details.storeId !== null) {
return Promise.reject({message: "Unknown storeId"});
}
let cookieAttrs = {host: details.domain, path: path, isSecure: secure};
if (!checkSetCookiePermissions(extension, uri, cookieAttrs)) {
return Promise.reject({message: `Permission denied to set cookie ${JSON.stringify(details)}`});
}
// The permission check may have modified the domain, so use
// the new value instead.
Services.cookies.usePrivateMode(isPrivate, () => {
Services.cookies.add(cookieAttrs.host, path, name, value,
secure, httpOnly, isSession, expiry, {userContextId});
});
return self.cookies.get(details);
},
remove: function(details) {
for (let {cookie, isPrivate, storeId} of query(details, ["url", "name", "storeId"], context)) {
Services.cookies.usePrivateMode(isPrivate, () => {
Services.cookies.remove(cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
});
// Todo: could there be multiple per subdomain?
return Promise.resolve({
url: details.url,
name: details.name,
storeId,
});
}
return Promise.resolve(null);
},
getAllCookieStores: function() {
let data = {};
for (let window of WindowListManager.browserWindows()) {
let tabs = TabManager.for(extension).getTabs(window);
for (let tab of tabs) {
if (!(tab.cookieStoreId in data)) {
data[tab.cookieStoreId] = [];
}
data[tab.cookieStoreId].push(tab);
}
}
let result = [];
for (let key in data) {
result.push({id: key, tabIds: data[key], incognito: key == PRIVATE_STORE});
}
return Promise.resolve(result);
},
onChanged: new EventManager(context, "cookies.onChanged", fire => {
let observer = (subject, topic, data) => {
let notify = (removed, cookie, cause) => {
cookie.QueryInterface(Ci.nsICookie2);
if (extension.whiteListedHosts.matchesCookie(cookie)) {
fire({removed, cookie: convert({cookie, isPrivate: topic == "private-cookie-changed"}), cause});
}
};
// We do our best effort here to map the incompatible states.
switch (data) {
case "deleted":
notify(true, subject, "explicit");
break;
case "added":
notify(false, subject, "explicit");
break;
case "changed":
notify(true, subject, "overwrite");
notify(false, subject, "explicit");
break;
case "batch-deleted":
subject.QueryInterface(Ci.nsIArray);
for (let i = 0; i < subject.length; i++) {
let cookie = subject.queryElementAt(i, Ci.nsICookie2);
if (!cookie.isSession && cookie.expiry * 1000 <= Date.now()) {
notify(true, cookie, "expired");
} else {
notify(true, cookie, "evicted");
}
}
break;
}
};
Services.obs.addObserver(observer, "cookie-changed", false);
Services.obs.addObserver(observer, "private-cookie-changed", false);
return () => {
Services.obs.removeObserver(observer, "cookie-changed");
Services.obs.removeObserver(observer, "private-cookie-changed");
};
}).api(),
},
};
return self;
});

View File

@ -0,0 +1,799 @@
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
ignoreEvent,
normalizeTime,
runSafeSync,
SingletonEventManager,
PlatformInfo,
} = ExtensionUtils;
const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
"danger", "mime", "startTime", "endTime",
"estimatedEndTime", "state",
"paused", "canResume", "error",
"bytesReceived", "totalBytes",
"fileSize", "exists",
"byExtensionId", "byExtensionName"];
// Fields that we generate onChanged events for.
const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
"error", "exists"];
// From https://fetch.spec.whatwg.org/#forbidden-header-name
const FORBIDDEN_HEADERS = ["ACCEPT-CHARSET", "ACCEPT-ENCODING",
"ACCESS-CONTROL-REQUEST-HEADERS", "ACCESS-CONTROL-REQUEST-METHOD",
"CONNECTION", "CONTENT-LENGTH", "COOKIE", "COOKIE2", "DATE", "DNT",
"EXPECT", "HOST", "KEEP-ALIVE", "ORIGIN", "REFERER", "TE", "TRAILER",
"TRANSFER-ENCODING", "UPGRADE", "VIA"];
const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i;
class DownloadItem {
constructor(id, download, extension) {
this.id = id;
this.download = download;
this.extension = extension;
this.prechange = {};
}
get url() { return this.download.source.url; }
get referrer() { return this.download.source.referrer; }
get filename() { return this.download.target.path; }
get incognito() { return this.download.source.isPrivate; }
get danger() { return "safe"; } // TODO
get mime() { return this.download.contentType; }
get startTime() { return this.download.startTime; }
get endTime() { return null; } // TODO
get estimatedEndTime() { return null; } // TODO
get state() {
if (this.download.succeeded) {
return "complete";
}
if (this.download.canceled) {
return "interrupted";
}
return "in_progress";
}
get paused() {
return this.download.canceled && this.download.hasPartialData && !this.download.error;
}
get canResume() {
return (this.download.stopped || this.download.canceled) &&
this.download.hasPartialData && !this.download.error;
}
get error() {
if (!this.download.stopped || this.download.succeeded) {
return null;
}
// TODO store this instead of calculating it
if (this.download.error) {
if (this.download.error.becauseSourceFailed) {
return "NETWORK_FAILED"; // TODO
}
if (this.download.error.becauseTargetFailed) {
return "FILE_FAILED"; // TODO
}
return "CRASH";
}
return "USER_CANCELED";
}
get bytesReceived() {
return this.download.currentBytes;
}
get totalBytes() {
return this.download.hasProgress ? this.download.totalBytes : -1;
}
get fileSize() {
// todo: this is supposed to be post-compression
return this.download.succeeded ? this.download.target.size : -1;
}
get exists() { return this.download.target.exists; }
get byExtensionId() { return this.extension ? this.extension.id : undefined; }
get byExtensionName() { return this.extension ? this.extension.name : undefined; }
/**
* Create a cloneable version of this object by pulling all the
* fields into simple properties (instead of getters).
*
* @returns {object} A DownloadItem with flat properties,
* suitable for cloning.
*/
serialize() {
let obj = {};
for (let field of DOWNLOAD_ITEM_FIELDS) {
obj[field] = this[field];
}
if (obj.startTime) {
obj.startTime = obj.startTime.toISOString();
}
return obj;
}
// When a change event fires, handlers can look at how an individual
// field changed by comparing item.fieldname with item.prechange.fieldname.
// After all handlers have been invoked, this gets called to store the
// current values of all fields ahead of the next event.
_change() {
for (let field of DOWNLOAD_ITEM_CHANGE_FIELDS) {
this.prechange[field] = this[field];
}
}
}
// DownloadMap maps back and forth betwen the numeric identifiers used in
// the downloads WebExtension API and a Download object from the Downloads jsm.
// todo: make id and extension info persistent (bug 1247794)
const DownloadMap = {
currentId: 0,
loadPromise: null,
// Maps numeric id -> DownloadItem
byId: new Map(),
// Maps Download object -> DownloadItem
byDownload: new WeakMap(),
lazyInit() {
if (this.loadPromise == null) {
EventEmitter.decorate(this);
this.loadPromise = Downloads.getList(Downloads.ALL).then(list => {
let self = this;
return list.addView({
onDownloadAdded(download) {
const item = self.newFromDownload(download, null);
self.emit("create", item);
},
onDownloadRemoved(download) {
const item = self.byDownload.get(download);
if (item != null) {
self.emit("erase", item);
self.byDownload.delete(download);
self.byId.delete(item.id);
}
},
onDownloadChanged(download) {
const item = self.byDownload.get(download);
if (item == null) {
Cu.reportError("Got onDownloadChanged for unknown download object");
} else {
// We get the first one of these when the download is started.
// In this case, don't emit anything, just initialize prechange.
if (Object.keys(item.prechange).length > 0) {
self.emit("change", item);
}
item._change();
}
},
}).then(() => list.getAll())
.then(downloads => {
downloads.forEach(download => {
this.newFromDownload(download, null);
});
})
.then(() => list);
});
}
return this.loadPromise;
},
getDownloadList() {
return this.lazyInit();
},
getAll() {
return this.lazyInit().then(() => this.byId.values());
},
fromId(id) {
const download = this.byId.get(id);
if (!download) {
throw new Error(`Invalid download id ${id}`);
}
return download;
},
newFromDownload(download, extension) {
if (this.byDownload.has(download)) {
return this.byDownload.get(download);
}
const id = ++this.currentId;
let item = new DownloadItem(id, download, extension);
this.byId.set(id, item);
this.byDownload.set(download, item);
return item;
},
erase(item) {
// This will need to get more complicated for bug 1255507 but for now we
// only work with downloads in the DownloadList from getAll()
return this.getDownloadList().then(list => {
list.remove(item.download);
});
},
};
// Create a callable function that filters a DownloadItem based on a
// query object of the type passed to search() or erase().
function downloadQuery(query) {
let queryTerms = [];
let queryNegativeTerms = [];
if (query.query != null) {
for (let term of query.query) {
if (term[0] == "-") {
queryNegativeTerms.push(term.slice(1).toLowerCase());
} else {
queryTerms.push(term.toLowerCase());
}
}
}
function normalizeDownloadTime(arg, before) {
if (arg == null) {
return before ? Number.MAX_VALUE : 0;
}
return normalizeTime(arg).getTime();
}
const startedBefore = normalizeDownloadTime(query.startedBefore, true);
const startedAfter = normalizeDownloadTime(query.startedAfter, false);
// const endedBefore = normalizeDownloadTime(query.endedBefore, true);
// const endedAfter = normalizeDownloadTime(query.endedAfter, false);
const totalBytesGreater = query.totalBytesGreater || 0;
const totalBytesLess = (query.totalBytesLess != null)
? query.totalBytesLess : Number.MAX_VALUE;
// Handle options for which we can have a regular expression and/or
// an explicit value to match.
function makeMatch(regex, value, field) {
if (value == null && regex == null) {
return input => true;
}
let re;
try {
re = new RegExp(regex || "", "i");
} catch (err) {
throw new Error(`Invalid ${field}Regex: ${err.message}`);
}
if (value == null) {
return input => re.test(input);
}
value = value.toLowerCase();
if (re.test(value)) {
return input => (value == input);
}
return input => false;
}
const matchFilename = makeMatch(query.filenameRegex, query.filename, "filename");
const matchUrl = makeMatch(query.urlRegex, query.url, "url");
return function(item) {
const url = item.url.toLowerCase();
const filename = item.filename.toLowerCase();
if (!queryTerms.every(term => url.includes(term) || filename.includes(term))) {
return false;
}
if (queryNegativeTerms.some(term => url.includes(term) || filename.includes(term))) {
return false;
}
if (!matchFilename(filename) || !matchUrl(url)) {
return false;
}
if (!item.startTime) {
if (query.startedBefore != null || query.startedAfter != null) {
return false;
}
} else if (item.startTime > startedBefore || item.startTime < startedAfter) {
return false;
}
// todo endedBefore, endedAfter
if (item.totalBytes == -1) {
if (query.totalBytesGreater != null || query.totalBytesLess != null) {
return false;
}
} else if (item.totalBytes <= totalBytesGreater || item.totalBytes >= totalBytesLess) {
return false;
}
// todo: include danger
const SIMPLE_ITEMS = ["id", "mime", "startTime", "endTime", "state",
"paused", "error",
"bytesReceived", "totalBytes", "fileSize", "exists"];
for (let field of SIMPLE_ITEMS) {
if (query[field] != null && item[field] != query[field]) {
return false;
}
}
return true;
};
}
function queryHelper(query) {
let matchFn;
try {
matchFn = downloadQuery(query);
} catch (err) {
return Promise.reject({message: err.message});
}
let compareFn;
if (query.orderBy != null) {
const fields = query.orderBy.map(field => field[0] == "-"
? {reverse: true, name: field.slice(1)}
: {reverse: false, name: field});
for (let field of fields) {
if (!DOWNLOAD_ITEM_FIELDS.includes(field.name)) {
return Promise.reject({message: `Invalid orderBy field ${field.name}`});
}
}
compareFn = (dl1, dl2) => {
for (let field of fields) {
const val1 = dl1[field.name];
const val2 = dl2[field.name];
if (val1 < val2) {
return field.reverse ? 1 : -1;
} else if (val1 > val2) {
return field.reverse ? -1 : 1;
}
}
return 0;
};
}
return DownloadMap.getAll().then(downloads => {
if (compareFn) {
downloads = Array.from(downloads);
downloads.sort(compareFn);
}
let results = [];
for (let download of downloads) {
if (query.limit && results.length >= query.limit) {
break;
}
if (matchFn(download)) {
results.push(download);
}
}
return results;
});
}
extensions.registerSchemaAPI("downloads", "addon_parent", context => {
let {extension} = context;
return {
downloads: {
download(options) {
let {filename} = options;
if (filename && PlatformInfo.os === "win") {
// cross platform javascript code uses "/"
filename = filename.replace(/\//g, "\\");
}
if (filename != null) {
if (filename.length == 0) {
return Promise.reject({message: "filename must not be empty"});
}
let path = OS.Path.split(filename);
if (path.absolute) {
return Promise.reject({message: "filename must not be an absolute path"});
}
if (path.components.some(component => component == "..")) {
return Promise.reject({message: "filename must not contain back-references (..)"});
}
}
if (options.conflictAction == "prompt") {
// TODO
return Promise.reject({message: "conflictAction prompt not yet implemented"});
}
if (options.headers) {
for (let {name} of options.headers) {
if (FORBIDDEN_HEADERS.includes(name.toUpperCase()) || name.match(FORBIDDEN_PREFIXES)) {
return Promise.reject({message: "Forbidden request header name"});
}
}
}
// Handle method, headers and body options.
function adjustChannel(channel) {
if (channel instanceof Ci.nsIHttpChannel) {
const method = options.method || "GET";
channel.requestMethod = method;
if (options.headers) {
for (let {name, value} of options.headers) {
channel.setRequestHeader(name, value, false);
}
}
if (options.body != null) {
const stream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stream.setData(options.body, options.body.length);
channel.QueryInterface(Ci.nsIUploadChannel2);
channel.explicitSetUploadStream(stream, null, -1, method, false);
}
}
return Promise.resolve();
}
function createTarget(downloadsDir) {
let target;
if (filename) {
target = OS.Path.join(downloadsDir, filename);
} else {
let uri = NetUtil.newURI(options.url);
let remote = "download";
if (uri instanceof Ci.nsIURL) {
remote = uri.fileName;
}
target = OS.Path.join(downloadsDir, remote);
}
// Create any needed subdirectories if required by filename.
const dir = OS.Path.dirname(target);
return OS.File.makeDir(dir, {from: downloadsDir}).then(() => {
return OS.File.exists(target);
}).then(exists => {
// This has a race, something else could come along and create
// the file between this test and them time the download code
// creates the target file. But we can't easily fix it without
// modifying DownloadCore so we live with it for now.
if (exists) {
switch (options.conflictAction) {
case "uniquify":
default:
target = DownloadPaths.createNiceUniqueFile(new FileUtils.File(target)).path;
break;
case "overwrite":
break;
}
}
}).then(() => {
if (!options.saveAs) {
return Promise.resolve(target);
}
// Setup the file picker Save As dialog.
const picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
const window = Services.wm.getMostRecentWindow("navigator:browser");
picker.init(window, null, Ci.nsIFilePicker.modeSave);
picker.displayDirectory = new FileUtils.File(dir);
picker.appendFilters(Ci.nsIFilePicker.filterAll);
picker.defaultString = OS.Path.basename(target);
// Open the dialog and resolve/reject with the result.
return new Promise((resolve, reject) => {
picker.open(result => {
if (result === Ci.nsIFilePicker.returnCancel) {
reject({message: "Download canceled by the user"});
} else {
resolve(picker.file.path);
}
});
});
});
}
let download;
return Downloads.getPreferredDownloadsDirectory()
.then(downloadsDir => createTarget(downloadsDir))
.then(target => {
const source = {
url: options.url,
};
if (options.method || options.headers || options.body) {
source.adjustChannel = adjustChannel;
}
return Downloads.createDownload({
source,
target: {
path: target,
partFilePath: target + ".part",
},
});
}).then(dl => {
download = dl;
return DownloadMap.getDownloadList();
}).then(list => {
list.add(download);
// This is necessary to make pause/resume work.
download.tryToKeepPartialData = true;
download.start();
const item = DownloadMap.newFromDownload(download, extension);
return item.id;
});
},
removeFile(id) {
return DownloadMap.lazyInit().then(() => {
let item;
try {
item = DownloadMap.fromId(id);
} catch (err) {
return Promise.reject({message: `Invalid download id ${id}`});
}
if (item.state !== "complete") {
return Promise.reject({message: `Cannot remove incomplete download id ${id}`});
}
return OS.File.remove(item.filename, {ignoreAbsent: false}).catch((err) => {
return Promise.reject({message: `Could not remove download id ${item.id} because the file doesn't exist`});
});
});
},
search(query) {
return queryHelper(query)
.then(items => items.map(item => item.serialize()));
},
pause(id) {
return DownloadMap.lazyInit().then(() => {
let item;
try {
item = DownloadMap.fromId(id);
} catch (err) {
return Promise.reject({message: `Invalid download id ${id}`});
}
if (item.state != "in_progress") {
return Promise.reject({message: `Download ${id} cannot be paused since it is in state ${item.state}`});
}
return item.download.cancel();
});
},
resume(id) {
return DownloadMap.lazyInit().then(() => {
let item;
try {
item = DownloadMap.fromId(id);
} catch (err) {
return Promise.reject({message: `Invalid download id ${id}`});
}
if (!item.canResume) {
return Promise.reject({message: `Download ${id} cannot be resumed`});
}
return item.download.start();
});
},
cancel(id) {
return DownloadMap.lazyInit().then(() => {
let item;
try {
item = DownloadMap.fromId(id);
} catch (err) {
return Promise.reject({message: `Invalid download id ${id}`});
}
if (item.download.succeeded) {
return Promise.reject({message: `Download ${id} is already complete`});
}
return item.download.finalize(true);
});
},
showDefaultFolder() {
Downloads.getPreferredDownloadsDirectory().then(dir => {
let dirobj = new FileUtils.File(dir);
if (dirobj.isDirectory()) {
dirobj.launch();
} else {
throw new Error(`Download directory ${dirobj.path} is not actually a directory`);
}
}).catch(Cu.reportError);
},
erase(query) {
return queryHelper(query).then(items => {
let results = [];
let promises = [];
for (let item of items) {
promises.push(DownloadMap.erase(item));
results.push(item.id);
}
return Promise.all(promises).then(() => results);
});
},
open(downloadId) {
return DownloadMap.lazyInit().then(() => {
let download = DownloadMap.fromId(downloadId).download;
if (download.succeeded) {
return download.launch();
}
return Promise.reject({message: "Download has not completed."});
}).catch((error) => {
return Promise.reject({message: error.message});
});
},
show(downloadId) {
return DownloadMap.lazyInit().then(() => {
let download = DownloadMap.fromId(downloadId);
return download.download.showContainingDirectory();
}).then(() => {
return true;
}).catch(error => {
return Promise.reject({message: error.message});
});
},
getFileIcon(downloadId, options) {
return DownloadMap.lazyInit().then(() => {
let size = options && options.size ? options.size : 32;
let download = DownloadMap.fromId(downloadId).download;
let pathPrefix = "";
let path;
if (download.succeeded) {
let file = FileUtils.File(download.target.path);
path = Services.io.newFileURI(file).spec;
} else {
path = OS.Path.basename(download.target.path);
pathPrefix = "//";
}
return new Promise((resolve, reject) => {
let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
chromeWebNav
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
let img = chromeWebNav.document.createElement("img");
img.width = size;
img.height = size;
let handleLoad;
let handleError;
const cleanup = () => {
img.removeEventListener("load", handleLoad);
img.removeEventListener("error", handleError);
chromeWebNav.close();
chromeWebNav = null;
};
handleLoad = () => {
let canvas = chromeWebNav.document.createElement("canvas");
canvas.width = size;
canvas.height = size;
let context = canvas.getContext("2d");
context.drawImage(img, 0, 0, size, size);
let dataURL = canvas.toDataURL("image/png");
cleanup();
resolve(dataURL);
};
handleError = (error) => {
Cu.reportError(error);
cleanup();
reject(new Error("An unexpected error occurred"));
};
img.addEventListener("load", handleLoad);
img.addEventListener("error", handleError);
img.src = `moz-icon:${pathPrefix}${path}?size=${size}`;
});
}).catch((error) => {
return Promise.reject({message: error.message});
});
},
// When we do setShelfEnabled(), check for additional "downloads.shelf" permission.
// i.e.:
// setShelfEnabled(enabled) {
// if (!extension.hasPermission("downloads.shelf")) {
// throw new context.cloneScope.Error("Permission denied because 'downloads.shelf' permission is missing.");
// }
// ...
// }
onChanged: new SingletonEventManager(context, "downloads.onChanged", fire => {
const handler = (what, item) => {
let changes = {};
const noundef = val => (val === undefined) ? null : val;
DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
if (item[fld] != item.prechange[fld]) {
changes[fld] = {
previous: noundef(item.prechange[fld]),
current: noundef(item[fld]),
};
}
});
if (Object.keys(changes).length > 0) {
changes.id = item.id;
runSafeSync(context, fire, changes);
}
};
let registerPromise = DownloadMap.getDownloadList().then(() => {
DownloadMap.on("change", handler);
});
return () => {
registerPromise.then(() => {
DownloadMap.off("change", handler);
});
};
}).api(),
onCreated: new SingletonEventManager(context, "downloads.onCreated", fire => {
const handler = (what, item) => {
runSafeSync(context, fire, item.serialize());
};
let registerPromise = DownloadMap.getDownloadList().then(() => {
DownloadMap.on("create", handler);
});
return () => {
registerPromise.then(() => {
DownloadMap.off("create", handler);
});
};
}).api(),
onErased: new SingletonEventManager(context, "downloads.onErased", fire => {
const handler = (what, item) => {
runSafeSync(context, fire, item.id);
};
let registerPromise = DownloadMap.getDownloadList().then(() => {
DownloadMap.on("erase", handler);
});
return () => {
registerPromise.then(() => {
DownloadMap.off("erase", handler);
});
};
}).api(),
onDeterminingFilename: ignoreEvent(context, "downloads.onDeterminingFilename"),
},
};
});

View File

@ -0,0 +1,20 @@
"use strict";
extensions.registerSchemaAPI("extension", "addon_parent", context => {
return {
extension: {
get lastError() {
return context.lastError;
},
isAllowedIncognitoAccess() {
return Promise.resolve(true);
},
isAllowedFileSchemeAccess() {
return Promise.resolve(false);
},
},
};
});

View File

@ -0,0 +1,34 @@
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
detectLanguage,
} = ExtensionUtils;
function i18nApiFactory(context) {
let {extension} = context;
return {
i18n: {
getMessage: function(messageName, substitutions) {
return extension.localizeMessage(messageName, substitutions, {cloneScope: context.cloneScope});
},
getAcceptLanguages: function() {
let result = extension.localeData.acceptLanguages;
return Promise.resolve(result);
},
getUILanguage: function() {
return extension.localeData.uiLocale;
},
detectLanguage: function(text) {
return detectLanguage(text);
},
},
};
}
extensions.registerSchemaAPI("i18n", "addon_child", i18nApiFactory);
extensions.registerSchemaAPI("i18n", "content_child", i18nApiFactory);

View File

@ -0,0 +1,94 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyServiceGetter(this, "idleService",
"@mozilla.org/widget/idleservice;1",
"nsIIdleService");
const {
SingletonEventManager,
} = ExtensionUtils;
// WeakMap[Extension -> Object]
var observersMap = new WeakMap();
function getObserverInfo(extension, context) {
let observerInfo = observersMap.get(extension);
if (!observerInfo) {
observerInfo = {
observer: null,
detectionInterval: 60,
};
observersMap.set(extension, observerInfo);
context.callOnClose({
close: () => {
let {observer, detectionInterval} = observersMap.get(extension);
if (observer) {
idleService.removeIdleObserver(observer, detectionInterval);
}
observersMap.delete(extension);
},
});
}
return observerInfo;
}
function getObserver(extension, context) {
let observerInfo = getObserverInfo(extension, context);
let {observer, detectionInterval} = observerInfo;
if (!observer) {
observer = {
observe: function(subject, topic, data) {
if (topic == "idle" || topic == "active") {
this.emit("stateChanged", topic);
}
},
};
EventEmitter.decorate(observer);
idleService.addIdleObserver(observer, detectionInterval);
observerInfo.observer = observer;
observerInfo.detectionInterval = detectionInterval;
}
return observer;
}
function setDetectionInterval(extension, context, newInterval) {
let observerInfo = getObserverInfo(extension, context);
let {observer, detectionInterval} = observerInfo;
if (observer) {
idleService.removeIdleObserver(observer, detectionInterval);
idleService.addIdleObserver(observer, newInterval);
}
observerInfo.detectionInterval = newInterval;
}
extensions.registerSchemaAPI("idle", "addon_parent", context => {
let {extension} = context;
return {
idle: {
queryState: function(detectionIntervalInSeconds) {
if (idleService.idleTime < detectionIntervalInSeconds * 1000) {
return Promise.resolve("active");
}
return Promise.resolve("idle");
},
setDetectionInterval: function(detectionIntervalInSeconds) {
setDetectionInterval(extension, context, detectionIntervalInSeconds);
},
onStateChanged: new SingletonEventManager(context, "idle.onStateChanged", fire => {
let listener = (event, data) => {
context.runSafe(fire, data);
};
getObserver(extension, context).on("stateChanged", listener);
return () => {
getObserver(extension, context).off("stateChanged", listener);
};
}).api(),
},
};
});

View File

@ -0,0 +1,109 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
const stringSvc = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);
return stringSvc.createBundle("chrome://global/locale/extensions.properties");
});
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "promptService",
"@mozilla.org/embedcomp/prompt-service;1",
"nsIPromptService");
function _(key, ...args) {
if (args.length) {
return strBundle.formatStringFromName(key, args, args.length);
}
return strBundle.GetStringFromName(key);
}
function installType(addon) {
if (addon.temporarilyInstalled) {
return "development";
} else if (addon.foreignInstall) {
return "sideload";
} else if (addon.isSystem) {
return "other";
}
return "normal";
}
extensions.registerSchemaAPI("management", "addon_parent", context => {
let {extension} = context;
return {
management: {
getSelf: function() {
return new Promise((resolve, reject) => AddonManager.getAddonByID(extension.id, addon => {
try {
let m = extension.manifest;
let extInfo = {
id: extension.id,
name: addon.name,
shortName: m.short_name || "",
description: addon.description || "",
version: addon.version,
mayDisable: !!(addon.permissions & AddonManager.PERM_CAN_DISABLE),
enabled: addon.isActive,
optionsUrl: addon.optionsURL || "",
permissions: Array.from(extension.permissions).filter(perm => {
return !extension.whiteListedHosts.pat.includes(perm);
}),
hostPermissions: extension.whiteListedHosts.pat,
installType: installType(addon),
};
if (addon.homepageURL) {
extInfo.homepageUrl = addon.homepageURL;
}
if (addon.updateURL) {
extInfo.updateUrl = addon.updateURL;
}
if (m.icons) {
extInfo.icons = Object.keys(m.icons).map(key => {
return {size: Number(key), url: m.icons[key]};
});
}
resolve(extInfo);
} catch (err) {
reject(err);
}
}));
},
uninstallSelf: function(options) {
return new Promise((resolve, reject) => {
if (options && options.showConfirmDialog) {
let message = _("uninstall.confirmation.message", extension.name);
if (options.dialogMessage) {
message = `${options.dialogMessage}\n${message}`;
}
let title = _("uninstall.confirmation.title", extension.name);
let buttonFlags = promptService.BUTTON_POS_0 * promptService.BUTTON_TITLE_IS_STRING +
promptService.BUTTON_POS_1 * promptService.BUTTON_TITLE_IS_STRING;
let button0Title = _("uninstall.confirmation.button-0.label");
let button1Title = _("uninstall.confirmation.button-1.label");
let response = promptService.confirmEx(null, title, message, buttonFlags, button0Title, button1Title, null, null, {value: 0});
if (response == 1) {
return reject({message: "User cancelled uninstall of extension"});
}
}
AddonManager.getAddonByID(extension.id, addon => {
let canUninstall = Boolean(addon.permissions & AddonManager.PERM_CAN_UNINSTALL);
if (!canUninstall) {
return reject({message: "The add-on cannot be uninstalled"});
}
try {
addon.uninstall();
} catch (err) {
return reject(err);
}
});
});
},
},
};
});

View File

@ -0,0 +1,161 @@
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
var {
EventManager,
ignoreEvent,
} = ExtensionUtils;
// WeakMap[Extension -> Map[id -> Notification]]
var notificationsMap = 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,
true, // 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.id);
},
observe(subject, topic, data) {
let notifications = notificationsMap.get(this.extension);
let emitAndDelete = event => {
notifications.emit(event, data);
notifications.delete(this.id);
};
// Don't try to emit events if the extension has been unloaded
if (!notifications) {
return;
}
if (topic === "alertclickcallback") {
emitAndDelete("clicked");
}
if (topic === "alertfinished") {
emitAndDelete("closed");
}
},
};
/* eslint-disable mozilla/balanced-listeners */
extensions.on("startup", (type, extension) => {
let map = new Map();
EventEmitter.decorate(map);
notificationsMap.set(extension, map);
});
extensions.on("shutdown", (type, extension) => {
if (notificationsMap.has(extension)) {
for (let notification of notificationsMap.get(extension).values()) {
notification.clear();
}
notificationsMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
var nextId = 0;
extensions.registerSchemaAPI("notifications", "addon_parent", context => {
let {extension} = context;
return {
notifications: {
create: function(notificationId, options) {
if (!notificationId) {
notificationId = String(nextId++);
}
let notifications = notificationsMap.get(extension);
if (notifications.has(notificationId)) {
notifications.get(notificationId).clear();
}
// FIXME: Lots of options still aren't supported, especially
// buttons.
let notification = new Notification(extension, notificationId, options);
notificationsMap.get(extension).set(notificationId, notification);
return Promise.resolve(notificationId);
},
clear: function(notificationId) {
let notifications = notificationsMap.get(extension);
if (notifications.has(notificationId)) {
notifications.get(notificationId).clear();
return Promise.resolve(true);
}
return Promise.resolve(false);
},
getAll: function() {
let result = {};
notificationsMap.get(extension).forEach((value, key) => {
result[key] = value.options;
});
return Promise.resolve(result);
},
onClosed: new EventManager(context, "notifications.onClosed", fire => {
let listener = (event, notificationId) => {
// FIXME: Support the byUser argument.
fire(notificationId, true);
};
notificationsMap.get(extension).on("closed", listener);
return () => {
notificationsMap.get(extension).off("closed", listener);
};
}).api(),
onClicked: new EventManager(context, "notifications.onClicked", fire => {
let listener = (event, notificationId) => {
fire(notificationId, true);
};
notificationsMap.get(extension).on("clicked", listener);
return () => {
notificationsMap.get(extension).off("clicked", listener);
};
}).api(),
// Intend to implement this later: https://bugzilla.mozilla.org/show_bug.cgi?id=1190681
onButtonClicked: ignoreEvent(context, "notifications.onButtonClicked"),
},
};
});

View File

@ -0,0 +1,134 @@
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
var {
SingletonEventManager,
} = ExtensionUtils;
extensions.registerSchemaAPI("runtime", "addon_parent", context => {
let {extension} = context;
return {
runtime: {
onStartup: new SingletonEventManager(context, "runtime.onStartup", fire => {
if (context.incognito) {
// This event should not fire if we are operating in a private profile.
return () => {};
}
let listener = () => {
if (extension.startupReason === "APP_STARTUP") {
fire();
}
};
extension.on("startup", listener);
return () => {
extension.off("startup", listener);
};
}).api(),
onInstalled: new SingletonEventManager(context, "runtime.onInstalled", fire => {
let listener = () => {
switch (extension.startupReason) {
case "APP_STARTUP":
if (Extension.browserUpdated) {
fire({reason: "browser_update"});
}
break;
case "ADDON_INSTALL":
fire({reason: "install"});
break;
case "ADDON_UPGRADE":
fire({reason: "update"});
break;
}
};
extension.on("startup", listener);
return () => {
extension.off("startup", listener);
};
}).api(),
onUpdateAvailable: new SingletonEventManager(context, "runtime.onUpdateAvailable", fire => {
let instanceID = extension.addonData.instanceID;
AddonManager.addUpgradeListener(instanceID, upgrade => {
extension.upgrade = upgrade;
let details = {
version: upgrade.version,
};
context.runSafe(fire, details);
});
return () => {
AddonManager.removeUpgradeListener(instanceID);
};
}).api(),
reload: () => {
if (extension.upgrade) {
// If there is a pending update, install it now.
extension.upgrade.install();
} else {
// Otherwise, reload the current extension.
AddonManager.getAddonByID(extension.id, addon => {
addon.reload();
});
}
},
get lastError() {
// TODO(robwu): Figure out how to make sure that errors in the parent
// process are propagated to the child process.
// lastError should not be accessed from the parent.
return context.lastError;
},
getBrowserInfo: function() {
const {name, vendor, version, appBuildID} = Services.appinfo;
const info = {name, vendor, version, buildID: appBuildID};
return Promise.resolve(info);
},
getPlatformInfo: function() {
return Promise.resolve(ExtensionUtils.PlatformInfo);
},
openOptionsPage: function() {
if (!extension.manifest.options_ui) {
return Promise.reject({message: "No `options_ui` declared"});
}
return openOptionsPage(extension).then(() => {});
},
setUninstallURL: function(url) {
if (url.length == 0) {
return Promise.resolve();
}
let uri;
try {
uri = NetUtil.newURI(url);
} catch (e) {
return Promise.reject({message: `Invalid URL: ${JSON.stringify(url)}`});
}
if (uri.scheme != "http" && uri.scheme != "https") {
return Promise.reject({message: "url must have the scheme http or https"});
}
extension.uninstallURL = url;
return Promise.resolve();
},
},
};
});

View File

@ -0,0 +1,46 @@
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
} = ExtensionUtils;
function storageApiFactory(context) {
let {extension} = context;
return {
storage: {
local: {
get: function(spec) {
return ExtensionStorage.get(extension.id, spec);
},
set: function(items) {
return ExtensionStorage.set(extension.id, items, context);
},
remove: function(keys) {
return ExtensionStorage.remove(extension.id, keys);
},
clear: function() {
return ExtensionStorage.clear(extension.id);
},
},
onChanged: new EventManager(context, "storage.onChanged", fire => {
let listenerLocal = changes => {
fire(changes, "local");
};
ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
return () => {
ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
};
}).api(),
},
};
}
extensions.registerSchemaAPI("storage", "addon_parent", storageApiFactory);
extensions.registerSchemaAPI("storage", "content_parent", storageApiFactory);

View File

@ -0,0 +1,24 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
extensions.registerSchemaAPI("topSites", "addon_parent", context => {
return {
topSites: {
get: function() {
let urls = NewTabUtils.links.getLinks()
.filter(link => !!link)
.map(link => {
return {
url: link.url,
title: link.title,
};
});
return Promise.resolve(urls);
},
},
};
});

View File

@ -0,0 +1,192 @@
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MatchURLFilters",
"resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigation",
"resource://gre/modules/WebNavigation.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
SingletonEventManager,
ignoreEvent,
} = ExtensionUtils;
const defaultTransitionTypes = {
topFrame: "link",
subFrame: "auto_subframe",
};
const frameTransitions = {
anyFrame: {
qualifiers: ["server_redirect", "client_redirect", "forward_back"],
},
topFrame: {
types: ["reload", "form_submit"],
},
};
const tabTransitions = {
topFrame: {
qualifiers: ["from_address_bar"],
types: ["auto_bookmark", "typed", "keyword", "generated", "link"],
},
subFrame: {
types: ["manual_subframe"],
},
};
function isTopLevelFrame({frameId, parentFrameId}) {
return frameId == 0 && parentFrameId == -1;
}
function fillTransitionProperties(eventName, src, dst) {
if (eventName == "onCommitted" || eventName == "onHistoryStateUpdated") {
let frameTransitionData = src.frameTransitionData || {};
let tabTransitionData = src.tabTransitionData || {};
let transitionType, transitionQualifiers = [];
// Fill transition properties for any frame.
for (let qualifier of frameTransitions.anyFrame.qualifiers) {
if (frameTransitionData[qualifier]) {
transitionQualifiers.push(qualifier);
}
}
if (isTopLevelFrame(dst)) {
for (let type of frameTransitions.topFrame.types) {
if (frameTransitionData[type]) {
transitionType = type;
}
}
for (let qualifier of tabTransitions.topFrame.qualifiers) {
if (tabTransitionData[qualifier]) {
transitionQualifiers.push(qualifier);
}
}
for (let type of tabTransitions.topFrame.types) {
if (tabTransitionData[type]) {
transitionType = type;
}
}
// If transitionType is not defined, defaults it to "link".
if (!transitionType) {
transitionType = defaultTransitionTypes.topFrame;
}
} else {
// If it is sub-frame, transitionType defaults it to "auto_subframe",
// "manual_subframe" is set only in case of a recent user interaction.
transitionType = tabTransitionData.link ?
"manual_subframe" : defaultTransitionTypes.subFrame;
}
// Fill the transition properties in the webNavigation event object.
dst.transitionType = transitionType;
dst.transitionQualifiers = transitionQualifiers;
}
}
// Similar to WebRequestEventManager but for WebNavigation.
function WebNavigationEventManager(context, eventName) {
let name = `webNavigation.${eventName}`;
let register = (callback, urlFilters) => {
// Don't create a MatchURLFilters instance if the listener does not include any filter.
let filters = urlFilters ?
new MatchURLFilters(urlFilters.url) : null;
let listener = data => {
if (!data.browser) {
return;
}
let data2 = {
url: data.url,
timeStamp: Date.now(),
frameId: ExtensionManagement.getFrameId(data.windowId),
parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
};
if (eventName == "onErrorOccurred") {
data2.error = data.error;
}
// Fills in tabId typically.
extensions.emit("fill-browser-data", data.browser, data2);
if (data2.tabId < 0) {
return;
}
fillTransitionProperties(eventName, data, data2);
context.runSafe(callback, data2);
};
WebNavigation[eventName].addListener(listener, filters);
return () => {
WebNavigation[eventName].removeListener(listener);
};
};
return SingletonEventManager.call(this, context, name, register);
}
WebNavigationEventManager.prototype = Object.create(SingletonEventManager.prototype);
function convertGetFrameResult(tabId, data) {
return {
errorOccurred: data.errorOccurred,
url: data.url,
tabId,
frameId: ExtensionManagement.getFrameId(data.windowId),
parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
};
}
extensions.registerSchemaAPI("webNavigation", "addon_parent", context => {
return {
webNavigation: {
onTabReplaced: ignoreEvent(context, "webNavigation.onTabReplaced"),
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(),
onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
getAllFrames(details) {
let tab = TabManager.getTab(details.tabId, context);
let {innerWindowID, messageManager} = tab.linkedBrowser;
let recipient = {innerWindowID};
return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, {recipient})
.then((results) => results.map(convertGetFrameResult.bind(null, details.tabId)));
},
getFrame(details) {
let tab = TabManager.getTab(details.tabId, context);
let recipient = {
innerWindowID: tab.linkedBrowser.innerWindowID,
};
let mm = tab.linkedBrowser.messageManager;
return context.sendMessage(mm, "WebNavigation:GetFrame", {options: details}, {recipient})
.then((result) => {
return result ?
convertGetFrameResult(details.tabId, result) :
Promise.reject({message: `No frame found with frameId: ${details.frameId}`});
});
},
},
};
});

View File

@ -0,0 +1,115 @@
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
"resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
"resource://gre/modules/WebRequest.jsm");
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
SingletonEventManager,
} = 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 => {
// Prevent listening in on requests originating from system principal to
// prevent tinkering with OCSP, app and addon updates, etc.
if (data.isSystemPrincipal) {
return;
}
let data2 = {
requestId: data.requestId,
url: data.url,
originUrl: data.originUrl,
method: data.method,
type: data.type,
timeStamp: Date.now(),
frameId: data.type == "main_frame" ? 0 : ExtensionManagement.getFrameId(data.windowId),
parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
};
const maybeCached = ["onResponseStarted", "onBeforeRedirect", "onCompleted", "onErrorOccurred"];
if (maybeCached.includes(eventName)) {
data2.fromCache = !!data.fromCache;
}
if ("ip" in data) {
data2.ip = data.ip;
}
extensions.emit("fill-browser-data", data.browser, data2);
let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl",
"requestBody"];
for (let opt of optional) {
if (opt in data) {
data2[opt] = data[opt];
}
}
return context.runSafe(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.registerSchemaAPI("webRequest", "addon_parent", 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(),
onBeforeRedirect: new WebRequestEventManager(context, "onBeforeRedirect").api(),
onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
onErrorOccurred: new WebRequestEventManager(context, "onErrorOccurred").api(),
onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
handlerBehaviorChanged: function() {
// TODO: Flush all caches.
},
},
};
});

View File

@ -0,0 +1,49 @@
# scripts
category webextension-scripts alarms chrome://extensions/content/ext-alarms.js
category webextension-scripts backgroundPage chrome://extensions/content/ext-backgroundPage.js
category webextension-scripts cookies chrome://extensions/content/ext-cookies.js
category webextension-scripts downloads chrome://extensions/content/ext-downloads.js
category webextension-scripts management chrome://extensions/content/ext-management.js
category webextension-scripts notifications chrome://extensions/content/ext-notifications.js
category webextension-scripts i18n chrome://extensions/content/ext-i18n.js
category webextension-scripts idle chrome://extensions/content/ext-idle.js
category webextension-scripts webRequest chrome://extensions/content/ext-webRequest.js
category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js
category webextension-scripts runtime chrome://extensions/content/ext-runtime.js
category webextension-scripts extension chrome://extensions/content/ext-extension.js
category webextension-scripts storage chrome://extensions/content/ext-storage.js
category webextension-scripts topSites chrome://extensions/content/ext-topSites.js
# scripts specific for content process.
category webextension-scripts-content extension chrome://extensions/content/ext-c-extension.js
category webextension-scripts-content i18n chrome://extensions/content/ext-i18n.js
category webextension-scripts-content runtime chrome://extensions/content/ext-c-runtime.js
category webextension-scripts-content test chrome://extensions/content/ext-c-test.js
category webextension-scripts-content storage chrome://extensions/content/ext-c-storage.js
# scripts that must run in the same process as addon code.
category webextension-scripts-addon backgroundPage chrome://extensions/content/ext-c-backgroundPage.js
category webextension-scripts-addon extension chrome://extensions/content/ext-c-extension.js
category webextension-scripts-addon i18n chrome://extensions/content/ext-i18n.js
category webextension-scripts-addon runtime chrome://extensions/content/ext-c-runtime.js
category webextension-scripts-addon test chrome://extensions/content/ext-c-test.js
category webextension-scripts-addon storage chrome://extensions/content/ext-c-storage.js
# schemas
category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json
category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json
category webextension-schemas downloads chrome://extensions/content/schemas/downloads.json
category webextension-schemas events chrome://extensions/content/schemas/events.json
category webextension-schemas extension chrome://extensions/content/schemas/extension.json
category webextension-schemas extension_types chrome://extensions/content/schemas/extension_types.json
category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json
category webextension-schemas idle chrome://extensions/content/schemas/idle.json
category webextension-schemas management chrome://extensions/content/schemas/management.json
category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
category webextension-schemas notifications chrome://extensions/content/schemas/notifications.json
category webextension-schemas runtime chrome://extensions/content/schemas/runtime.json
category webextension-schemas storage chrome://extensions/content/schemas/storage.json
category webextension-schemas test chrome://extensions/content/schemas/test.json
category webextension-schemas top_sites chrome://extensions/content/schemas/top_sites.json
category webextension-schemas web_navigation chrome://extensions/content/schemas/web_navigation.json
category webextension-schemas web_request chrome://extensions/content/schemas/web_request.json

View File

@ -0,0 +1,26 @@
# 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
content/extensions/ext-backgroundPage.js
content/extensions/ext-browser-content.js
content/extensions/ext-cookies.js
content/extensions/ext-downloads.js
content/extensions/ext-management.js
content/extensions/ext-notifications.js
content/extensions/ext-i18n.js
content/extensions/ext-idle.js
content/extensions/ext-webRequest.js
content/extensions/ext-webNavigation.js
content/extensions/ext-runtime.js
content/extensions/ext-extension.js
content/extensions/ext-storage.js
content/extensions/ext-topSites.js
content/extensions/ext-c-backgroundPage.js
content/extensions/ext-c-extension.js
content/extensions/ext-c-runtime.js
content/extensions/ext-c-storage.js
content/extensions/ext-c-test.js

View File

@ -0,0 +1,41 @@
# -*- Mode: python; 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',
'ExtensionAPI.jsm',
'ExtensionChild.jsm',
'ExtensionCommon.jsm',
'ExtensionContent.jsm',
'ExtensionManagement.jsm',
'ExtensionParent.jsm',
'ExtensionStorage.jsm',
'ExtensionUtils.jsm',
'LegacyExtensionsUtils.jsm',
'MessageChannel.jsm',
'NativeMessaging.jsm',
'Schemas.jsm',
]
EXTRA_COMPONENTS += [
'extensions-toolkit.manifest',
]
TESTING_JS_MODULES += [
'ExtensionTestCommon.jsm',
'ExtensionXPCShellUtils.jsm',
]
DIRS += ['schemas']
JAR_MANIFESTS += ['jar.mn']
MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += [
'test/xpcshell/native_messaging.ini',
'test/xpcshell/xpcshell.ini',
]

View File

@ -0,0 +1,27 @@
// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,145 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "alarms",
"permissions": ["alarms"],
"types": [
{
"id": "Alarm",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of this alarm."
},
"scheduledTime": {
"type": "number",
"description": "Time when the alarm is scheduled to fire, in milliseconds past the epoch."
},
"periodInMinutes": {
"type": "number",
"optional": true,
"description": "When present, signals that the alarm triggers periodically after so many minutes."
}
}
}
],
"functions": [
{
"name": "create",
"type": "function",
"description": "Creates an alarm. After the delay is expired, the onAlarm event is fired. If there is another alarm with the same name (or no name if none is specified), it will be cancelled and replaced by this alarm.",
"parameters": [
{
"type": "string",
"name": "name",
"optional": true,
"description": "Optional name to identify this alarm. Defaults to the empty string."
},
{
"type": "object",
"name": "alarmInfo",
"description": "Details about the alarm. The alarm first fires either at 'when' milliseconds past the epoch (if 'when' is provided), after 'delayInMinutes' minutes from the current time (if 'delayInMinutes' is provided instead), or after 'periodInMinutes' minutes from the current time (if only 'periodInMinutes' is provided). Users should never provide both 'when' and 'delayInMinutes'. If 'periodInMinutes' is provided, then the alarm recurs repeatedly after that many minutes.",
"properties": {
"when": {"type": "number", "optional": true,
"description": "Time when the alarm is scheduled to first fire, in milliseconds past the epoch."},
"delayInMinutes": {"type": "number", "optional": true,
"description": "Number of minutes from the current time after which the alarm should first fire."},
"periodInMinutes": {"type": "number", "optional": true,
"description": "Number of minutes after which the alarm should recur repeatedly."}
}
}
]
},
{
"name": "get",
"type": "function",
"description": "Retrieves details about the specified alarm.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "name",
"optional": true,
"description": "The name of the alarm to get. Defaults to the empty string."
},
{
"type": "function",
"name": "callback",
"parameters": [
{ "name": "alarm", "$ref": "Alarm" }
]
}
]
},
{
"name": "getAll",
"type": "function",
"description": "Gets an array of all the alarms.",
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"parameters": [
{ "name": "alarms", "type": "array", "items": { "$ref": "Alarm" } }
]
}
]
},
{
"name": "clear",
"type": "function",
"description": "Clears the alarm with the given name.",
"async": "callback",
"parameters": [
{
"type": "string",
"name": "name",
"optional": true,
"description": "The name of the alarm to clear. Defaults to the empty string."
},
{
"type": "function",
"name": "callback",
"parameters": [
{ "name": "wasCleared", "type": "boolean", "description": "Whether an alarm of the given name was found to clear." }
]
}
]
},
{
"name": "clearAll",
"type": "function",
"description": "Clears all alarms.",
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"parameters": [
{ "name": "wasCleared", "type": "boolean", "description": "Whether any alarm was found to clear." }
]
}
]
}
],
"events": [
{
"name": "onAlarm",
"type": "function",
"description": "Fired when an alarm has expired. Useful for transient background pages.",
"parameters": [
{
"name": "name",
"$ref": "Alarm",
"description": "The alarm that has expired."
}
]
}
]
}
]

View File

@ -0,0 +1,224 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": [
"cookies"
]
}]
}
]
},
{
"namespace": "cookies",
"description": "Use the <code>browser.cookies</code> API to query and modify cookies, and to be notified when they change.",
"permissions": ["cookies"],
"types": [
{
"id": "Cookie",
"type": "object",
"description": "Represents information about an HTTP cookie.",
"properties": {
"name": {"type": "string", "description": "The name of the cookie."},
"value": {"type": "string", "description": "The value of the cookie."},
"domain": {"type": "string", "description": "The domain of the cookie (e.g. \"www.google.com\", \"example.com\")."},
"hostOnly": {"type": "boolean", "description": "True if the cookie is a host-only cookie (i.e. a request's host must exactly match the domain of the cookie)."},
"path": {"type": "string", "description": "The path of the cookie."},
"secure": {"type": "boolean", "description": "True if the cookie is marked as Secure (i.e. its scope is limited to secure channels, typically HTTPS)."},
"httpOnly": {"type": "boolean", "description": "True if the cookie is marked as HttpOnly (i.e. the cookie is inaccessible to client-side scripts)."},
"session": {"type": "boolean", "description": "True if the cookie is a session cookie, as opposed to a persistent cookie with an expiration date."},
"expirationDate": {"type": "number", "optional": true, "description": "The expiration date of the cookie as the number of seconds since the UNIX epoch. Not provided for session cookies."},
"storeId": {"type": "string", "description": "The ID of the cookie store containing this cookie, as provided in getAllCookieStores()."}
}
},
{
"id": "CookieStore",
"type": "object",
"description": "Represents a cookie store in the browser. An incognito mode window, for instance, uses a separate cookie store from a non-incognito window.",
"properties": {
"id": {"type": "string", "description": "The unique identifier for the cookie store."},
"tabIds": {"type": "array", "items": {"type": "integer"}, "description": "Identifiers of all the browser tabs that share this cookie store."}
}
},
{
"id": "OnChangedCause",
"type": "string",
"enum": ["evicted", "expired", "explicit", "expired_overwrite", "overwrite"],
"description": "The underlying reason behind the cookie's change. If a cookie was inserted, or removed via an explicit call to $(ref:cookies.remove), \"cause\" will be \"explicit\". If a cookie was automatically removed due to expiry, \"cause\" will be \"expired\". If a cookie was removed due to being overwritten with an already-expired expiration date, \"cause\" will be set to \"expired_overwrite\". If a cookie was automatically removed due to garbage collection, \"cause\" will be \"evicted\". If a cookie was automatically removed due to a \"set\" call that overwrote it, \"cause\" will be \"overwrite\". Plan your response accordingly."
}
],
"functions": [
{
"name": "get",
"type": "function",
"description": "Retrieves information about a single cookie. If more than one cookie of the same name exists for the given URL, the one with the longest path will be returned. For cookies with the same path length, the cookie with the earliest creation time will be returned.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "details",
"description": "Details to identify the cookie being retrieved.",
"properties": {
"url": {"type": "string", "description": "The URL with which the cookie to retrieve is associated. This argument may be a full URL, in which case any data following the URL path (e.g. the query string) is simply ignored. If host permissions for this URL are not specified in the manifest file, the API call will fail."},
"name": {"type": "string", "description": "The name of the cookie to retrieve."},
"storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store in which to look for the cookie. By default, the current execution context's cookie store will be used."}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "cookie", "$ref": "Cookie", "optional": true, "description": "Contains details about the cookie. This parameter is null if no such cookie was found."
}
]
}
]
},
{
"name": "getAll",
"type": "function",
"description": "Retrieves all cookies from a single cookie store that match the given information. The cookies returned will be sorted, with those with the longest path first. If multiple cookies have the same path length, those with the earliest creation time will be first.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "details",
"description": "Information to filter the cookies being retrieved.",
"properties": {
"url": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those that would match the given URL."},
"name": {"type": "string", "optional": true, "description": "Filters the cookies by name."},
"domain": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those whose domains match or are subdomains of this one."},
"path": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those whose path exactly matches this string."},
"secure": {"type": "boolean", "optional": true, "description": "Filters the cookies by their Secure property."},
"session": {"type": "boolean", "optional": true, "description": "Filters out session vs. persistent cookies."},
"storeId": {"type": "string", "optional": true, "description": "The cookie store to retrieve cookies from. If omitted, the current execution context's cookie store will be used."}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "cookies", "type": "array", "items": {"$ref": "Cookie"}, "description": "All the existing, unexpired cookies that match the given cookie info."
}
]
}
]
},
{
"name": "set",
"type": "function",
"description": "Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "details",
"description": "Details about the cookie being set.",
"properties": {
"url": {"type": "string", "description": "The request-URI to associate with the setting of the cookie. This value can affect the default domain and path values of the created cookie. If host permissions for this URL are not specified in the manifest file, the API call will fail."},
"name": {"type": "string", "optional": true, "description": "The name of the cookie. Empty by default if omitted."},
"value": {"type": "string", "optional": true, "description": "The value of the cookie. Empty by default if omitted."},
"domain": {"type": "string", "optional": true, "description": "The domain of the cookie. If omitted, the cookie becomes a host-only cookie."},
"path": {"type": "string", "optional": true, "description": "The path of the cookie. Defaults to the path portion of the url parameter."},
"secure": {"type": "boolean", "optional": true, "description": "Whether the cookie should be marked as Secure. Defaults to false."},
"httpOnly": {"type": "boolean", "optional": true, "description": "Whether the cookie should be marked as HttpOnly. Defaults to false."},
"expirationDate": {"type": "number", "optional": true, "description": "The expiration date of the cookie as the number of seconds since the UNIX epoch. If omitted, the cookie becomes a session cookie."},
"storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store in which to set the cookie. By default, the cookie is set in the current execution context's cookie store."}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"name": "cookie", "$ref": "Cookie", "optional": true, "description": "Contains details about the cookie that's been set. If setting failed for any reason, this will be \"null\", and $(ref:runtime.lastError) will be set."
}
]
}
]
},
{
"name": "remove",
"type": "function",
"description": "Deletes a cookie by name.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "details",
"description": "Information to identify the cookie to remove.",
"properties": {
"url": {"type": "string", "description": "The URL associated with the cookie. If host permissions for this URL are not specified in the manifest file, the API call will fail."},
"name": {"type": "string", "description": "The name of the cookie to remove."},
"storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store to look in for the cookie. If unspecified, the cookie is looked for by default in the current execution context's cookie store."}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
"name": "details",
"type": "object",
"description": "Contains details about the cookie that's been removed. If removal failed for any reason, this will be \"null\", and $(ref:runtime.lastError) will be set.",
"optional": true,
"properties": {
"url": {"type": "string", "description": "The URL associated with the cookie that's been removed."},
"name": {"type": "string", "description": "The name of the cookie that's been removed."},
"storeId": {"type": "string", "description": "The ID of the cookie store from which the cookie was removed."}
}
}
]
}
]
},
{
"name": "getAllCookieStores",
"type": "function",
"description": "Lists all existing cookie stores.",
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "cookieStores", "type": "array", "items": {"$ref": "CookieStore"}, "description": "All the existing cookie stores."
}
]
}
]
}
],
"events": [
{
"name": "onChanged",
"type": "function",
"description": "Fired when a cookie is set or removed. As a special case, note that updating a cookie's properties is implemented as a two step process: the cookie to be updated is first removed entirely, generating a notification with \"cause\" of \"overwrite\" . Afterwards, a new cookie is written with the updated values, generating a second notification with \"cause\" \"explicit\".",
"parameters": [
{
"type": "object",
"name": "changeInfo",
"properties": {
"removed": {"type": "boolean", "description": "True if a cookie was removed."},
"cookie": {"$ref": "Cookie", "description": "Information about the cookie that was set or removed."},
"cause": {"$ref": "OnChangedCause", "description": "The underlying reason behind the cookie's change."}
}
}
]
}
]
}
]

View File

@ -0,0 +1,793 @@
[
{
"namespace": "manifest",
"types": [
{
"$extend": "Permission",
"choices": [{
"type": "string",
"enum": [
"downloads",
"downloads.open",
"downloads.shelf"
]
}]
}
]
},
{
"namespace": "downloads",
"permissions": ["downloads"],
"types": [
{
"id": "FilenameConflictAction",
"type": "string",
"enum": [
"uniquify",
"overwrite",
"prompt"
]
},
{
"id": "InterruptReason",
"type": "string",
"enum": [
"FILE_FAILED",
"FILE_ACCESS_DENIED",
"FILE_NO_SPACE",
"FILE_NAME_TOO_LONG",
"FILE_TOO_LARGE",
"FILE_VIRUS_INFECTED",
"FILE_TRANSIENT_ERROR",
"FILE_BLOCKED",
"FILE_SECURITY_CHECK_FAILED",
"FILE_TOO_SHORT",
"NETWORK_FAILED",
"NETWORK_TIMEOUT",
"NETWORK_DISCONNECTED",
"NETWORK_SERVER_DOWN",
"NETWORK_INVALID_REQUEST",
"SERVER_FAILED",
"SERVER_NO_RANGE",
"SERVER_BAD_CONTENT",
"SERVER_UNAUTHORIZED",
"SERVER_CERT_PROBLEM",
"SERVER_FORBIDDEN",
"USER_CANCELED",
"USER_SHUTDOWN",
"CRASH"
]
},
{
"id": "DangerType",
"type": "string",
"enum": [
"file",
"url",
"content",
"uncommon",
"host",
"unwanted",
"safe",
"accepted"
],
"description": "<dl><dt>file</dt><dd>The download's filename is suspicious.</dd><dt>url</dt><dd>The download's URL is known to be malicious.</dd><dt>content</dt><dd>The downloaded file is known to be malicious.</dd><dt>uncommon</dt><dd>The download's URL is not commonly downloaded and could be dangerous.</dd><dt>safe</dt><dd>The download presents no known danger to the user's computer.</dd></dl>These string constants will never change, however the set of DangerTypes may change."
},
{
"id": "State",
"type": "string",
"enum": [
"in_progress",
"interrupted",
"complete"
],
"description": "<dl><dt>in_progress</dt><dd>The download is currently receiving data from the server.</dd><dt>interrupted</dt><dd>An error broke the connection with the file host.</dd><dt>complete</dt><dd>The download completed successfully.</dd></dl>These string constants will never change, however the set of States may change."
},
{
"id": "DownloadItem",
"type": "object",
"properties": {
"id": {
"description": "An identifier that is persistent across browser sessions.",
"type": "integer"
},
"url": {
"description": "Absolute URL.",
"type": "string"
},
"referrer": {
"type": "string"
},
"filename": {
"description": "Absolute local path.",
"type": "string"
},
"incognito": {
"description": "False if this download is recorded in the history, true if it is not recorded.",
"type": "boolean"
},
"danger": {
"$ref": "DangerType",
"description": "Indication of whether this download is thought to be safe or known to be suspicious."
},
"mime": {
"description": "The file's MIME type.",
"type": "string"
},
"startTime": {
"description": "Number of milliseconds between the unix epoch and when this download began.",
"type": "string"
},
"endTime": {
"description": "Number of milliseconds between the unix epoch and when this download ended.",
"optional": true,
"type": "string"
},
"estimatedEndTime": {
"type": "string",
"optional": true
},
"state": {
"$ref": "State",
"description": "Indicates whether the download is progressing, interrupted, or complete."
},
"paused": {
"description": "True if the download has stopped reading data from the host, but kept the connection open.",
"type": "boolean"
},
"canResume": {
"type": "boolean"
},
"error": {
"description": "Number indicating why a download was interrupted.",
"optional": true,
"$ref": "InterruptReason"
},
"bytesReceived": {
"description": "Number of bytes received so far from the host, without considering file compression.",
"type": "number"
},
"totalBytes": {
"description": "Number of bytes in the whole file, without considering file compression, or -1 if unknown.",
"type": "number"
},
"fileSize": {
"description": "Number of bytes in the whole file post-decompression, or -1 if unknown.",
"type": "number"
},
"exists": {
"type": "boolean"
},
"byExtensionId": {
"type": "string",
"optional": true
},
"byExtensionName": {
"type": "string",
"optional": true
}
}
},
{
"id": "StringDelta",
"type": "object",
"properties": {
"current": {
"optional": true,
"type": "string"
},
"previous": {
"optional": true,
"type": "string"
}
}
},
{
"id": "DoubleDelta",
"type": "object",
"properties": {
"current": {
"optional": true,
"type": "number"
},
"previous": {
"optional": true,
"type": "number"
}
}
},
{
"id": "BooleanDelta",
"type": "object",
"properties": {
"current": {
"optional": true,
"type": "boolean"
},
"previous": {
"optional": true,
"type": "boolean"
}
}
},
{
"id": "DownloadTime",
"description": "A time specified as a Date object, a number or string representing milliseconds since the epoch, or an ISO 8601 string",
"choices": [
{
"type": "string",
"pattern": "^[1-9]\\d*$"
},
{
"$ref": "extensionTypes.Date"
}
]
},
{
"id": "DownloadQuery",
"description": "Parameters that combine to specify a predicate that can be used to select a set of downloads. Used for example in search() and erase()",
"type": "object",
"properties": {
"query": {
"description": "This array of search terms limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>filename</code> or <code>url</code> contain all of the search terms that do not begin with a dash '-' and none of the search terms that do begin with a dash.",
"optional": true,
"type": "array",
"items": { "type": "string" }
},
"startedBefore": {
"description": "Limits results to downloads that started before the given ms since the epoch.",
"optional": true,
"$ref": "DownloadTime"
},
"startedAfter": {
"description": "Limits results to downloads that started after the given ms since the epoch.",
"optional": true,
"$ref": "DownloadTime"
},
"endedBefore": {
"description": "Limits results to downloads that ended before the given ms since the epoch.",
"optional": true,
"$ref": "DownloadTime"
},
"endedAfter": {
"description": "Limits results to downloads that ended after the given ms since the epoch.",
"optional": true,
"$ref": "DownloadTime"
},
"totalBytesGreater": {
"description": "Limits results to downloads whose totalBytes is greater than the given integer.",
"optional": true,
"type": "number"
},
"totalBytesLess": {
"description": "Limits results to downloads whose totalBytes is less than the given integer.",
"optional": true,
"type": "number"
},
"filenameRegex": {
"description": "Limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>filename</code> matches the given regular expression.",
"optional": true,
"type": "string"
},
"urlRegex": {
"description": "Limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>url</code> matches the given regular expression.",
"optional": true,
"type": "string"
},
"limit": {
"description": "Setting this integer limits the number of results. Otherwise, all matching <a href='#type-DownloadItem'>DownloadItems</a> will be returned.",
"optional": true,
"type": "integer"
},
"orderBy": {
"description": "Setting elements of this array to <a href='#type-DownloadItem'>DownloadItem</a> properties in order to sort the search results. For example, setting <code>orderBy='startTime'</code> sorts the <a href='#type-DownloadItem'>DownloadItems</a> by their start time in ascending order. To specify descending order, prefix <code>orderBy</code> with a hyphen: '-startTime'.",
"optional": true,
"type": "array",
"items": { "type": "string" }
},
"id": {
"type": "integer",
"optional": true
},
"url": {
"description": "Absolute URL.",
"optional": true,
"type": "string"
},
"filename": {
"description": "Absolute local path.",
"optional": true,
"type": "string"
},
"danger": {
"$ref": "DangerType",
"description": "Indication of whether this download is thought to be safe or known to be suspicious.",
"optional": true
},
"mime": {
"description": "The file's MIME type.",
"optional": true,
"type": "string"
},
"startTime": {
"optional": true,
"type": "string"
},
"endTime": {
"optional": true,
"type": "string"
},
"state": {
"$ref": "State",
"description": "Indicates whether the download is progressing, interrupted, or complete.",
"optional": true
},
"paused": {
"description": "True if the download has stopped reading data from the host, but kept the connection open.",
"optional": true,
"type": "boolean"
},
"error": {
"description": "Why a download was interrupted.",
"optional": true,
"$ref": "InterruptReason"
},
"bytesReceived": {
"description": "Number of bytes received so far from the host, without considering file compression.",
"optional": true,
"type": "number"
},
"totalBytes": {
"description": "Number of bytes in the whole file, without considering file compression, or -1 if unknown.",
"optional": true,
"type": "number"
},
"fileSize": {
"description": "Number of bytes in the whole file post-decompression, or -1 if unknown.",
"optional": true,
"type": "number"
},
"exists": {
"type": "boolean",
"optional": true
}
}
}
],
"functions": [
{
"name": "download",
"type": "function",
"async": "callback",
"description": "Download a URL. If the URL uses the HTTP[S] protocol, then the request will include all cookies currently set for its hostname. If both <code>filename</code> and <code>saveAs</code> are specified, then the Save As dialog will be displayed, pre-populated with the specified <code>filename</code>. If the download started successfully, <code>callback</code> will be called with the new <a href='#type-DownloadItem'>DownloadItem</a>'s <code>downloadId</code>. If there was an error starting the download, then <code>callback</code> will be called with <code>downloadId=undefined</code> and <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain a descriptive string. The error strings are not guaranteed to remain backwards compatible between releases. You must not parse it.",
"parameters": [
{
"description": "What to download and how.",
"name": "options",
"type": "object",
"properties": {
"url": {
"description": "The URL to download.",
"type": "string",
"format": "url"
},
"filename": {
"description": "A file path relative to the Downloads directory to contain the downloaded file.",
"optional": true,
"type": "string"
},
"conflictAction": {
"$ref": "FilenameConflictAction",
"optional": true
},
"saveAs": {
"description": "Use a file-chooser to allow the user to select a filename.",
"optional": true,
"type": "boolean"
},
"method": {
"description": "The HTTP method to use if the URL uses the HTTP[S] protocol.",
"enum": [
"GET",
"POST"
],
"optional": true,
"type": "string"
},
"headers": {
"optional": true,
"type": "array",
"description": "Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>, restricted to those allowed by XMLHttpRequest.",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Name of the HTTP header.",
"type": "string"
},
"value": {
"description": "Value of the HTTP header.",
"type": "string"
}
}
}
},
"body": {
"description": "Post body.",
"optional": true,
"type": "string"
}
}
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": [
{
"name": "downloadId",
"type": "integer"
}
]
}
]
},
{
"name": "search",
"type": "function",
"async": "callback",
"description": "Find <a href='#type-DownloadItem'>DownloadItems</a>. Set <code>query</code> to the empty object to get all <a href='#type-DownloadItem'>DownloadItems</a>. To get a specific <a href='#type-DownloadItem'>DownloadItem</a>, set only the <code>id</code> field.",
"parameters": [
{
"name": "query",
"$ref": "DownloadQuery"
},
{
"name": "callback",
"type": "function",
"parameters": [
{
"items": {
"$ref": "DownloadItem"
},
"name": "results",
"type": "array"
}
]
}
]
},
{
"name": "pause",
"type": "function",
"async": "callback",
"description": "Pause the download. If the request was successful the download is in a paused state. Otherwise <a href='extension.html#property-lastError'>chrome.extension.lastError</a> contains an error message. The request will fail if the download is not active.",
"parameters": [
{
"description": "The id of the download to pause.",
"name": "downloadId",
"type": "integer"
},
{
"name": "callback",
"optional": true,
"parameters": [],
"type": "function"
}
]
},
{
"name": "resume",
"type": "function",
"async": "callback",
"description": "Resume a paused download. If the request was successful the download is in progress and unpaused. Otherwise <a href='extension.html#property-lastError'>chrome.extension.lastError</a> contains an error message. The request will fail if the download is not active.",
"parameters": [
{
"description": "The id of the download to resume.",
"name": "downloadId",
"type": "integer"
},
{
"name": "callback",
"optional": true,
"parameters": [],
"type": "function"
}
]
},
{
"name": "cancel",
"type": "function",
"async": "callback",
"description": "Cancel a download. When <code>callback</code> is run, the download is cancelled, completed, interrupted or doesn't exist anymore.",
"parameters": [
{
"description": "The id of the download to cancel.",
"name": "downloadId",
"type": "integer"
},
{
"name": "callback",
"optional": true,
"parameters": [],
"type": "function"
}
]
},
{
"name": "getFileIcon",
"type": "function",
"async": "callback",
"description": "Retrieve an icon for the specified download. For new downloads, file icons are available after the <a href='#event-onCreated'>onCreated</a> event has been received. The image returned by this function while a download is in progress may be different from the image returned after the download is complete. Icon retrieval is done by querying the underlying operating system or toolkit depending on the platform. The icon that is returned will therefore depend on a number of factors including state of the download, platform, registered file types and visual theme. If a file icon cannot be determined, <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain an error message.",
"parameters": [
{
"description": "The identifier for the download.",
"name": "downloadId",
"type": "integer"
},
{
"name": "options",
"optional": true,
"properties": {
"size": {
"description": "The size of the icon. The returned icon will be square with dimensions size * size pixels. The default size for the icon is 32x32 pixels.",
"optional": true,
"minimum": 1,
"maximum": 127,
"type": "integer"
}
},
"type": "object"
},
{
"name": "callback",
"parameters": [
{
"name": "iconURL",
"optional": true,
"type": "string"
}
],
"type": "function"
}
]
},
{
"name": "open",
"type": "function",
"async": "callback",
"description": "Open the downloaded file.",
"permissions": ["downloads.open"],
"parameters": [
{
"name": "downloadId",
"type": "integer"
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": []
}
]
},
{
"name": "show",
"type": "function",
"description": "Show the downloaded file in its folder in a file manager.",
"async": "callback",
"parameters": [
{
"name": "downloadId",
"type": "integer"
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": [
{
"name": "success",
"type": "boolean"
}
]
}
]
},
{
"name": "showDefaultFolder",
"type": "function",
"parameters": []
},
{
"name": "erase",
"type": "function",
"async": "callback",
"description": "Erase matching <a href='#type-DownloadItem'>DownloadItems</a> from history",
"parameters": [
{
"name": "query",
"$ref": "DownloadQuery"
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": [
{
"items": {
"type": "integer"
},
"name": "erasedIds",
"type": "array"
}
]
}
]
},
{
"name": "removeFile",
"async": "callback",
"type": "function",
"parameters": [
{
"name": "downloadId",
"type": "integer"
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": [ ]
}
]
},
{
"description": "Prompt the user to either accept or cancel a dangerous download. <code>acceptDanger()</code> does not automatically accept dangerous downloads.",
"name": "acceptDanger",
"unsupported": true,
"parameters": [
{
"name": "downloadId",
"type": "integer"
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": [ ]
}
],
"type": "function"
},
{
"description": "Initiate dragging the file to another application.",
"name": "drag",
"unsupported": true,
"parameters": [
{
"name": "downloadId",
"type": "integer"
}
],
"type": "function"
},
{
"name": "setShelfEnabled",
"type": "function",
"unsupported": true,
"parameters": [
{
"name": "enabled",
"type": "boolean"
}
]
}
],
"events": [
{
"description": "This event fires with the <a href='#type-DownloadItem'>DownloadItem</a> object when a download begins.",
"name": "onCreated",
"parameters": [
{
"$ref": "DownloadItem",
"name": "downloadItem"
}
],
"type": "function"
},
{
"description": "Fires with the <code>downloadId</code> when a download is erased from history.",
"name": "onErased",
"parameters": [
{
"name": "downloadId",
"description": "The <code>id</code> of the <a href='#type-DownloadItem'>DownloadItem</a> that was erased.",
"type": "integer"
}
],
"type": "function"
},
{
"name": "onChanged",
"description": "When any of a <a href='#type-DownloadItem'>DownloadItem</a>'s properties except <code>bytesReceived</code> changes, this event fires with the <code>downloadId</code> and an object containing the properties that changed.",
"parameters": [
{
"name": "downloadDelta",
"type": "object",
"properties": {
"id": {
"description": "The <code>id</code> of the <a href='#type-DownloadItem'>DownloadItem</a> that changed.",
"type": "integer"
},
"url": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>url</code>.",
"optional": true,
"$ref": "StringDelta"
},
"filename": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>filename</code>.",
"optional": true,
"$ref": "StringDelta"
},
"danger": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>danger</code>.",
"optional": true,
"$ref": "StringDelta"
},
"mime": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>mime</code>.",
"optional": true,
"$ref": "StringDelta"
},
"startTime": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>startTime</code>.",
"optional": true,
"$ref": "StringDelta"
},
"endTime": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>endTime</code>.",
"optional": true,
"$ref": "StringDelta"
},
"state": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>state</code>.",
"optional": true,
"$ref": "StringDelta"
},
"canResume": {
"optional": true,
"$ref": "BooleanDelta"
},
"paused": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>paused</code>.",
"optional": true,
"$ref": "BooleanDelta"
},
"error": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>error</code>.",
"optional": true,
"$ref": "StringDelta"
},
"totalBytes": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>totalBytes</code>.",
"optional": true,
"$ref": "DoubleDelta"
},
"fileSize": {
"description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>fileSize</code>.",
"optional": true,
"$ref": "DoubleDelta"
},
"exists": {
"optional": true,
"$ref": "BooleanDelta"
}
}
}
],
"type": "function"
}
]
}
]

View File

@ -0,0 +1,322 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "events",
"description": "The <code>chrome.events</code> namespace contains common types used by APIs dispatching events to notify you when something interesting happens.",
"types": [
{
"id": "Rule",
"type": "object",
"description": "Description of a declarative rule for handling events.",
"properties": {
"id": {
"type": "string",
"optional": true,
"description": "Optional identifier that allows referencing this rule."
},
"tags": {
"type": "array",
"items": {"type": "string"},
"optional": true,
"description": "Tags can be used to annotate rules and perform operations on sets of rules."
},
"conditions": {
"type": "array",
"items": {"type": "any"},
"description": "List of conditions that can trigger the actions."
},
"actions": {
"type": "array",
"items": {"type": "any"},
"description": "List of actions that are triggered if one of the condtions is fulfilled."
},
"priority": {
"type": "integer",
"optional": true,
"description": "Optional priority of this rule. Defaults to 100."
}
}
},
{
"id": "Event",
"type": "object",
"description": "An object which allows the addition and removal of listeners for a Chrome event.",
"functions": [
{
"name": "addListener",
"type": "function",
"description": "Registers an event listener <em>callback</em> to an event.",
"parameters": [
{
"name": "callback",
"type": "function",
"description": "Called when an event occurs. The parameters of this function depend on the type of event."
}
]
},
{
"name": "removeListener",
"type": "function",
"description": "Deregisters an event listener <em>callback</em> from an event.",
"parameters": [
{
"name": "callback",
"type": "function",
"description": "Listener that shall be unregistered."
}
]
},
{
"name": "hasListener",
"type": "function",
"parameters": [
{
"name": "callback",
"type": "function",
"description": "Listener whose registration status shall be tested."
}
],
"returns": {
"type": "boolean",
"description": "True if <em>callback</em> is registered to the event."
}
},
{
"name": "hasListeners",
"type": "function",
"parameters": [],
"returns": {
"type": "boolean",
"description": "True if any event listeners are registered to the event."
}
},
{
"name": "addRules",
"unsupported": true,
"type": "function",
"description": "Registers rules to handle events.",
"parameters": [
{
"name": "eventName",
"type": "string",
"description": "Name of the event this function affects."
},
{
"name": "webViewInstanceId",
"type": "integer",
"description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call."
},
{
"name": "rules",
"type": "array",
"items": {"$ref": "Rule"},
"description": "Rules to be registered. These do not replace previously registered rules."
},
{
"name": "callback",
"optional": true,
"type": "function",
"parameters": [
{
"name": "rules",
"type": "array",
"items": {"$ref": "Rule"},
"description": "Rules that were registered, the optional parameters are filled with values."
}
],
"description": "Called with registered rules."
}
]
},
{
"name": "getRules",
"unsupported": true,
"type": "function",
"description": "Returns currently registered rules.",
"parameters": [
{
"name": "eventName",
"type": "string",
"description": "Name of the event this function affects."
},
{
"name": "webViewInstanceId",
"type": "integer",
"description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call."
},
{
"name": "ruleIdentifiers",
"optional": true,
"type": "array",
"items": {"type": "string"},
"description": "If an array is passed, only rules with identifiers contained in this array are returned."
},
{
"name": "callback",
"type": "function",
"parameters": [
{
"name": "rules",
"type": "array",
"items": {"$ref": "Rule"},
"description": "Rules that were registered, the optional parameters are filled with values."
}
],
"description": "Called with registered rules."
}
]
},
{
"name": "removeRules",
"unsupported": true,
"type": "function",
"description": "Unregisters currently registered rules.",
"parameters": [
{
"name": "eventName",
"type": "string",
"description": "Name of the event this function affects."
},
{
"name": "webViewInstanceId",
"type": "integer",
"description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call."
},
{
"name": "ruleIdentifiers",
"optional": true,
"type": "array",
"items": {"type": "string"},
"description": "If an array is passed, only rules with identifiers contained in this array are unregistered."
},
{
"name": "callback",
"optional": true,
"type": "function",
"parameters": [],
"description": "Called when rules were unregistered."
}
]
}
]
},
{
"id": "UrlFilter",
"type": "object",
"description": "Filters URLs for various criteria. See <a href='events#filtered'>event filtering</a>. All criteria are case sensitive.",
"properties": {
"hostContains": {
"type": "string",
"description": "Matches if the host name of the URL contains a specified string. To test whether a host name component has a prefix 'foo', use hostContains: '.foo'. This matches 'www.foobar.com' and 'foo.com', because an implicit dot is added at the beginning of the host name. Similarly, hostContains can be used to match against component suffix ('foo.') and to exactly match against components ('.foo.'). Suffix- and exact-matching for the last components need to be done separately using hostSuffix, because no implicit dot is added at the end of the host name.",
"optional": true
},
"hostEquals": {
"type": "string",
"description": "Matches if the host name of the URL is equal to a specified string.",
"optional": true
},
"hostPrefix": {
"type": "string",
"description": "Matches if the host name of the URL starts with a specified string.",
"optional": true
},
"hostSuffix": {
"type": "string",
"description": "Matches if the host name of the URL ends with a specified string.",
"optional": true
},
"pathContains": {
"type": "string",
"description": "Matches if the path segment of the URL contains a specified string.",
"optional": true
},
"pathEquals": {
"type": "string",
"description": "Matches if the path segment of the URL is equal to a specified string.",
"optional": true
},
"pathPrefix": {
"type": "string",
"description": "Matches if the path segment of the URL starts with a specified string.",
"optional": true
},
"pathSuffix": {
"type": "string",
"description": "Matches if the path segment of the URL ends with a specified string.",
"optional": true
},
"queryContains": {
"type": "string",
"description": "Matches if the query segment of the URL contains a specified string.",
"optional": true
},
"queryEquals": {
"type": "string",
"description": "Matches if the query segment of the URL is equal to a specified string.",
"optional": true
},
"queryPrefix": {
"type": "string",
"description": "Matches if the query segment of the URL starts with a specified string.",
"optional": true
},
"querySuffix": {
"type": "string",
"description": "Matches if the query segment of the URL ends with a specified string.",
"optional": true
},
"urlContains": {
"type": "string",
"description": "Matches if the URL (without fragment identifier) contains a specified string. Port numbers are stripped from the URL if they match the default port number.",
"optional": true
},
"urlEquals": {
"type": "string",
"description": "Matches if the URL (without fragment identifier) is equal to a specified string. Port numbers are stripped from the URL if they match the default port number.",
"optional": true
},
"urlMatches": {
"type": "string",
"description": "Matches if the URL (without fragment identifier) matches a specified regular expression. Port numbers are stripped from the URL if they match the default port number. The regular expressions use the <a href=\"https://github.com/google/re2/blob/master/doc/syntax.txt\">RE2 syntax</a>.",
"optional": true
},
"originAndPathMatches": {
"type": "string",
"description": "Matches if the URL without query segment and fragment identifier matches a specified regular expression. Port numbers are stripped from the URL if they match the default port number. The regular expressions use the <a href=\"https://github.com/google/re2/blob/master/doc/syntax.txt\">RE2 syntax</a>.",
"optional": true
},
"urlPrefix": {
"type": "string",
"description": "Matches if the URL (without fragment identifier) starts with a specified string. Port numbers are stripped from the URL if they match the default port number.",
"optional": true
},
"urlSuffix": {
"type": "string",
"description": "Matches if the URL (without fragment identifier) ends with a specified string. Port numbers are stripped from the URL if they match the default port number.",
"optional": true
},
"schemes": {
"type": "array",
"description": "Matches if the scheme of the URL is equal to any of the schemes specified in the array.",
"optional": true,
"items": { "type": "string" }
},
"ports": {
"type": "array",
"description": "Matches if the port of the URL is contained in any of the specified port lists. For example <code>[80, 443, [1000, 1200]]</code> matches all requests on port 80, 443 and in the range 1000-1200.",
"optional": true,
"items": {
"choices": [
{"type": "integer", "description": "A specific port."},
{"type": "array", "minItems": 2, "maxItems": 2, "items": {"type": "integer"}, "description": "A pair of integers identiying the start and end (both inclusive) of a port range."}
]
}
}
}
}
]
}
]

Some files were not shown because too many files have changed in this diff Show More