forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			421 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			421 lines
		
	
	
	
		
			15 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/. */
 | |
| 
 | |
| /*
 | |
|  * @fileOverview
 | |
|  * This module exports a class that can be used to handle displaying a popup
 | |
|  * doorhanger with a primary action to not show a popup for this extension again
 | |
|  * and a secondary action disables the addon, or brings the user to their settings.
 | |
|  *
 | |
|  * The original purpose of the popup was to notify users of an extension that has
 | |
|  * changed the New Tab or homepage. Users would see this popup the first time they
 | |
|  * view those pages after a change to the setting in each session until they confirm
 | |
|  * the change by triggering the primary action.
 | |
|  */
 | |
| 
 | |
| import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
 | |
|   BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
 | |
|   CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
 | |
|   ExtensionSettingsStore:
 | |
|     "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
| });
 | |
| 
 | |
| let { makeWidgetId } = ExtensionCommon;
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "strBundle", function () {
 | |
|   return Services.strings.createBundle(
 | |
|     "chrome://global/locale/extensions.properties"
 | |
|   );
 | |
| });
 | |
| 
 | |
| const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "distributionAddonsList", function () {
 | |
|   let addonList = Services.prefs
 | |
|     .getChildList(PREF_BRANCH_INSTALLED_ADDON)
 | |
|     .map(id => id.replace(PREF_BRANCH_INSTALLED_ADDON, ""));
 | |
|   return new Set(addonList);
 | |
| });
 | |
| 
 | |
