forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			390 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			390 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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 BROWSER_PROPERTIES = "chrome://browser/locale/browser.properties";
 | |
| const BRAND_PROPERTIES = "chrome://browser/locale/brand.properties";
 | |
| 
 | |
| 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, strings, icon) {
 | |
|     let global = browser.selectedBrowser.ownerGlobal;
 | |
|     return global.BrowserOpenAddonsMgr("addons://list/extension").then(aomWin => {
 | |
|       let aomBrowser = aomWin.QueryInterface(Ci.nsIInterfaceRequestor)
 | |
|                              .getInterface(Ci.nsIDocShell)
 | |
|                              .chromeEventHandler;
 | |
|       return this.showPermissionsPrompt(aomBrowser, strings, icon);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   showSideloaded(browser, addon) {
 | |
|     addon.markAsSeen();
 | |
|     this.sideloaded.delete(addon);
 | |
|     this.emit("change");
 | |
| 
 | |
|     let strings = this._buildStrings({
 | |
|       addon,
 | |
|       permissions: addon.userPermissions,
 | |
|       type: "sideload",
 | |
|     });
 | |
|     this.showAddonsManager(browser, strings, addon.iconURL).then(answer => {
 | |
|       addon.userDisabled = !answer;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   showUpdate(browser, info) {
 | |
|     this.showAddonsManager(browser, info.strings, info.addon.iconURL)
 | |
|         .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 => {
 | |
|         if (answer) {
 | |
|           info.resolve();
 | |
|         } else {
 | |
|           info.reject();
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       let perms = info.addon.userPermissions;
 | |
|       if (!perms) {
 | |
|         reply(true);
 | |
|       } else {
 | |
|         info.permissions = perms;
 | |
|         let strings = this._buildStrings(info);
 | |
|         this.showPermissionsPrompt(target, strings, info.icon).then(reply);
 | |
|       }
 | |
|     } else if (topic == "webextension-update-permissions") {
 | |
|       let info = subject.wrappedJSObject;
 | |
|       info.type = "update";
 | |
|       let strings = this._buildStrings(info);
 | |
| 
 | |
|       // If we don't prompt for any new permissions, just apply it
 | |
|       if (strings.msgs.length == 0) {
 | |
|         info.resolve();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let update = {
 | |
|         strings,
 | |
|         addon: info.addon,
 | |
|         resolve: info.resolve,
 | |
|         reject: info.reject,
 | |
|       };
 | |
| 
 | |
|       this.updates.add(update);
 | |
|       this.emit("change");
 | |
|     } else if (topic == "webextension-install-notify") {
 | |
|       let {target, addon, callback} = subject.wrappedJSObject;
 | |
|       this.showInstallNotification(target, addon).then(() => {
 | |
|         if (callback) {
 | |
|           callback();
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Escape &, <, and > characters in a string so that it may be
 | |
|   // injected as part of raw markup.
 | |
|   _sanitizeName(name) {
 | |
|     return name.replace(/&/g, "&")
 | |
|                .replace(/</g, "<")
 | |
|                .replace(/>/g, ">");
 | |
|   },
 | |
| 
 | |
|   // Create a set of formatted strings for a permission prompt
 | |
|   _buildStrings(info) {
 | |
|     let result = {};
 | |
| 
 | |
|     let bundle = Services.strings.createBundle(BROWSER_PROPERTIES);
 | |
| 
 | |
|     let name = info.addon.name;
 | |
|     if (name.length > 50) {
 | |
|       name = name.slice(0, 49) + "…";
 | |
|     }
 | |
|     name = this._sanitizeName(name);
 | |
|     let addonName = `<span class="addon-webext-name">${name}</span>`;
 | |
| 
 | |
|     result.header = bundle.formatStringFromName("webextPerms.header", [addonName], 1);
 | |
|     result.text = "";
 | |
|     result.listIntro = bundle.GetStringFromName("webextPerms.listIntro");
 | |
| 
 | |
|     result.acceptText = bundle.GetStringFromName("webextPerms.add.label");
 | |
|     result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey");
 | |
|     result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label");
 | |
|     result.cancelKey = bundle.GetStringFromName("webextPerms.cancel.accessKey");
 | |
| 
 | |
|     if (info.type == "sideload") {
 | |
|       result.header = bundle.formatStringFromName("webextPerms.sideloadHeader", [addonName], 1);
 | |
|       result.text = bundle.GetStringFromName("webextPerms.sideloadText2");
 | |
|       result.acceptText = bundle.GetStringFromName("webextPerms.sideloadEnable.label");
 | |
|       result.acceptKey = bundle.GetStringFromName("webextPerms.sideloadEnable.accessKey");
 | |
|       result.cancelText = bundle.GetStringFromName("webextPerms.sideloadCancel.label");
 | |
|       result.cancelKey = bundle.GetStringFromName("webextPerms.sideloadCancel.accessKey");
 | |
|     } else if (info.type == "update") {
 | |
|       result.header = "";
 | |
|       result.text = bundle.formatStringFromName("webextPerms.updateText", [addonName], 1);
 | |
|       result.acceptText = bundle.GetStringFromName("webextPerms.updateAccept.label");
 | |
|       result.acceptKey = bundle.GetStringFromName("webextPerms.updateAccept.accessKey");
 | |
|     }
 | |
| 
 | |
|     let perms = info.permissions || {hosts: [], permissions: []};
 | |
| 
 | |
|     result.msgs = [];
 | |
|     for (let permission of perms.permissions) {
 | |
|       let key = `webextPerms.description.${permission}`;
 | |
|       if (permission == "nativeMessaging") {
 | |
|         let brandBundle = Services.strings.createBundle(BRAND_PROPERTIES);
 | |
|         let appName = brandBundle.GetStringFromName("brandShortName");
 | |
|         result.msgs.push(bundle.formatStringFromName(key, [appName], 1));
 | |
|       } else {
 | |
|         try {
 | |
|           result.msgs.push(bundle.GetStringFromName(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 == "<all_urls>") {
 | |
|         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) {
 | |
|       result.msgs.push(bundle.GetStringFromName("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) {
 | |
|           result.msgs.push(...items.map(item => bundle.formatStringFromName(itemKey, [item], 1)));
 | |
|         }
 | |
|         if (list.length < 5) {
 | |
|           formatItems(list);
 | |
|         } else {
 | |
|           formatItems(list.slice(0, 3));
 | |
| 
 | |
|           let remaining = list.length - 3;
 | |
|           result.msgs.push(PluralForm.get(remaining, bundle.GetStringFromName(moreKey))
 | |
|                                      .replace("#1", remaining));
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       format(wildcards, "webextPerms.hostDescription.wildcard",
 | |
|              "webextPerms.hostDescription.tooManyWildcards");
 | |
|       format(sites, "webextPerms.hostDescription.oneSite",
 | |
|              "webextPerms.hostDescription.tooManySites");
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
|   },
 | |
| 
 | |
|   showPermissionsPrompt(browser, strings, icon) {
 | |
|     function eventCallback(topic) {
 | |
|       if (topic == "showing") {
 | |
|         let doc = this.browser.ownerDocument;
 | |
|         doc.getElementById("addon-webext-perm-header").innerHTML = strings.header;
 | |
| 
 | |
|         let textEl = doc.getElementById("addon-webext-perm-text");
 | |
|         textEl.innerHTML = strings.text;
 | |
|         textEl.hidden = !strings.text;
 | |
| 
 | |
|         let listIntroEl = doc.getElementById("addon-webext-perm-intro");
 | |
|         listIntroEl.value = strings.listIntro;
 | |
|         listIntroEl.hidden = (strings.msgs.length == 0);
 | |
| 
 | |
|         let list = doc.getElementById("addon-webext-perm-list");
 | |
|         while (list.firstChild) {
 | |
|           list.firstChild.remove();
 | |
|         }
 | |
| 
 | |
|         for (let msg of strings.msgs) {
 | |
|           let item = doc.createElementNS(HTML_NS, "li");
 | |
|           item.textContent = msg;
 | |
|           list.appendChild(item);
 | |
|         }
 | |
|       } else if (topic == "swapping") {
 | |
|         return true;
 | |
|       }
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     let popupOptions = {
 | |
|       hideClose: true,
 | |
|       popupIconURL: icon || DEFAULT_EXTENSION_ICON,
 | |
|       persistent: true,
 | |
|       eventCallback,
 | |
|     };
 | |
| 
 | |
|     let win = browser.ownerGlobal;
 | |
|     return new Promise(resolve => {
 | |
|       let action = {
 | |
|         label: strings.acceptText,
 | |
|         accessKey: strings.acceptKey,
 | |
|         callback: () => resolve(true),
 | |
|       };
 | |
|       let secondaryActions = [
 | |
|         {
 | |
|           label: strings.cancelText,
 | |
|           accessKey: strings.cancelKey,
 | |
|           callback: () => resolve(false),
 | |
|         },
 | |
|       ];
 | |
| 
 | |
|       win.PopupNotifications.show(browser, "addon-webext-permissions", "",
 | |
|                                   "addons-notification-icon",
 | |
|                                   action, secondaryActions, popupOptions);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   showInstallNotification(target, addon) {
 | |
|     let win = target.ownerGlobal;
 | |
|     let popups = win.PopupNotifications;
 | |
| 
 | |
|     let name = this._sanitizeName(addon.name);
 | |
|     let addonName = `<span class="addon-webext-name">${name}</span>`;
 | |
|     let addonIcon = '<image class="addon-addon-icon"/>';
 | |
|     let toolbarIcon = '<image class="addon-toolbar-icon"/>';
 | |
| 
 | |
|     let brandBundle = win.document.getElementById("bundle_brand");
 | |
|     let appName = brandBundle.getString("brandShortName");
 | |
| 
 | |
|     let bundle = win.gNavigatorBundle;
 | |
|     let msg1 = bundle.getFormattedString("addonPostInstall.message1",
 | |
|                                          [addonName, appName]);
 | |
|     let msg2 = bundle.getFormattedString("addonPostInstall.messageDetail",
 | |
|                                          [addonIcon, toolbarIcon]);
 | |
| 
 | |
|     return new Promise(resolve => {
 | |
|       let action = {
 | |
|         label: bundle.getString("addonPostInstall.okay.label"),
 | |
|         accessKey: bundle.getString("addonPostInstall.okay.key"),
 | |
|         callback: resolve,
 | |
|       };
 | |
| 
 | |
|       let icon = addon.isWebExtension ?
 | |
|                  addon.iconURL || DEFAULT_EXTENSION_ICON :
 | |
|                  "chrome://browser/skin/addons/addon-install-installed.svg";
 | |
|       let options = {
 | |
|         hideClose: true,
 | |
|         timeout: Date.now() + 30000,
 | |
|         popupIconURL: 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;
 | |
|           } else if (topic == "dismissed") {
 | |
|             resolve();
 | |
|           }
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       popups.show(target, "addon-installed", "", "addons-notification-icon",
 | |
|                   action, null, options);
 | |
|     });
 | |
|   },
 | |
| };
 | |
| 
 | |
| EventEmitter.decorate(ExtensionsUI);
 | 
