Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/mozapps/extensions/content/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 126 kB image not shown  

SSL aboutaddons.js   Sprache: JAVA

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

/* eslint max-len: ["error", 80] */
/* import-globals-from aboutaddonsCommon.js */
/* import-globals-from abuse-reports.js */
/* import-globals-from view-controller.js */
/* global windowRoot */

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs",
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
  BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs",
  ClientID: "resource://gre/modules/ClientID.sys.mjs",
  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
  ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
  ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "manifestV3enabled",
  "extensions.manifestV3.enabled"
);

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "XPINSTALL_ENABLED",
  "xpinstall.enabled",
  true
);

const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds)

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "ABUSE_REPORT_ENABLED",
  "extensions.abuseReport.enabled",
  false
);
XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "LIST_RECOMMENDATIONS_ENABLED",
  "extensions.htmlaboutaddons.recommendations.enabled",
  false
);

const PLUGIN_ICON_URL = "chrome://global/skin/icons/plugin.svg";
const EXTENSION_ICON_URL =
  "chrome://mozapps/skin/extensions/extensionGeneric.svg";

const PERMISSION_MASKS = {
  enable: AddonManager.PERM_CAN_ENABLE,
  "always-activate": AddonManager.PERM_CAN_ENABLE,
  disable: AddonManager.PERM_CAN_DISABLE,
  "never-activate": AddonManager.PERM_CAN_DISABLE,
  uninstall: AddonManager.PERM_CAN_UNINSTALL,
  upgrade: AddonManager.PERM_CAN_UPGRADE,
  "change-privatebrowsing": AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS,
};

const PREF_DISCOVERY_API_URL = "extensions.getAddons.discovery.api_url";
const PREF_THEME_RECOMMENDATION_URL =
  "extensions.recommendations.themeRecommendationUrl";
const PREF_RECOMMENDATION_HIDE_NOTICE = "extensions.recommendations.hideNotice";
const PREF_PRIVACY_POLICY_URL = "extensions.recommendations.privacyPolicyUrl";
const PREF_RECOMMENDATION_ENABLED = "browser.discovery.enabled";
const PREF_TELEMETRY_ENABLED = "datareporting.healthreport.uploadEnabled";
const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
const PRIVATE_BROWSING_PERMS = {
  permissions: [PRIVATE_BROWSING_PERM_NAME],
  origins: [],
};

const L10N_ID_MAPPING = {
  "theme-disabled-heading""theme-disabled-heading2",
};

function getL10nIdMapping(id) {
  return L10N_ID_MAPPING[id] || id;
}

function shouldSkipAnimations() {
  return (
    document.body.hasAttribute("skip-animations") ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches
  );
}

function callListeners(name, args, listeners) {
  for (let listener of listeners) {
    try {
      if (name in listener) {
        listener[name](...args);
      }
    } catch (e) {
      Cu.reportError(e);
    }
  }
}

function getUpdateInstall(addon) {
  return (
    // Install object for a pending update.
    addon.updateInstall ||
    // Install object for a postponed upgrade (only for extensions,
    // because is the only addon type that can postpone their own
    // updates).
    (addon.type === "extension" &&
      addon.pendingUpgrade &&
      addon.pendingUpgrade.install)
  );
}

function isManualUpdate(install) {
  let isManual =
    install.existingAddon &&
    !AddonManager.shouldAutoUpdate(install.existingAddon);
  let isExtension =
    install.existingAddon && install.existingAddon.type == "extension";
  return (
    (isManual && isInState(install, "available")) ||
    (isExtension && isInState(install, "postponed"))
  );
}

const AddonManagerListenerHandler = {
  listeners: new Set(),

  addListener(listener) {
    this.listeners.add(listener);
  },

  removeListener(listener) {
    this.listeners.delete(listener);
  },

  delegateEvent(name, args) {
    callListeners(name, args, this.listeners);
  },

  startup() {
    this._listener = new Proxy(
      {},
      {
        has: () => true,
        get:
          (_, name) =>
          (...args) =>
            this.delegateEvent(name, args),
      }
    );
    AddonManager.addAddonListener(this._listener);
    AddonManager.addInstallListener(this._listener);
    AddonManager.addManagerListener(this._listener);
    this._permissionHandler = (type, data) => {
      if (type == "change-permissions") {
        this.delegateEvent("onChangePermissions", [data]);
      }
    };
    ExtensionPermissions.addListener(this._permissionHandler);
  },

  shutdown() {
    AddonManager.removeAddonListener(this._listener);
    AddonManager.removeInstallListener(this._listener);
    AddonManager.removeManagerListener(this._listener);
    ExtensionPermissions.removeListener(this._permissionHandler);
  },
};

/**
 * This object wires the AddonManager event listeners into addon-card and
 * addon-details elements rather than needing to add/remove listeners all the
 * time as the view changes.
 */

const AddonCardListenerHandler = new Proxy(
  {},
  {
    has: () => true,
    get(_, name) {
      return (...args) => {
        let elements = [];
        let addonId;

        // We expect args[0] to be of type:
        // - AddonInstall, on AddonManager install events
        // - AddonWrapper, on AddonManager addon events
        // - undefined, on AddonManager manage events
        if (args[0]) {
          addonId =
            args[0].addon?.id ||
            args[0].existingAddon?.id ||
            args[0].extensionId ||
            args[0].id;
        }

        if (addonId) {
          let cardSelector = `addon-card[addon-id="${addonId}"]`;
          elements = document.querySelectorAll(
            `${cardSelector}, ${cardSelector} addon-details`
          );
        } else if (name == "onUpdateModeChanged") {
          elements = document.querySelectorAll("addon-card");
        }

        callListeners(name, args, elements);
      };
    },
  }
);
AddonManagerListenerHandler.addListener(AddonCardListenerHandler);

function isAbuseReportSupported(addon) {
  return (
    ABUSE_REPORT_ENABLED &&
    AbuseReporter.isSupportedAddonType(addon.type) &&
    !(addon.isBuiltin || addon.isSystem)
  );
}

async function isAllowedInPrivateBrowsing(addon) {
  // Use the Promise directly so this function stays sync for the other case.
  let perms = await ExtensionPermissions.get(addon.id);
  return perms.permissions.includes(PRIVATE_BROWSING_PERM_NAME);
}

function hasPermission(addon, permission) {
  return !!(addon.permissions & PERMISSION_MASKS[permission]);
}

function isInState(install, state) {
  return install.state == AddonManager["STATE_" + state.toUpperCase()];
}

async function getAddonMessageInfo(
  addon,
  { isCardExpanded, isInDisabledSection }
) {
  const { name } = addon;
  const { STATE_BLOCKED, STATE_SOFTBLOCKED } = Ci.nsIBlocklistService;

  if (addon.blocklistState === STATE_BLOCKED) {
    let typeSuffix = addon.type === "extension" ? "extension" : "other";
    return {
      linkUrl: await addon.getBlocklistURL(),
      linkId: "details-notification-blocked-link2",
      messageId: `details-notification-hard-blocked-${typeSuffix}`,
      type: "error",
    };
  } else if (isDisabledUnsigned(addon)) {
    return {
      linkSumoPage: "unsigned-addons",
      messageId: "details-notification-unsigned-and-disabled2",
      messageArgs: { name },
      type: "error",
    };
  } else if (
    !addon.isCompatible &&
    (AddonManager.checkCompatibility ||
      addon.blocklistState !== STATE_SOFTBLOCKED)
  ) {
    return {
      // TODO: (Bug 1921870) consider adding a SUMO page.
      // NOTE: this messagebar is customized by Thunderbird to include
      // a non-SUMO link (see Bug 1921870 comment 0).
      messageId: "details-notification-incompatible2",
      messageArgs: { name, version: Services.appinfo.version },
      type: "error",
    };
  } else if (!isCorrectlySigned(addon)) {
    return {
      linkSumoPage: "unsigned-addons",
      messageId: "details-notification-unsigned2",
      messageArgs: { name },
      type: "warning",
    };
  } else if (addon.blocklistState === STATE_SOFTBLOCKED) {
    const fluentBaseId = "details-notification-soft-blocked";
    let typeSuffix = addon.type === "extension" ? "extension" : "other";
    let stateSuffix;
    // If the Addon Card is not expanded, delay changing the messagebar
    // string to when the Addon card is refreshed as part of moving
    // it between the enabled and disabled sections.
    if (isCardExpanded) {
      stateSuffix = addon.isActive ? "enabled" : "disabled";
    } else {
      stateSuffix = !isInDisabledSection ? "enabled" : "disabled";
    }
    let messageId = `${fluentBaseId}-${typeSuffix}-${stateSuffix}`;

    return {
      linkUrl: await addon.getBlocklistURL(),
      linkId: "details-notification-softblocked-link2",
      messageId,
      type: "warning",
    };
  } else if (addon.isGMPlugin && !addon.isInstalled && addon.isActive) {
    return {
      messageId: "details-notification-gmp-pending2",
      messageArgs: { name },
      type: "warning",
    };
  }
  return {};
}

function checkForUpdate(addon) {
  return new Promise(resolve => {
    let listener = {
      onUpdateAvailable(addon, install) {
        if (AddonManager.shouldAutoUpdate(addon)) {
          // Make sure that an update handler is attached to all the install
          // objects when updated xpis are going to be installed automatically.
          attachUpdateHandler(install);

          let failed = () => {
            detachUpdateHandler(install);
            install.removeListener(updateListener);
            resolve({ installed: false, pending: false, found: true });
          };
          let updateListener = {
            onDownloadFailed: failed,
            onInstallCancelled: failed,
            onInstallFailed: failed,
            onInstallEnded: () => {
              detachUpdateHandler(install);
              install.removeListener(updateListener);
              resolve({ installed: true, pending: false, found: true });
            },
            onInstallPostponed: () => {
              detachUpdateHandler(install);
              install.removeListener(updateListener);
              resolve({ installed: false, pending: true, found: true });
            },
          };
          install.addListener(updateListener);
          install.install();
        } else {
          resolve({ installed: false, pending: true, found: true });
        }
      },
      onNoUpdateAvailable() {
        resolve({ found: false });
      },
    };
    addon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
  });
}

async function checkForUpdates() {
  let addons = await AddonManager.getAddonsByTypes(null);
  addons = addons.filter(addon => hasPermission(addon, "upgrade"));
  let updates = await Promise.all(addons.map(addon => checkForUpdate(addon)));
  gViewController.notifyEMUpdateCheckFinished();
  return updates.reduce(
    (counts, update) => ({
      installed: counts.installed + (update.installed ? 1 : 0),
      pending: counts.pending + (update.pending ? 1 : 0),
      found: counts.found + (update.found ? 1 : 0),
    }),
    { installed: 0, pending: 0, found: 0 }
  );
}

// Don't change how we handle this while the page is open.
const INLINE_OPTIONS_ENABLED = Services.prefs.getBoolPref(
  "extensions.htmlaboutaddons.inline-options.enabled"
);
const OPTIONS_TYPE_MAP = {
  [AddonManager.OPTIONS_TYPE_TAB]: "tab",
  [AddonManager.OPTIONS_TYPE_INLINE_BROWSER]: INLINE_OPTIONS_ENABLED
    ? "inline"
    : "tab",
};

// Check if an add-on has the provided options type, accounting for the pref
// to disable inline options.
function getOptionsType(addon) {
  return OPTIONS_TYPE_MAP[addon.optionsType];
}

// Check whether the options page can be loaded in the current browser window.
async function isAddonOptionsUIAllowed(addon) {
  if (addon.type !== "extension" || !getOptionsType(addon)) {
    // Themes never have options pages.
    // Some plugins have preference pages, and they can always be shown.
    // Extensions do not need to be checked if they do not have options pages.
    return true;
  }
  if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
    return true;
  }
  if (addon.incognito === "not_allowed") {
    return false;
  }
  // The current page is in a private browsing window, and the add-on does not
  // have the permission to access private browsing windows. Block access.
  return (
    // Note: This function is async because isAllowedInPrivateBrowsing is async.
    isAllowedInPrivateBrowsing(addon)
  );
}

let _templates = {};

/**
 * Import a template from the main document.
 */

function importTemplate(name) {
  if (!_templates.hasOwnProperty(name)) {
    _templates[name] = document.querySelector(`template[name="${name}"]`);
  }
  let template = _templates[name];
  if (template) {
    return document.importNode(template.content, true);
  }
  throw new Error(`Unknown template: ${name}`);
}

function nl2br(text) {
  let frag = document.createDocumentFragment();
  let hasAppended = false;
  for (let part of text.split("\n")) {
    if (hasAppended) {
      frag.appendChild(document.createElement("br"));
    }
    frag.appendChild(new Text(part));
    hasAppended = true;
  }
  return frag;
}

/**
 * Select the screeenshot to display above an add-on card.
 *
 * @param {AddonWrapper|DiscoAddonWrapper} addon
 * @returns {string|null}
 *          The URL of the best fitting screenshot, if any.
 */

function getScreenshotUrlForAddon(addon) {
  if (addon.id == "default-theme@mozilla.org") {
    return "chrome://mozapps/content/extensions/default-theme/preview.svg";
  }
  const builtInThemePreview = BuiltInThemes.previewForBuiltInThemeId(addon.id);
  if (builtInThemePreview) {
    return builtInThemePreview;
  }

  let { screenshots } = addon;
  if (!screenshots || !screenshots.length) {
    return null;
  }

  // The image size is defined at .card-heading-image in aboutaddons.css, and
  // is based on the aspect ratio for a 680x92 image. Use the image if possible,
  // and otherwise fall back to the first image and hope for the best.
  let screenshot = screenshots.find(s => s.width === 680 && s.height === 92);
  if (!screenshot) {
    console.warn(`Did not find screenshot with desired size for ${addon.id}.`);
    screenshot = screenshots[0];
  }
  return screenshot.url;
}

/**
 * Adds UTM parameters to a given URL, if it is an AMO URL.
 *
 * @param {string} contentAttribute
 *        Identifies the part of the UI with which the link is associated.
 * @param {string} url
 * @returns {string}
 *          The url with UTM parameters if it is an AMO URL.
 *          Otherwise the url in unmodified form.
 */

function formatUTMParams(contentAttribute, url) {
  let parsedUrl = new URL(url);
  let domain = `.${parsedUrl.hostname}`;
  if (
    !domain.endsWith(".mozilla.org") &&
    // For testing: addons-dev.allizom.org and addons.allizom.org
    !domain.endsWith(".allizom.org")
  ) {
    return url;
  }

  parsedUrl.searchParams.set("utm_source""firefox-browser");
  parsedUrl.searchParams.set("utm_medium""firefox-browser");
  parsedUrl.searchParams.set("utm_content", contentAttribute);
  return parsedUrl.href;
}

// A wrapper around an item from the "results" array from AMO's discovery API.
// See https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html
class DiscoAddonWrapper {
  /**
   * @param {object} details
   *        An item in the "results" array from AMO's discovery API.
   */

  constructor(details) {
    // Reuse AddonRepository._parseAddon to have the AMO response parsing logic
    // in one place.
    let repositoryAddon = AddonRepository._parseAddon(details.addon);

    // Note: Any property used by RecommendedAddonCard should appear here.
    // The property names and values should have the same semantics as
    // AddonWrapper, to ease the reuse of helper functions in this file.
    this.id = repositoryAddon.id;
    this.type = repositoryAddon.type;
    this.name = repositoryAddon.name;
    this.screenshots = repositoryAddon.screenshots;
    this.sourceURI = repositoryAddon.sourceURI;
    this.creator = repositoryAddon.creator;
    this.averageRating = repositoryAddon.averageRating;

    this.dailyUsers = details.addon.average_daily_users;

    this.editorialDescription = details.description_text;
    this.iconURL = details.addon.icon_url;
    this.amoListingUrl = details.addon.url;

    this.taarRecommended = details.is_recommendation;
  }
}

/**
 * A helper to retrieve the list of recommended add-ons via AMO's discovery API.
 */

var DiscoveryAPI = {
  // Map<boolean, Promise> Promises from fetching the API results with or
  // without a client ID. The `false` (no client ID) case could actually
  // have been fetched with a client ID. See getResults() for more info.
  _resultPromises: new Map(),

  /**
   * Fetch the list of recommended add-ons. The results are cached.
   *
   * Pending requests are coalesced, so there is only one request at any given
   * time. If a request fails, the pending promises are rejected, but a new
   * call will result in a new request. A succesful response is cached for the
   * lifetime of the document.
   *
   * @param {boolean} preferClientId
   *                  A boolean indicating a preference for using a client ID.
   *                  This will not overwrite the user preference but will
   *                  avoid sending a client ID if no request has been made yet.
   * @returns {Promise<DiscoAddonWrapper[]>}
   */

  async getResults(preferClientId = true) {
    // Allow a caller to set preferClientId to false, but not true if discovery
    // is disabled.
    preferClientId = preferClientId && this.clientIdDiscoveryEnabled;

    // Reuse a request for this preference first.
    let resultPromise =
      this._resultPromises.get(preferClientId) ||
      // If the client ID isn't preferred, we can still reuse a request with the
      // client ID.
      (!preferClientId && this._resultPromises.get(true));

    if (resultPromise) {
      return resultPromise;
    }

    // Nothing is prepared for this preference, make a new request.
    resultPromise = this._fetchRecommendedAddons(preferClientId).catch(e => {
      // Delete the pending promise, so _fetchRecommendedAddons can be
      // called again at the next property access.
      this._resultPromises.delete(preferClientId);
      Cu.reportError(e);
      throw e;
    });

    // Store the new result for the preference.
    this._resultPromises.set(preferClientId, resultPromise);

    return resultPromise;
  },

  get clientIdDiscoveryEnabled() {
    // These prefs match Discovery.sys.mjs for enabling clientId cookies.
    return (
      Services.prefs.getBoolPref(PREF_RECOMMENDATION_ENABLED, false) &&
      Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED, false) &&
      !PrivateBrowsingUtils.isContentWindowPrivate(window)
    );
  },

  async _fetchRecommendedAddons(useClientId) {
    let discoveryApiUrl = new URL(
      Services.urlFormatter.formatURLPref(PREF_DISCOVERY_API_URL)
    );

    if (useClientId) {
      let clientId = await ClientID.getClientIdHash();
      discoveryApiUrl.searchParams.set("telemetry-client-id", clientId);
    }
    let res = await fetch(discoveryApiUrl.href, {
      credentials: "omit",
    });
    if (!res.ok) {
      throw new Error(`Failed to fetch recommended add-ons, ${res.status}`);
    }
    let { results } = await res.json();
    return results.map(details => new DiscoAddonWrapper(details));
  },
};

class SearchAddons extends HTMLElement {
  connectedCallback() {
    if (this.childElementCount === 0) {
      this.input = document.createXULElement("search-textbox");
      this.input.setAttribute("searchbutton"true);
      this.input.setAttribute("maxlength", 100);
      this.input.setAttribute("data-l10n-attrs""placeholder");
      document.l10n.setAttributes(this.input, "addons-heading-search-input");
      this.append(this.input);
    }
    this.input.addEventListener("command"this);
  }

  disconnectedCallback() {
    this.input.removeEventListener("command"this);
  }

  handleEvent(e) {
    if (e.type === "command") {
      this.searchAddons(this.value);
    }
  }

  get value() {
    return this.input.value;
  }

  searchAddons(query) {
    if (query.length === 0) {
      return;
    }

    let url = formatUTMParams(
      "addons-manager-search",
      AddonRepository.getSearchURL(query)
    );

    let browser = getBrowserElement();
    let chromewin = browser.ownerGlobal;
    chromewin.openWebLinkIn(url, "tab");
  }
}
customElements.define("search-addons", SearchAddons);

class MessageBarStackElement extends HTMLElement {
  constructor() {
    super();
    this._observer = null;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.append(this.constructor.template.content.cloneNode(true));
  }

  connectedCallback() {
    // Close any message bar that should be allowed based on the
    // maximum number of message bars.
    this.closeMessageBars();

    // Observe mutations to close older bars when new ones have been
    // added.
    this._observer = new MutationObserver(() => {
      this._observer.disconnect();
      this.closeMessageBars();
      this._observer.observe(this, { childList: true });
    });
    this._observer.observe(this, { childList: true });
  }

  disconnectedCallback() {
    this._observer.disconnect();
    this._observer = null;
  }

  closeMessageBars() {
    const { maxMessageBarCount } = this;
    if (maxMessageBarCount > 1) {
      // Remove the older message bars if the stack reached the
      // maximum number of message bars allowed.
      while (this.childElementCount > maxMessageBarCount) {
        this.firstElementChild.remove();
      }
    }
  }

  get maxMessageBarCount() {
    return parseInt(this.getAttribute("max-message-bar-count"), 10);
  }

  static get template() {
    const template = document.createElement("template");

    const style = document.createElement("style");
    // Render the stack in the reverse order if the stack has the
    // reverse attribute set.
    style.textContent = `
      :host {
        display: block;
      }
      :host([reverse]) > slot {
        display: flex;
        flex-direction: column-reverse;
      }
    `;
    template.content.append(style);
    template.content.append(document.createElement("slot"));

    Object.defineProperty(this"template", {
      value: template,
    });

    return template;
  }
}

customElements.define("message-bar-stack", MessageBarStackElement);

class GlobalWarnings extends MessageBarStackElement {
  constructor() {
    super();
    // This won't change at runtime, but we'll want to fake it in tests.
    this.inSafeMode = Services.appinfo.inSafeMode;
    this.globalWarning = null;
  }

  connectedCallback() {
    this.refresh();
    this.addEventListener("click"this);
    AddonManagerListenerHandler.addListener(this);
  }

  disconnectedCallback() {
    this.removeEventListener("click"this);
    AddonManagerListenerHandler.removeListener(this);
  }

  refresh() {
    if (this.inSafeMode) {
      this.setWarning("safe-mode");
    } else if (
      AddonManager.checkUpdateSecurityDefault &&
      !AddonManager.checkUpdateSecurity
    ) {
      this.setWarning("update-security", { action: true });
    } else if (!AddonManager.checkCompatibility) {
      this.setWarning("check-compatibility", { action: true });
    } else if (AMBrowserExtensionsImport.canCompleteOrCancelInstalls) {
      this.setWarning("imported-addons", { action: true });
    } else {
      this.removeWarning();
    }
  }

  setWarning(type, opts) {
    if (
      this.globalWarning &&
      this.globalWarning.getAttribute("warning-type") !== type
    ) {
      this.removeWarning();
    }
    if (!this.globalWarning) {
      this.globalWarning = document.createElement("moz-message-bar");
      this.globalWarning.setAttribute("warning-type", type);
      let { messageId, buttonId } = this.getGlobalWarningL10nIds(type);
      document.l10n.setAttributes(this.globalWarning, messageId);
      this.globalWarning.setAttribute("data-l10n-attrs""message");
      if (opts && opts.action) {
        let button = document.createElement("button");
        document.l10n.setAttributes(button, buttonId);
        button.setAttribute("action", type);
        button.setAttribute("slot""actions");
        this.globalWarning.appendChild(button);
      }
      this.appendChild(this.globalWarning);
    }
  }

  getGlobalWarningL10nIds(type) {
    const WARNING_TYPE_TO_L10NID_MAPPING = {
      "safe-mode": {
        messageId: "extensions-warning-safe-mode2",
      },
      "update-security": {
        messageId: "extensions-warning-update-security2",
        buttonId: "extensions-warning-update-security-button",
      },
      "check-compatibility": {
        messageId: "extensions-warning-check-compatibility2",
        buttonId: "extensions-warning-check-compatibility-button",
      },
      "imported-addons": {
        messageId: "extensions-warning-imported-addons2",
        buttonId: "extensions-warning-imported-addons-button",
      },
    };

    return WARNING_TYPE_TO_L10NID_MAPPING[type];
  }

  removeWarning() {
    if (this.globalWarning) {
      this.globalWarning.remove();
      this.globalWarning = null;
    }
  }

  handleEvent(e) {
    if (e.type === "click") {
      switch (e.target.getAttribute("action")) {
        case "update-security":
          AddonManager.checkUpdateSecurity = true;
          break;
        case "check-compatibility":
          AddonManager.checkCompatibility = true;
          break;
        case "imported-addons":
          AMBrowserExtensionsImport.completeInstalls();
          break;
      }
    }
  }

  /**
   * AddonManager listener events.
   */


  onCompatibilityModeChanged() {
    this.refresh();
  }

  onCheckUpdateSecurityChanged() {
    this.refresh();
  }

  onBrowserExtensionsImportChanged() {
    this.refresh();
  }
}
customElements.define("global-warnings", GlobalWarnings);

class AddonPageHeader extends HTMLElement {
  connectedCallback() {
    if (this.childElementCount === 0) {
      this.appendChild(importTemplate("addon-page-header"));
      this.heading = this.querySelector(".header-name");
      this.backButton = this.querySelector(".back-button");
      this.pageOptionsMenuButton = this.querySelector(
        '[action="page-options"]'
      );
      // The addon-page-options element is outside of this element since this is
      // position: sticky and that would break the positioning of the menu.
      this.pageOptionsMenu = document.getElementById(
        this.getAttribute("page-options-id")
      );
    }
    document.addEventListener("view-selected"this);
    this.addEventListener("click"this);
    this.addEventListener("mousedown"this);
    // Use capture since the event is actually triggered on the internal
    // panel-list and it doesn't bubble.
    this.pageOptionsMenu.addEventListener("shown"thistrue);
    this.pageOptionsMenu.addEventListener("hidden"thistrue);
  }

  disconnectedCallback() {
    document.removeEventListener("view-selected"this);
    this.removeEventListener("click"this);
    this.removeEventListener("mousedown"this);
    this.pageOptionsMenu.removeEventListener("shown"thistrue);
    this.pageOptionsMenu.removeEventListener("hidden"thistrue);
  }

  setViewInfo({ type, param }) {
    this.setAttribute("current-view", type);
    this.setAttribute("current-param", param);
    let viewType = type === "list" ? param : type;
    this.setAttribute("type", viewType);

    this.heading.hidden = viewType === "detail";
    this.backButton.hidden = viewType !== "detail" && viewType !== "shortcuts";

    this.backButton.disabled = !history.state?.previousView;

    if (viewType !== "detail") {
      document.l10n.setAttributes(this.heading, `${viewType}-heading`);
    }
  }

  handleEvent(e) {
    let { backButton, pageOptionsMenu, pageOptionsMenuButton } = this;
    if (e.type === "click") {
      switch (e.target) {
        case backButton:
          window.history.back();
          break;
        case pageOptionsMenuButton:
          if (e.inputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
            this.pageOptionsMenu.toggle(e);
          }
          break;
      }
    } else if (
      e.type == "mousedown" &&
      e.target == pageOptionsMenuButton &&
      e.button == 0
    ) {
      this.pageOptionsMenu.toggle(e);
    } else if (
      e.target == pageOptionsMenu.panel &&
      (e.type == "shown" || e.type == "hidden")
    ) {
      this.pageOptionsMenuButton.setAttribute(
        "aria-expanded",
        this.pageOptionsMenu.open
      );
    } else if (e.target == document && e.type == "view-selected") {
      const { type, param } = e.detail;
      this.setViewInfo({ type, param });
    }
  }
}
customElements.define("addon-page-header", AddonPageHeader);

class AddonUpdatesMessage extends HTMLElement {
  static get observedAttributes() {
    return ["state"];
  }

  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    let style = document.createElement("style");
    style.textContent = `
      @import "chrome://global/skin/in-content/common.css";
      button {
        margin: 0;
      }
    `;
    this.message = document.createElement("span");
    this.message.hidden = true;
    this.button = document.createElement("button");
    this.button.addEventListener("click", e => {
      if (e.button === 0) {
        gViewController.loadView("updates/available");
      }
    });
    this.button.hidden = true;
    this.shadowRoot.append(style, this.message, this.button);
  }

  connectedCallback() {
    document.l10n.connectRoot(this.shadowRoot);
    document.l10n.translateFragment(this.shadowRoot);
  }

  disconnectedCallback() {
    document.l10n.disconnectRoot(this.shadowRoot);
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === "state" && oldVal !== newVal) {
      let l10nId = `addon-updates-${newVal}`;
      switch (newVal) {
        case "updating":
        case "installed":
        case "none-found":
          this.button.hidden = true;
          this.message.hidden = false;
          document.l10n.setAttributes(this.message, l10nId);
          break;
        case "manual-updates-found":
          this.message.hidden = true;
          this.button.hidden = false;
          document.l10n.setAttributes(this.button, l10nId);
          break;
      }
    }
  }

  set state(val) {
    this.setAttribute("state", val);
  }
}
customElements.define("addon-updates-message", AddonUpdatesMessage);

class AddonPageOptions extends HTMLElement {
  connectedCallback() {
    if (this.childElementCount === 0) {
      this.render();
    }
    this.addEventListener("click"this);
    this.panel.addEventListener("showing"this);
    AddonManagerListenerHandler.addListener(this);
  }

  disconnectedCallback() {
    this.removeEventListener("click"this);
    this.panel.removeEventListener("showing"this);
    AddonManagerListenerHandler.removeListener(this);
  }

  toggle(...args) {
    return this.panel.toggle(...args);
  }

  get open() {
    return this.panel.open;
  }

  render() {
    this.appendChild(importTemplate("addon-page-options"));
    this.panel = this.querySelector("panel-list");
    this.installFromFile = this.querySelector('[action="install-from-file"]');
    this.toggleUpdatesEl = this.querySelector(
      '[action="set-update-automatically"]'
    );
    this.resetUpdatesEl = this.querySelector('[action="reset-update-states"]');
    this.onUpdateModeChanged();
  }

  async handleEvent(e) {
    if (e.type === "click") {
      e.target.disabled = true;
      try {
        await this.onClick(e);
      } finally {
        e.target.disabled = false;
      }
    } else if (e.type === "showing") {
      this.installFromFile.hidden = !XPINSTALL_ENABLED;
    }
  }

  async onClick(e) {
    switch (e.target.getAttribute("action")) {
      case "check-for-updates":
        await this.checkForUpdates();
        break;
      case "view-recent-updates":
        gViewController.loadView("updates/recent");
        break;
      case "install-from-file":
        if (XPINSTALL_ENABLED) {
          installAddonsFromFilePicker();
        }
        break;
      case "debug-addons":
        this.openAboutDebugging();
        break;
      case "set-update-automatically":
        await this.toggleAutomaticUpdates();
        break;
      case "reset-update-states":
        await this.resetAutomaticUpdates();
        break;
      case "manage-shortcuts":
        gViewController.loadView("shortcuts/shortcuts");
        break;
    }
  }

  async checkForUpdates() {
    let message = document.getElementById("updates-message");
    message.state = "updating";
    message.hidden = false;
    let { installed, pending } = await checkForUpdates();
    if (pending > 0) {
      message.state = "manual-updates-found";
    } else if (installed > 0) {
      message.state = "installed";
    } else {
      message.state = "none-found";
    }
  }

  openAboutDebugging() {
    let mainWindow = window.windowRoot.ownerGlobal;
    if ("switchToTabHavingURI" in mainWindow) {
      let principal = Services.scriptSecurityManager.getSystemPrincipal();
      mainWindow.switchToTabHavingURI(
        `about:debugging#/runtime/this-firefox`,
        true,
        {
          ignoreFragment: "whenComparing",
          triggeringPrincipal: principal,
        }
      );
    }
  }

  automaticUpdatesEnabled() {
    return AddonManager.updateEnabled && AddonManager.autoUpdateDefault;
  }

  toggleAutomaticUpdates() {
    if (!this.automaticUpdatesEnabled()) {
      // One or both of the prefs is false, i.e. the checkbox is not
      // checked. Now toggle both to true. If the user wants us to
      // auto-update add-ons, we also need to auto-check for updates.
      AddonManager.updateEnabled = true;
      AddonManager.autoUpdateDefault = true;
    } else {
      // Both prefs are true, i.e. the checkbox is checked.
      // Toggle the auto pref to false, but don't touch the enabled check.
      AddonManager.autoUpdateDefault = false;
    }
  }

  async resetAutomaticUpdates() {
    let addons = await AddonManager.getAllAddons();
    for (let addon of addons) {
      if ("applyBackgroundUpdates" in addon) {
        addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
      }
    }
  }

  /**
   * AddonManager listener events.
   */


  onUpdateModeChanged() {
    let updatesEnabled = this.automaticUpdatesEnabled();
    this.toggleUpdatesEl.checked = updatesEnabled;
    let resetType = updatesEnabled ? "automatic" : "manual";
    let resetStringId = `addon-updates-reset-updates-to-${resetType}`;
    document.l10n.setAttributes(this.resetUpdatesEl, resetStringId);
  }
}
customElements.define("addon-page-options", AddonPageOptions);

class CategoryButton extends HTMLButtonElement {
  connectedCallback() {
    if (this.childElementCount != 0) {
      return;
    }

    // Make sure the aria-selected attribute is set correctly.
    this.selected = this.hasAttribute("selected");

    document.l10n.setAttributes(this, `addon-category-${this.name}-title`);

    let text = document.createElement("span");
    text.classList.add("category-name");
    document.l10n.setAttributes(text, `addon-category-${this.name}`);

    this.append(text);
  }

  load() {
    gViewController.loadView(this.viewId);
  }

  get isVisible() {
    // Make a category button visible only if the related addon type is
    // supported by the AddonManager Providers actually registered to
    // the AddonManager.
    return AddonManager.hasAddonType(this.name);
  }

  get badgeCount() {
    return parseInt(this.getAttribute("badge-count"), 10) || 0;
  }

  set badgeCount(val) {
    let count = parseInt(val, 10);
    if (count) {
      this.setAttribute("badge-count", count);
    } else {
      this.removeAttribute("badge-count");
    }
  }

  get selected() {
    return this.hasAttribute("selected");
  }

  set selected(val) {
    this.toggleAttribute("selected", !!val);
    this.setAttribute("aria-selected", !!val);
  }

  get name() {
    return this.getAttribute("name");
  }

  get viewId() {
    return this.getAttribute("viewid");
  }

  // Just setting the hidden attribute isn't enough in case the category gets
  // hidden while about:addons is closed since it could be the last active view
  // which will unhide the button when it gets selected.
  get defaultHidden() {
    return this.hasAttribute("default-hidden");
  }
}
customElements.define("category-button", CategoryButton, { extends"button" });

class DiscoverButton extends CategoryButton {
  get isVisible() {
    return isDiscoverEnabled();
  }
}
customElements.define("discover-button", DiscoverButton, { extends"button" });

// Create the button-group element so it gets loaded.
document.createElement("button-group");
class CategoriesBox extends customElements.get("button-group") {
  constructor() {
    super();
    // This will resolve when the initial category states have been set from
    // our cached prefs. This is intended for use in testing to verify that we
    // are caching the previous state.
    this.promiseRendered = new Promise(resolve => {
      this._resolveRendered = resolve;
    });
  }

  handleEvent(e) {
    if (e.target == document && e.type == "view-selected") {
      const { type, param } = e.detail;
      this.select(`addons://${type}/${param}`);
      return;
    }

    if (e.target == this && e.type == "button-group:key-selected") {
      this.activeChild.load();
      return;
    }

    if (e.type == "click") {
      const button = e.target.closest("[viewid]");
      if (button) {
        button.load();
        return;
      }
    }

    // Forward the unhandled events to the button-group custom element.
    super.handleEvent(e);
  }

  disconnectedCallback() {
    document.removeEventListener("view-selected"this);
    this.removeEventListener("button-group:key-selected"this);
    this.removeEventListener("click"this);
    AddonManagerListenerHandler.removeListener(this);
    super.disconnectedCallback();
  }

  async initialize() {
    let hiddenTypes = new Set([]);

    for (let button of this.children) {
      let { defaultHidden, name } = button;
      button.hidden =
        !button.isVisible || (defaultHidden && this.shouldHideCategory(name));

      if (defaultHidden && AddonManager.hasAddonType(name)) {
        hiddenTypes.add(name);
      }
    }

    let hiddenUpdated;
    if (hiddenTypes.size) {
      hiddenUpdated = this.updateHiddenCategories(Array.from(hiddenTypes));
    }

    this.updateAvailableCount();

    document.addEventListener("view-selected"this);
    this.addEventListener("button-group:key-selected"this);
    this.addEventListener("click"this);
    AddonManagerListenerHandler.addListener(this);

    this._resolveRendered();
    await hiddenUpdated;
  }

  shouldHideCategory(name) {
    return Services.prefs.getBoolPref(`extensions.ui.${name}.hidden`, true);
  }

  setShouldHideCategory(name, hide) {
    Services.prefs.setBoolPref(`extensions.ui.${name}.hidden`, hide);
  }

  getButtonByName(name) {
    return this.querySelector(`[name="${name}"]`);
  }

  get selectedChild() {
    return this._selectedChild;
  }

  set selectedChild(node) {
    if (node && this.contains(node)) {
      if (this._selectedChild) {
        this._selectedChild.selected = false;
      }
      this._selectedChild = node;
      this._selectedChild.selected = true;
    }
  }

  select(viewId) {
    let button = this.querySelector(`[viewid="${viewId}"]`);
    if (button) {
      this.activeChild = button;
      this.selectedChild = button;
      button.hidden = false;
      Services.prefs.setStringPref(PREF_UI_LASTCATEGORY, viewId);
    }
  }

  selectType(type) {
    this.select(`addons://list/${type}`);
  }

  onInstalled(addon) {
    let button = this.getButtonByName(addon.type);
    if (button) {
      button.hidden = false;
      this.setShouldHideCategory(addon.type, false);
    }
    this.updateAvailableCount();
  }

  onInstallStarted(install) {
    this.onInstalled(install);
  }

  onNewInstall() {
    this.updateAvailableCount();
  }

  onInstallPostponed() {
    this.updateAvailableCount();
  }

  onInstallCancelled() {
    this.updateAvailableCount();
  }

  async updateAvailableCount() {
    let installs = await AddonManager.getAllInstalls();
    var count = installs.filter(install => {
      return isManualUpdate(install) && !install.installed;
    }).length;
    let availableButton = this.getButtonByName("available-updates");
    availableButton.hidden = !availableButton.selected && count == 0;
    availableButton.badgeCount = count;
  }

  async updateHiddenCategories(types) {
    let hiddenTypes = new Set(types);
    let getAddons = AddonManager.getAddonsByTypes(types);
    let getInstalls = AddonManager.getInstallsByTypes(types);

    for (let addon of await getAddons) {
      if (addon.hidden) {
        continue;
      }

      this.onInstalled(addon);
      hiddenTypes.delete(addon.type);

      if (!hiddenTypes.size) {
        return;
      }
    }

    for (let install of await getInstalls) {
      if (
        install.existingAddon ||
        install.state == AddonManager.STATE_AVAILABLE
      ) {
        continue;
      }

      this.onInstalled(install);
      hiddenTypes.delete(install.type);

      if (!hiddenTypes.size) {
        return;
      }
    }

    for (let type of hiddenTypes) {
      let button = this.getButtonByName(type);
      if (button.selected) {
        // Cancel the load if this view should be hidden.
        gViewController.resetState();
      }
      this.setShouldHideCategory(type, true);
      button.hidden = true;
    }
  }
}
customElements.define("categories-box", CategoriesBox);

class SidebarFooter extends HTMLElement {
  connectedCallback() {
    let list = document.createElement("ul");
    list.classList.add("sidebar-footer-list");

    let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
    let prefsItem = this.createItem({
      icon: "chrome://global/skin/icons/settings.svg",
      createLinkElement: () => {
        let link = document.createElement("a");
        link.href = "about:preferences";
        link.id = "preferencesButton";
        return link;
      },
      titleL10nId: "sidebar-settings-button-title",
      labelL10nId: "addons-settings-button",
      onClick: e => {
        e.preventDefault();
        let hasAboutSettings = windowRoot.ownerGlobal.switchToTabHavingURI(
          "about:settings",
          false,
          {
            ignoreFragment: "whenComparing",
          }
        );
        if (!hasAboutSettings) {
          windowRoot.ownerGlobal.switchToTabHavingURI(
            "about:preferences",
            true,
            {
              ignoreFragment: "whenComparing",
              triggeringPrincipal: systemPrincipal,
            }
          );
        }
      },
    });

    let supportItem = this.createItem({
      icon: "chrome://global/skin/icons/help.svg",
      createLinkElement: () => {
        let link = document.createElement("a", { is: "moz-support-link" });
        link.setAttribute("support-page""addons-help");
        link.id = "help-button";
        return link;
      },
      titleL10nId: "sidebar-help-button-title",
      labelL10nId: "help-button",
    });

    list.append(prefsItem, supportItem);
    this.append(list);
  }

  createItem({ onClick, titleL10nId, labelL10nId, icon, createLinkElement }) {
    let listItem = document.createElement("li");

    let link = createLinkElement();
    link.classList.add("sidebar-footer-link");
    link.addEventListener("click", onClick);
    document.l10n.setAttributes(link, titleL10nId);

    let img = document.createElement("img");
    img.src = icon;
    img.className = "sidebar-footer-icon";

    let label = document.createElement("span");
    label.className = "sidebar-footer-label";
    document.l10n.setAttributes(label, labelL10nId);

    link.append(img, label);
    listItem.append(link);
    return listItem;
  }
}
customElements.define("sidebar-footer", SidebarFooter, { extends"footer" });

class AddonOptions extends HTMLElement {
  connectedCallback() {
    if (!this.children.length) {
      this.render();
    }
  }

  get panel() {
    return this.querySelector("panel-list");
  }

  updateSeparatorsVisibility() {
    let lastSeparator;
    let elWasVisible = false;

    // Collect the panel-list children that are not already hidden.
    const children = Array.from(this.panel.children).filter(el => !el.hidden);

    for (let child of children) {
      if (child.localName == "hr") {
        child.hidden = !elWasVisible;
        if (!child.hidden) {
          lastSeparator = child;
        }
        elWasVisible = false;
      } else {
        elWasVisible = true;
      }
    }
    if (!elWasVisible && lastSeparator) {
      lastSeparator.hidden = true;
    }
  }

  get template() {
    return "addon-options";
  }

  render() {
    this.appendChild(importTemplate(this.template));
  }

  setElementState(el, card, addon, updateInstall) {
    switch (el.getAttribute("action")) {
      case "remove":
        if (hasPermission(addon, "uninstall")) {
          // Regular add-on that can be uninstalled.
          el.disabled = false;
          el.hidden = false;
          document.l10n.setAttributes(el, "remove-addon-button");
        } else if (addon.isBuiltin) {
          // Likely the built-in themes, can't be removed, that's fine.
          el.hidden = true;
        } else {
          // Likely sideloaded, mention that it can't be removed with a link.
          el.hidden = false;
          el.disabled = true;
          if (!el.querySelector('[slot="support-link"]')) {
            let link = document.createElement("a", { is: "moz-support-link" });
            link.setAttribute("data-l10n-name""link");
            link.setAttribute("support-page""cant-remove-addon");
            link.setAttribute("slot""support-link");
            el.appendChild(link);
            document.l10n.setAttributes(el, "remove-addon-disabled-button");
          }
        }
        break;
      case "report":
        el.hidden = !isAbuseReportSupported(addon);
        break;
      case "install-update":
        el.hidden = !updateInstall;
        break;
      case "expand":
        el.hidden = card.expanded;
        break;
      case "preferences":
        el.hidden =
          getOptionsType(addon) !== "tab" &&
          (getOptionsType(addon) !== "inline" || card.expanded);
        if (!el.hidden) {
          isAddonOptionsUIAllowed(addon).then(allowed => {
            el.hidden = !allowed;
          });
        }
        break;
    }
  }

  update(card, addon, updateInstall) {
    for (let el of this.items) {
      this.setElementState(el, card, addon, updateInstall);
    }

    // Update the separators visibility based on the updated visibility
    // of the actions in the panel-list.
    this.updateSeparatorsVisibility();
  }

  get items() {
    return this.querySelectorAll("panel-item");
  }

  get visibleItems() {
    return Array.from(this.items).filter(item => !item.hidden);
  }
}
customElements.define("addon-options", AddonOptions);

class PluginOptions extends AddonOptions {
  get template() {
    return "plugin-options";
  }

  setElementState(el, card, addon) {
    const userDisabledStates = {
      "always-activate"false,
      "never-activate"true,
    };
    const action = el.getAttribute("action");
    if (action in userDisabledStates) {
      let userDisabled = userDisabledStates[action];
      el.checked = addon.userDisabled === userDisabled;
      el.disabled = !(el.checked || hasPermission(addon, action));
    } else {
      super.setElementState(el, card, addon);
    }
  }
}
customElements.define("plugin-options", PluginOptions);

class ProxyContextMenu extends HTMLElement {
  openPopupAtScreen(...args) {
    // prettier-ignore
    const parentContextMenuPopup =
      windowRoot.ownerGlobal.document.getElementById("contentAreaContextMenu");
    return parentContextMenuPopup.openPopupAtScreen(...args);
  }
}
customElements.define("proxy-context-menu", ProxyContextMenu);

class InlineOptionsBrowser extends HTMLElement {
  constructor() {
    super();
    // Force the options_ui remote browser to recompute window.mozInnerScreenX
    // and window.mozInnerScreenY when the "addon details" page has been
    // scrolled (See Bug 1390445 for rationale).
    // Also force a repaint to fix an issue where the click location was
    // getting out of sync (see bug 1548687).
    this.updatePositionTask = new DeferredTask(() => {
      if (this.browser && this.browser.isRemoteBrowser) {
        // Select boxes can appear in the wrong spot after scrolling, this will
        // clear that up. Bug 1390445.
        this.browser.frameLoader.requestUpdatePosition();
      }
    }, 100);

    this._embedderElement = null;
    this._promiseDisconnected = new Promise(
      resolve => (this._resolveDisconnected = resolve)
    );
  }

  connectedCallback() {
    window.addEventListener("scroll"thistrue);
    const { embedderElement } = top.browsingContext;
    this._embedderElement = embedderElement;
    embedderElement.addEventListener("FullZoomChange"this);
    embedderElement.addEventListener("TextZoomChange"this);
  }

  disconnectedCallback() {
    this._resolveDisconnected();
    window.removeEventListener("scroll"thistrue);
    this._embedderElement?.removeEventListener("FullZoomChange"this);
    this._embedderElement?.removeEventListener("TextZoomChange"this);
    this._embedderElement = null;
  }

  handleEvent(e) {
    switch (e.type) {
      case "scroll":
        return this.updatePositionTask.arm();
      case "FullZoomChange":
      case "TextZoomChange":
        return this.maybeUpdateZoom();
    }
    return undefined;
  }

  maybeUpdateZoom() {
    let bc = this.browser?.browsingContext;
    let topBc = top.browsingContext;
    if (!bc || !topBc) {
      return;
    }
    // Use the same full-zoom as our top window.
    bc.fullZoom = topBc.fullZoom;
    bc.textZoom = topBc.textZoom;
  }

  setAddon(addon) {
    this.addon = addon;
  }

  destroyBrowser() {
    this.textContent = "";
  }

  ensureBrowserCreated() {
    if (this.childElementCount === 0) {
      this.render();
    }
  }

  async render() {
    let { addon } = this;
    if (!addon) {
      throw new Error("addon required to create inline options");
    }

    let browser = document.createXULElement("browser");
    browser.setAttribute("type""content");
    browser.setAttribute("disableglobalhistory""true");
    browser.setAttribute("messagemanagergroup""webext-browsers");
    browser.setAttribute("id""addon-inline-options");
    browser.setAttribute("class""addon-inline-options");
    browser.setAttribute("transparent""true");
    browser.setAttribute("forcemessagemanager""true");
    browser.setAttribute("autocompletepopup""PopupAutoComplete");

    let { optionsURL, optionsBrowserStyle } = addon;
    if (addon.isWebExtension) {
      let policy = ExtensionParent.WebExtensionPolicy.getByID(addon.id);
      browser.setAttribute(
        "initialBrowsingContextGroupId",
        policy.browsingContextGroupId
      );
    }

    let readyPromise;
    let remoteSubframes = window.docShell.QueryInterface(
      Ci.nsILoadContext
    ).useRemoteSubframes;
    // For now originAttributes have no effect, which will change if the
    // optionsURL becomes anything but moz-extension* or we start considering
    // OA for extensions.
    var oa = E10SUtils.predictOriginAttributes({ browser });
    let loadRemote = E10SUtils.canLoadURIInRemoteType(
      optionsURL,
      remoteSubframes,
      E10SUtils.EXTENSION_REMOTE_TYPE,
      oa
    );
    if (loadRemote) {
      browser.setAttribute("remote""true");
      browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);

      readyPromise = promiseEvent("XULFrameLoaderCreated", browser);
    } else {
      readyPromise = promiseEvent("load", browser, true);
    }

    this.appendChild(browser);
    this.browser = browser;

    // Force bindings to apply synchronously.
    browser.clientTop;

    await readyPromise;

    this.maybeUpdateZoom();

    if (!browser.messageManager) {
      // If the browser.messageManager is undefined, the browser element has
      // been removed from the document in the meantime (e.g. due to a rapid
      // sequence of addon reload), return null.
      return;
    }

    ExtensionParent.apiManager.emit("extension-browser-inserted", browser);

    await new Promise(resolve => {
      let messageListener = {
        receiveMessage({ name, data }) {
          if (name === "Extension:BrowserResized") {
            browser.style.height = `${data.height}px`;
          } else if (name === "Extension:BrowserContentLoaded") {
            resolve();
          }
        },
      };

      let mm = browser.messageManager;

      if (!mm) {
        // If the browser.messageManager is undefined, the browser element has
        // been removed from the document in the meantime (e.g. due to a rapid
        // sequence of addon reload), return null.
        resolve();
        return;
      }

      mm.loadFrameScript(
        "chrome://extensions/content/ext-browser-content.js",
        false,
        true
      );
      mm.addMessageListener("Extension:BrowserContentLoaded", messageListener);
      mm.addMessageListener("Extension:BrowserResized", messageListener);

      let browserOptions = {
        fixedWidth: true,
        isInline: true,
      };

      if (optionsBrowserStyle) {
        // aboutaddons.js is not used on Android. extension.css is included in
        // Firefox desktop and Thunderbird.
        // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
        browserOptions.stylesheets = ["chrome://browser/content/extension.css"];
      }

      mm.sendAsyncMessage("Extension:InitBrowser", browserOptions);

      if (browser.isConnectedAndReady) {
        this.fixupAndLoadURIString(optionsURL);
      } else {
        // browser custom element does opt-in the delayConnectedCallback
        // behavior (see connectedCallback in the custom element definition
        // from browser-custom-element.js) and so calling browser.loadURI
        // would fail if the about:addons document is not yet fully loaded.
        Promise.race([
          promiseEvent("DOMContentLoaded", document),
          this._promiseDisconnected,
        ]).then(() => {
          this.fixupAndLoadURIString(optionsURL);
        });
      }
    });
  }

  fixupAndLoadURIString(uriString) {
    if (!this.browser || !this.browser.isConnectedAndReady) {
      throw new Error("Fail to loadURI");
    }

    this.browser.fixupAndLoadURIString(uriString, {
      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    });
  }
}
customElements.define("inline-options-browser", InlineOptionsBrowser);

class UpdateReleaseNotes extends HTMLElement {
  connectedCallback() {
    this.addEventListener("click"this);
  }

  disconnectedCallback() {
    this.removeEventListener("click"this);
  }

  handleEvent(e) {
    // We used to strip links, but ParserUtils.parseFragment() leaves them in,
    // so just make sure we open them using the null principal in a new tab.
    if (e.type == "click" && e.target.localName == "a" && e.target.href) {
      e.preventDefault();
      e.stopPropagation();
      windowRoot.ownerGlobal.openWebLinkIn(e.target.href, "tab");
    }
  }

  async loadForUri(uri) {
    // Can't load the release notes without a URL to load.
    if (!uri || !uri.spec) {
      this.setErrorMessage();
      this.dispatchEvent(new CustomEvent("release-notes-error"));
      return;
    }

    // Don't try to load for the same update a second time.
    if (this.url == uri.spec) {
      this.dispatchEvent(new CustomEvent("release-notes-cached"));
      return;
    }

    // Store the URL to skip the network if loaded again.
    this.url = uri.spec;

    // Set the loading message before hitting the network.
    this.setLoadingMessage();
    this.dispatchEvent(new CustomEvent("release-notes-loading"));

    try {
      // loadReleaseNotes will fetch and sanitize the release notes.
      let fragment = await loadReleaseNotes(uri);
      this.textContent = "";
      this.appendChild(fragment);
      this.dispatchEvent(new CustomEvent("release-notes-loaded"));
    } catch (e) {
      this.setErrorMessage();
      this.dispatchEvent(new CustomEvent("release-notes-error"));
    }
  }

