Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  ExtensionsUI.sys.mjs   Sprache: unbekannt

 
/* 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/. */

import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs",
  AMTelemetry: "resource://gre/modules/AddonManager.sys.mjs",
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
  AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
  ExtensionData: "resource://gre/modules/Extension.sys.mjs",
  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
  OriginControls: "resource://gre/modules/ExtensionPermissions.sys.mjs",
  QuarantinedDomains: "resource://gre/modules/ExtensionPermissions.sys.mjs",
});

ChromeUtils.defineLazyGetter(
  lazy,
  "l10n",
  () =>
    new Localization(["browser/extensionsUI.ftl", "branding/brand.ftl"], true)
);

ChromeUtils.defineLazyGetter(lazy, "logConsole", () =>
  console.createInstance({
    prefix: "ExtensionsUI",
    maxLogLevelPref: "extensions.webextensions.log.level",
  })
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "POSTINSTALL_PRIVATEBROWSING_CHECKBOX",
  "extensions.ui.postInstallPrivateBrowsingCheckbox",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "SHOW_FULL_DOMAINS_LIST",
  "extensions.ui.installDialogFullDomains",
  true
);

const DEFAULT_EXTENSION_ICON =
  "chrome://mozapps/skin/extensions/extensionGeneric.svg";

function getTabBrowser(browser) {
  while (browser.ownerGlobal.docShell.itemType !== Ci.nsIDocShell.typeChrome) {
    browser = browser.ownerGlobal.docShell.chromeEventHandler;
  }
  let window = browser.ownerGlobal;
  let viewType = browser.getAttribute("webextension-view-type");
  if (viewType == "sidebar") {
    window = window.browsingContext.topChromeWindow;
  }
  if (viewType == "popup" || viewType == "sidebar") {
    browser = window.gBrowser.selectedBrowser;
  }
  return { browser, window };
}

