/* 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/XPCOMUtils.jsm"); Cu.import("resource://devtools/shared/event-emitter.js"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource:///modules/RecentWindow.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS", "extensions.webextPermissionPrompts", false); const DEFAULT_EXTENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg"; const HTML_NS = "http://www.w3.org/1999/xhtml"; this.ExtensionsUI = { sideloaded: new Set(), updates: new Set(), init() { Services.obs.addObserver(this, "webextension-permission-prompt", false); Services.obs.addObserver(this, "webextension-update-permissions", false); Services.obs.addObserver(this, "webextension-install-notify", false); this._checkForSideloaded(); }, _checkForSideloaded() { AddonManager.getAllAddons(addons => { // Check for any side-loaded addons that the user is allowed // to enable. let sideloaded = addons.filter( addon => addon.seen === false && (addon.permissions & AddonManager.PERM_CAN_ENABLE)); if (!sideloaded.length) { return; } if (WEBEXT_PERMISSION_PROMPTS) { for (let addon of sideloaded) { this.sideloaded.add(addon); } this.emit("change"); } else { // This and all the accompanying about:newaddon code can eventually // be removed. See bug 1331521. let win = RecentWindow.getMostRecentBrowserWindow(); for (let addon of sideloaded) { win.openUILinkIn(`about:newaddon?id=${addon.id}`, "tab"); } } }); }, showAddonsManager(browser, info) { let loadPromise = new Promise(resolve => { let listener = (subject, topic) => { if (subject.location.href == "about:addons") { Services.obs.removeObserver(listener, topic); resolve(subject); } }; Services.obs.addObserver(listener, "EM-loaded", false); }); let tab = browser.addTab("about:addons"); browser.selectedTab = tab; return loadPromise.then(win => { win.loadView("addons://list/extension"); return this.showPermissionsPrompt(browser.selectedBrowser, info); }); }, showSideloaded(browser, addon) { addon.markAsSeen(); this.sideloaded.delete(addon); this.emit("change"); let info = { addon, permissions: addon.userPermissions, icon: addon.iconURL, type: "sideload", }; this.showAddonsManager(browser, info).then(answer => { addon.userDisabled = !answer; }); }, showUpdate(browser, info) { info.type = "update"; this.showAddonsManager(browser, info).then(answer => { if (answer) { info.resolve(); } else { info.reject(); } // At the moment, this prompt will re-appear next time we do an update // check. See bug 1332360 for proposal to avoid this. this.updates.delete(info); this.emit("change"); }); }, observe(subject, topic, data) { if (topic == "webextension-permission-prompt") { let {target, info} = subject.wrappedJSObject; // Dismiss the progress notification. Note that this is bad if // there are multiple simultaneous installs happening, see // bug 1329884 for a longer explanation. let progressNotification = target.ownerGlobal.PopupNotifications.getNotification("addon-progress", target); if (progressNotification) { progressNotification.remove(); } let reply = answer => { Services.obs.notifyObservers(subject, "webextension-permission-response", JSON.stringify(answer)); }; let perms = info.addon.userPermissions; if (!perms) { reply(true); } else { info.permissions = perms; this.showPermissionsPrompt(target, info).then(reply); } } else if (topic == "webextension-update-permissions") { this.updates.add(subject.wrappedJSObject); this.emit("change"); } else if (topic == "webextension-install-notify") { let {target, addon} = subject.wrappedJSObject; this.showInstallNotification(target, addon); } }, showPermissionsPrompt(target, info) { let perms = info.permissions; if (!perms) { return Promise.resolve(); } let win = target.ownerGlobal; let name = info.addon.name; if (name.length > 50) { name = name.slice(0, 49) + "…"; } name = name.replace(/&/g, "&") .replace(//g, ">"); let addonLabel = ``; let bundle = win.gNavigatorBundle; let header = bundle.getFormattedString("webextPerms.header", [addonLabel]); let text = ""; let listIntro = bundle.getString("webextPerms.listIntro"); let acceptText = bundle.getString("webextPerms.add.label"); let acceptKey = bundle.getString("webextPerms.add.accessKey"); let cancelText = bundle.getString("webextPerms.cancel.label"); let cancelKey = bundle.getString("webextPerms.cancel.accessKey"); if (info.type == "sideload") { header = bundle.getFormattedString("webextPerms.sideloadHeader", [addonLabel]); text = bundle.getString("webextPerms.sideloadText"); acceptText = bundle.getString("webextPerms.sideloadEnable.label"); acceptKey = bundle.getString("webextPerms.sideloadEnable.accessKey"); cancelText = bundle.getString("webextPerms.sideloadDisable.label"); cancelKey = bundle.getString("webextPerms.sideloadDisable.accessKey"); } else if (info.type == "update") { header = ""; text = bundle.getFormattedString("webextPerms.updateText", [addonLabel]); acceptText = bundle.getString("webextPerms.updateAccept.label"); acceptKey = bundle.getString("webextPerms.updateAccept.accessKey"); } let msgs = []; for (let permission of perms.permissions) { let key = `webextPerms.description.${permission}`; if (permission == "nativeMessaging") { let brandBundle = win.document.getElementById("bundle_brand"); let appName = brandBundle.getString("brandShortName"); msgs.push(bundle.getFormattedString(key, [appName])); } else { try { msgs.push(bundle.getString(key)); } catch (err) { // We deliberately do not include all permissions in the prompt. // So if we don't find one then just skip it. } } } let allUrls = false, wildcards = [], sites = []; for (let permission of perms.hosts) { if (permission == "") { allUrls = true; break; } let match = /^[htps*]+:\/\/([^/]+)\//.exec(permission); if (!match) { throw new Error("Unparseable host permission"); } if (match[1] == "*") { allUrls = true; } else if (match[1].startsWith("*.")) { wildcards.push(match[1].slice(2)); } else { sites.push(match[1]); } } if (allUrls) { msgs.push(bundle.getString("webextPerms.hostDescription.allUrls")); } else { // Formats a list of host permissions. If we have 4 or fewer, display // them all, otherwise display the first 3 followed by an item that // says "...plus N others" function format(list, itemKey, moreKey) { function formatItems(items) { msgs.push(...items.map(item => bundle.getFormattedString(itemKey, [item]))); } if (list.length < 5) { formatItems(list); } else { formatItems(list.slice(0, 3)); let remaining = list.length - 3; msgs.push(PluralForm.get(remaining, bundle.getString(moreKey)) .replace("#1", remaining)); } } format(wildcards, "webextPerms.hostDescription.wildcard", "webextPerms.hostDescription.tooManyWildcards"); format(sites, "webextPerms.hostDescription.oneSite", "webextPerms.hostDescription.tooManySites"); } let popupOptions = { hideClose: true, popupIconURL: info.icon, persistent: true, eventCallback(topic) { if (topic == "showing") { let doc = this.browser.ownerDocument; doc.getElementById("addon-webext-perm-header").innerHTML = header; if (text) { doc.getElementById("addon-webext-perm-text").innerHTML = text; } let listIntroEl = doc.getElementById("addon-webext-perm-intro"); listIntroEl.value = listIntro; listIntroEl.hidden = (msgs.length == 0); let list = doc.getElementById("addon-webext-perm-list"); while (list.firstChild) { list.firstChild.remove(); } for (let msg of msgs) { let item = doc.createElementNS(HTML_NS, "li"); item.textContent = msg; list.appendChild(item); } } else if (topic == "swapping") { 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); }); }, showInstallNotification(target, addon) { let win = target.ownerGlobal; let popups = win.PopupNotifications; let addonLabel = ``; let addonIcon = ''; let toolbarIcon = ''; let brandBundle = win.document.getElementById("bundle_brand"); let appName = brandBundle.getString("brandShortName"); let bundle = win.gNavigatorBundle; let msg1 = bundle.getFormattedString("addonPostInstall.message1", [addonLabel, appName]); let msg2 = bundle.getFormattedString("addonPostInstall.message2", [addonLabel, addonIcon, toolbarIcon]); let action = { label: bundle.getString("addonPostInstall.okay.label"), accessKey: bundle.getString("addonPostInstall.okay.key"), callback: () => {}, }; let options = { hideClose: true, popupIconURL: addon.iconURL || DEFAULT_EXTENSION_ICON, eventCallback(topic) { if (topic == "showing") { let doc = this.browser.ownerDocument; doc.getElementById("addon-installed-notification-header") .innerHTML = msg1; doc.getElementById("addon-installed-notification-message") .innerHTML = msg2; } } }; popups.show(target, "addon-installed", "", "addons-notification-icon", action, null, options); }, }; EventEmitter.decorate(ExtensionsUI);