diff --git a/browser/components/extensions/.eslintrc b/browser/components/extensions/.eslintrc index 94af6f349122..b446fe45d7f3 100644 --- a/browser/components/extensions/.eslintrc +++ b/browser/components/extensions/.eslintrc @@ -5,6 +5,7 @@ "AllWindowEvents": true, "currentWindow": true, "EventEmitter": true, + "IconDetails": true, "makeWidgetId": true, "pageActionFor": true, "PanelPopup": true, diff --git a/browser/components/extensions/ext-pageAction.js b/browser/components/extensions/ext-pageAction.js index 61d1dffbd715..d1a18bc1aff2 100644 --- a/browser/components/extensions/ext-pageAction.js +++ b/browser/components/extensions/ext-pageAction.js @@ -6,7 +6,6 @@ Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { EventManager, - IconDetails, } = ExtensionUtils; // WeakMap[Extension -> PageAction] diff --git a/browser/components/extensions/ext-utils.js b/browser/components/extensions/ext-utils.js index 945e9e60deb4..19c66fbfebdd 100644 --- a/browser/components/extensions/ext-utils.js +++ b/browser/components/extensions/ext-utils.js @@ -8,10 +8,13 @@ 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*$/; + var { EventManager, instanceOf, @@ -21,6 +24,102 @@ var { // 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 = 18) { + const DEFAULT = "chrome://browser/content/extension.svg"; + + return AddonManager.getPreferredIconURL({icons: icons}, size, window) || 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. diff --git a/browser/components/extensions/schemas/page_action.json b/browser/components/extensions/schemas/page_action.json index 8f6384fc315a..6ac0d642f148 100644 --- a/browser/components/extensions/schemas/page_action.json +++ b/browser/components/extensions/schemas/page_action.json @@ -55,7 +55,6 @@ { "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."} @@ -64,7 +63,6 @@ { "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."} diff --git a/mobile/android/components/extensions/ext-pageAction.js b/mobile/android/components/extensions/ext-pageAction.js index 4788d948332d..dcf38ccad897 100644 --- a/mobile/android/components/extensions/ext-pageAction.js +++ b/mobile/android/components/extensions/ext-pageAction.js @@ -15,7 +15,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "PageActions", Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { - IconDetails, SingletonEventManager, } = ExtensionUtils; @@ -25,13 +24,13 @@ var pageActionMap = new WeakMap(); function PageAction(options, extension) { this.id = null; - this.extension = extension; - this.icons = IconDetails.normalize({path: options.default_icon}, extension); + 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.popupUrl = options.default_popup; this.options = { title: options.default_title || extension.name, + icon: DEFAULT_ICON, id: extension.id, clickCallback: () => { if (this.popupUrl) { @@ -46,40 +45,18 @@ function PageAction(options, extension) { }, }; - this.shouldShow = false; - EventEmitter.decorate(this); } PageAction.prototype = { - show(tabId, context) { - if (this.id) { - return Promise.resolve(); - } - - if (this.options.icon) { + show(tabId) { + // TODO: Only show the PageAction for the tab with the provided tabId. + if (!this.id) { this.id = PageActions.add(this.options); - return Promise.resolve(); } - - this.shouldShow = true; - - let imageURL = IconDetails.getURL(this.icons, context.contentWindow, this.extension); - let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); - return IconDetails.convertImageURLToDataURL(imageURL, 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; @@ -129,14 +106,11 @@ extensions.registerSchemaAPI("pageAction", (extension, context) => { }).api(), show(tabId) { - return pageActionMap.get(extension) - .show(tabId, context) - .then(() => {}); + pageActionMap.get(extension).show(tabId); }, hide(tabId) { pageActionMap.get(extension).hide(tabId); - return Promise.resolve(); }, setPopup(details) { diff --git a/mobile/android/components/extensions/schemas/page_action.json b/mobile/android/components/extensions/schemas/page_action.json index 40e58cb6d3c1..5f999f7a01c5 100644 --- a/mobile/android/components/extensions/schemas/page_action.json +++ b/mobile/android/components/extensions/schemas/page_action.json @@ -19,6 +19,7 @@ "preprocess": "localize" }, "default_icon": { + "unsupported": true, "$ref": "IconPath", "optional": true }, @@ -56,7 +57,6 @@ "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."} ] @@ -65,7 +65,6 @@ "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."} ] @@ -162,7 +161,6 @@ { "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": [ { @@ -182,7 +180,7 @@ "name": "getPopup", "type": "function", "description": "Gets the html document set as the popup for this page action.", - "async": true, + "async": "callback", "parameters": [ { "name": "details", @@ -193,6 +191,16 @@ "description": "Specify the tab to get the popup from." } } + }, + { + "type": "function", + "name": "callback", + "parameters": [ + { + "name": "result", + "type": "string" + } + ] } ] } diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html index 6cf575337de5..b4900c7491de 100644 --- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html +++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html @@ -13,11 +13,6 @@