export var ExtensionsUI = {
  sideloaded: new Set(),
  updates: new Set(),
  sideloadListener: null,

  pendingNotifications: new WeakMap(),

  get SHOW_FULL_DOMAINS_LIST() {
    return lazy.SHOW_FULL_DOMAINS_LIST;
  },

  get POSTINSTALL_PRIVATEBROWSING_CHECKBOX() {
    return lazy.POSTINSTALL_PRIVATEBROWSING_CHECKBOX;
  },

  async init() {
    Services.obs.addObserver(this, "webextension-permission-prompt");
    Services.obs.addObserver(this, "webextension-update-permissions");
    Services.obs.addObserver(this, "webextension-install-notify");
    Services.obs.addObserver(this, "webextension-optional-permission-prompt");
    Services.obs.addObserver(this, "webextension-defaultsearch-prompt");
    Services.obs.addObserver(this, "webextension-imported-addons-cancelled");
    Services.obs.addObserver(this, "webextension-imported-addons-complete");
    Services.obs.addObserver(this, "webextension-imported-addons-pending");

    await Services.wm.getMostRecentWindow("navigator:browser")
      .delayedStartupPromise;

    this._checkForSideloaded();
  },

  async _checkForSideloaded() {
    let sideloaded = await lazy.AddonManagerPrivate.getNewSideloads();

    if (!sideloaded.length) {
      // No new side-loads. We're done.
      return;
    }

    // The ordering shouldn't matter, but tests depend on notifications
    // happening in a specific order.
    sideloaded.sort((a, b) => a.id.localeCompare(b.id));

    if (!this.sideloadListener) {
      this.sideloadListener = {
        onEnabled: addon => {
          if (!this.sideloaded.has(addon)) {
            return;
          }

          this.sideloaded.delete(addon);
          this._updateNotifications();

          if (this.sideloaded.size == 0) {
            lazy.AddonManager.removeAddonListener(this.sideloadListener);
            this.sideloadListener = null;
          }
        },
      };
      lazy.AddonManager.addAddonListener(this.sideloadListener);
    }

    for (let addon of sideloaded) {
      this.sideloaded.add(addon);
    }
    this._updateNotifications();
  },

  _updateNotifications() {
    const { sideloaded, updates } = this;
    const { importedAddonIDs } = lazy.AMBrowserExtensionsImport;

    if (importedAddonIDs.length + sideloaded.size + updates.size == 0) {
      lazy.AppMenuNotifications.removeNotification("addon-alert");
    } else {
      lazy.AppMenuNotifications.showBadgeOnlyNotification("addon-alert");
    }
    this.emit("change");
  },

  showAddonsManager(
    tabbrowser,
    strings,
    icon,
    addon = undefined,
    shouldShowIncognitoCheckbox = false
  ) {
    let global = tabbrowser.selectedBrowser.ownerGlobal;
    return global.BrowserAddonUI.openAddonsMgr("addons://list/extension").then(
      aomWin => {
        let aomBrowser = aomWin.docShell.chromeEventHandler;
        return this.showPermissionsPrompt(
          aomBrowser,
          strings,
          icon,
          addon,
          shouldShowIncognitoCheckbox
        );
      }
    );
  },

  showSideloaded(tabbrowser, addon) {
    addon.markAsSeen();
    this.sideloaded.delete(addon);
    this._updateNotifications();

    let strings = this._buildStrings({
      addon,
      permissions: addon.installPermissions,
      type: "sideload",
    });

    lazy.AMTelemetry.recordManageEvent(addon, "sideload_prompt", {
      num_strings: strings.msgs.length,
    });

    this.showAddonsManager(
      tabbrowser,
      strings,
      addon.iconURL,
      addon,
      true /* shouldShowIncognitoCheckbox */
    ).then(async answer => {
      if (answer) {
        await addon.enable();

        this._updateNotifications();

        // The user has just enabled a sideloaded extension, if the permission
        // can be changed for the extension, show the post-install panel to
        // give the user that opportunity.
        if (
          ExtensionsUI.POSTINSTALL_PRIVATEBROWSING_CHECKBOX &&
          addon.permissions &
            lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
        ) {
          this.showInstallNotification(tabbrowser.selectedBrowser, addon);
        }
      }
      this.emit("sideload-response");
    });
  },

  showUpdate(browser, info) {
    lazy.AMTelemetry.recordInstallEvent(info.install, {
      step: "permissions_prompt",
      num_strings: info.strings.msgs.length,
    });

    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._updateNotifications();
      }
    );
  },

  observe(subject, topic) {
    if (topic == "webextension-permission-prompt") {
      let { target, info } = subject.wrappedJSObject;

      let { browser, window } = getTabBrowser(target);

      // 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 = window.PopupNotifications.getNotification(
        "addon-progress",
        browser
      );
      if (progressNotification) {
        progressNotification.remove();
      }

      info.unsigned =
        info.addon.signedState <= lazy.AddonManager.SIGNEDSTATE_MISSING;
      if (
        info.unsigned &&
        Cu.isInAutomation &&
        Services.prefs.getBoolPref(
          "extensions.ui.showAddonIconForUnsigned",
          false
        )
      ) {
        info.unsigned = false;
      }

      let strings = this._buildStrings(info);

      // If this is an update with no promptable permissions, just apply it
      if (info.type == "update" && !strings.msgs.length) {
        info.resolve();
        return;
      }

      let icon = info.unsigned
        ? "chrome://global/skin/icons/warning.svg"
        : info.icon;

      if (info.type == "sideload") {
        lazy.AMTelemetry.recordManageEvent(info.addon, "sideload_prompt", {
          num_strings: strings.msgs.length,
        });
      } else {
        lazy.AMTelemetry.recordInstallEvent(info.install, {
          step: "permissions_prompt",
          num_strings: strings.msgs.length,
        });
      }

      this.showPermissionsPrompt(
        browser,
        strings,
        icon,
        info.addon,
        true /* shouldShowIncognitoCheckbox */
      ).then(answer => {
        if (answer) {
          info.resolve();
        } else {
          info.reject();
        }
      });
    } 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) {
        info.resolve();
        return;
      }

      let update = {
        strings,
        permissions: info.permissions,
        install: info.install,
        addon: info.addon,
        resolve: info.resolve,
        reject: info.reject,
      };

      this.updates.add(update);
      this._updateNotifications();
    } else if (topic == "webextension-install-notify") {
      let { target, addon, callback } = subject.wrappedJSObject;
      this.showInstallNotification(target, addon).then(() => {
        if (callback) {
          callback();
        }
      });
    } else if (topic == "webextension-optional-permission-prompt") {
      let { browser, name, icon, permissions, resolve } =
        subject.wrappedJSObject;
      let strings = this._buildStrings({
        type: "optional",
        addon: { name },
        permissions,
      });

      // If we don't have any promptable permissions, just proceed
      if (!strings.msgs.length) {
        resolve(true);
        return;
      }
      // "userScripts" is an OptionalOnlyPermission, which means that it can
      // only be requested through the permissions.request() API, without other
      // permissions in the same request.
      let isUserScriptsRequest =
        permissions.permissions.length === 1 &&
        permissions.permissions[0] === "userScripts";
      resolve(
        this.showPermissionsPrompt(
          browser,
          strings,
          icon,
          /* addon */ undefined,
          /* shouldShowIncognitoCheckbox */ false,
          isUserScriptsRequest
        )
      );
    } else if (topic == "webextension-defaultsearch-prompt") {
      let { browser, name, icon, respond, currentEngine, newEngine } =
        subject.wrappedJSObject;

      const [searchDesc, searchYes, searchNo] = lazy.l10n.formatMessagesSync([
        {
          id: "webext-default-search-description",
          args: { addonName: "<>", currentEngine, newEngine },
        },
        "webext-default-search-yes",
        "webext-default-search-no",
      ]);

      const strings = { addonName: name, text: searchDesc.value };
      for (let attr of searchYes.attributes) {
        if (attr.name === "label") {
          strings.acceptText = attr.value;
        } else if (attr.name === "accesskey") {
          strings.acceptKey = attr.value;
        }
      }
      for (let attr of searchNo.attributes) {
        if (attr.name === "label") {
          strings.cancelText = attr.value;
        } else if (attr.name === "accesskey") {
          strings.cancelKey = attr.value;
        }
      }

      this.showDefaultSearchPrompt(browser, strings, icon).then(respond);
    } else if (
      [
        "webextension-imported-addons-cancelled",
        "webextension-imported-addons-complete",
        "webextension-imported-addons-pending",
      ].includes(topic)
    ) {
      this._updateNotifications();
    }
  },

  // Create a set of formatted strings for a permission prompt
  _buildStrings(info) {
    const strings = lazy.ExtensionData.formatPermissionStrings(
      info,
      this.SHOW_FULL_DOMAINS_LIST
        ? { fullDomainsList: true }
        : { collapseOrigins: true }
    );
    strings.addonName = info.addon.name;
    return strings;
  },

  async showPermissionsPrompt(
    target,
    strings,
    icon,
    addon = undefined,
    shouldShowIncognitoCheckbox = false,
    isUserScriptsRequest = false
  ) {
    let { browser, window } = getTabBrowser(target);

    let showIncognitoCheckbox =
      shouldShowIncognitoCheckbox && !lazy.POSTINSTALL_PRIVATEBROWSING_CHECKBOX;

    if (showIncognitoCheckbox) {
      showIncognitoCheckbox = !!(
        addon.permissions &
        lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
      );
    }

    const incognitoPermissionName = "internal:privateBrowsingAllowed";
    let grantPrivateBrowsingAllowed = false;
    if (showIncognitoCheckbox) {
      const { permissions } = await lazy.ExtensionPermissions.get(addon.id);
      grantPrivateBrowsingAllowed = permissions.includes(
        incognitoPermissionName
      );
    }

    // Wait for any pending prompts to complete before showing the next one.
    let pending;
    while ((pending = this.pendingNotifications.get(browser))) {
      await pending;
    }

    let promise = new Promise(resolve => {
      function eventCallback(topic) {
        if (topic == "swapping") {
          return true;
        }
        if (topic == "removed") {
          Services.tm.dispatchToMainThread(() => {
            resolve(false);
          });
        }
        return false;
      }

      // Show the SUMO link already part of the popupnotification by
      // setting learnMoreURL option if there are permissions to be
      // granted to the addon being installed (or if the private
      // browsing checkbox is shown).
      const learnMoreURL =
        strings.msgs.length || showIncognitoCheckbox
          ? Services.urlFormatter.formatURLPref("app.support.baseURL") +
            "extension-permissions"
          : undefined;

      let options = {
        hideClose: true,
        popupIconURL: icon || DEFAULT_EXTENSION_ICON,
        popupIconClass: icon ? "" : "addon-warning-icon",
        learnMoreURL,
        persistent: true,
        eventCallback,
        removeOnDismissal: true,
        popupOptions: {
          position: "bottomright topright",
        },
        // Pass additional options used internally by the
        // addon-webext-permissions-notification custom element
        // (defined and registered by browser-addons.js).
        customElementOptions: {
          strings,
          showIncognitoCheckbox,
          grantPrivateBrowsingAllowed,
          onPrivateBrowsingAllowedChanged(value) {
            grantPrivateBrowsingAllowed = value;
          },
          isUserScriptsRequest,
        },
      };
      // The prompt/notification machinery has a special affordance wherein
      // certain subsets of the header string can be designated "names", and
      // referenced symbolically as "<>" and "{}" to receive special formatting.
      // That code assumes that the existence of |name| and |secondName| in the
      // options object imply the presence of "<>" and "{}" (respectively) in
      // in the string.
      //
      // At present, WebExtensions use this affordance while SitePermission
      // add-ons don't, so we need to conditionally set the |name| field.
      //
      // NB: This could potentially be cleaned up, see bug 1799710.
      if (strings.header.includes("<>")) {
        options.name = strings.addonName;
      }

      let action = {
        label: strings.acceptText,
        accessKey: strings.acceptKey,
        callback: () => {
          resolve(true);
        },
      };
      let secondaryActions = [
        {
          label: strings.cancelText,
          accessKey: strings.cancelKey,
          callback: () => {
            resolve(false);
          },
        },
      ];

      window.PopupNotifications.show(
        browser,
        "addon-webext-permissions",
        strings.header,
        browser.ownerGlobal.gUnifiedExtensions.getPopupAnchorID(
          browser,
          window
        ),
        action,
        secondaryActions,
        options
      );
    });

    this.pendingNotifications.set(browser, promise);
    promise.finally(() => this.pendingNotifications.delete(browser));
    // NOTE: this method is also called from showQuarantineConfirmation and some of its
    // related test cases (from browser_ext_originControls.js) seem to be hitting a race
    // if the promise returned requires an additional tick to be resolved.
    // Look more into the failure and determine a better option to avoid those failures.
    if (!showIncognitoCheckbox) {
      return promise;
    }
    return promise.then(continueInstall => {
      if (!continueInstall) {
        return continueInstall;
      }
      const incognitoPermission = {
        permissions: [incognitoPermissionName],
        origins: [],
      };
      let permUpdatePromise;
      if (grantPrivateBrowsingAllowed) {
        permUpdatePromise = lazy.ExtensionPermissions.add(
          addon.id,
          incognitoPermission
        ).catch(err =>
          lazy.logConsole.warn(
            `Error on adding "${incognitoPermissionName}" permission to addon id "${addon.id}`,
            err
          )
        );
      } else {
        permUpdatePromise = lazy.ExtensionPermissions.remove(
          addon.id,
          incognitoPermission
        ).catch(err =>
          lazy.logConsole.warn(
            `Error on removing "${incognitoPermissionName}" permission to addon id "${addon.id}`,
            err
          )
        );
      }
      return permUpdatePromise.then(() => continueInstall);
    });
  },

  showDefaultSearchPrompt(target, strings, icon) {
    return new Promise(resolve => {
      let options = {
        hideClose: true,
        popupIconURL: icon || DEFAULT_EXTENSION_ICON,
        persistent: true,
        removeOnDismissal: true,
        eventCallback(topic) {
          if (topic == "removed") {
            resolve(false);
          }
        },
        name: strings.addonName,
      };

      let action = {
        label: strings.acceptText,
        accessKey: strings.acceptKey,
        callback: () => {
          resolve(true);
        },
      };
      let secondaryActions = [
        {
          label: strings.cancelText,
          accessKey: strings.cancelKey,
          callback: () => {
            resolve(false);
          },
        },
      ];

      let { browser, window } = getTabBrowser(target);

      window.PopupNotifications.show(
        browser,
        "addon-webext-defaultsearch",
        strings.text,
        "addons-notification-icon",
        action,
        secondaryActions,
        options
      );
    });
  },

  async showInstallNotification(target, addon) {
    let { window } = getTabBrowser(target);

    const message = await lazy.l10n.formatValue("addon-post-install-message", {
      addonName: "<>",
    });

    const hideIncognitoCheckbox = !lazy.POSTINSTALL_PRIVATEBROWSING_CHECKBOX;
    const permissionName = "internal:privateBrowsingAllowed";
    const { permissions } = await lazy.ExtensionPermissions.get(addon.id);
    const hasIncognito = permissions.includes(permissionName);

    return new Promise(resolve => {
      let icon = addon.isWebExtension
        ? lazy.AddonManager.getPreferredIconURL(addon, 32, window) ||
          DEFAULT_EXTENSION_ICON
        : "chrome://browser/skin/addons/addon-install-installed.svg";

      if (addon.type == "theme") {
        const { previousActiveThemeID } = addon;

        async function themeActionUndo() {
          try {
            // Undoing a theme install means re-enabling the previous active theme
            // ID, and uninstalling the theme that was just installed
            const theme = await lazy.AddonManager.getAddonByID(
              previousActiveThemeID
            );

            if (theme) {
              await theme.enable();
            }

            // `addon` is the theme that was just installed
            await addon.uninstall();
          } finally {
            resolve();
          }
        }

        let themePrimaryAction = { callback: resolve };

        // Show the undo button if previousActiveThemeID is set.
        let themeSecondaryAction = previousActiveThemeID
          ? { callback: themeActionUndo }
          : null;

        let options = {
          name: addon.name,
          message,
          popupIconURL: icon,
          onDismissed: () => {
            lazy.AppMenuNotifications.removeNotification("theme-installed");
            resolve();
          },
        };
        lazy.AppMenuNotifications.showNotification(
          "theme-installed",
          themePrimaryAction,
          themeSecondaryAction,
          options
        );
      } else {
        // Show or hide private permission ui based on the pref.
        function setCheckbox(win) {
          let checkbox = win.document.getElementById(
            "addon-incognito-checkbox"
          );
          checkbox.checked = hasIncognito;
          checkbox.hidden =
            hideIncognitoCheckbox ||
            !(
              addon.permissions &
              lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
            );
        }

        async function actionResolve(win) {
          let checkbox = win.document.getElementById(
            "addon-incognito-checkbox"
          );

          if (hideIncognitoCheckbox || checkbox.checked == hasIncognito) {
            resolve();
            return;
          }

          let incognitoPermission = {
            permissions: [permissionName],
            origins: [],
          };

          // The checkbox has been changed at this point, otherwise we would
          // have exited early above.
          if (checkbox.checked) {
            await lazy.ExtensionPermissions.add(addon.id, incognitoPermission);
          } else if (hasIncognito) {
            await lazy.ExtensionPermissions.remove(
              addon.id,
              incognitoPermission
            );
          }
          // Reload the extension if it is already enabled.  This ensures any change
          // on the private browsing permission is properly handled.
          if (addon.isActive) {
            await addon.reload();
          }

          resolve();
        }

        let action = {
          callback: actionResolve,
        };

        let options = {
          name: addon.name,
          message,
          popupIconURL: icon,
          onRefresh: setCheckbox,
          onDismissed: win => {
            lazy.AppMenuNotifications.removeNotification("addon-installed");
            actionResolve(win);
          },
        };
        lazy.AppMenuNotifications.showNotification(
          "addon-installed",
          action,
          null,
          options
        );
      }
    });
  },

  async showQuarantineConfirmation(browser, policy) {
    let [title, line1, line2, allow, deny] = await lazy.l10n.formatMessages([
      {
        id: "webext-quarantine-confirmation-title",
        args: { addonName: "<>" },
      },
      "webext-quarantine-confirmation-line-1",
      "webext-quarantine-confirmation-line-2",
      "webext-quarantine-confirmation-allow",
      "webext-quarantine-confirmation-deny",
    ]);

    let attr = (msg, name) => msg.attributes.find(a => a.name === name)?.value;

    let strings = {
      addonName: policy.name,
      header: title.value,
      text: line1.value + "\n\n" + line2.value,
      msgs: [],
      acceptText: attr(allow, "label"),
      acceptKey: attr(allow, "accesskey"),
      cancelText: attr(deny, "label"),
      cancelKey: attr(deny, "accesskey"),
    };

    let icon = policy.extension?.getPreferredIcon(32);

    if (await ExtensionsUI.showPermissionsPrompt(browser, strings, icon)) {
      lazy.QuarantinedDomains.setUserAllowedAddonIdPref(policy.id, true);
    }
  },

  // Populate extension toolbar popup menu with origin controls.
  originControlsMenu(popup, extensionId) {
    let policy = WebExtensionPolicy.getByID(extensionId);

    let win = popup.ownerGlobal;
    let doc = popup.ownerDocument;
    let tab = win.gBrowser.selectedTab;
    let uri = tab.linkedBrowser?.currentURI;
    let state = lazy.OriginControls.getState(policy, tab);

    let headerItem = doc.createXULElement("menuitem");
    headerItem.setAttribute("disabled", true);
    let items = [headerItem];

    // MV2 normally don't have controls, but we show the quarantined state.
    if (!policy?.extension.originControls && !state.quarantined) {
      return;
    }

    if (state.noAccess) {
      doc.l10n.setAttributes(headerItem, "origin-controls-no-access");
    } else {
      doc.l10n.setAttributes(headerItem, "origin-controls-options");
    }

    if (state.quarantined) {
      doc.l10n.setAttributes(headerItem, "origin-controls-quarantined-status");

      let allowQuarantined = doc.createXULElement("menuitem");
      doc.l10n.setAttributes(
        allowQuarantined,
        "origin-controls-quarantined-allow"
      );
      allowQuarantined.addEventListener("command", () => {
        this.showQuarantineConfirmation(tab.linkedBrowser, policy);
      });
      items.push(allowQuarantined);
    }

    if (state.allDomains) {
      let allDomains = doc.createXULElement("menuitem");
      allDomains.setAttribute("type", "radio");
      allDomains.setAttribute("checked", state.hasAccess);
      doc.l10n.setAttributes(allDomains, "origin-controls-option-all-domains");
      items.push(allDomains);
    }

    if (state.whenClicked) {
      let whenClicked = doc.createXULElement("menuitem");
      whenClicked.setAttribute("type", "radio");
      whenClicked.setAttribute("checked", !state.hasAccess);
      doc.l10n.setAttributes(
        whenClicked,
        "origin-controls-option-when-clicked"
      );
      whenClicked.addEventListener("command", async () => {
        await lazy.OriginControls.setWhenClicked(policy, uri);
        win.gUnifiedExtensions.updateAttention();
      });
      items.push(whenClicked);
    }

    if (state.alwaysOn) {
      let alwaysOn = doc.createXULElement("menuitem");
      alwaysOn.setAttribute("type", "radio");
      alwaysOn.setAttribute("checked", state.hasAccess);
      doc.l10n.setAttributes(alwaysOn, "origin-controls-option-always-on", {
        domain: uri.host,
      });
      alwaysOn.addEventListener("command", async () => {
        await lazy.OriginControls.setAlwaysOn(policy, uri);
        win.gUnifiedExtensions.updateAttention();
      });
      items.push(alwaysOn);
    }

    items.push(doc.createXULElement("menuseparator"));

    // Insert all items before Pin to toolbar OR Manage Extension, but after
    // any extension's menu items.
    let manageItem =
      popup.querySelector(".customize-context-manageExtension") ||
      popup.querySelector(".unified-extensions-context-menu-pin-to-toolbar");
    items.forEach(item => item && popup.insertBefore(item, manageItem));

    let cleanup = e => {
      if (e.target === popup) {
        items.forEach(item => item?.remove());
        popup.removeEventListener("popuphidden", cleanup);
      }
    };
    popup.addEventListener("popuphidden", cleanup);
  },
};

EventEmitter.decorate(ExtensionsUI);

[ Dauer der Verarbeitung: 0.5 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge