--- a/browser/components/extensions/.eslintrc
+++ b/browser/components/extensions/.eslintrc
@@ -1,18 +1,18 @@
{
"extends": "../../../toolkit/components/extensions/.eslintrc",
"globals": {
"AllWindowEvents": true,
"currentWindow": true,
"EventEmitter": true,
- "IconDetails": true,
"makeWidgetId": true,
"pageActionFor": true,
+ "IconDetails": true,
"PanelPopup": true,
"TabContext": true,
"ViewPopup": true,
"WindowEventManager": true,
"WindowListManager": true,
"WindowManager": true,
},
}
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -5,16 +5,17 @@
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
Cu.import("resource://devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
+ IconDetails,
} = ExtensionUtils;
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// WeakMap[Extension -> BrowserAction]
var browserActionMap = new WeakMap();
// Responsible for the browser_action section of the manifest as well
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -1,16 +1,17 @@
/* -*- 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) {
@@ -218,21 +219,23 @@ extensions.registerSchemaAPI("pageAction
return () => {
pageAction.off("click", listener);
};
}).api(),
show(tabId) {
let tab = TabManager.getTab(tabId);
PageAction.for(extension).setProperty(tab, "show", true);
+ return Promise.resolve();
},
hide(tabId) {
let tab = TabManager.getTab(tabId);
PageAction.for(extension).setProperty(tab, "show", false);
+ return Promise.resolve();
},
setTitle(details) {
let tab = TabManager.getTab(details.tabId);
// Clear the tab-specific title when given a null string.
PageAction.for(extension).setProperty(tab, "title", details.title || null);
},
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -3,148 +3,31 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const INTEGER = /^[1-9]\d*$/;
+
// Minimum time between two resizes.
const RESIZE_TIMEOUT = 100;
var {
EventManager,
- instanceOf,
} = ExtensionUtils;
// This file provides some useful code for the |tabs| and |windows|
// modules. All of the code is installed on |global|, which is a scope
// shared among the different ext-*.js scripts.
-
-// Manages icon details for toolbar buttons in the |pageAction| and
-// |browserAction| APIs.
-global.IconDetails = {
- // Normalizes the various acceptable input formats into an object
- // with icon size as key and icon URL as value.
- //
- // If a context is specified (function is called from an extension):
- // Throws an error if an invalid icon size was provided or the
- // extension is not allowed to load the specified resources.
- //
- // If no context is specified, instead of throwing an error, this
- // function simply logs a warning message.
- normalize(details, extension, context = null) {
- let result = {};
-
- try {
- if (details.imageData) {
- let imageData = details.imageData;
-
- // The global might actually be from Schema.jsm, which
- // normalizes most of our arguments. In that case it won't have
- // an ImageData property. But Schema.jsm doesn't normalize
- // actual ImageData objects, so they will come from a global
- // with the right property.
- if (instanceOf(imageData, "ImageData")) {
- imageData = {"19": imageData};
- }
-
- for (let size of Object.keys(imageData)) {
- if (!INTEGER.test(size)) {
- throw new Error(`Invalid icon size ${size}, must be an integer`);
- }
- result[size] = this.convertImageDataToPNG(imageData[size], context);
- }
- }
-
- if (details.path) {
- let path = details.path;
- if (typeof path != "object") {
- path = {"19": path};
- }
-
- let baseURI = context ? context.uri : extension.baseURI;
-
- for (let size of Object.keys(path)) {
- if (!INTEGER.test(size)) {
- throw new Error(`Invalid icon size ${size}, must be an integer`);
- }
-
- let url = baseURI.resolve(path[size]);
-
- // The Chrome documentation specifies these parameters as
- // relative paths. We currently accept absolute URLs as well,
- // which means we need to check that the extension is allowed
- // to load them. This will throw an error if it's not allowed.
- Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
- extension.principal, url,
- Services.scriptSecurityManager.DISALLOW_SCRIPT);
-
- result[size] = url;
- }
- }
- } catch (e) {
- // Function is called from extension code, delegate error.
- if (context) {
- throw e;
- }
- // If there's no context, it's because we're handling this
- // as a manifest directive. Log a warning rather than
- // raising an error.
- extension.manifestError(`Invalid icon data: ${e}`);
- }
-
- return result;
- },
-
- // Returns the appropriate icon URL for the given icons object and the
- // screen resolution of the given window.
- getURL(icons, window, extension, size = 16) {
- const DEFAULT = "chrome://browser/content/extension.svg";
-
- size *= window.devicePixelRatio;
-
- let bestSize = null;
- if (icons[size]) {
- bestSize = size;
- } else if (icons[2 * size]) {
- bestSize = 2 * size;
- } else {
- let sizes = Object.keys(icons)
- .map(key => parseInt(key, 10))
- .sort((a, b) => a - b);
-
- bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
- }
-
- if (bestSize) {
- return {size: bestSize, icon: icons[bestSize]};
- }
-
- return {size, icon: DEFAULT};
- },
-
- convertImageDataToPNG(imageData, context) {
- let document = context.contentWindow.document;
- let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
- canvas.width = imageData.width;
- canvas.height = imageData.height;
- canvas.getContext("2d").putImageData(imageData, 0, 0);
-
- return canvas.toDataURL("image/png");
- },
-};
-
global.makeWidgetId = id => {
id = id.toLowerCase();
// FIXME: This allows for collisions.
return id.replace(/[^a-z0-9_-]/g, "_");
};
function promisePopupShown(popup) {
return new Promise(resolve => {
--- a/browser/components/extensions/schemas/page_action.json
+++ b/browser/components/extensions/schemas/page_action.json
@@ -50,24 +50,26 @@
"additionalProperties": { "type": "any" },
"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": true,
"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."}
]
},
{
"name": "hide",
"type": "function",
+ "async": true,
"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."}
]
},
{
"name": "setTitle",
"type": "function",
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -196,19 +196,19 @@ add_task(function* testDetailsObjects()
}
// Sort by resolution, so we don't needlessly switch back and forth
// between each test.
tests.sort(test => test.resolution);
browser.tabs.query({active: true, currentWindow: true}, tabs => {
tabId = tabs[0].id;
- browser.pageAction.show(tabId);
-
- browser.test.sendMessage("ready", tests);
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("ready", tests);
+ });
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"browser_action": {},
"page_action": {},
"background": {
@@ -332,18 +332,19 @@ add_task(function* testDefaultDetails()
"browser_action": {"default_icon": icon},
"page_action": {"default_icon": icon},
},
background: function() {
browser.tabs.query({active: true, currentWindow: true}, tabs => {
let tabId = tabs[0].id;
- browser.pageAction.show(tabId);
- browser.test.sendMessage("ready");
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("ready");
+ });
});
}
});
yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
let pageActionId = makeWidgetId(extension.id) + "-page-action";
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -207,18 +207,19 @@ add_task(function* testTabSwitchContext(
};
return [
expect => {
browser.test.log("Initial state. No icon visible.");
expect(null);
},
expect => {
browser.test.log("Show the icon on the first tab, expect default properties.");
- browser.pageAction.show(tabs[0]);
- expect(details[0]);
+ browser.pageAction.show(tabs[0]).then(() => {
+ expect(details[0]);
+ });
},
expect => {
browser.test.log("Change the icon. Expect default properties excluding the icon.");
browser.pageAction.setIcon({tabId: tabs[0], path: "1.png"});
expect(details[1]);
},
expect => {
browser.test.log("Create a new tab. No icon visible.");
@@ -229,22 +230,23 @@ add_task(function* testTabSwitchContext(
},
expect => {
browser.test.log("Await tab load. No icon visible.");
expect(null);
},
expect => {
browser.test.log("Change properties. Expect new properties.");
let tabId = tabs[1];
- browser.pageAction.show(tabId);
- browser.pageAction.setIcon({tabId, path: "2.png"});
- browser.pageAction.setPopup({tabId, popup: "2.html"});
- browser.pageAction.setTitle({tabId, title: "Title 2"});
+ browser.pageAction.show(tabId).then(() => {
+ browser.pageAction.setIcon({tabId, path: "2.png"});
+ browser.pageAction.setPopup({tabId, popup: "2.html"});
+ browser.pageAction.setTitle({tabId, title: "Title 2"});
- expect(details[2]);
+ expect(details[2]);
+ });
},
expect => {
browser.test.log("Clear the title. Expect default title.");
browser.pageAction.setTitle({tabId: tabs[1], title: ""});
expect(details[3]);
},
expect => {
@@ -255,42 +257,45 @@ add_task(function* testTabSwitchContext(
promiseTabLoad({id: tabs[1], url: "about:blank?1"}).then(() => {
expect(null);
});
browser.tabs.update(tabs[1], {url: "about:blank?1"});
},
expect => {
browser.test.log("Show the icon. Expect default properties again.");
- browser.pageAction.show(tabs[1]);
- expect(details[0]);
+ browser.pageAction.show(tabs[1]).then(() => {
+ expect(details[0]);
+ });
},
expect => {
browser.test.log("Switch back to the first tab. Expect previously set properties.");
browser.tabs.update(tabs[0], {active: true}, () => {
expect(details[1]);
});
},
expect => {
browser.test.log("Hide the icon on tab 2. Switch back, expect hidden.");
- browser.pageAction.hide(tabs[1]);
- browser.tabs.update(tabs[1], {active: true}, () => {
- expect(null);
+ browser.pageAction.hide(tabs[1]).then(() => {
+ browser.tabs.update(tabs[1], {active: true}, () => {
+ expect(null);
+ });
});
},
expect => {
browser.test.log("Switch back to tab 1. Expect previous results again.");
browser.tabs.remove(tabs[1], () => {
expect(details[1]);
});
},
expect => {
browser.test.log("Hide the icon. Expect hidden.");
- browser.pageAction.hide(tabs[0]);
- expect(null);
+ browser.pageAction.hide(tabs[0]).then(() => {
+ expect(null);
+ });
},
];
},
});
});
add_task(function* testDefaultTitle() {
yield runTests({
@@ -316,18 +321,19 @@ add_task(function* testDefaultTitle() {
return [
expect => {
browser.test.log("Initial state. No icon visible.");
expect(null);
},
expect => {
browser.test.log("Show the icon on the first tab, expect extension title as default title.");
- browser.pageAction.show(tabs[0]);
- expect(details[0]);
+ browser.pageAction.show(tabs[0]).then(() => {
+ expect(details[0]);
+ });
},
expect => {
browser.test.log("Change the title. Expect new title.");
browser.pageAction.setTitle({tabId: tabs[0], title: "Foo Title"});
expect(details[1]);
},
expect => {
browser.test.log("Clear the title. Expect extension title.");
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -123,18 +123,19 @@ add_task(function* testPageActionPopup()
} else {
browser.test.notifyPass("pageaction-tests-done");
}
});
browser.tabs.query({active: true, currentWindow: true}, tabs => {
tabId = tabs[0].id;
- browser.pageAction.show(tabId);
- browser.test.sendMessage("next-test");
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("next-test");
+ });
});
},
},
});
let pageActionId = makeWidgetId(extension.id) + "-page-action";
let panelId = makeWidgetId(extension.id) + "-panel";
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js
@@ -9,18 +9,19 @@ add_task(function* testPageActionPopupRe
"default_popup": "popup.html",
"browser_style": true,
},
},
background: function() {
browser.tabs.query({active: true, currentWindow: true}, tabs => {
const tabId = tabs[0].id;
- browser.pageAction.show(tabId);
- browser.test.sendMessage("action-shown");
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("action-shown");
+ });
});
},
files: {
"popup.html": "<html><head><meta charset=\"utf-8\"></head></html>",
},
});
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js
@@ -27,18 +27,19 @@ add_task(function* () {
background: function() {
browser.runtime.onMessage.addListener(msg => {
browser.test.assertEq(msg, "from-popup", "correct message received");
browser.test.sendMessage("popup");
});
browser.tabs.query({active: true, currentWindow: true}, tabs => {
let tabId = tabs[0].id;
- browser.pageAction.show(tabId);
- browser.test.sendMessage("page-action-shown");
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
});
},
});
SimpleTest.waitForExplicitFinish();
let waitForConsole = new Promise(resolve => {
SimpleTest.monitorConsole(resolve, [{
message: /Reading manifest: Error processing page_action.unrecognized_property: An unexpected property was found/,
--- a/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js
+++ b/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js
@@ -24,18 +24,19 @@ add_task(function* testPageActionPopup()
<script type="application/javascript" src="popup-b.js"></script></head></html>`,
"popup-b.js": 'browser.test.sendMessage("from-popup-b");',
},
background: function() {
let tabId;
browser.tabs.query({active: true, currentWindow: true}, tabs => {
tabId = tabs[0].id;
- browser.pageAction.show(tabId);
- browser.test.sendMessage("ready");
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("ready");
+ });
});
browser.test.onMessage.addListener(() => {
browser.browserAction.setPopup({popup: "/popup-a.html"});
browser.pageAction.setPopup({tabId, popup: "popup-b.html"});
browser.test.sendMessage("ok");
});
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
@@ -90,18 +90,19 @@ add_task(function* testBadPermissions()
yield testHasNoPermission({
manifest: {
"permissions": ["http://example.com/", "activeTab"],
"page_action": {},
},
contentSetup() {
return new Promise(resolve => {
browser.tabs.query({active: true, currentWindow: true}, tabs => {
- browser.pageAction.show(tabs[0].id);
- resolve();
+ browser.pageAction.show(tabs[0].id).then(() => {
+ resolve();
+ });
});
});
},
});
yield BrowserTestUtils.removeTab(tab2);
yield BrowserTestUtils.removeTab(tab1);
});
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
@@ -94,18 +94,19 @@ add_task(function* testGoodPermissions()
yield testHasPermission({
manifest: {
"permissions": ["activeTab"],
"page_action": {},
},
contentSetup() {
return new Promise(resolve => {
browser.tabs.query({active: true, currentWindow: true}, tabs => {
- browser.pageAction.show(tabs[0].id);
- resolve();
+ browser.pageAction.show(tabs[0].id).then(() => {
+ resolve();
+ });
});
});
},
setup: clickPageAction,
tearDown: closePageAction,
});
info("Test activeTab permission with a browser action w/popup click");
@@ -122,18 +123,19 @@ add_task(function* testGoodPermissions()
yield testHasPermission({
manifest: {
"permissions": ["activeTab"],
"page_action": {"default_popup": "_blank.html"},
},
contentSetup() {
return new Promise(resolve => {
browser.tabs.query({active: true, currentWindow: true}, tabs => {
- browser.pageAction.show(tabs[0].id);
- resolve();
+ browser.pageAction.show(tabs[0].id).then(() => {
+ resolve();
+ });
});
});
},
setup: clickPageAction,
tearDown: closePageAction,
});
info("Test activeTab permission with a context menu click");
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -10,58 +10,82 @@ XPCOMUtils.defineLazyModuleGetter(this,
// Import the android PageActions module.
XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
"resource://gre/modules/PageActions.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
+ IconDetails,
SingletonEventManager,
} = ExtensionUtils;
// WeakMap[Extension -> PageAction]
var pageActionMap = new WeakMap();
function PageAction(options, extension) {
this.id = null;
- let DEFAULT_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+ this.extension = extension;
+ this.icons = IconDetails.normalize({path: options.default_icon}, extension);
this.popupUrl = options.default_popup;
this.options = {
title: options.default_title || extension.name,
- icon: DEFAULT_ICON,
id: extension.id,
clickCallback: () => {
if (this.popupUrl) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.BrowserApp.addTab(this.popupUrl, {
selected: true,
parentId: win.BrowserApp.selectedTab.id,
});
} else {
this.emit("click");
}
},
};
+ this.shouldShow = false;
+
EventEmitter.decorate(this);
}
PageAction.prototype = {
- show(tabId) {
- // TODO: Only show the PageAction for the tab with the provided tabId.
- if (!this.id) {
+ show(tabId, context) {
+ if (this.id) {
+ return Promise.resolve();
+ }
+
+ if (this.options.icon) {
this.id = PageActions.add(this.options);
+ return Promise.resolve();
}
+
+ this.shouldShow = true;
+
+ let {icon} = IconDetails.getURL(this.icons, context.contentWindow, this.extension, 18);
+
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ return IconDetails.convertImageURLToDataURL(icon, context, browserWindow).then(dataURI => {
+ if (this.shouldShow) {
+ this.options.icon = dataURI;
+ this.id = PageActions.add(this.options);
+ }
+ }).catch(() => {
+ return Promise.reject({
+ message: "Failed to load PageAction icon",
+ });
+ });
},
hide(tabId) {
+ this.shouldShow = false;
if (this.id) {
PageActions.remove(this.id);
this.id = null;
}
},
setPopup(tab, url) {
// TODO: Only set the popup for the specified tab once we have Tabs API support.
@@ -101,21 +125,24 @@ extensions.registerSchemaAPI("pageAction
};
pageActionMap.get(extension).on("click", listener);
return () => {
pageActionMap.get(extension).off("click", listener);
};
}).api(),
show(tabId) {
- pageActionMap.get(extension).show(tabId);
+ return pageActionMap.get(extension)
+ .show(tabId, context)
+ .then(() => {});
},
hide(tabId) {
pageActionMap.get(extension).hide(tabId);
+ return Promise.resolve();
},
setPopup(details) {
// TODO: Use the Tabs API to get the tab from details.tabId.
let tab = null;
let url = details.popup && context.uri.resolve(details.popup);
pageActionMap.get(extension).setPopup(tab, url);
},
--- a/mobile/android/components/extensions/schemas/page_action.json
+++ b/mobile/android/components/extensions/schemas/page_action.json
@@ -14,17 +14,16 @@
"additionalProperties": { "$ref": "UnrecognizedProperty" },
"properties": {
"default_title": {
"type": "string",
"optional": true,
"preprocess": "localize"
},
"default_icon": {
- "unsupported": true,
"$ref": "IconPath",
"optional": true
},
"default_popup": {
"type": "string",
"format": "relativeUrl",
"optional": true,
"preprocess": "localize"
@@ -52,24 +51,26 @@
"description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
}
],
"functions": [
{
"name": "show",
"type": "function",
"description": "Shows the page action. The page action is shown whenever the tab is selected.",
+ "async": true,
"parameters": [
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
]
},
{
"name": "hide",
"type": "function",
"description": "Hides the page action.",
+ "async": true,
"parameters": [
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
]
},
{
"name": "setTitle",
"unsupported": true,
"type": "function",
@@ -156,16 +157,17 @@
"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": {
@@ -175,37 +177,27 @@
}
}
]
},
{
"name": "getPopup",
"type": "function",
"description": "Gets the html document set as the popup for this page action.",
- "async": "callback",
+ "async": true,
"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",
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
@@ -8,29 +8,36 @@
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="text/javascript">
"use strict";
+let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+
+let image = atob(dataURI);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
function backgroundScript() {
browser.test.assertTrue("pageAction" in browser, "Namespace 'pageAction' exists in browser");
browser.test.assertTrue("show" in browser.pageAction, "API method 'show' exists in browser.pageAction");
// TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
let tabId = 1;
browser.test.onMessage.addListener(msg => {
if (msg === "pageAction-show") {
- browser.pageAction.show(tabId);
- browser.test.sendMessage("page-action-shown");
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
} else if (msg === "pageAction-hide") {
- browser.pageAction.hide(tabId);
- browser.test.sendMessage("page-action-hidden");
+ browser.pageAction.hide(tabId).then(() => {
+ browser.test.sendMessage("page-action-hidden");
+ });
}
});
browser.pageAction.onClicked.addListener(tab => {
// TODO: Make sure we get the correct tab once basic tabs support is added.
browser.test.sendMessage("page-action-clicked");
});
@@ -39,18 +46,24 @@ function backgroundScript() {
add_task(function* test_contentscript() {
let extension = ExtensionTestUtils.loadExtension({
background: "(" + backgroundScript.toString() + ")()",
manifest: {
"name": "PageAction Extension",
"page_action": {
"default_title": "Page Action",
+ "default_icon": {
+ "18": "extension.png",
+ },
},
},
+ files: {
+ "extension.png": IMAGE_ARRAYBUFFER,
+ },
});
yield extension.startup();
yield extension.awaitMessage("ready");
extension.sendMessage("pageAction-show");
yield extension.awaitMessage("page-action-shown");
ok(isPageActionShown(extension.id), "The PageAction should be shown");
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
@@ -10,30 +10,37 @@
</head>
<body>
<script type="text/javascript">
"use strict";
Cu.import("resource://gre/modules/Services.jsm");
+let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+
+let image = atob(dataURI);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
add_task(function* test_contentscript() {
function backgroundScript() {
// TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
let tabId = 1;
let onClickedListenerEnabled = false;
browser.test.onMessage.addListener((msg, details) => {
if (msg === "page-action-show") {
// TODO: switch to using .show(tabId).then(...) once bug 1270742 lands.
- browser.pageAction.show(tabId);
- browser.test.sendMessage("page-action-shown");
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
} else if (msg == "page-action-set-popup") {
- browser.pageAction.setPopup({popup: details.name, tabId: tabId});
- browser.test.sendMessage("page-action-popup-set");
+ browser.pageAction.setPopup({popup: details.name, tabId: tabId}).then(() => {
+ browser.test.sendMessage("page-action-popup-set");
+ });
} else if (msg == "page-action-get-popup") {
browser.pageAction.getPopup({tabId: tabId}).then(url => {
browser.test.sendMessage("page-action-got-popup", url);
});
} else if (msg == "page-action-enable-onClicked-listener") {
onClickedListenerEnabled = true;
browser.test.sendMessage("page-action-onClicked-listener-enabled");
} else if (msg == "page-action-disable-onClicked-listener") {
@@ -65,20 +72,24 @@ add_task(function* test_contentscript()
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript}())`,
manifest: {
"name": "PageAction Extension",
"page_action": {
"default_title": "Page Action",
"default_popup": "default.html",
+ "default_icon": {
+ "18": "extension.png",
+ },
},
},
files: {
"default.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
+ "extension.png": IMAGE_ARRAYBUFFER,
"a.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
"b.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
"popup.js": `(${popupScript})()`,
},
});
let tabClosedPromise = () => {
return new Promise(resolve => {
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -6,19 +6,23 @@
this.EXPORTED_SYMBOLS = ["ExtensionUtils"];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
+const INTEGER = /^[1-9]\d*$/;
+
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, "LanguageDetector",
"resource:///modules/translation/LanguageDetector.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
@@ -386,16 +390,162 @@ class BaseContext {
});
for (let obj of this.onClose) {
obj.close();
}
}
}
+// Manages icon details for toolbar buttons in the |pageAction| and
+// |browserAction| APIs.
+let IconDetails = {
+ // Normalizes the various acceptable input formats into an object
+ // with icon size as key and icon URL as value.
+ //
+ // If a context is specified (function is called from an extension):
+ // Throws an error if an invalid icon size was provided or the
+ // extension is not allowed to load the specified resources.
+ //
+ // If no context is specified, instead of throwing an error, this
+ // function simply logs a warning message.
+ normalize(details, extension, context = null) {
+ let result = {};
+
+ try {
+ if (details.imageData) {
+ let imageData = details.imageData;
+
+ // The global might actually be from Schema.jsm, which
+ // normalizes most of our arguments. In that case it won't have
+ // an ImageData property. But Schema.jsm doesn't normalize
+ // actual ImageData objects, so they will come from a global
+ // with the right property.
+ if (instanceOf(imageData, "ImageData")) {
+ imageData = {"19": imageData};
+ }
+
+ for (let size of Object.keys(imageData)) {
+ if (!INTEGER.test(size)) {
+ throw new Error(`Invalid icon size ${size}, must be an integer`);
+ }
+ result[size] = this.convertImageDataToDataURL(imageData[size], context);
+ }
+ }
+
+ if (details.path) {
+ let path = details.path;
+ if (typeof path != "object") {
+ path = {"19": path};
+ }
+
+ let baseURI = context ? context.uri : extension.baseURI;
+
+ for (let size of Object.keys(path)) {
+ if (!INTEGER.test(size)) {
+ throw new Error(`Invalid icon size ${size}, must be an integer`);
+ }
+
+ let url = baseURI.resolve(path[size]);
+
+ // The Chrome documentation specifies these parameters as
+ // relative paths. We currently accept absolute URLs as well,
+ // which means we need to check that the extension is allowed
+ // to load them. This will throw an error if it's not allowed.
+ Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
+ extension.principal, url,
+ Services.scriptSecurityManager.DISALLOW_SCRIPT);
+
+ result[size] = url;
+ }
+ }
+ } catch (e) {
+ // Function is called from extension code, delegate error.
+ if (context) {
+ throw e;
+ }
+ // If there's no context, it's because we're handling this
+ // as a manifest directive. Log a warning rather than
+ // raising an error.
+ extension.manifestError(`Invalid icon data: ${e}`);
+ }
+
+ return result;
+ },
+
+ // Returns the appropriate icon URL for the given icons object and the
+ // screen resolution of the given window.
+ getURL(icons, window, extension, size = 16) {
+ const DEFAULT = "chrome://browser/content/extension.svg";
+
+ size *= window.devicePixelRatio;
+
+ let bestSize = null;
+ if (icons[size]) {
+ bestSize = size;
+ } else if (icons[2 * size]) {
+ bestSize = 2 * size;
+ } else {
+ let sizes = Object.keys(icons)
+ .map(key => parseInt(key, 10))
+ .sort((a, b) => a - b);
+
+ bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
+ }
+
+ if (bestSize) {
+ return {size: bestSize, icon: icons[bestSize]};
+ }
+
+ return {size, icon: DEFAULT};
+ },
+
+ convertImageURLToDataURL(imageURL, context, browserWindow, size = 18) {
+ return new Promise((resolve, reject) => {
+ let image = new context.contentWindow.Image();
+ image.onload = function() {
+ let canvas = context.contentWindow.document.createElement("canvas");
+ let ctx = canvas.getContext("2d");
+ let dSize = size * browserWindow.devicePixelRatio;
+
+ // Scales the image while maintaing width to height ratio.
+ // If the width and height differ, the image is centered using the
+ // smaller of the two dimensions.
+ let dWidth, dHeight, dx, dy;
+ if (this.width > this.height) {
+ dWidth = dSize;
+ dHeight = image.height * (dSize / image.width);
+ dx = 0;
+ dy = (dSize - dHeight) / 2;
+ } else {
+ dWidth = image.width * (dSize / image.height);
+ dHeight = dSize;
+ dx = (dSize - dWidth) / 2;
+ dy = 0;
+ }
+
+ ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
+ resolve(canvas.toDataURL("image/png"));
+ };
+ image.onerror = reject;
+ image.src = imageURL;
+ });
+ },
+
+ convertImageDataToDataURL(imageData, context) {
+ let document = context.contentWindow.document;
+ let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.width = imageData.width;
+ canvas.height = imageData.height;
+ canvas.getContext("2d").putImageData(imageData, 0, 0);
+
+ return canvas.toDataURL("image/png");
+ },
+};
+
function LocaleData(data) {
this.defaultLocale = data.defaultLocale;
this.selectedLocale = data.selectedLocale;
this.locales = data.locales || new Map();
this.warnedMissingKeys = new Set();
// Map(locale-name -> Map(message-key -> localized-string))
//
@@ -403,16 +553,17 @@ function LocaleData(data) {
// Map of message keys to their localized strings.
this.messages = data.messages || new Map();
if (data.builtinMessages) {
this.messages.set(this.BUILTIN, data.builtinMessages);
}
}
+
LocaleData.prototype = {
// Representation of the object to send to content processes. This
// should include anything the content process might need.
serialize() {
return {
defaultLocale: this.defaultLocale,
selectedLocale: this.selectedLocale,
messages: this.messages,
@@ -1251,15 +1402,16 @@ this.ExtensionUtils = {
promiseDocumentReady,
runSafe,
runSafeSync,
runSafeSyncWithoutClone,
runSafeWithoutClone,
BaseContext,
DefaultWeakMap,
EventManager,
+ IconDetails,
LocaleData,
Messenger,
PlatformInfo,
SingletonEventManager,
SpreadArgs,
ChildAPIManager,
};
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -941,20 +941,21 @@ class ArrayType extends Type {
}
checkBaseType(baseType) {
return baseType == "array";
}
}
class FunctionType extends Type {
- constructor(schema, parameters, isAsync) {
+ constructor(schema, parameters, isAsync, hasAsyncCallback) {
super(schema);
this.parameters = parameters;
this.isAsync = isAsync;
+ this.hasAsyncCallback = hasAsyncCallback;
}
normalize(value, context) {
return this.normalizeBase("function", value, context);
}
checkBaseType(baseType) {
return baseType == "function";
@@ -1151,33 +1152,37 @@ class CallEntry extends Entry {
class FunctionEntry extends CallEntry {
constructor(schema, path, name, type, unsupported, allowAmbiguousOptionalArguments, returns, permissions) {
super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
this.unsupported = unsupported;
this.returns = returns;
this.permissions = permissions;
this.isAsync = type.isAsync;
+ this.hasAsyncCallback = type.hasAsyncCallback;
}
inject(path, name, dest, context) {
if (this.unsupported) {
return;
}
if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
return;
}
let stub;
if (this.isAsync) {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
- let callback = actuals.pop();
+ let callback = null;
+ if (this.hasAsyncCallback) {
+ callback = actuals.pop();
+ }
return context.callAsyncFunction(path, name, actuals, callback);
};
} else if (!this.returns) {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
return context.callFunctionNoReturn(path, name, actuals);
};
@@ -1398,17 +1403,17 @@ this.Schemas = {
return new NumberType(type);
} else if (type.type == "integer") {
checkTypeProperties("minimum", "maximum");
return new IntegerType(type, type.minimum || -Infinity, type.maximum || Infinity);
} else if (type.type == "boolean") {
checkTypeProperties();
return new BooleanType(type);
} else if (type.type == "function") {
- let isAsync = typeof(type.async) == "string";
+ let isAsync = Boolean(type.async);
let parameters = null;
if ("parameters" in type) {
parameters = [];
for (let param of type.parameters) {
// Callbacks default to optional for now, because of promise
// handling.
let isCallback = isAsync && param.name == type.async;
@@ -1416,27 +1421,28 @@ this.Schemas = {
parameters.push({
type: this.parseType(path, param, ["name", "optional"]),
name: param.name,
optional: param.optional == null ? isCallback : param.optional,
});
}
}
+ let hasAsyncCallback = false;
if (isAsync) {
- if (!parameters || !parameters.length || parameters[parameters.length - 1].name != type.async) {
- throw new Error(`Internal error: "async" property must name the last parameter of the function.`);
+ if (parameters && parameters.length && parameters[parameters.length - 1].name == type.async) {
+ hasAsyncCallback = true;
}
if (type.returns || type.allowAmbiguousOptionalArguments) {
throw new Error(`Internal error: Async functions must not have return values or ambiguous arguments.`);
}
}
checkTypeProperties("parameters", "async", "returns");
- return new FunctionType(type, parameters, isAsync);
+ return new FunctionType(type, parameters, isAsync, hasAsyncCallback);
} else if (type.type == "any") {
// Need to see what minimum and maximum are supposed to do here.
checkTypeProperties("minimum", "maximum");
return new AnyType(type);
} else {
throw new Error(`Unexpected type ${type.type}`);
}
},