  setMessage(id) {
    this.textContent = "";
    let message = document.createElement("p");
    document.l10n.setAttributes(message, id);
    this.appendChild(message);
  }

  setLoadingMessage() {
    this.setMessage("release-notes-loading");
  }

  setErrorMessage() {
    this.setMessage("release-notes-error");
  }
}
customElements.define("update-release-notes", UpdateReleaseNotes);

class AddonPermissionsList extends HTMLElement {
  setAddon(addon) {
    this.addon = addon;
    this.render();
  }

  async render() {
    let empty = { origins: [], permissions: [] };
    let requiredPerms = { ...(this.addon.userPermissions ?? empty) };
    let optionalPerms = { ...(this.addon.optionalPermissions ?? empty) };
    let grantedPerms = await ExtensionPermissions.get(this.addon.id);

    if (manifestV3enabled) {
      // If optional permissions include <all_urls>, extension can request and
      // be granted permission for individual sites not listed in the manifest.
      // Include them as well in the optional origins list.
      let origins = [
        ...(this.addon.optionalOriginsNormalized ?? []),
        ...grantedPerms.origins.filter(o => !requiredPerms.origins.includes(o)),
      ];
      optionalPerms.origins = [...new Set(origins)];
    }

    let permissions = Extension.formatPermissionStrings(
      {
        permissions: requiredPerms,
        optionalPermissions: optionalPerms,
      },
      { buildOptionalOrigins: manifestV3enabled }
    );
    let optionalEntries = [
      ...Object.entries(permissions.optionalPermissions),
      ...Object.entries(permissions.optionalOrigins),
    ];

    this.textContent = "";
    let frag = importTemplate("addon-permissions-list");

    if (permissions.msgs.length) {
      let section = frag.querySelector(".addon-permissions-required");
      section.hidden = false;
      let list = section.querySelector(".addon-permissions-list");

      for (let msg of permissions.msgs) {
        let item = document.createElement("li");
        item.classList.add("permission-info""permission-checked");
        item.appendChild(document.createTextNode(msg));
        list.appendChild(item);
      }
    }

    if (optionalEntries.length) {
      let section = frag.querySelector(".addon-permissions-optional");
      section.hidden = false;
      let list = section.querySelector(".addon-permissions-list");

      for (let id = 0; id < optionalEntries.length; id++) {
        let [perm, msg] = optionalEntries[id];

        let type = "permission";
        if (permissions.optionalOrigins[perm]) {
          type = "origin";
        }
        let item = document.createElement("li");
        item.classList.add("permission-info");

        let toggle = document.createElement("moz-toggle");
        toggle.setAttribute("label", msg);
        toggle.id = `permission-${id}`;
        toggle.setAttribute("permission-type", type);

        let checked =
          grantedPerms.permissions.includes(perm) ||
          grantedPerms.origins.includes(perm);

        // If this is one of the "all sites" permissions
        if (Extension.isAllSitesPermission(perm)) {
          // mark it as checked if ANY of the "all sites" permission is granted.
          checked = await AddonCard.optionalAllSitesGranted(this.addon.id);
          toggle.toggleAttribute("permission-all-sites"true);
        }

        toggle.pressed = checked;
        item.classList.toggle("permission-checked", checked);

        toggle.setAttribute("permission-key", perm);
        toggle.setAttribute("action""toggle-permission");

        if (perm === "userScripts") {
          let mb = document.createElement("moz-message-bar");
          mb.setAttribute("type""warning");
          mb.messageL10nId = "webext-perms-extra-warning-userScripts-long";
          mb.slot = "nested";
          toggle.append(mb);
        }
        item.appendChild(toggle);
        list.appendChild(item);
      }
    }
    if (!permissions.msgs.length && !optionalEntries.length) {
      let row = frag.querySelector(".addon-permissions-empty");
      row.hidden = false;
    }

    this.appendChild(frag);
  }
}
customElements.define("addon-permissions-list", AddonPermissionsList);

class AddonSitePermissionsList extends HTMLElement {
  setAddon(addon) {
    this.addon = addon;
    this.render();
  }

  async render() {
    let permissions = Extension.formatPermissionStrings({
      sitePermissions: this.addon.sitePermissions,
      siteOrigin: this.addon.siteOrigin,
    });

    this.textContent = "";
    let frag = importTemplate("addon-sitepermissions-list");

    if (permissions.msgs.length) {
      let section = frag.querySelector(".addon-permissions-required");
      section.hidden = false;
      let list = section.querySelector(".addon-permissions-list");
      let header = section.querySelector(".permission-header");
      document.l10n.setAttributes(header, "addon-sitepermissions-required", {
        hostname: new URL(this.addon.siteOrigin).hostname,
      });

      for (let msg of permissions.msgs) {
        let item = document.createElement("li");
        item.classList.add("permission-info""permission-checked");
        item.appendChild(document.createTextNode(msg));
        list.appendChild(item);
      }
    }

    this.appendChild(frag);
  }
}
customElements.define("addon-sitepermissions-list", AddonSitePermissionsList);

class AddonDetails extends HTMLElement {
  connectedCallback() {
    if (!this.children.length) {
      this.render();
    }
    this.deck.addEventListener("view-changed"this);
    this.descriptionShowMoreButton.addEventListener("click"this);
  }

  disconnectedCallback() {
    this.inlineOptions.destroyBrowser();
    this.deck.removeEventListener("view-changed"this);
    this.descriptionShowMoreButton.removeEventListener("click"this);
  }

  handleEvent(e) {
    if (e.type == "view-changed" && e.target == this.deck) {
      switch (this.deck.selectedViewName) {
        case "release-notes":
          let releaseNotes = this.querySelector("update-release-notes");
          let uri = this.releaseNotesUri;
          if (uri) {
            releaseNotes.loadForUri(uri);
          }
          break;
        case "preferences":
          if (getOptionsType(this.addon) == "inline") {
            this.inlineOptions.ensureBrowserCreated();
          }
          break;
      }

      // When a details view is rendered again, the default details view is
      // unconditionally shown. So if any other tab is selected, do not save
      // the current scroll offset, but start at the top of the page instead.
      ScrollOffsets.canRestore = this.deck.selectedViewName === "details";
    } else if (
      e.type == "click" &&
      e.target == this.descriptionShowMoreButton
    ) {
      this.toggleDescription();
    }
  }

  onInstalled() {
    let policy = WebExtensionPolicy.getByID(this.addon.id);
    let extension = policy && policy.extension;
    if (extension && extension.startupReason === "ADDON_UPGRADE") {
      // Ensure the options browser is recreated when a new version starts.
      this.extensionShutdown();
      this.extensionStartup();
    }
  }

  onDisabled() {
    this.extensionShutdown();
  }

  onEnabled() {
    this.extensionStartup();
  }

  extensionShutdown() {
    this.inlineOptions.destroyBrowser();
  }

  extensionStartup() {
    if (this.deck.selectedViewName === "preferences") {
      this.inlineOptions.ensureBrowserCreated();
    }
  }

  toggleDescription() {
    this.descriptionCollapsed = !this.descriptionCollapsed;

    this.descriptionWrapper.classList.toggle(
      "addon-detail-description-collapse",
      this.descriptionCollapsed
    );

    this.descriptionShowMoreButton.hidden = false;
    document.l10n.setAttributes(
      this.descriptionShowMoreButton,
      this.descriptionCollapsed
        ? "addon-detail-description-expand"
        : "addon-detail-description-collapse"
    );
  }

  get releaseNotesUri() {
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=93 H=93 G=92

¤ Dauer der Verarbeitung: 0.49 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.