Bug 1156826 - Implement browser.runtime.setUninstallURL(). r=kmag

This commit is contained in:
Andrew Swan 2016-02-08 16:00:25 -08:00
parent 047c72c007
commit e40f02b580
8 changed files with 167 additions and 1 deletions

View File

@ -0,0 +1,10 @@
"use strict";
/* eslint-disable mozilla/balanced-listeners */
extensions.on("uninstall", (msg, extension) => {
if (extension.uninstallURL) {
let browser = Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
browser.addTab(extension.uninstallURL, { relatedToCurrent: true });
}
});

View File

@ -8,6 +8,7 @@ browser.jar:
content/browser/ext-contextMenus.js
content/browser/ext-browserAction.js
content/browser/ext-pageAction.js
content/browser/ext-desktop-runtime.js
content/browser/ext-tabs.js
content/browser/ext-windows.js
content/browser/ext-bookmarks.js

View File

@ -21,6 +21,7 @@ support-files =
[browser_ext_contextMenus.js]
[browser_ext_getViews.js]
[browser_ext_lastError.js]
[browser_ext_runtime_setUninstallURL.js]
[browser_ext_tabs_audio.js]
[browser_ext_tabs_captureVisibleTab.js]
[browser_ext_tabs_executeScript.js]

View File

@ -0,0 +1,114 @@
"use strict";
let { AddonManager } = Components.utils.import("resource://gre/modules/AddonManager.jsm", {});
let { Extension } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
function install(url) {
return new Promise((resolve, reject) => {
AddonManager.getInstallForURL(url, (install) => {
install.addListener({
onInstallEnded: (i, addon) => resolve(addon),
onInstallFailed: () => reject(),
});
install.install();
}, "application/x-xpinstall");
});
}
function* makeAndInstallXPI(id, backgroundScript, loadedURL) {
let xpi = Extension.generateXPI(id, {
background: "(" + backgroundScript.toString() + ")()",
});
SimpleTest.registerCleanupFunction(function cleanupXPI() {
Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
xpi.remove(false);
});
let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, loadedURL);
let fileURI = Services.io.newFileURI(xpi);
info(`installing ${fileURI.spec}`);
let addon = yield install(fileURI.spec);
info("installed");
// A WebExtension is started asynchronously, we have our test extension
// open a new tab to signal that the background script has executed.
let loadTab = yield loadPromise;
yield BrowserTestUtils.removeTab(loadTab);
return addon;
}
add_task(function* test_setuninstallurl_badargs() {
function backgroundScript() {
let promises = [
browser.runtime.setUninstallURL("this is not a url")
.then(() => {
browser.test.notifyFail("setUninstallURL should have failed with bad url");
})
.catch(error => {
browser.test.assertTrue(/Invalid URL/.test(error.message), "error message indicates malformed url");
}),
browser.runtime.setUninstallURL("file:///etc/passwd")
.then(() => {
browser.test.notifyFail("setUninstallURL should have failed with non-http[s] url");
})
.catch(error => {
browser.test.assertTrue(/must have the scheme http or https/.test(error.message), "error message indicates bad scheme");
}),
];
Promise.all(promises)
.then(() => browser.test.notifyPass("setUninstallURL bad params"));
}
let extension = ExtensionTestUtils.loadExtension({
background: "(" + backgroundScript.toString() + ")()",
});
yield extension.startup();
yield extension.awaitFinish();
yield extension.unload();
});
// Test the documented behavior of setUninstallURL() that passing an
// empty string is equivalent to not setting an uninstall URL
// (i.e., no new tab is opened upon uninstall)
add_task(function* test_setuninstall_empty_url() {
function backgroundScript() {
browser.runtime.setUninstallURL("")
.then(() => browser.tabs.create({ url: "http://example.com/addon_loaded" }));
}
let addon = yield makeAndInstallXPI("test_uinstallurl2@tests.mozilla.org",
backgroundScript,
"http://example.com/addon_loaded");
addon.uninstall(true);
info("uninstalled");
// no need to explicitly check for the absence of a new tab,
// BrowserTestUtils will eventually complain if one is opened.
});
add_task(function* test_setuninstallurl() {
function backgroundScript() {
browser.runtime.setUninstallURL("http://example.com/addon_uninstalled")
.then(() => browser.tabs.create({ url: "http://example.com/addon_loaded" }));
}
let addon = yield makeAndInstallXPI("test_uinstallurl@tests.mozilla.org",
backgroundScript,
"http://example.com/addon_loaded");
// look for a new tab with the uninstall url.
let uninstallPromise = BrowserTestUtils.waitForNewTab(gBrowser, "http://example.com/addon_uninstalled");
addon.uninstall(true);
info("uninstalled");
let uninstalledTab = yield uninstallPromise;
isnot(uninstalledTab, null, "opened tab with uninstall url");
yield BrowserTestUtils.removeTab(uninstalledTab);
});

View File

@ -551,6 +551,7 @@ BrowserGlue.prototype = {
ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-desktop-runtime.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
ExtensionManagement.registerScript("chrome://browser/content/ext-bookmarks.js");

View File

@ -52,6 +52,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
@ -311,6 +313,21 @@ ExtensionPage = class extends BaseContext {
}
};
// For extensions that have called setUninstallURL(), send an event
// so the browser can display the URL.
let UninstallObserver = {
init: function() {
AddonManager.addAddonListener(this);
},
onUninstalling: function(addon) {
let extension = GlobalManager.extensionMap.get(addon.id);
if (extension) {
Management.emit("uninstall", extension);
}
},
};
// Responsible for loading extension APIs into the right globals.
GlobalManager = {
// Number of extensions currently enabled.
@ -326,6 +343,7 @@ GlobalManager = {
init(extension) {
if (this.count == 0) {
Services.obs.addObserver(this, "content-document-global-created", false);
UninstallObserver.init();
}
this.count++;
@ -806,6 +824,8 @@ this.Extension = function(addonData) {
this.hasShutdown = false;
this.onShutdown = new Set();
this.uninstallURL = null;
this.permissions = new Set();
this.whiteListedHosts = null;
this.webAccessibleResources = new Set();

View File

@ -85,6 +85,26 @@ extensions.registerSchemaAPI("runtime", null, (extension, context) => {
let info = {os, arch};
return Promise.resolve(info);
},
setUninstallURL: function(url) {
if (url.length == 0) {
return Promise.resolve();
}
let uri;
try {
uri = NetUtil.newURI(url);
} catch (e) {
return Promise.reject({ message: `Invalid URL: ${JSON.stringify(url)}` });
}
if (uri.scheme != "http" && uri.scheme != "https") {
return Promise.reject({ message: "url must have the scheme http or https" });
}
extension.uninstallURL = url;
return Promise.resolve();
},
},
};
});

View File

@ -175,7 +175,6 @@
},
{
"name": "setUninstallURL",
"unsupported": true,
"type": "function",
"description": "Sets the URL to be visited upon uninstallation. This may be used to clean up server-side data, do analytics, and implement surveys. Maximum 255 characters.",
"async": "callback",