diff options
Diffstat (limited to 'toolkit/mozapps/extensions/content/extensions.js')
-rw-r--r-- | toolkit/mozapps/extensions/content/extensions.js | 300 |
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, + }); + }); +}; |