--- a/browser/base/content/browser-sidebar.js
+++ b/browser/base/content/browser-sidebar.js
@@ -26,34 +26,16 @@ var SidebarUI = {
_title: null,
_splitter: null,
init() {
this._box = document.getElementById("sidebar-box");
this.browser = document.getElementById("sidebar");
this._title = document.getElementById("sidebar-title");
this._splitter = document.getElementById("sidebar-splitter");
-
- if (!this.adoptFromWindow(window.opener)) {
- let commandID = this._box.getAttribute("sidebarcommand");
- if (commandID) {
- let command = document.getElementById(commandID);
- if (command) {
- this._delayedLoad = true;
- this._box.hidden = false;
- this._splitter.hidden = false;
- command.setAttribute("checked", "true");
- } else {
- // Remove the |sidebarcommand| attribute, because the element it
- // refers to no longer exists, so we should assume this sidebar
- // panel has been uninstalled. (249883)
- this._box.removeAttribute("sidebarcommand");
- }
- }
- }
},
uninit() {
let enumerator = Services.wm.getEnumerator(null);
enumerator.getNext();
if (!enumerator.hasMoreElements()) {
document.persist("sidebar-box", "sidebarcommand");
document.persist("sidebar-box", "width");
@@ -64,24 +46,16 @@ var SidebarUI = {
/**
* Try and adopt the status of the sidebar from another window.
* @param {Window} sourceWindow - Window to use as a source for sidebar status.
* @return true if we adopted the state, or false if the caller should
* initialize the state itself.
*/
adoptFromWindow(sourceWindow) {
- // No source window, or it being closed, or not chrome, or in a different
- // private-browsing context means we can't adopt.
- if (!sourceWindow || sourceWindow.closed ||
- !sourceWindow.document.documentURIObject.schemeIs("chrome") ||
- PrivateBrowsingUtils.isWindowPrivate(window) != PrivateBrowsingUtils.isWindowPrivate(sourceWindow)) {
- return false;
- }
-
// If the opener had a sidebar, open the same sidebar in our window.
// The opener can be the hidden window too, if we're coming from the state
// where no windows are open, and the hidden window has no sidebar box.
let sourceUI = sourceWindow.SidebarUI;
if (!sourceUI || !sourceUI._box) {
// no source UI or no _box means we also can't adopt the state.
return false;
}
@@ -103,33 +77,65 @@ var SidebarUI = {
sourceUI._title.getAttribute("value"));
this._box.setAttribute("width", sourceUI._box.boxObject.width);
this._box.setAttribute("sidebarcommand", commandID);
// Note: we're setting 'src' on this._box, which is a <vbox>, not on
// the <browser id="sidebar">. This lets us delay the actual load until
// delayedStartup().
this._box.setAttribute("src", sourceUI.browser.getAttribute("src"));
- this._delayedLoad = true;
this._box.hidden = false;
this._splitter.hidden = false;
commandElem.setAttribute("checked", "true");
+ this.browser.setAttribute("src", this._box.getAttribute("src"));
return true;
},
+ windowPrivacyMatches(w1, w2) {
+ return PrivateBrowsingUtils.isWindowPrivate(w1) === PrivateBrowsingUtils.isWindowPrivate(w2);
+ },
+
/**
* If loading a sidebar was delayed on startup, start the load now.
*/
startDelayedLoad() {
- if (!this._delayedLoad) {
+ let sourceWindow = window.opener;
+ // No source window means this is the initial window. If we're being
+ // opened from another window, check that it is one we might open a sidebar
+ // for.
+ if (sourceWindow) {
+ if (sourceWindow.closed || sourceWindow.location.protocol != "chrome:" ||
+ !this.windowPrivacyMatches(sourceWindow, window)) {
+ return;
+ }
+ // Try to adopt the sidebar state from the source window
+ if (this.adoptFromWindow(sourceWindow)) {
+ return;
+ }
+ }
+
+ // If we're not adopting settings from a parent window, set them now.
+ let commandID = this._box.getAttribute("sidebarcommand");
+ if (!commandID) {
return;
}
- this.browser.setAttribute("src", this._box.getAttribute("src"));
+ let command = document.getElementById(commandID);
+ if (command) {
+ this._box.hidden = false;
+ this._splitter.hidden = false;
+ command.setAttribute("checked", "true");
+ this.browser.setAttribute("src", this._box.getAttribute("src"));
+ } else {
+ // Remove the |sidebarcommand| attribute, because the element it
+ // refers to no longer exists, so we should assume this sidebar
+ // panel has been uninstalled. (249883)
+ this._box.removeAttribute("sidebarcommand");
+ }
},
/**
* Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
* a chance to adjust focus as needed. An additional event is needed, because
* we don't want to focus the sidebar when it's opened on startup or in a new
* window, only when the user opens the sidebar.
*/
@@ -230,31 +236,28 @@ var SidebarUI = {
// We set this attribute here in addition to setting it on the <browser>
// element itself, because the code in SidebarUI.uninit() persists this
// attribute, not the "src" of the <browser id="sidebar">. The reason it
// does that is that we want to delay sidebar load a bit when a browser
// window opens. See delayedStartup() and SidebarUI.startDelayedLoad().
this._box.setAttribute("src", url);
if (this.browser.contentDocument.location.href != url) {
- let onLoad = event => {
- this.browser.removeEventListener("load", onLoad, true);
+ this.browser.addEventListener("load", event => {
// We're handling the 'load' event before it bubbles up to the usual
// (non-capturing) event handlers. Let it bubble up before firing the
// SidebarFocused event.
setTimeout(() => this._fireFocusedEvent(), 0);
// Run the original function for backwards compatibility.
sidebarOnLoad(event);
resolve();
- };
-
- this.browser.addEventListener("load", onLoad, true);
+ }, {capture: true, once: true});
} else {
// Older code handled this case, so we do it too.
this._fireFocusedEvent();
resolve();
}
let selBrowser = gBrowser.selectedBrowser;
selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
@@ -298,35 +301,35 @@ var SidebarUI = {
selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
{commandID, isOpen: false}
);
BrowserUITelemetry.countSidebarEvent(commandID, "hide");
},
};
/**
- * This exists for backards compatibility - it will be called once a sidebar is
+ * This exists for backwards compatibility - it will be called once a sidebar is
* ready, following any request to show it.
*
* @deprecated
*/
function fireSidebarFocusedEvent() {}
/**
- * This exists for backards compatibility - it gets called when a sidebar has
+ * This exists for backwards compatibility - it gets called when a sidebar has
* been loaded.
*
* @deprecated
*/
function sidebarOnLoad(event) {}
/**
- * This exists for backards compatibility, and is equivilent to
+ * This exists for backwards compatibility, and is equivilent to
* SidebarUI.toggle() without the forceOpen param. With forceOpen set to true,
- * it is equalivent to SidebarUI.show().
+ * it is equivalent to SidebarUI.show().
*
* @deprecated
*/
function toggleSidebar(commandID, forceOpen = false) {
Deprecated.warning("toggleSidebar() is deprecated, please use SidebarUI.toggle() or SidebarUI.show() instead",
"https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Sidebar");
if (forceOpen) {
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -319,31 +319,39 @@ toolbarpaletteitem > toolbaritem[sdkstyl
.webextension-browser-action[cui-areatype="menu-panel"],
toolbarpaletteitem[place="palette"] > .webextension-browser-action {
list-style-image: var(--webextension-menupanel-image);
}
.webextension-page-action {
list-style-image: var(--webextension-urlbar-image);
}
+
+ .webextension-menuitem {
+ list-style-image: var(--webextension-menuitem-image);
+ }
}
@media (min-resolution: 1.1dppx) {
.webextension-browser-action {
list-style-image: var(--webextension-toolbar-image-2x);
}
.webextension-browser-action[cui-areatype="menu-panel"],
toolbarpaletteitem[place="palette"] > .webextension-browser-action {
list-style-image: var(--webextension-menupanel-image-2x);
}
.webextension-page-action {
list-style-image: var(--webextension-urlbar-image-2x);
}
+
+ .webextension-menuitem {
+ list-style-image: var(--webextension-menuitem-image-2x);
+ }
}
toolbarpaletteitem[removable="false"] {
opacity: 0.5;
cursor: default;
}
%ifndef XP_MACOSX
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1280,18 +1280,16 @@ var gBrowserInit = {
// Initialize the full zoom setting.
// We do this before the session restore service gets initialized so we can
// apply full zoom settings to tabs restored by the session restore service.
FullZoom.init();
PanelUI.init();
LightweightThemeListener.init();
- SidebarUI.startDelayedLoad();
-
UpdateUrlbarSearchSplitterState();
if (!(isBlankPageURL(uriToLoad) || uriToLoad == "about:privatebrowsing") ||
!focusAndSelectUrlBar()) {
if (gBrowser.selectedBrowser.isRemoteBrowser) {
// If the initial browser is remote, in order to optimize for first paint,
// we'll defer switching focus to that browser until it has painted.
let focusedElement = document.commandDispatcher.focusedElement;
@@ -1455,16 +1453,17 @@ var gBrowserInit = {
// Bail out if the window has been closed in the meantime.
if (window.closed) {
return;
}
// Enable the Restore Last Session command if needed
RestoreLastSessionObserver.init();
+ SidebarUI.startDelayedLoad();
SocialUI.init();
// Start monitoring slow add-ons
AddonWatcher.init();
// Telemetry for master-password - we do this after 5 seconds as it
// can cause IO if NSS/PSM has not already initialized.
setTimeout(() => {
new file mode 100644
--- /dev/null
+++ b/browser/base/content/webext-panels.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* 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/. */
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm");
+
+function loadWebPanel() {
+ let sidebarURI = new URL(location);
+ let uri = sidebarURI.searchParams.get("panel");
+ let remote = sidebarURI.searchParams.get("remote");
+ let browser = document.getElementById("webext-panels-browser");
+ if (remote) {
+ let remoteType = E10SUtils.getRemoteTypeForURI(uri, true,
+ E10SUtils.EXTENSION_REMOTE_TYPE);
+ browser.setAttribute("remote", "true");
+ browser.setAttribute("remoteType", remoteType);
+ } else {
+ browser.removeAttribute("remote");
+ browser.removeAttribute("remoteType");
+ }
+ browser.loadURI(uri);
+}
+
+function load() {
+ let browser = document.getElementById("webext-panels-browser");
+ browser.messageManager.loadFrameScript("chrome://browser/content/content.js", true);
+ ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
+
+ this.loadWebPanel();
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/webext-panels.xul
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# 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/.
+
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<!DOCTYPE page [
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+%browserDTD;
+<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
+%textcontextDTD;
+]>
+
+<page id="webextpanels-window"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="load()">
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
+ <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
+ <script type="application/javascript" src="chrome://browser/content/webext-panels.js"/>
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
+ </stringbundleset>
+
+ <broadcasterset id="mainBroadcasterSet">
+ <broadcaster id="isFrameImage"/>
+ </broadcasterset>
+
+ <commandset id="mainCommandset">
+ <command id="Browser:Back"
+ oncommand="getPanelBrowser().webNavigation.goBack();"
+ disabled="true"/>
+ <command id="Browser:Forward"
+ oncommand="getPanelBrowser().webNavigation.goForward();"
+ disabled="true"/>
+ <command id="Browser:Stop" oncommand="PanelBrowserStop();"/>
+ <command id="Browser:Reload" oncommand="PanelBrowserReload();"/>
+ </commandset>
+
+ <popupset id="mainPopupSet">
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <menupopup id="contentAreaContextMenu" pagemenu="start"
+ onpopupshowing="if (event.target != this)
+ return true;
+ gContextMenu = new nsContextMenu(this, event.shiftKey);
+ if (gContextMenu.shouldDisplay)
+ document.popupNode = this.triggerNode;
+ return gContextMenu.shouldDisplay;"
+ onpopuphiding="if (event.target != this)
+ return;
+ gContextMenu.hiding();
+ gContextMenu = null;">
+#include browser-context.inc
+ </menupopup>
+ </popupset>
+
+ <commandset id="editMenuCommands"/>
+ <browser id="webext-panels-browser"
+ type="content" flex="1"
+ webextension-view-type="sidebar"
+ context="contentAreaContextMenu" tooltip="aHTMLTooltip"
+ onclick="window.parent.contentAreaClick(event, true);"/>
+</page>
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -150,16 +150,18 @@ 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/utilityOverlay.js (content/utilityOverlay.js)
content/browser/usercontext.svg (content/usercontext.svg)
content/browser/web-panels.js (content/web-panels.js)
* content/browser/web-panels.xul (content/web-panels.xul)
+ content/browser/webext-panels.js (content/webext-panels.js)
+* content/browser/webext-panels.xul (content/webext-panels.xul)
* content/browser/baseMenuOverlay.xul (content/baseMenuOverlay.xul)
* content/browser/nsContextMenu.js (content/nsContextMenu.js)
# XXX: We should exclude this one as well (bug 71895)
* content/browser/hiddenWindow.xul (content/hiddenWindow.xul)
#ifdef XP_MACOSX
* content/browser/macBrowserOverlay.xul (content/macBrowserOverlay.xul)
* content/browser/softwareUpdateOverlay.xul (content/softwareUpdateOverlay.xul)
#endif
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -97,17 +97,17 @@ function updateCombinedWidgetStyle(aNode
continue;
setAttributes(aNode.childNodes[i], attrs);
}
}
function fillSubviewFromMenuItems(aMenuItems, aSubview) {
let attrs = ["oncommand", "onclick", "label", "key", "disabled",
"command", "observes", "hidden", "class", "origin",
- "image", "checked"];
+ "image", "checked", "style"];
let doc = aSubview.ownerDocument;
let fragment = doc.createDocumentFragment();
for (let menuChild of aMenuItems) {
if (menuChild.hidden)
continue;
let subviewItem;
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -350,27 +350,23 @@ BrowserAction.prototype = {
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);
+ let getIcon = size => IconDetails.escapeUrl(
+ 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: url("${IconDetails.escapeUrl(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) {
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-sidebarAction.js
@@ -0,0 +1,349 @@
+/* -*- 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, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+ "resource:///modules/CustomizableUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+let {
+ ExtensionError,
+ IconDetails,
+} = ExtensionUtils;
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+// WeakMap[Extension -> SidebarAction]
+let sidebarActionMap = new WeakMap();
+
+const sidebarURL = "chrome://browser/content/webext-panels.xul";
+
+/**
+ * Responsible for the sidebar_action section of the manifest as well
+ * as the associated sidebar browser.
+ */
+class SidebarAction {
+ constructor(options, extension) {
+ this.extension = extension;
+
+ // Add the extension to the sidebar menu. The sidebar widget will copy
+ // from that when it is viewed, so we shouldn't need to update that.
+ let widgetId = makeWidgetId(extension.id);
+ this.id = `${widgetId}-sidebar-action`;
+ this.menuId = `menu_${this.id}`;
+
+ this.defaults = {
+ enabled: true,
+ title: options.default_title || extension.name,
+ icon: IconDetails.normalize({path: options.default_icon}, extension),
+ panel: options.default_panel || "",
+ };
+
+ this.tabContext = new TabContext(tab => Object.create(this.defaults),
+ extension);
+
+ // We need to ensure our elements are available before session restore.
+ this.windowOpenListener = (window) => {
+ this.createMenuItem(window, this.defaults);
+ };
+ windowTracker.addOpenListener(this.windowOpenListener);
+ }
+
+ build() {
+ this.tabContext.on("tab-select", // eslint-disable-line mozilla/balanced-listeners
+ (evt, tab) => { this.updateWindow(tab.ownerGlobal); });
+
+ let install = this.extension.startupReason === "ADDON_INSTALL";
+ for (let window of windowTracker.browserWindows()) {
+ this.updateWindow(window);
+ if (install) {
+ let {SidebarUI} = window;
+ SidebarUI.show(this.id);
+ }
+ }
+
+ // Bug 1331507: UX review/analysis of sidebar-button injection.
+ if (AppConstants.RELEASE_OR_BETA) {
+ return;
+ }
+
+ if (install && !Services.prefs.prefHasUserValue("extensions.sidebar-button.shown")) {
+ Services.prefs.setBoolPref("extensions.sidebar-button.shown", true);
+ // If the sidebar button has never been moved to the toolbar, move it now
+ // so the user can see/access the sidebars.
+ let widget = CustomizableUI.getWidget("sidebar-button");
+ if (!widget.areaType) {
+ CustomizableUI.addWidgetToArea("sidebar-button", CustomizableUI.AREA_NAVBAR, 0);
+ }
+ }
+ }
+
+ sidebarUrl(panel) {
+ if (this.extension.remote) {
+ return `${sidebarURL}?remote=1&panel=${encodeURIComponent(panel)}`;
+ }
+ return `${sidebarURL}?&panel=${encodeURIComponent(panel)}`;
+ }
+
+ createMenuItem(window, details) {
+ let {document} = window;
+
+ // Use of the broadcaster allows browser-sidebar.js to properly manage the
+ // checkmarks in the menus.
+ let broadcaster = document.createElementNS(XUL_NS, "broadcaster");
+ broadcaster.setAttribute("id", this.id);
+ broadcaster.setAttribute("autoCheck", "false");
+ broadcaster.setAttribute("type", "checkbox");
+ broadcaster.setAttribute("group", "sidebar");
+ broadcaster.setAttribute("label", details.title);
+ broadcaster.setAttribute("sidebarurl", this.sidebarUrl(details.panel));
+ // oncommand gets attached to menuitem, so we use the observes attribute to
+ // get the command id we pass to SidebarUI.
+ broadcaster.setAttribute("oncommand", "SidebarUI.toggle(this.getAttribute('observes'))");
+
+ let menuitem = document.createElementNS(XUL_NS, "menuitem");
+ menuitem.setAttribute("id", this.menuId);
+ menuitem.setAttribute("observes", this.id);
+ menuitem.setAttribute("class", "menuitem-iconic webextension-menuitem");
+
+ document.getElementById("mainBroadcasterSet").appendChild(broadcaster);
+ document.getElementById("viewSidebarMenu").appendChild(menuitem);
+
+ return menuitem;
+ }
+
+ /**
+ * Update the broadcaster and menuitem `node` with the tab context data
+ * in `tabData`.
+ *
+ * @param {ChromeWindow} window
+ * Browser chrome window.
+ * @param {object} tabData
+ * Tab specific sidebar configuration.
+ */
+ updateButton(window, tabData) {
+ let {document, SidebarUI} = window;
+ let title = tabData.title || this.extension.name;
+ let menu = document.getElementById(this.menuId);
+ if (!menu) {
+ menu = this.createMenuItem(window, tabData);
+ }
+
+ // Update the broadcaster first, it will update both menus.
+ let broadcaster = document.getElementById(this.id);
+ broadcaster.setAttribute("tooltiptext", title);
+ broadcaster.setAttribute("label", title);
+
+ let url = this.sidebarUrl(tabData.panel);
+ let urlChanged = url !== broadcaster.getAttribute("sidebarurl");
+ if (urlChanged) {
+ broadcaster.setAttribute("sidebarurl", url);
+ }
+
+ let getIcon = size => IconDetails.escapeUrl(
+ IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
+
+ menu.setAttribute("style", `
+ --webextension-menuitem-image: url("${getIcon(16)}");
+ --webextension-menuitem-image-2x: url("${getIcon(32)}");
+ `);
+
+ // Update the sidebar if this extension is the current sidebar.
+ if (SidebarUI.currentID === this.id) {
+ SidebarUI.title = title;
+ if (SidebarUI.isOpen && urlChanged) {
+ SidebarUI.show(this.id);
+ }
+ }
+ }
+
+ /**
+ * Update the broadcaster and menuitem for a given window.
+ *
+ * @param {ChromeWindow} window
+ * Browser chrome window.
+ */
+ updateWindow(window) {
+ let nativeTab = window.gBrowser.selectedTab;
+ this.updateButton(window, this.tabContext.get(nativeTab));
+ }
+
+ /**
+ * Update the broadcaster and menuitem when the extension changes the icon,
+ * title, url, etc. If it only changes a parameter for a single
+ * tab, `tab` will be that tab. Otherwise it will be null.
+ *
+ * @param {XULElement|null} nativeTab
+ * Browser tab, may be null.
+ */
+ updateOnChange(nativeTab) {
+ if (nativeTab) {
+ if (nativeTab.selected) {
+ this.updateWindow(nativeTab.ownerGlobal);
+ }
+ } else {
+ for (let window of windowTracker.browserWindows()) {
+ this.updateWindow(window);
+ }
+ }
+ }
+
+ /**
+ * Set a default or tab specific property.
+ *
+ * @param {XULElement|null} nativeTab
+ * Webextension tab object, may be null.
+ * @param {string} prop
+ * String property to retrieve ["icon", "title", or "panel"].
+ * @param {string} value
+ * Value for property.
+ */
+ setProperty(nativeTab, prop, value) {
+ if (nativeTab === null) {
+ this.defaults[prop] = value;
+ } else if (value !== null) {
+ this.tabContext.get(nativeTab)[prop] = value;
+ } else {
+ delete this.tabContext.get(nativeTab)[prop];
+ }
+
+ this.updateOnChange(nativeTab);
+ }
+
+ /**
+ * Retrieve a property from the tab or defaults if tab is null.
+ *
+ * @param {XULElement|null} nativeTab
+ * Browser tab object, may be null.
+ * @param {string} prop
+ * String property to retrieve ["icon", "title", or "panel"]
+ * @returns {string} value
+ * Value for prop.
+ */
+ getProperty(nativeTab, prop) {
+ if (nativeTab === null) {
+ return this.defaults[prop];
+ }
+ return this.tabContext.get(nativeTab)[prop];
+ }
+
+ shutdown() {
+ this.tabContext.shutdown();
+ for (let window of windowTracker.browserWindows()) {
+ let {document, SidebarUI} = window;
+ if (SidebarUI.currentID === this.id) {
+ SidebarUI.hide();
+ }
+ let menu = document.getElementById(this.menuId);
+ if (menu) {
+ menu.remove();
+ }
+ let broadcaster = document.getElementById(this.id);
+ if (broadcaster) {
+ broadcaster.remove();
+ }
+ }
+ windowTracker.removeOpenListener(this.windowOpenListener);
+ }
+}
+
+SidebarAction.for = (extension) => {
+ return sidebarActionMap.get(extension);
+};
+
+global.sidebarActionFor = SidebarAction.for;
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_sidebar_action", (type, directive, extension, manifest) => {
+ let sidebarAction = new SidebarAction(manifest.sidebar_action, extension);
+ sidebarActionMap.set(extension, sidebarAction);
+});
+
+extensions.on("ready", (type, extension) => {
+ // We build sidebars during ready to ensure the background scripts are ready.
+ if (sidebarActionMap.has(extension)) {
+ sidebarActionMap.get(extension).build();
+ }
+});
+
+extensions.on("shutdown", (type, extension) => {
+ if (sidebarActionMap.has(extension)) {
+ // Don't remove everything on app shutdown so session restore can handle
+ // restoring open sidebars.
+ if (extension.shutdownReason !== "APP_SHUTDOWN") {
+ sidebarActionMap.get(extension).shutdown();
+ }
+ sidebarActionMap.delete(extension);
+ }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("sidebarAction", "addon_parent", context => {
+ let {extension} = context;
+
+ function getTab(tabId) {
+ if (tabId !== null) {
+ return tabTracker.getTab(tabId);
+ }
+ return null;
+ }
+
+ return {
+ sidebarAction: {
+ async setTitle(details) {
+ let nativeTab = getTab(details.tabId);
+
+ let title = details.title;
+ // Clear the tab-specific title when given a null string.
+ if (nativeTab && title === "") {
+ title = null;
+ }
+ SidebarAction.for(extension).setProperty(nativeTab, "title", title);
+ },
+
+ getTitle(details) {
+ let nativeTab = getTab(details.tabId);
+
+ let title = SidebarAction.for(extension).getProperty(nativeTab, "title");
+ return Promise.resolve(title);
+ },
+
+ async setIcon(details) {
+ let nativeTab = getTab(details.tabId);
+
+ let icon = IconDetails.normalize(details, extension, context);
+ SidebarAction.for(extension).setProperty(nativeTab, "icon", icon);
+ },
+
+ async setPanel(details) {
+ let nativeTab = getTab(details.tabId);
+
+ let url;
+ // Clear the tab-specific url when given a null string.
+ if (nativeTab && details.panel === "") {
+ url = null;
+ } else if (details.panel !== "") {
+ url = context.uri.resolve(details.panel);
+ } else {
+ throw new ExtensionError("Invalid url for sidebar panel.");
+ }
+
+ SidebarAction.for(extension).setProperty(nativeTab, "panel", url);
+ },
+
+ getPanel(details) {
+ let nativeTab = getTab(details.tabId);
+
+ let panel = SidebarAction.for(extension).getProperty(nativeTab, "panel");
+ return Promise.resolve(panel);
+ },
+ },
+ };
+});
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -6,16 +6,17 @@ category webextension-scripts commands c
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 devtools chrome://browser/content/ext-devtools.js
category webextension-scripts devtools-inspectedWindow chrome://browser/content/ext-devtools-inspectedWindow.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 sidebarAction chrome://browser/content/ext-sidebarAction.js
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
category webextension-scripts theme chrome://browser/content/ext-theme.js
category webextension-scripts url-overrides chrome://browser/content/ext-url-overrides.js
category webextension-scripts utils chrome://browser/content/ext-utils.js
category webextension-scripts windows chrome://browser/content/ext-windows.js
# scripts specific for devtools extension contexts.
category webextension-scripts-devtools devtools-inspectedWindow chrome://browser/content/ext-c-devtools-inspectedWindow.js
@@ -33,12 +34,13 @@ category webextension-schemas commands c
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 devtools chrome://browser/content/schemas/devtools.json
category webextension-schemas devtools_inspected_window chrome://browser/content/schemas/devtools_inspected_window.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 sidebar_action chrome://browser/content/schemas/sidebar_action.json
category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
category webextension-schemas theme chrome://browser/content/schemas/theme.json
category webextension-schemas url_overrides chrome://browser/content/schemas/url_overrides.json
category webextension-schemas windows chrome://browser/content/schemas/windows.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -19,16 +19,17 @@ browser.jar:
content/browser/ext-contextMenus.js
content/browser/ext-desktop-runtime.js
content/browser/ext-devtools.js
content/browser/ext-devtools-inspectedWindow.js
content/browser/ext-history.js
content/browser/ext-omnibox.js
content/browser/ext-pageAction.js
content/browser/ext-sessions.js
+ content/browser/ext-sidebarAction.js
content/browser/ext-tabs.js
content/browser/ext-theme.js
content/browser/ext-url-overrides.js
content/browser/ext-utils.js
content/browser/ext-windows.js
content/browser/ext-c-contextMenus.js
content/browser/ext-c-devtools-inspectedWindow.js
content/browser/ext-c-omnibox.js
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -10,12 +10,13 @@ browser.jar:
content/browser/schemas/context_menus.json
content/browser/schemas/context_menus_internal.json
content/browser/schemas/devtools.json
content/browser/schemas/devtools_inspected_window.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/sidebar_action.json
content/browser/schemas/tabs.json
content/browser/schemas/theme.json
content/browser/schemas/url_overrides.json
content/browser/schemas/windows.json
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/sidebar_action.json
@@ -0,0 +1,183 @@
+/* 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/. */
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "WebExtensionManifest",
+ "properties": {
+ "sidebar_action": {
+ "type": "object",
+ "additionalProperties": { "$ref": "UnrecognizedProperty" },
+ "properties": {
+ "default_title": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+ "default_icon": {
+ "$ref": "IconPath",
+ "optional": true
+ },
+ "default_panel": {
+ "type": "string",
+ "format": "strictRelativeUrl",
+ "preprocess": "localize"
+ }
+ },
+ "optional": true
+ }
+ }
+ }
+ ]
+ },
+ {
+ "namespace": "sidebarAction",
+ "description": "Use sidebar actions to add a sidebar to Firefox.",
+ "permissions": ["manifest:sidebar_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": "setTitle",
+ "type": "function",
+ "description": "Sets the title of the sidebar action. This shows up in the tooltip.",
+ "async": true,
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "The string the sidebar action should display when moused over."
+ },
+ "tabId": {
+ "type": "integer",
+ "optional": true,
+ "description": "Sets the sidebar title for the tab specified by tabId. Automatically resets when the tab is closed."
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "getTitle",
+ "type": "function",
+ "description": "Gets the title of the sidebar action.",
+ "async": true,
+ "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."
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "setIcon",
+ "type": "function",
+ "description": "Sets the icon for the sidebar 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 <strong>path</strong> or the <strong>imageData</strong> property must be specified.",
+ "async": true,
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "imageData": {
+ "choices": [
+ { "$ref": "ImageDataType" },
+ {
+ "type": "object",
+ "patternProperties": {
+ "^[1-9]\\d*$": { "$ref": "ImageDataType" }
+ },
+ "additionalProperties": false
+ }
+ ],
+ "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": "Sets the sidebar icon for the tab specified by tabId. Automatically resets when the tab is closed."
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "setPanel",
+ "type": "function",
+ "description": "Sets the url to the html document to be opened in the sidebar when the user clicks on the sidebar action's icon.",
+ "async": true,
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {
+ "type": "integer",
+ "optional": true,
+ "minimum": 0,
+ "description": "Sets the sidebar url for the tab specified by tabId. Automatically resets when the tab is closed."
+ },
+ "panel": {
+ "type": "string",
+ "description": "The url to the html file to show in a sidebar. If set to the empty string (''), no sidebar is shown."
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "getPanel",
+ "type": "function",
+ "description": "Gets the url to the html document set as the panel for this sidebar action.",
+ "async": true,
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {
+ "type": "integer",
+ "optional": true,
+ "description": "Specify the tab to get the sidebar from. If no tab is specified, the non-tab-specific sidebar is returned."
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+]
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -71,16 +71,18 @@ support-files =
[browser_ext_popup_shutdown.js]
[browser_ext_runtime_openOptionsPage.js]
[browser_ext_runtime_openOptionsPage_uninstall.js]
[browser_ext_runtime_setUninstallURL.js]
[browser_ext_sessions_getRecentlyClosed.js]
[browser_ext_sessions_getRecentlyClosed_private.js]
[browser_ext_sessions_getRecentlyClosed_tabs.js]
[browser_ext_sessions_restore.js]
+[browser_ext_sidebarAction.js]
+[browser_ext_sidebarAction_context.js]
[browser_ext_simple.js]
[browser_ext_tab_runtimeConnect.js]
[browser_ext_tabs_audio.js]
[browser_ext_tabs_captureVisibleTab.js]
[browser_ext_tabs_create.js]
[browser_ext_tabs_create_invalid_url.js]
[browser_ext_tabs_detectLanguage.js]
[browser_ext_tabs_duplicate.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
@@ -0,0 +1,122 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let extData = {
+ manifest: {
+ "sidebar_action": {
+ "default_panel": "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"/>
+ <script src="sidebar.js"></script>
+ </head>
+ <body>
+ A Test Sidebar
+ </body></html>
+ `,
+
+ "sidebar.js": function() {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+
+ background: function() {
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "set-panel") {
+ browser.sidebarAction.setPanel({panel: ""}).then(() => {
+ browser.test.notifyFail("empty panel settable");
+ }).catch(() => {
+ browser.test.notifyPass("unable to set empty panel");
+ });
+ }
+ });
+ },
+};
+
+add_task(function* sidebar_initial_install() {
+ ok(document.getElementById("sidebar-box").hidden, "sidebar box is not visible");
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ yield extension.startup();
+ // Test sidebar is opened on install
+ yield extension.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+ // Test toolbar button is available
+ ok(document.getElementById("sidebar-button"), "sidebar button is in UI");
+
+ yield extension.unload();
+ // Test that the sidebar was closed on unload.
+ ok(document.getElementById("sidebar-box").hidden, "sidebar box is not visible");
+
+ // Move toolbar button back to customization.
+ CustomizableUI.removeWidgetFromArea("sidebar-button", CustomizableUI.AREA_NAVBAR);
+ ok(!document.getElementById("sidebar-button"), "sidebar button is not in UI");
+});
+
+
+add_task(function* sidebar_two_sidebar_addons() {
+ let extension2 = ExtensionTestUtils.loadExtension(extData);
+ yield extension2.startup();
+ // Test sidebar is opened on install
+ yield extension2.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+ // Test toolbar button is NOT available after first install
+ ok(!document.getElementById("sidebar-button"), "sidebar button is not in UI");
+
+ // Test second sidebar install opens new sidebar
+ let extension3 = ExtensionTestUtils.loadExtension(extData);
+ yield extension3.startup();
+ // Test sidebar is opened on install
+ yield extension3.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+ yield extension3.unload();
+
+ // We just close the sidebar on uninstall of the current sidebar.
+ ok(document.getElementById("sidebar-box").hidden, "sidebar box is not visible");
+
+ yield extension2.unload();
+});
+
+add_task(function* sidebar_windows() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ yield extension.startup();
+ // Test sidebar is opened on install
+ yield extension.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible in first window");
+
+ let secondSidebar = extension.awaitMessage("sidebar");
+
+ // SidebarUI relies on window.opener being set, which is normal behavior when
+ // using menu or key commands to open a new browser window.
+ let win = yield BrowserTestUtils.openNewBrowserWindow({opener: window});
+
+ yield secondSidebar;
+ ok(!win.document.getElementById("sidebar-box").hidden, "sidebar box is visible in second window");
+
+ yield extension.unload();
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+add_task(function* sidebar_empty_panel() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ yield extension.startup();
+ // Test sidebar is opened on install
+ yield extension.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible in first window");
+ extension.sendMessage("set-panel");
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(function* cleanup() {
+ // This is set on initial sidebar install.
+ Services.prefs.clearUserPref("extensions.sidebar-button.shown");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -0,0 +1,381 @@
+/* -*- 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;
+Cu.import("resource://gre/modules/Services.jsm");
+
+SpecialPowers.pushPrefEnv({
+ // Ignore toolbarbutton stuff, other test covers it.
+ set: [["extensions.sidebar-button.shown", true]],
+});
+
+function* runTests(options) {
+ async function background(getTests) {
+ async function checkDetails(expecting, tabId) {
+ let title = await browser.sidebarAction.getTitle({tabId});
+ browser.test.assertEq(expecting.title, title,
+ "expected value from getTitle");
+
+ let panel = await browser.sidebarAction.getPanel({tabId});
+ browser.test.assertEq(expecting.panel, panel,
+ "expected value from getPanel");
+ }
+
+ let expectDefaults = expecting => {
+ return checkDetails(expecting);
+ };
+
+ let tabs = [];
+ let tests = getTests(tabs, expectDefaults);
+
+ {
+ let tabId = 0xdeadbeef;
+ let calls = [
+ () => browser.sidebarAction.setTitle({tabId, title: "foo"}),
+ () => browser.sidebarAction.setIcon({tabId, path: "foo.png"}),
+ () => browser.sidebarAction.setPanel({tabId, panel: "foo.html"}),
+ ];
+
+ for (let call of calls) {
+ await browser.test.assertRejects(
+ new Promise(resolve => resolve(call())),
+ RegExp(`Invalid tab ID: ${tabId}`),
+ "Expected invalid tab ID error");
+ }
+ }
+
+ // Runs the next test in the `tests` array, checks the results,
+ // and passes control back to the outer test scope.
+ function nextTest() {
+ let test = tests.shift();
+
+ test(async expecting => {
+ // Check that the API returns the expected values, and then
+ // run the next test.
+ let tabs = await browser.tabs.query({active: true, currentWindow: true});
+ await checkDetails(expecting, tabs[0].id);
+
+ // Check that the actual icon has the expected values, then
+ // run the next test.
+ browser.test.sendMessage("nextTest", expecting, tests.length);
+ });
+ }
+
+ browser.test.onMessage.addListener((msg) => {
+ if (msg != "runNextTest") {
+ browser.test.fail("Expecting 'runNextTest' message");
+ }
+
+ nextTest();
+ });
+
+ browser.tabs.query({active: true, currentWindow: true}, resultTabs => {
+ tabs[0] = resultTabs[0].id;
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: options.manifest,
+ useAddonManager: "temporary",
+
+ files: options.files || {},
+
+ background: `(${background})(${options.getTests})`,
+ });
+
+ let sidebarActionId;
+ function checkDetails(details) {
+ if (!sidebarActionId) {
+ sidebarActionId = `${makeWidgetId(extension.id)}-sidebar-action`;
+ }
+
+ let command = document.getElementById(sidebarActionId);
+ ok(command, "command exists");
+
+ let menuId = `menu_${sidebarActionId}`;
+ let menu = document.getElementById(menuId);
+ ok(menu, "menu exists");
+
+ let title = details.title || options.manifest.name;
+
+ is(getListStyleImage(menu), details.icon, "icon URL is correct");
+ is(menu.getAttribute("label"), title, "image label is correct");
+ }
+
+ let awaitFinish = new Promise(resolve => {
+ extension.onMessage("nextTest", (expecting, testsRemaining) => {
+ checkDetails(expecting);
+
+ if (testsRemaining) {
+ extension.sendMessage("runNextTest");
+ } else {
+ resolve();
+ }
+ });
+ });
+
+ // Wait for initial sidebar load to start tests.
+ SidebarUI.browser.addEventListener("load", event => {
+ extension.sendMessage("runNextTest");
+ }, {capture: true, once: true});
+
+ yield extension.startup();
+ yield awaitFinish;
+ yield extension.unload();
+}
+
+let sidebar = `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"/></head>
+ <body>
+ A Test Sidebar
+ </body></html>
+`;
+
+add_task(function* testTabSwitchContext() {
+ yield runTests({
+ manifest: {
+ "sidebar_action": {
+ "default_icon": "default.png",
+ "default_panel": "__MSG_panel__",
+ "default_title": "Default __MSG_title__",
+ },
+
+ "default_locale": "en",
+
+ "permissions": ["tabs"],
+ },
+
+ "files": {
+ "default.html": sidebar,
+ "default-2.html": sidebar,
+ "2.html": sidebar,
+
+ "_locales/en/messages.json": {
+ "panel": {
+ "message": "default.html",
+ "description": "Panel",
+ },
+
+ "title": {
+ "message": "Title",
+ "description": "Title",
+ },
+ },
+
+ "default.png": imageBuffer,
+ "default-2.png": imageBuffer,
+ "1.png": imageBuffer,
+ "2.png": imageBuffer,
+ },
+
+ getTests(tabs, expectDefaults) {
+ let details = [
+ {"icon": browser.runtime.getURL("default.png"),
+ "panel": browser.runtime.getURL("default.html"),
+ "title": "Default Title",
+ },
+ {"icon": browser.runtime.getURL("1.png"),
+ "panel": browser.runtime.getURL("default.html"),
+ "title": "Default Title",
+ },
+ {"icon": browser.runtime.getURL("2.png"),
+ "panel": browser.runtime.getURL("2.html"),
+ "title": "Title 2",
+ },
+ {"icon": browser.runtime.getURL("1.png"),
+ "panel": browser.runtime.getURL("default-2.html"),
+ "title": "Default Title 2",
+ },
+ {"icon": browser.runtime.getURL("1.png"),
+ "panel": browser.runtime.getURL("default-2.html"),
+ "title": "Default Title 2",
+ },
+ {"icon": browser.runtime.getURL("default-2.png"),
+ "panel": browser.runtime.getURL("default-2.html"),
+ "title": "Default Title 2",
+ },
+ {"icon": browser.runtime.getURL("1.png"),
+ "panel": browser.runtime.getURL("2.html"),
+ "title": "Default Title 2",
+ },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+
+ await expectDefaults(details[0]);
+ expect(details[0]);
+ },
+ async expect => {
+ browser.test.log("Change the icon in the current tab. Expect default properties excluding the icon.");
+ await browser.sidebarAction.setIcon({tabId: tabs[0], path: "1.png"});
+
+ await expectDefaults(details[0]);
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. Expect default properties.");
+ let tab = await browser.tabs.create({active: true, url: "about:blank?0"});
+ tabs.push(tab.id);
+
+ await expectDefaults(details[0]);
+ expect(details[0]);
+ },
+ async expect => {
+ browser.test.log("Change properties. Expect new properties.");
+ let tabId = tabs[1];
+ await Promise.all([
+ browser.sidebarAction.setIcon({tabId, path: "2.png"}),
+ browser.sidebarAction.setPanel({tabId, panel: "2.html"}),
+ browser.sidebarAction.setTitle({tabId, title: "Title 2"}),
+ ]);
+ await expectDefaults(details[0]);
+ expect(details[2]);
+ },
+ expect => {
+ browser.test.log("Navigate to a new page. Expect no changes.");
+
+ // TODO: This listener should not be necessary, but the |tabs.update|
+ // callback currently fires too early in e10s windows.
+ browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
+ if (tabId == tabs[1] && changed.url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ expect(details[2]);
+ }
+ });
+
+ browser.tabs.update(tabs[1], {url: "about:blank?1"});
+ },
+ async expect => {
+ browser.test.log("Switch back to the first tab. Expect previously set properties.");
+ await browser.tabs.update(tabs[0], {active: true});
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Change default values, expect those changes reflected.");
+ await Promise.all([
+ browser.sidebarAction.setIcon({path: "default-2.png"}),
+ browser.sidebarAction.setPanel({panel: "default-2.html"}),
+ browser.sidebarAction.setTitle({title: "Default Title 2"}),
+ ]);
+
+ await expectDefaults(details[3]);
+ expect(details[3]);
+ },
+ async expect => {
+ browser.test.log("Switch back to tab 2. Expect former value, unaffected by changes to defaults in previous step.");
+ await browser.tabs.update(tabs[1], {active: true});
+
+ await expectDefaults(details[3]);
+ expect(details[2]);
+ },
+ async expect => {
+ browser.test.log("Delete tab, switch back to tab 1. Expect previous results again.");
+ await browser.tabs.remove(tabs[1]);
+ expect(details[4]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. Expect new default properties.");
+ let tab = await browser.tabs.create({active: true, url: "about:blank?2"});
+ tabs.push(tab.id);
+ expect(details[5]);
+ },
+ async expect => {
+ browser.test.log("Delete tab.");
+ await browser.tabs.remove(tabs[2]);
+ expect(details[4]);
+ },
+ async expect => {
+ browser.test.log("Change tab panel.");
+ let tabId = tabs[0];
+ await browser.sidebarAction.setPanel({tabId, panel: "2.html"});
+ expect(details[6]);
+ },
+ async expect => {
+ browser.test.log("Revert tab panel.");
+ let tabId = tabs[0];
+ await browser.sidebarAction.setPanel({tabId, panel: ""});
+ expect(details[4]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(function* testDefaultTitle() {
+ yield runTests({
+ manifest: {
+ "name": "Foo Extension",
+
+ "sidebar_action": {
+ "default_icon": "icon.png",
+ "default_panel": "sidebar.html",
+ },
+
+ "permissions": ["tabs"],
+ },
+
+ files: {
+ "sidebar.html": sidebar,
+ "icon.png": imageBuffer,
+ },
+
+ getTests(tabs, expectDefaults) {
+ let details = [
+ {"title": "Foo Extension",
+ "panel": browser.runtime.getURL("sidebar.html"),
+ "icon": browser.runtime.getURL("icon.png")},
+ {"title": "Foo Title",
+ "panel": browser.runtime.getURL("sidebar.html"),
+ "icon": browser.runtime.getURL("icon.png")},
+ {"title": "Bar Title",
+ "panel": browser.runtime.getURL("sidebar.html"),
+ "icon": browser.runtime.getURL("icon.png")},
+ {"title": "",
+ "panel": browser.runtime.getURL("sidebar.html"),
+ "icon": browser.runtime.getURL("icon.png")},
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state. Expect extension title as default title.");
+
+ await expectDefaults(details[0]);
+ expect(details[0]);
+ },
+ async expect => {
+ browser.test.log("Change the title. Expect new title.");
+ browser.sidebarAction.setTitle({tabId: tabs[0], title: "Foo Title"});
+
+ await expectDefaults(details[0]);
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Change the default. Expect same properties.");
+ browser.sidebarAction.setTitle({title: "Bar Title"});
+
+ await expectDefaults(details[2]);
+ expect(details[1]);
+ },
+ async expect => {
+ browser.test.log("Clear the title. Expect new default title.");
+ browser.sidebarAction.setTitle({tabId: tabs[0], title: ""});
+
+ await expectDefaults(details[2]);
+ expect(details[2]);
+ },
+ async expect => {
+ browser.test.log("Set default title to null string. Expect null string from API, extension title in UI.");
+ browser.sidebarAction.setTitle({title: ""});
+
+ await expectDefaults(details[3]);
+ expect(details[3]);
+ },
+ ];
+ },
+ });
+});
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js
@@ -5,42 +5,19 @@
// This test makes sure that Sidebars do not migrate across windows with
// different privacy states
// See Bug 885054: https://bugzilla.mozilla.org/show_bug.cgi?id=885054
function test() {
waitForExplicitFinish();
- let { utils: Cu } = Components;
-
- let { Promise: { defer } } = Cu.import("resource://gre/modules/Promise.jsm", {});
-
// opens a sidebar
function openSidebar(win) {
- let { promise, resolve } = defer();
- let doc = win.document;
-
- let sidebarID = 'viewBookmarksSidebar';
-
- let sidebar = doc.getElementById('sidebar');
-
- let sidebarurl = doc.getElementById(sidebarID).getAttribute('sidebarurl');
-
- sidebar.addEventListener('load', function onSidebarLoad() {
- if (sidebar.contentWindow.location.href != sidebarurl)
- return;
- sidebar.removeEventListener('load', onSidebarLoad, true);
-
- resolve(win);
- }, true);
-
- win.SidebarUI.show(sidebarID);
-
- return promise;
+ return win.SidebarUI.show("viewBookmarksSidebar").then(() => win);
}
let windowCache = [];
function cacheWindow(w) {
windowCache.push(w);
return w;
}
function closeCachedWindows () {
--- a/browser/components/privatebrowsing/test/browser/head.js
+++ b/browser/components/privatebrowsing/test/browser/head.js
@@ -11,27 +11,20 @@ function whenNewWindowLoaded(aOptions, a
let startupFinished = TestUtils.topicObserved("browser-delayed-startup-finished",
subject => subject == win).then(() => win);
Promise.all([focused, startupFinished])
.then(results => executeSoon(() => aCallback(results[1])));
return win;
}
-function openWindow(aParent, aOptions, a3) {
- let { Promise: { defer } } = Components.utils.import("resource://gre/modules/Promise.jsm", {});
- let { promise, resolve } = defer();
-
+function openWindow(aParent, aOptions) {
let win = aParent.OpenBrowserWindow(aOptions);
-
- win.addEventListener("load", function() {
- resolve(win);
- }, {once: true});
-
- return promise;
+ return TestUtils.topicObserved("browser-delayed-startup-finished",
+ subject => subject == win).then(() => win);
}
function newDirectory() {
let FileUtils =
Cu.import("resource://gre/modules/FileUtils.jsm", {}).FileUtils;
let tmpDir = FileUtils.getDir("TmpD", [], true);
let dir = tmpDir.clone();
dir.append("testdir");
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -471,16 +471,21 @@ this.BrowserTestUtils = {
* @return {Promise}
* Resolves with the new window once it is loaded.
*/
openNewBrowserWindow: Task.async(function*(options={}) {
let argString = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
argString.data = "";
let features = "chrome,dialog=no,all";
+ let opener = null;
+
+ if (options.opener) {
+ opener = options.opener;
+ }
if (options.private) {
features += ",private";
}
if (options.width) {
features += ",width=" + options.width;
}
@@ -489,17 +494,17 @@ this.BrowserTestUtils = {
}
if (options.hasOwnProperty("remote")) {
let remoteState = options.remote ? "remote" : "non-remote";
features += `,${remoteState}`;
}
let win = Services.ww.openWindow(
- null, Services.prefs.getCharPref("browser.chromeURL"), "_blank",
+ opener, Services.prefs.getCharPref("browser.chromeURL"), "_blank",
features, argString);
// Wait for browser-delayed-startup-finished notification, it indicates
// that the window has loaded completely and is ready to be used for
// testing.
let startupPromise =
TestUtils.topicObserved("browser-delayed-startup-finished",
subject => subject == win).then(() => win);
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -920,17 +920,18 @@ this.Extension = class extends Extension
// 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() {
+ shutdown(reason) {
+ this.shutdownReason = reason;
this.hasShutdown = true;
Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
if (!this.manifest) {
ExtensionManagement.shutdownExtension(this.uuid);
this.cleanupGeneratedFile();
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -760,17 +760,17 @@ class ExtensionBaseContextChild extends
* This ExtensionBaseContextChild represents an addon execution environment
* that is running in an addon or devtools child process.
*
* @param {BrowserExtensionContent} extension This context's owner.
* @param {object} params
* @param {string} params.envType One of "addon_child" or "devtools_child".
* @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
* @param {string} params.viewType One of "background", "popup", "tab",
- * "devtools_page" or "devtools_panel".
+ * "sidebar", "devtools_page" or "devtools_panel".
* @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
*/
constructor(extension, params) {
if (!params.envType) {
throw new Error("Missing envType");
}
super(params.envType, extension);
@@ -812,17 +812,17 @@ class ExtensionBaseContextChild extends
return this.contentWindow;
}
get principal() {
return this.contentWindow.document.nodePrincipal;
}
get windowId() {
- if (this.viewType == "tab" || this.viewType == "popup") {
+ if (["tab", "popup", "sidebar"].includes(this.viewType)) {
let globalView = ExtensionChild.contentGlobals.get(this.messageManager);
return globalView ? globalView.windowId : -1;
}
}
// Called when the extension shuts down.
shutdown() {
this.unload();
@@ -864,18 +864,18 @@ class ExtensionPageContextChild extends
* APIs (provided that the correct permissions have been requested).
*
* This is the child side of the ExtensionPageContextParent class
* defined in ExtensionParent.jsm.
*
* @param {BrowserExtensionContent} extension This context's owner.
* @param {object} params
* @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
- * @param {string} params.viewType One of "background", "popup" or "tab".
- * "background" and "tab" are used by `browser.extension.getViews`.
+ * @param {string} params.viewType One of "background", "popup", "sidebar" or "tab".
+ * "background", "sidebar" and "tab" are used by `browser.extension.getViews`.
* "popup" is only used internally to identify page action and browser
* action popups and options_ui pages.
* @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
*/
constructor(extension, params) {
super(extension, Object.assign(params, {envType: "addon_child"}));
this.extension.views.add(this);
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -312,16 +312,23 @@ let IconDetails = {
ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
resolve(canvas.toDataURL("image/png"));
};
image.onerror = reject;
image.src = imageURL;
});
},
+
+ // 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.
+ escapeUrl(url) {
+ return url.replace(/[\\\s"]/g, encodeURIComponent);
+ },
};
const LISTENERS = Symbol("listeners");
class EventEmitter {
constructor() {
this[LISTENERS] = new Map();
}
--- a/toolkit/components/extensions/schemas/extension.json
+++ b/toolkit/components/extensions/schemas/extension.json
@@ -26,17 +26,17 @@
"allowedContexts": ["content", "devtools"],
"description": "True for content scripts running inside incognito tabs, and for extension pages running inside an incognito process. The latter only applies to extensions with 'split' incognito_behavior."
}
},
"types": [
{
"id": "ViewType",
"type": "string",
- "enum": ["tab", "notification", "popup"],
+ "enum": ["tab", "notification", "popup", "sidebar"],
"description": "The type of extension view."
}
],
"functions": [
{
"name": "getURL",
"type": "function",
"allowedContexts": ["content", "devtools"],
--- a/toolkit/mozapps/extensions/internal/WebExtensionBootstrap.js
+++ b/toolkit/mozapps/extensions/internal/WebExtensionBootstrap.js
@@ -25,13 +25,13 @@ function install(data, reason) {
}
function startup(data, reason) {
extension = new Extension(data, BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
extension.startup();
}
function shutdown(data, reason) {
- extension.shutdown();
+ extension.shutdown(BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
}
function uninstall(data, reason) {
}