summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/content/extensions.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/content/extensions.js')
-rw-r--r--toolkit/mozapps/extensions/content/extensions.js300
1 files changed, 300 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js
new file mode 100644
index 0000000000..10e28e6980
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -0,0 +1,300 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../../content/customElements.js */
+/* import-globals-from aboutaddonsCommon.js */
+/* exported loadView */
+
+const { AddonManager } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AMTelemetry",
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+document.addEventListener("load", initialize, true);
+window.addEventListener("unload", shutdown);
+
+var gPendingInitializations = 1;
+Object.defineProperty(this, "gIsInitializing", {
+ get: () => gPendingInitializations > 0,
+});
+
+function initialize(event) {
+ document.removeEventListener("load", initialize, true);
+
+ // Support focusing the search bar from the XUL document.
+ document.addEventListener("keypress", e => {
+ getHtmlBrowser()
+ .contentDocument.querySelector("search-addons")
+ .handleEvent(e);
+ });
+
+ gViewController.initialize();
+ Services.obs.addObserver(sendEMPong, "EM-ping");
+ Services.obs.notifyObservers(window, "EM-loaded");
+
+ // If the initial view has already been selected (by a call to loadView from
+ // the above notifications) then bail out now
+ if (gViewController.initialViewSelected) {
+ return;
+ }
+
+ // If there is a history state to restore then use that
+ if (history.state) {
+ gViewController.updateState(history.state);
+ }
+}
+
+function notifyInitialized() {
+ if (!gIsInitializing) {
+ return;
+ }
+
+ gPendingInitializations--;
+ if (!gIsInitializing) {
+ var event = document.createEvent("Events");
+ event.initEvent("Initialized", true, true);
+ document.dispatchEvent(event);
+ }
+}
+
+function shutdown() {
+ Services.obs.removeObserver(sendEMPong, "EM-ping");
+}
+
+function sendEMPong(aSubject, aTopic, aData) {
+ Services.obs.notifyObservers(window, "EM-pong");
+}
+
+async function recordViewTelemetry(param) {
+ let type;
+ let addon;
+
+ if (
+ param in AddonManager.addonTypes ||
+ ["recent", "available"].includes(param)
+ ) {
+ type = param;
+ } else if (param) {
+ let id = param.replace("/preferences", "");
+ addon = await AddonManager.getAddonByID(id);
+ }
+
+ let { currentViewId } = gViewController;
+ let viewType = gViewController.parseViewId(currentViewId)?.type;
+ AMTelemetry.recordViewEvent({
+ view: viewType || "other",
+ addon,
+ type,
+ });
+}
+
+// Used by external callers to load a specific view into the manager
+function loadView(aViewId) {
+ if (!gViewController.initialViewSelected) {
+ // The caller opened the window and immediately loaded the view so it
+ // should be the initial history entry
+
+ gViewController.loadInitialView(aViewId);
+ } else {
+ gViewController.loadView(aViewId);
+ }
+}
+
+var gViewController = {
+ defaultViewId: "addons://discover/",
+ currentViewId: "",
+ isLoading: true,
+ // All historyEntryId values must be unique within one session, because the
+ // IDs are used to map history entries to page state. It is not possible to
+ // see whether a historyEntryId was used in history entries before this page
+ // was loaded, so start counting from a random value to avoid collisions.
+ // This is used for scroll offsets in aboutaddons.js
+ nextHistoryEntryId: Math.floor(Math.random() * 2 ** 32),
+ initialViewSelected: false,
+
+ initialize() {
+ if (!isDiscoverEnabled()) {
+ this.defaultViewId = "addons://list/extension";
+ }
+
+ gCategories.initialize();
+
+ window.addEventListener("popstate", e => {
+ this.updateState(e.state);
+ });
+ },
+
+ updateState(state) {
+ this.loadViewInternal(state.view, state.previousView, state);
+ },
+
+ parseViewId(aViewId) {
+ var matchRegex = /^addons:\/\/([^\/]+)\/(.*)$/;
+ var [, viewType, viewParam] = aViewId.match(matchRegex) || [];
+ return { type: viewType, param: decodeURIComponent(viewParam) };
+ },
+
+ loadView(aViewId) {
+ if (aViewId == this.currentViewId) {
+ return;
+ }
+
+ var state = {
+ view: aViewId,
+ previousView: this.currentViewId,
+ historyEntryId: ++this.nextHistoryEntryId,
+ };
+ history.pushState(state, "");
+ this.loadViewInternal(aViewId, this.currentViewId, state);
+ },
+
+ // Replaces the existing view with a new one, rewriting the current history
+ // entry to match.
+ replaceView(aViewId) {
+ if (aViewId == this.currentViewId) {
+ return;
+ }
+
+ var state = {
+ view: aViewId,
+ previousView: null,
+ historyEntryId: ++this.nextHistoryEntryId,
+ };
+ history.replaceState(state, "");
+ this.loadViewInternal(aViewId, null, state);
+ },
+
+ loadInitialView(aViewId) {
+ var state = {
+ view: aViewId,
+ previousView: null,
+ historyEntryId: ++this.nextHistoryEntryId,
+ };
+ history.replaceState(state, "");
+
+ this.loadViewInternal(aViewId, null, state);
+ notifyInitialized();
+ },
+
+ loadViewInternal(aViewId, aPreviousView, aState) {
+ const view = this.parseViewId(aViewId);
+ const viewTypes = ["shortcuts", "list", "detail", "updates", "discover"];
+
+ if (!view.type || !viewTypes.includes(view.type)) {
+ throw Components.Exception("Invalid view: " + view.type);
+ }
+
+ if (aViewId != aPreviousView) {
+ promiseHtmlBrowserLoaded()
+ .then(browser => browser.contentWindow.hide())
+ .catch(err => Cu.reportError(err));
+ }
+
+ this.currentViewId = aViewId;
+ this.isLoading = true;
+
+ recordViewTelemetry(view.param);
+
+ if (aViewId != aPreviousView) {
+ promiseHtmlBrowserLoaded()
+ .then(browser =>
+ browser.contentWindow.show(view.type, view.param, aState)
+ )
+ .then(() => {
+ this.isLoading = false;
+
+ var event = document.createEvent("Events");
+ event.initEvent("ViewChanged", true, true);
+ document.dispatchEvent(event);
+ });
+ }
+
+ this.initialViewSelected = true;
+ },
+};
+
+var gCategories = {
+ initialize() {
+ gPendingInitializations++;
+ promiseHtmlBrowserLoaded().then(async browser => {
+ await browser.contentWindow.customElements.whenDefined("categories-box");
+ let categoriesBox = browser.contentDocument.getElementById("categories");
+ await categoriesBox.promiseInitialized;
+ notifyInitialized();
+ });
+ },
+};
+
+const htmlViewOpts = {
+ loadViewFn(view) {
+ let viewId = view.startsWith("addons://") ? view : `addons://${view}`;
+ gViewController.loadView(viewId);
+ },
+ loadInitialViewFn(viewId) {
+ gViewController.loadInitialView(viewId);
+ },
+ replaceWithDefaultViewFn() {
+ gViewController.replaceView(gViewController.defaultViewId);
+ },
+ get shouldLoadInitialView() {
+ // Let the HTML document load the view if `loadView` hasn't been called
+ // externally and we don't have history to refresh from.
+ return !gViewController.currentViewId && !window.history.state;
+ },
+};
+
+// View wrappers for the HTML version of about:addons. These delegate to an
+// HTML browser that renders the actual views.
+let htmlBrowser;
+let _htmlBrowserLoaded;
+function getHtmlBrowser() {
+ if (!htmlBrowser) {
+ gPendingInitializations++;
+ htmlBrowser = document.getElementById("html-view-browser");
+ htmlBrowser.loadURI(
+ "chrome://mozapps/content/extensions/aboutaddons.html",
+ {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ _htmlBrowserLoaded = new Promise(resolve =>
+ htmlBrowser.addEventListener("load", function loadListener() {
+ if (htmlBrowser.contentWindow.location.href != "about:blank") {
+ htmlBrowser.removeEventListener("load", loadListener);
+ resolve();
+ }
+ })
+ ).then(() => {
+ htmlBrowser.contentWindow.initialize(htmlViewOpts);
+ notifyInitialized();
+ });
+ }
+ return htmlBrowser;
+}
+
+async function promiseHtmlBrowserLoaded() {
+ // Call getHtmlBrowser() first to ensure _htmlBrowserLoaded has been defined.
+ let browser = getHtmlBrowser();
+ await _htmlBrowserLoaded;
+ return browser;
+}
+
+// Helper method exported into the about:addons global, used to open the
+// abuse report panel from outside of the about:addons page
+// (e.g. triggered from the browserAction context menu).
+window.openAbuseReport = ({ addonId, reportEntryPoint }) => {
+ promiseHtmlBrowserLoaded().then(browser => {
+ browser.contentWindow.openAbuseReport({
+ addonId,
+ reportEntryPoint,
+ });
+ });
+};