| export class ExtensionControlledPopup {
 | |
|   /* Provide necessary options for the popup.
 | |
|    *
 | |
|    * @param {object} opts Options for configuring popup.
 | |
|    * @param {string} opts.confirmedType
 | |
|    *                 The type to use for storing a user's confirmation in
 | |
|    *                 ExtensionSettingsStore.
 | |
|    * @param {string} opts.observerTopic
 | |
|    *                 An observer topic to trigger the popup on with Services.obs. If the
 | |
|    *                 doorhanger should appear on a specific window include it as the
 | |
|    *                 subject in the observer event.
 | |
|    * @param {string} opts.anchorId
 | |
|    *                 The id to anchor the popupnotification on. If it is not provided
 | |
|    *                 then it will anchor to a browser action or the app menu.
 | |
|    * @param {string} opts.popupnotificationId
 | |
|    *                 The id for the popupnotification element in the markup. This
 | |
|    *                 element should be defined in panelUI.inc.xhtml.
 | |
|    * @param {string} opts.settingType
 | |
|    *                 The setting type to check in ExtensionSettingsStore to retrieve
 | |
|    *                 the controlling extension.
 | |
|    * @param {string} opts.settingKey
 | |
|    *                 The setting key to check in ExtensionSettingsStore to retrieve
 | |
|    *                 the controlling extension.
 | |
|    * @param {string} opts.descriptionId
 | |
|    *                 The id of the element where the description should be displayed.
 | |
|    * @param {string} opts.descriptionMessageId
 | |
|    *                 The message id to be used for the description. The translated
 | |
|    *                 string will have the add-on's name and icon injected into it.
 | |
|    * @param {string} opts.getLocalizedDescription
 | |
|    *                 A function to get the localized message string. This
 | |
|    *                 function is passed doc, message and addonDetails (the
 | |
|    *                 add-on's icon and name). If not provided, then the add-on's
 | |
|    *                 icon and name are added to the description.
 | |
|    * @param {string} opts.learnMoreLink
 | |
|    *                 The name of the SUMO page to link to, this is added to
 | |
|    *                 app.support.baseURL.
 | |
|    * @param optional {string} opts.preferencesLocation
 | |
|    *                 If included, the name of the preferences tab that will be opened
 | |
|    *                 by the secondary action. If not included, the secondary option will
 | |
|    *                 disable the addon.
 | |
|    * @param optional {string} opts.preferencesEntrypoint
 | |
|    *                 The entrypoint to pass to preferences telemetry.
 | |
|    * @param {function} opts.onObserverAdded
 | |
|    *                   A callback that is triggered when an observer is registered to
 | |
|    *                   trigger the popup on the next observerTopic.
 | |
|    * @param {function} opts.onObserverRemoved
 | |
|    *                   A callback that is triggered when the observer is removed,
 | |
|    *                   either because the popup is opening or it was explicitly
 | |
|    *                   cancelled by calling removeObserver.
 | |
|    * @param {function} opts.beforeDisableAddon
 | |
|    *                   A function that is called before disabling an extension when the
 | |
|    *                   user decides to disable the extension. If this function is async
 | |
|    *                   then the extension won't be disabled until it is fulfilled.
 | |
|    *                   This function gets two arguments, the ExtensionControlledPopup
 | |
|    *                   instance for the panel and the window that the popup appears on.
 | |
|    */
 | |
|   constructor(opts) {
 | |
|     this.confirmedType = opts.confirmedType;
 | |
|     this.observerTopic = opts.observerTopic;
 | |
|     this.anchorId = opts.anchorId;
 | |
|     this.popupnotificationId = opts.popupnotificationId;
 | |
|     this.settingType = opts.settingType;
 | |
|     this.settingKey = opts.settingKey;
 | |
|     this.descriptionId = opts.descriptionId;
 | |
|     this.descriptionMessageId = opts.descriptionMessageId;
 | |
|     this.getLocalizedDescription = opts.getLocalizedDescription;
 | |
|     this.learnMoreLink = opts.learnMoreLink;
 | |
|     this.preferencesLocation = opts.preferencesLocation;
 | |
|     this.preferencesEntrypoint = opts.preferencesEntrypoint;
 | |
|     this.onObserverAdded = opts.onObserverAdded;
 | |
|     this.onObserverRemoved = opts.onObserverRemoved;
 | |
|     this.beforeDisableAddon = opts.beforeDisableAddon;
 | |
|     this.observerRegistered = false;
 | |
|   }
 | |
| 
 | |
|   get topWindow() {
 | |
|     return Services.wm.getMostRecentWindow("navigator:browser");
 | |
|   }
 | |
| 
 | |
|   userHasConfirmed(id) {
 | |
|     // We don't show a doorhanger for distribution installed add-ons.
 | |
|     if (lazy.distributionAddonsList.has(id)) {
 | |
|       return true;
 | |
|     }
 | |
|     let setting = lazy.ExtensionSettingsStore.getSetting(
 | |
|       this.confirmedType,
 | |
|       id
 | |
|     );
 | |
|     return !!(setting && setting.value);
 | |
|   }
 | |
| 
 | |
|   async setConfirmation(id) {
 | |
|     await lazy.ExtensionSettingsStore.initialize();
 | |
|     return lazy.ExtensionSettingsStore.addSetting(
 | |
|       id,
 | |
|       this.confirmedType,
 | |
|       id,
 | |
|       true,
 | |
|       () => false
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   async clearConfirmation(id) {
 | |
|     await lazy.ExtensionSettingsStore.initialize();
 | |
|     return lazy.ExtensionSettingsStore.removeSetting(
 | |
|       id,
 | |
|       this.confirmedType,
 | |
|       id
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   observe(subject, topic, data) {
 | |
|     // Remove the observer here so we don't get multiple open() calls if we get
 | |
|     // multiple observer events in quick succession.
 | |
|     this.removeObserver();
 | |
| 
 | |
|     let targetWindow;
 | |
|     // Some notifications (e.g. browser-open-newtab-start) do not have a window subject.
 | |
|     if (subject && subject.document) {
 | |
|       targetWindow = subject;
 | |
|     }
 | |
| 
 | |
|     // Do this work in an idle callback to avoid interfering with new tab performance tracking.
 | |
|     this.topWindow.requestIdleCallback(() => this.open(targetWindow));
 | |
|   }
 | |
| 
 | |
|   removeObserver() {
 | |
|     if (this.observerRegistered) {
 | |
|       Services.obs.removeObserver(this, this.observerTopic);
 | |
|       this.observerRegistered = false;
 | |
|       if (this.onObserverRemoved) {
 | |
|         this.onObserverRemoved();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async addObserver(extensionId) {
 | |
|     await lazy.ExtensionSettingsStore.initialize();
 | |
| 
 | |
|     if (!this.observerRegistered && !this.userHasConfirmed(extensionId)) {
 | |
|       Services.obs.addObserver(this, this.observerTopic);
 | |
|       this.observerRegistered = true;
 | |
|       if (this.onObserverAdded) {
 | |
|         this.onObserverAdded();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // The extensionId will be looked up in ExtensionSettingsStore if it is not
 | |
|   // provided using this.settingType and this.settingKey.
 | |
|   async open(targetWindow, extensionId) {
 | |
|     await lazy.ExtensionSettingsStore.initialize();
 | |
| 
 | |
|     // Remove the observer since it would open the same dialog again the next time
 | |
|     // the observer event fires.
 | |
|     this.removeObserver();
 | |
| 
 | |
|     if (!extensionId) {
 | |
|       let item = lazy.ExtensionSettingsStore.getSetting(
 | |
|         this.settingType,
 | |
|         this.settingKey
 | |
|       );
 | |
|       extensionId = item && item.id;
 | |
|     }
 | |
| 
 | |
|     let win = targetWindow || this.topWindow;
 | |
|     let isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(win);
 | |
|     if (
 | |
|       isPrivate &&
 | |
|       extensionId &&
 | |
|       !WebExtensionPolicy.getByID(extensionId).privateBrowsingAllowed
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // The item should have an extension and the user shouldn't have confirmed
 | |
|     // the change here, but just to be sure check that it is still controlled
 | |
|     // and the user hasn't already confirmed the change.
 | |
|     // If there is no id, then the extension is no longer in control.
 | |
|     if (!extensionId || this.userHasConfirmed(extensionId)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // If the window closes while waiting for focus, this might reject/throw,
 | |
|     // and we should stop trying to show the popup.
 | |
|     try {
 | |
|       await this._ensureWindowReady(win);
 | |
|     } catch (ex) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     win.ownerGlobal.ensureCustomElements("moz-support-link");
 | |
| 
 | |
|     // Find the elements we need.
 | |
|     let doc = win.document;
 | |
|     let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(doc);
 | |
|     let popupnotification = doc.getElementById(this.popupnotificationId);
 | |
|     let urlBarWasFocused = win.gURLBar.focused;
 | |
| 
 | |
|     if (!popupnotification) {
 | |
|       throw new Error(
 | |
|         `No popupnotification found for id "${this.popupnotificationId}"`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     let elementsToTranslate = panel.querySelectorAll("[data-lazy-l10n-id]");
 | |
|     if (elementsToTranslate.length) {
 | |
|       win.MozXULElement.insertFTLIfNeeded("browser/appMenuNotifications.ftl");
 | |
|       for (let el of elementsToTranslate) {
 | |
|         el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
 | |
|         el.removeAttribute("data-lazy-l10n-id");
 | |
|       }
 | |
|       await win.document.l10n.translateFragment(panel);
 | |
|     }
 | |
|     let addon = await lazy.AddonManager.getAddonByID(extensionId);
 | |
|     this.populateDescription(doc, addon);
 | |
| 
 | |
|     // Setup the command handler.
 | |
|     let handleCommand = async event => {
 | |
|       panel.hidePopup();
 | |
|       if (event.originalTarget == popupnotification.button) {
 | |
|         // Main action is to keep changes.
 | |
|         await this.setConfirmation(extensionId);
 | |
|       } else if (this.preferencesLocation) {
 | |
|         // Secondary action opens Preferences, if a preferencesLocation option is included.
 | |
|         let options = this.Entrypoint
 | |
|           ? { urlParams: { entrypoint: this.Entrypoint } }
 | |
|           : {};
 | |
|         win.openPreferences(this.preferencesLocation, options);
 | |
|       } else {
 | |
|         // Secondary action is to restore settings.
 | |
|         if (this.beforeDisableAddon) {
 | |
|           await this.beforeDisableAddon(this, win);
 | |
|         }
 | |
|         await addon.disable();
 | |
|       }
 | |
| 
 | |
|       // If the page this is appearing on is the New Tab page then the URL bar may
 | |
|       // have been focused when the doorhanger stole focus away from it. Once an
 | |
|       // action is taken the focus state should be restored to what the user was
 | |
|       // expecting.
 | |
|       if (urlBarWasFocused) {
 | |
|         win.gURLBar.focus();
 | |
|       }
 | |
|     };
 | |
|     panel.addEventListener("command", handleCommand);
 | |
|     panel.addEventListener(
 | |
|       "popuphidden",
 | |
|       () => {
 | |
|         popupnotification.hidden = true;
 | |
|         panel.removeEventListener("command", handleCommand);
 | |
|       },
 | |
|       { once: true }
 | |
|     );
 | |
| 
 | |
|     let anchorButton;
 | |
|     if (this.anchorId) {
 | |
|       // If there's an anchorId, use that right away.
 | |
|       anchorButton = doc.getElementById(this.anchorId);
 | |
|     } else {
 | |
|       // Look for a browserAction on the toolbar.
 | |
|       let action = lazy.CustomizableUI.getWidget(
 | |
|         `${makeWidgetId(extensionId)}-browser-action`
 | |
|       );
 | |
|       if (action) {
 | |
|         action =
 | |
|           action.areaType == "toolbar" &&
 | |
|           action.forWindow(win).node.firstElementChild;
 | |
|       }
 | |
| 
 | |
|       // Anchor to a toolbar browserAction if found, otherwise use the menu button.
 | |
|       anchorButton = action || doc.getElementById("PanelUI-menu-button");
 | |
|     }
 | |
|     let anchor = anchorButton.icon;
 | |
|     popupnotification.show();
 | |
|     panel.openPopup(anchor);
 | |
|   }
 | |
| 
 | |
|   getAddonDetails(doc, addon) {
 | |
|     const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 | |
| 
 | |
|     let image = doc.createXULElement("image");
 | |
|     image.setAttribute("src", addon.iconURL || defaultIcon);
 | |
|     image.classList.add("extension-controlled-icon");
 | |
| 
 | |
|     let addonDetails = doc.createDocumentFragment();
 | |
|     addonDetails.appendChild(image);
 | |
|     addonDetails.appendChild(doc.createTextNode(" " + addon.name));
 | |
| 
 | |
|     return addonDetails;
 | |
|   }
 | |
| 
 | |
|   populateDescription(doc, addon) {
 | |
|     let description = doc.getElementById(this.descriptionId);
 | |
|     description.textContent = "";
 | |
| 
 | |
|     let addonDetails = this.getAddonDetails(doc, addon);
 | |
|     let message = lazy.strBundle.GetStringFromName(this.descriptionMessageId);
 | |
|     if (this.getLocalizedDescription) {
 | |
|       description.appendChild(
 | |
|         this.getLocalizedDescription(doc, message, addonDetails)
 | |
|       );
 | |
|     } else {
 | |
|       description.appendChild(
 | |
|         lazy.BrowserUIUtils.getLocalizedFragment(doc, message, addonDetails)
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     let link = doc.createElement("a", { is: "moz-support-link" });
 | |
|     link.setAttribute("class", "learnMore");
 | |
|     link.setAttribute("support-page", this.learnMoreLink);
 | |
|     description.appendChild(link);
 | |
|   }
 | |
| 
 | |
|   async _ensureWindowReady(win) {
 | |
|     if (win.closed) {
 | |
|       throw new Error("window is closed");
 | |
|     }
 | |
|     let promises = [];
 | |
|     let listenersToRemove = [];
 | |
|     function promiseEvent(type) {
 | |
|       promises.push(
 | |
|         new Promise(resolve => {
 | |
|           let listener = () => {
 | |
|             win.removeEventListener(type, listener);
 | |
|             resolve();
 | |
|           };
 | |
|           win.addEventListener(type, listener);
 | |
|           listenersToRemove.push([type, listener]);
 | |
|         })
 | |
|       );
 | |
|     }
 | |
|     let { focusedWindow, activeWindow } = Services.focus;
 | |
|     if (activeWindow != win) {
 | |
|       promiseEvent("activate");
 | |
|     }
 | |
|     if (focusedWindow) {
 | |
|       // We may have focused a non-remote child window, find the browser window:
 | |
|       let { rootTreeItem } = focusedWindow.docShell;
 | |
|       rootTreeItem.QueryInterface(Ci.nsIDocShell);
 | |
|       focusedWindow = rootTreeItem.docViewer.DOMDocument.defaultView;
 | |
|     }
 | |
|     if (focusedWindow != win) {
 | |
|       promiseEvent("focus");
 | |
|     }
 | |
|     if (promises.length) {
 | |
|       let unloadListener;
 | |
|       let unloadPromise = new Promise((resolve, reject) => {
 | |
|         unloadListener = () => {
 | |
|           for (let [type, listener] of listenersToRemove) {
 | |
|             win.removeEventListener(type, listener);
 | |
|           }
 | |
|           reject(new Error("window unloaded"));
 | |
|         };
 | |
|         win.addEventListener("unload", unloadListener, { once: true });
 | |
|       });
 | |
|       try {
 | |
|         let allPromises = Promise.all(promises);
 | |
|         await Promise.race([allPromises, unloadPromise]);
 | |
|       } finally {
 | |
|         win.removeEventListener("unload", unloadListener);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   static _getAndMaybeCreatePanel(doc) {
 | |
|     // // Lazy load the extension-notification panel the first time we need to display it.
 | |
|     let template = doc.getElementById("extensionNotificationTemplate");
 | |
|     if (template) {
 | |
|       template.replaceWith(template.content);
 | |
|     }
 | |
| 
 | |
|     return doc.getElementById("extension-notification-panel");
 | |
|   }
 | |
| }
 | 
