Bug 1308309 Prompt for webextensions permissions r=florian,rhelmer

MozReview-Commit-ID: 6rTGvjKcx3H

--HG--
extra : rebase_source : b17acde9a1ebf4a378b5f4e02f411b2aef8cde10
extra : intermediate-source : a9f326b034571a273b0087a3c3d9c7738093d903
extra : source : 320fb02a40dc18208bb00cef3ba7e1de2ca89938
This commit is contained in:
Andrew Swan 2017-01-03 10:55:25 -08:00
Родитель 83d7a52a60
Коммит 005c2ba3de
15 изменённых файлов: 381 добавлений и 2 удалений

Просмотреть файл

@ -71,3 +71,11 @@
<popupnotification id="addon-install-confirmation-notification" hidden="true">
<popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
</popupnotification>
<popupnotification id="addon-webext-permissions-notification" hidden="true">
<popupnotificationcontent orient="vertical">
<description id="addon-webext-perm-header" class="addon-webext-perm-header"/>
<label id="addon-webext-perm-text" class="addon-webext-perm-text"/>
<html:ul id="addon-webext-perm-list" class="addon-webext-perm-list"/>
</popupnotificationcontent>
</popupnotification>

Просмотреть файл

@ -117,6 +117,9 @@ support-files =
file_bug1045809_2.html
file_csp_block_all_mixedcontent.html
file_csp_block_all_mixedcontent.js
file_install_extensions.html
browser_webext_permissions.xpi
browser_webext_nopermissions.xpi
!/image/test/mochitest/blue.png
!/toolkit/components/passwordmgr/test/browser/form_basic.html
!/toolkit/components/passwordmgr/test/browser/insecure_test.html
@ -298,6 +301,7 @@ skip-if = os == "mac" # decoder doctor isn't implemented on osx
[browser_duplicateIDs.js]
[browser_drag.js]
skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
[browser_extension_permissions.js]
[browser_favicon_change.js]
[browser_favicon_change_not_in_document.js]
[browser_findbarClose.js]

Просмотреть файл

@ -0,0 +1,111 @@
"use strict";
const BASE = getRootDirectory(gTestPath)
.replace("chrome://mochitests/content/", "https://example.com/");
const PAGE = `${BASE}/file_install_extensions.html`;
const PERMS_XPI = `${BASE}/browser_webext_permissions.xpi`;
const NO_PERMS_XPI = `${BASE}/browser_webext_nopermissions.xpi`;
const ID = "permissions@test.mozilla.org";
const DEFAULT_EXTENSION_ICON = "chrome://browser/content/extension.svg";
function promisePopupNotificationShown(name) {
return new Promise(resolve => {
PopupNotifications.panel.addEventListener("popupshown", () => {
let notification = PopupNotifications.getNotification(name);
ok(notification, `${name} notification shown`);
ok(PopupNotifications.isPanelOpen, "notification panel open");
resolve(PopupNotifications.panel.firstChild);
}, {once: true});
});
}
function promiseGetAddonByID(id) {
return new Promise(resolve => {
AddonManager.getAddonByID(id, resolve);
});
}
function checkNotification(panel, url) {
let icon = panel.getAttribute("icon");
let uls = panel.firstChild.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "ul");
is(uls.length, 1, "Found the permissions list");
let ul = uls[0];
let headers = panel.firstChild.getElementsByClassName("addon-webext-perm-text");
is(headers.length, 1, "Found the header");
let header = headers[0];
if (url == PERMS_XPI) {
// The icon should come from the extension, don't bother with the precise
// path, just make sure we've got a jar url pointing to the right path
// inside the jar.
ok(icon.startsWith("jar:file://"), "Icon is a jar url");
ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
is(header.getAttribute("hidden"), "", "Permission list header is visible");
is(ul.childElementCount, 4, "Permissions list has 4 entries");
// Real checking of the contents here is deferred until bug 1316996 lands
} else if (url == NO_PERMS_XPI) {
// This extension has no icon, it should have the default
is(icon, DEFAULT_EXTENSION_ICON, "Icon is the default extension icon");
is(header.getAttribute("hidden"), "true", "Permission list header is hidden");
is(ul.childElementCount, 0, "Permissions list has 0 entries");
}
}
const INSTALL_FUNCTIONS = [
function installMozAM(url) {
return ContentTask.spawn(gBrowser.selectedBrowser, url, function*(cUrl) {
return content.wrappedJSObject.installMozAM(cUrl);
});
},
];
add_task(function* () {
yield SpecialPowers.pushPrefEnv({set: [
["extensions.webapi.testing", true],
["extensions.install.requireBuiltInCerts", false],
// XXX remove this when prompts are enabled by default
["extensions.webextPermissionPrompts", true],
]});
function* runOnce(installFn, url, cancel) {
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
let installPromise = installFn(url);
let panel = yield promisePopupNotificationShown("addon-webext-permissions");
checkNotification(panel, url);
if (cancel) {
panel.secondaryButton.click();
} else {
panel.button.click();
}
let result = yield installPromise;
let addon = yield promiseGetAddonByID(ID);
if (cancel) {
is(result, "onInstallCancelled", "Installation was cancelled");
is(addon, null, "Extension is not installed");
} else {
is(result, "onInstallEnded", "Installation completed");
isnot(addon, null, "Extension is installed");
addon.uninstall();
}
yield BrowserTestUtils.removeTab(tab);
}
for (let installFn of INSTALL_FUNCTIONS) {
yield runOnce(installFn, NO_PERMS_XPI, true);
yield runOnce(installFn, PERMS_XPI, true);
yield runOnce(installFn, PERMS_XPI, false);
}
});

Двоичные данные
browser/base/content/test/general/browser_webext_nopermissions.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/base/content/test/general/browser_webext_permissions.xpi Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type="text/javascript">
function installMozAM(url) {
return navigator.mozAddonManager.createInstall({url}).then(install => new Promise(resolve => {
const EVENTS = [
"onDownloadCancelled",
"onDownloadFailed",
"onInstallCancelled",
"onInstallEnded",
"onInstallFailed",
];
for (let event of EVENTS) {
install.addEventListener(event, () => { resolve(event); });
}
install.install();
}));
}
</script>
</body>
</html>

Просмотреть файл

@ -57,6 +57,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", "@mozilla.org/alerts-s
["ContentSearch", "resource:///modules/ContentSearch.jsm"],
["DateTimePickerHelper", "resource://gre/modules/DateTimePickerHelper.jsm"],
["DirectoryLinksProvider", "resource:///modules/DirectoryLinksProvider.jsm"],
["ExtensionsUI", "resource:///modules/ExtensionsUI.jsm"],
["Feeds", "resource:///modules/Feeds.jsm"],
["FileUtils", "resource://gre/modules/FileUtils.jsm"],
["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
@ -1065,6 +1066,8 @@ BrowserGlue.prototype = {
}
});
ExtensionsUI.init();
let signingRequired;
if (AppConstants.MOZ_REQUIRE_SIGNING) {
signingRequired = true;

Просмотреть файл

@ -0,0 +1,155 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
this.EXPORTED_SYMBOLS = ["ExtensionsUI"];
Cu.import("resource://gre/modules/Services.jsm");
const DEFAULT_EXENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
const HTML_NS = "http://www.w3.org/1999/xhtml";
this.ExtensionsUI = {
init() {
Services.obs.addObserver(this, "webextension-permission-prompt", false);
},
observe(subject, topic, data) {
if (topic == "webextension-permission-prompt") {
let {target, info} = subject.wrappedJSObject;
this.showPermissionsPrompt(target, info).then(answer => {
Services.obs.notifyObservers(subject, "webextension-permission-response",
JSON.stringify(answer));
});
}
},
showPermissionsPrompt(target, info) {
let perms = info.addon.userPermissions;
if (!perms) {
return Promise.resolve();
}
let win = target.ownerGlobal;
let name = info.addon.name;
if (name.length > 50) {
name = name.slice(0, 49) + "…";
}
// The strings below are placeholders, they will switch over to the
// bundle.get*String() calls as part of bug 1316996.
// let bundle = win.gNavigatorBundle;
// let header = bundle.getFormattedString("webextPerms.header", [name])
// let listHeader = bundle.getString("webextPerms.listHeader");
let header = "Add ADDON?".replace("ADDON", name);
let listHeader = "It can:";
let formatPermission = perm => {
try {
// return bundle.getString(`webextPerms.description.${perm}`);
return `localized description of permission ${perm}`;
} catch (err) {
// return bundle.getFormattedString("webextPerms.description.unknown",
// [perm]);
return `localized description of unknown permission ${perm}`;
}
};
let formatHostPermission = perm => {
if (perm == "<all_urls>") {
// return bundle.getString("webextPerms.hostDescription.allUrls");
return "localized description of <all_urls> host permission";
}
let match = /^[htps*]+:\/\/([^/]+)\//.exec(perm);
if (!match) {
throw new Error("Unparseable host permission");
}
if (match[1].startsWith("*.")) {
let domain = match[1].slice(2);
// return bundle.getFormattedString("webextPerms.hostDescription.wildcard", [domain]);
return `localized description of wildcard host permission for ${domain}`;
}
// return bundle.getFormattedString("webextPerms.hostDescription.oneSite", [match[1]]);
return `localized description of single host permission for ${match[1]}`;
};
let msgs = [
...perms.permissions.map(formatPermission),
...perms.hosts.map(formatHostPermission),
];
// let acceptText = bundle.getString("webextPerms.accept.label");
// let acceptKey = bundle.getString("webextPerms.accept.accessKey");
// let cancelText = bundle.getString("webextPerms.cancel.label");
// let cancelKey = bundle.getString("webextPerms.cancel.accessKey");
let acceptText = "Add extension";
let acceptKey = "A";
let cancelText = "Cancel";
let cancelKey = "C";
let rendered = false;
let popupOptions = {
hideClose: true,
popupIconURL: info.icon,
persistent: true,
eventCallback(topic) {
if (topic == "showing") {
// This check can be removed when bug 1325223 is resolved.
if (rendered) {
return false;
}
let doc = this.browser.ownerDocument;
doc.getElementById("addon-webext-perm-header").textContent = header;
let list = doc.getElementById("addon-webext-perm-list");
while (list.firstChild) {
list.firstChild.remove();
}
let listHeaderEl = doc.getElementById("addon-webext-perm-text");
listHeaderEl.value = listHeader;
listHeaderEl.hidden = (msgs.length == 0);
for (let msg of msgs) {
let item = doc.createElementNS(HTML_NS, "li");
item.textContent = msg;
list.appendChild(item);
}
rendered = true;
} else if (topic == "dismissed") {
rendered = false;
} else if (topic == "swapping") {
rendered = false;
return true;
}
return false;
},
};
return new Promise(resolve => {
win.PopupNotifications.show(target, "addon-webext-permissions", "",
"addons-notification-icon",
{
label: acceptText,
accessKey: acceptKey,
callback: () => resolve(true),
},
[
{
label: cancelText,
accessKey: cancelKey,
callback: () => resolve(false),
},
], popupOptions);
});
},
};

Просмотреть файл

@ -26,6 +26,7 @@ EXTRA_JS_MODULES += [
'ContentWebRTC.jsm',
'DirectoryLinksProvider.jsm',
'E10SUtils.jsm',
'ExtensionsUI.jsm',
'Feeds.jsm',
'FormSubmitObserver.jsm',
'FormValidationHandler.jsm',

Просмотреть файл

@ -833,6 +833,11 @@ menuitem.bookmark-item {
font-weight: bold;
}
.addon-webext-perm-header {
font-weight: bold;
font-size: 1.3em;
}
/* Notification icon box */
.notification-anchor-icon:-moz-focusring {

Просмотреть файл

@ -3091,6 +3091,11 @@ menulist.translate-infobar-element > .menulist-dropmarker {
font-weight: bold;
}
.addon-webext-perm-header {
font-weight: bold;
font-size: 1.3em;
}
/* Status panel */
.statuspanel-label {

Просмотреть файл

@ -2136,6 +2136,11 @@ toolbarbutton.bookmark-item[dragover="true"][open="true"] {
font-weight: bold;
}
.addon-webext-perm-header {
font-weight: bold;
font-size: 1.3em;
}
/* Notification icon box */
.notification-anchor-icon:-moz-focusring {

Просмотреть файл

@ -377,6 +377,12 @@ this.ExtensionData = class {
hosts: this.whiteListedHosts.pat,
apis: [...this.apiNames],
};
if (Array.isArray(this.manifest.content_scripts)) {
for (let entry of this.manifest.content_scripts) {
result.hosts.push(...entry.matches);
}
}
const EXP_PATTERN = /^experiments\.\w+/;
result.permissions = [...this.permissions]
.filter(p => !result.hosts.includes(p) && !EXP_PATTERN.test(p));

Просмотреть файл

@ -47,6 +47,7 @@ const UNKNOWN_XPCOM_ABI = "unknownABI";
const PREF_MIN_WEBEXT_PLATFORM_VERSION = "extensions.webExtensionsMinPlatformVersion";
const PREF_WEBAPI_TESTING = "extensions.webapi.testing";
const PREF_WEBEXT_PREF_PROMPTS = "extensions.webextPermissionPrompts";
const UPDATE_REQUEST_VERSION = 2;
const CATEGORY_UPDATE_PARAMS = "extension-update-params";
@ -78,6 +79,8 @@ Cu.import("resource://gre/modules/AsyncShutdown.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
@ -2801,6 +2804,30 @@ var AddonManagerInternal = {
return AddonManagerInternal.getInstallForURL(options.url, "application/x-xpinstall",
options.hash).then(install => {
if (Preferences.get(PREF_WEBEXT_PREF_PROMPTS, false)) {
install._permHandler = info => new Promise((resolve, reject) => {
const observer = {
observe(subject, topic, data) {
if (topic == "webextension-permission-response" &&
subject.wrappedJSObject.info.addon == info.addon) {
let answer = JSON.parse(data);
Services.obs.removeObserver(this, "webextension-permission-response");
if (answer) {
resolve();
} else {
reject();
}
}
}
};
Services.obs.addObserver(observer, "webextension-permission-response", false);
let subject = {wrappedJSObject: {target, info}};
Services.obs.notifyObservers(subject, "webextension-permission-prompt", null);
});
}
let id = this.nextInstall++;
let listener = this.makeListener(id, target.messageManager);
install.addListener(listener);

Просмотреть файл

@ -87,6 +87,12 @@ XPCOMUtils.defineLazyGetter(this, "CertUtils", function() {
return certUtils;
});
XPCOMUtils.defineLazyGetter(this, "IconDetails", () => {
const {ExtensionUtils} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
return ExtensionUtils.IconDetails;
});
Cu.importGlobalProperties(["URL"]);
const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
@ -5597,6 +5603,18 @@ class AddonInstall {
}).bind(this));
}
getIcon(desiredSize = 64) {
if (!this.addon.icons || !this.file) {
return null;
}
let {icon} = IconDetails.getPreferredIcon(this.addon.icons, null, desiredSize);
if (icon.startsWith("chrome://")) {
return icon;
}
return buildJarURI(this.file, icon).spec;
}
/**
* This method should be called when the XPI is ready to be installed,
* i.e., when a download finishes or when a local file has been verified.
@ -5608,8 +5626,9 @@ class AddonInstall {
Task.spawn((function*() {
if (this.permHandler) {
let info = {
existinAddon: this.existingAddon,
addon: this.addon,
existingAddon: this.existingAddon ? this.existingAddon.wrapper : null,
addon: this.addon.wrapper,
icon: this.getIcon(),
};
try {
@ -7525,6 +7544,10 @@ AddonWrapper.prototype = {
return (addon._installLocation.name == KEY_APP_PROFILE);
},
get userPermissions() {
return addonFor(this).userPermissions;
},
isCompatibleWith(aAppVersion, aPlatformVersion) {
return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion);
},