summaryrefslogtreecommitdiffstats
path: root/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs')
-rw-r--r--toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs560
1 files changed, 560 insertions, 0 deletions
diff --git a/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs b/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs
new file mode 100644
index 0000000000..7b5151dc23
--- /dev/null
+++ b/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs
@@ -0,0 +1,560 @@
+/* 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/. */
+
+const DOH_DOORHANGER_DECISION_PREF = "doh-rollout.doorhanger-decision";
+const NETWORK_TRR_MODE_PREF = "network.trr.mode";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
+ MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
+ // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
+ Spotlight: "resource:///modules/asrouter/Spotlight.sys.mjs",
+ UIState: "resource://services-sync/UIState.sys.mjs",
+ UITour: "resource:///modules/UITour.sys.mjs",
+});
+
+export const SpecialMessageActions = {
+ // This is overridden by ASRouter.init
+ blockMessageById() {
+ throw new Error("ASRouter not intialized yet");
+ },
+
+ /**
+ * loadAddonIconInURLBar - load addons-notification icon by displaying
+ * box containing addons icon in urlbar. See Bug 1513882
+ *
+ * @param {Browser} browser browser element for showing addons icon
+ */
+ loadAddonIconInURLBar(browser) {
+ if (!browser) {
+ return;
+ }
+ const chromeDoc = browser.ownerDocument;
+ let notificationPopupBox = chromeDoc.getElementById(
+ "notification-popup-box"
+ );
+ if (!notificationPopupBox) {
+ return;
+ }
+ if (
+ notificationPopupBox.style.display === "none" ||
+ notificationPopupBox.style.display === ""
+ ) {
+ notificationPopupBox.style.display = "block";
+ }
+ },
+
+ /**
+ *
+ * @param {Browser} browser The revelant Browser
+ * @param {string} url URL to look up install location
+ * @param {string} telemetrySource Telemetry information to pass to getInstallForURL
+ */
+ async installAddonFromURL(browser, url, telemetrySource = "amo") {
+ try {
+ this.loadAddonIconInURLBar(browser);
+ const aUri = Services.io.newURI(url);
+ const systemPrincipal =
+ Services.scriptSecurityManager.getSystemPrincipal();
+
+ // AddonManager installation source associated to the addons installed from activitystream's CFR
+ // and RTAMO (source is going to be "amo" if not configured explicitly in the message provider).
+ const telemetryInfo = { source: telemetrySource };
+ const install = await lazy.AddonManager.getInstallForURL(aUri.spec, {
+ telemetryInfo,
+ });
+ await lazy.AddonManager.installAddonFromWebpage(
+ "application/x-xpinstall",
+ browser,
+ systemPrincipal,
+ install
+ );
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Pin Firefox to taskbar.
+ *
+ * @param {Window} window Reference to a window object
+ * @param {boolean} pin Private Browsing Mode if true
+ */
+ pinFirefoxToTaskbar(window, privateBrowsing = false) {
+ return window.getShellService().pinToTaskbar(privateBrowsing);
+ },
+
+ /**
+ * Set browser as the operating system default browser.
+ *
+ * @param {Window} window Reference to a window object
+ */
+ async setDefaultBrowser(window) {
+ await window.getShellService().setAsDefault();
+ },
+
+ /**
+ * Set browser as the default PDF handler.
+ *
+ * @param {Window} window Reference to a window object
+ */
+ setDefaultPDFHandler(window, onlyIfKnownBrowser = false) {
+ window.getShellService().setAsDefaultPDFHandler(onlyIfKnownBrowser);
+ },
+
+ /**
+ * Reset browser homepage and newtab to default with a certain section configuration
+ *
+ * @param {"default"|null} home Value to set for browser homepage
+ * @param {"default"|null} newtab Value to set for browser newtab
+ * @param {obj} layout Configuration options for newtab sections
+ * @returns {undefined}
+ */
+ configureHomepage({ homePage = null, newtab = null, layout = null }) {
+ // Homepage can be default, blank or a custom url
+ if (homePage === "default") {
+ Services.prefs.clearUserPref("browser.startup.homepage");
+ }
+ // Newtab page can only be default or blank
+ if (newtab === "default") {
+ Services.prefs.clearUserPref("browser.newtabpage.enabled");
+ }
+ if (layout) {
+ // Existing prefs that interact with the newtab page layout, we default to true
+ // or payload configuration
+ let newtabConfigurations = [
+ [
+ // controls the search bar
+ "browser.newtabpage.activity-stream.showSearch",
+ layout.search,
+ ],
+ [
+ // controls the topsites
+ "browser.newtabpage.activity-stream.feeds.topsites",
+ layout.topsites,
+ // User can control number of topsite rows
+ ["browser.newtabpage.activity-stream.topSitesRows"],
+ ],
+ [
+ // controls the highlights section
+ "browser.newtabpage.activity-stream.feeds.section.highlights",
+ layout.highlights,
+ // User can control number of rows and highlight sources
+ [
+ "browser.newtabpage.activity-stream.section.highlights.rows",
+ "browser.newtabpage.activity-stream.section.highlights.includeVisited",
+ "browser.newtabpage.activity-stream.section.highlights.includePocket",
+ "browser.newtabpage.activity-stream.section.highlights.includeDownloads",
+ "browser.newtabpage.activity-stream.section.highlights.includeBookmarks",
+ ],
+ ],
+ [
+ // controls the topstories section
+ "browser.newtabpage.activity-stream.feeds.system.topstories",
+ layout.topstories,
+ ],
+ ].filter(
+ // If a section has configs that the user changed we will skip that section
+ ([, , sectionConfigs]) =>
+ !sectionConfigs ||
+ sectionConfigs.every(
+ prefName => !Services.prefs.prefHasUserValue(prefName)
+ )
+ );
+
+ for (let [prefName, prefValue] of newtabConfigurations) {
+ Services.prefs.setBoolPref(prefName, prefValue);
+ }
+ }
+ },
+
+ /**
+ * Set prefs with special message actions
+ *
+ * @param {Object} pref - A pref to be updated.
+ * @param {string} pref.name - The name of the pref to be updated
+ * @param {string} [pref.value] - The value of the pref to be updated. If not included, the pref will be reset.
+ */
+ setPref(pref) {
+ // Array of prefs that are allowed to be edited by SET_PREF
+ const allowedPrefs = [
+ "browser.dataFeatureRecommendations.enabled",
+ "browser.migrate.content-modal.about-welcome-behavior",
+ "browser.migrate.content-modal.import-all.enabled",
+ "browser.migrate.preferences-entrypoint.enabled",
+ "browser.shopping.experience2023.active",
+ "browser.shopping.experience2023.optedIn",
+ "browser.shopping.experience2023.survey.optedInTime",
+ "browser.shopping.experience2023.survey.hasSeen",
+ "browser.shopping.experience2023.survey.pdpVisits",
+ "browser.startup.homepage",
+ "browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt",
+ "browser.privateWindowSeparation.enabled",
+ "browser.firefox-view.feature-tour",
+ "browser.pdfjs.feature-tour",
+ "browser.newtab.feature-tour",
+ "cookiebanners.service.mode",
+ "cookiebanners.service.mode.privateBrowsing",
+ "cookiebanners.service.detectOnly",
+ "messaging-system.askForFeedback",
+ ];
+
+ if (
+ !allowedPrefs.includes(pref.name) &&
+ !pref.name.startsWith("messaging-system-action.")
+ ) {
+ pref.name = `messaging-system-action.${pref.name}`;
+ }
+ // If pref has no value, reset it, otherwise set it to desired value
+ switch (typeof pref.value) {
+ case "object":
+ case "undefined":
+ Services.prefs.clearUserPref(pref.name);
+ break;
+ case "string":
+ Services.prefs.setStringPref(pref.name, pref.value);
+ break;
+ case "number":
+ Services.prefs.setIntPref(pref.name, pref.value);
+ break;
+ case "boolean":
+ Services.prefs.setBoolPref(pref.name, pref.value);
+ break;
+ default:
+ throw new Error(
+ `Special message action with type SET_PREF, pref of "${pref.name}" is an unsupported type.`
+ );
+ }
+ },
+
+ /**
+ * Open an FxA sign-in page and automatically close it once sign-in
+ * completes.
+ *
+ * @param {any=} data
+ * @param {Browser} browser the xul:browser rendering the page
+ * @returns {Promise<boolean>} true if the user signed in, false otherwise
+ */
+ async fxaSignInFlow(data, browser) {
+ if (!(await lazy.FxAccounts.canConnectAccount())) {
+ return false;
+ }
+ const url = await lazy.FxAccounts.config.promiseConnectAccountURI(
+ data?.entrypoint || "activity-stream-firstrun",
+ data?.extraParams || {}
+ );
+
+ let window = browser.ownerGlobal;
+
+ let fxaBrowser = await new Promise(resolve => {
+ window.openLinkIn(url, data?.where || "tab", {
+ private: false,
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ csp: null,
+ resolveOnContentBrowserCreated: resolve,
+ forceForeground: true,
+ });
+ });
+
+ let gBrowser = fxaBrowser.getTabBrowser();
+ let fxaTab = gBrowser.getTabForBrowser(fxaBrowser);
+
+ let didSignIn = await new Promise(resolve => {
+ // We're going to be setting up a listener and an observer for this
+ // mechanism.
+ //
+ // 1. An event listener for the TabClose event, to detect if the user
+ // closes the tab before completing sign-in
+ // 2. An nsIObserver that listens for the UIState for FxA to reach
+ // STATUS_SIGNED_IN.
+ //
+ // We want to clean up both the listener and observer when all of this
+ // is done.
+ //
+ // We use an AbortController to make it easier to manage the cleanup.
+ let controller = new AbortController();
+ let { signal } = controller;
+
+ // This nsIObserver will listen for the UIState status to change to
+ // STATUS_SIGNED_IN as our definitive signal that FxA sign-in has
+ // completed. It will then resolve the outer Promise to `true`.
+ let fxaObserver = {
+ QueryInterface: ChromeUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ]),
+
+ observe(aSubject, aTopic, aData) {
+ let state = lazy.UIState.get();
+ if (state.status === lazy.UIState.STATUS_SIGNED_IN) {
+ // We completed sign-in, so tear down our listener / observer and resolve
+ // didSignIn to true.
+ controller.abort();
+ resolve(true);
+ }
+ },
+ };
+
+ // The TabClose event listener _can_ accept the AbortController signal,
+ // which will then remove the event listener after controller.abort is
+ // called.
+ fxaTab.addEventListener(
+ "TabClose",
+ () => {
+ // If the TabClose event was fired before the event handler was
+ // removed, this means that the tab was closed and sign-in was
+ // not completed, which means we should resolve didSignIn to false.
+ controller.abort();
+ resolve(false);
+ },
+ { once: true, signal }
+ );
+
+ let window = fxaTab.ownerGlobal;
+ window.addEventListener("unload", () => {
+ // If the hosting window unload event was fired before the event handler
+ // was removed, this means that the window was closed and sign-in was
+ // not completed, which means we should resolve didSignIn to false.
+ controller.abort();
+ resolve(false);
+ });
+
+ Services.obs.addObserver(fxaObserver, lazy.UIState.ON_UPDATE);
+
+ // Unfortunately, nsIObserverService.addObserver does not accept an
+ // AbortController signal as a parameter, so instead we listen for the
+ // abort event on the signal to remove the observer.
+ signal.addEventListener(
+ "abort",
+ () => {
+ Services.obs.removeObserver(fxaObserver, lazy.UIState.ON_UPDATE);
+ },
+ { once: true }
+ );
+ });
+
+ // If the user completed sign-in, we'll close the fxaBrowser tab for
+ // them to bring them back to the about:welcome flow.
+ //
+ // If the sign-in page was loaded in a new window, this will close the
+ // tab for that window. That will close the window as well if it's the
+ // last tab in that window.
+ if (didSignIn && data?.autoClose !== false) {
+ gBrowser.removeTab(fxaTab);
+ }
+
+ return didSignIn;
+ },
+
+ /**
+ * Processes "Special Message Actions", which are definitions of behaviors such as opening tabs
+ * installing add-ons, or focusing the awesome bar that are allowed to can be triggered from
+ * Messaging System interactions.
+ *
+ * @param {{type: string, data?: any}} action User action defined in message JSON.
+ * @param browser {Browser} The browser most relevant to the message.
+ * @returns {Promise<unknown>} Type depends on action type. See cases below.
+ */
+ /* eslint-disable-next-line complexity */
+ async handleAction(action, browser) {
+ const window = browser.ownerGlobal;
+ switch (action.type) {
+ case "SHOW_MIGRATION_WIZARD":
+ Services.tm.dispatchToMainThread(() =>
+ lazy.MigrationUtils.showMigrationWizard(window, {
+ entrypoint: lazy.MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB,
+ migratorKey: action.data?.source,
+ })
+ );
+ break;
+ case "OPEN_PRIVATE_BROWSER_WINDOW":
+ // Forcefully open about:privatebrowsing
+ window.OpenBrowserWindow({ private: true });
+ break;
+ case "OPEN_URL":
+ window.openLinkIn(
+ Services.urlFormatter.formatURL(action.data.args),
+ action.data.where || "current",
+ {
+ private: false,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.createNullPrincipal({}),
+ csp: null,
+ }
+ );
+ break;
+ case "OPEN_ABOUT_PAGE":
+ let aboutPageURL = new URL(`about:${action.data.args}`);
+ if (action.data.entrypoint) {
+ aboutPageURL.search = action.data.entrypoint;
+ }
+ window.openTrustedLinkIn(
+ aboutPageURL.toString(),
+ action.data.where || "tab"
+ );
+ break;
+ case "OPEN_FIREFOX_VIEW":
+ window.FirefoxViewHandler.openTab();
+ break;
+ case "OPEN_PREFERENCES_PAGE":
+ window.openPreferences(
+ action.data.category || action.data.args,
+ action.data.entrypoint && {
+ urlParams: { entrypoint: action.data.entrypoint },
+ }
+ );
+ break;
+ case "OPEN_APPLICATIONS_MENU":
+ lazy.UITour.showMenu(window, action.data.args);
+ break;
+ case "HIGHLIGHT_FEATURE":
+ const highlight = await lazy.UITour.getTarget(window, action.data.args);
+ if (highlight) {
+ await lazy.UITour.showHighlight(window, highlight, "none", {
+ autohide: true,
+ });
+ }
+ break;
+ case "INSTALL_ADDON_FROM_URL":
+ await this.installAddonFromURL(
+ browser,
+ action.data.url,
+ action.data.telemetrySource
+ );
+ break;
+ case "PIN_FIREFOX_TO_TASKBAR":
+ await this.pinFirefoxToTaskbar(window, action.data?.privatePin);
+ break;
+ case "PIN_AND_DEFAULT":
+ await this.pinFirefoxToTaskbar(window, action.data?.privatePin);
+ await this.setDefaultBrowser(window);
+ break;
+ case "SET_DEFAULT_BROWSER":
+ await this.setDefaultBrowser(window);
+ break;
+ case "SET_DEFAULT_PDF_HANDLER":
+ this.setDefaultPDFHandler(
+ window,
+ action.data?.onlyIfKnownBrowser ?? false
+ );
+ break;
+ case "DECLINE_DEFAULT_PDF_HANDLER":
+ Services.prefs.setBoolPref(
+ "browser.shell.checkDefaultPDF.silencedByUser",
+ true
+ );
+ break;
+ case "CONFIRM_LAUNCH_ON_LOGIN":
+ const { WindowsLaunchOnLogin } = ChromeUtils.importESModule(
+ "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs"
+ );
+ await WindowsLaunchOnLogin.createLaunchOnLoginRegistryKey();
+ break;
+ case "PIN_CURRENT_TAB":
+ let tab = window.gBrowser.selectedTab;
+ window.gBrowser.pinTab(tab);
+ window.ConfirmationHint.show(tab, "confirmation-hint-pin-tab", {
+ descriptionId: "confirmation-hint-pin-tab-description",
+ });
+ break;
+ case "SHOW_FIREFOX_ACCOUNTS":
+ if (!(await lazy.FxAccounts.canConnectAccount())) {
+ break;
+ }
+ const data = action.data;
+ const url = await lazy.FxAccounts.config.promiseConnectAccountURI(
+ data && data.entrypoint,
+ (data && data.extraParams) || {}
+ );
+ // Use location provided; if not specified, replace the current tab.
+ window.openLinkIn(url, data.where || "current", {
+ private: false,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.createNullPrincipal({}),
+ csp: null,
+ });
+ break;
+ case "FXA_SIGNIN_FLOW":
+ /** @returns {Promise<boolean>} */
+ return this.fxaSignInFlow(action.data, browser);
+ case "OPEN_PROTECTION_PANEL":
+ let { gProtectionsHandler } = window;
+ gProtectionsHandler.showProtectionsPopup({});
+ break;
+ case "OPEN_PROTECTION_REPORT":
+ window.gProtectionsHandler.openProtections();
+ break;
+ case "OPEN_AWESOME_BAR":
+ window.gURLBar.search("");
+ break;
+ case "DISABLE_STP_DOORHANGERS":
+ await this.blockMessageById([
+ "SOCIAL_TRACKING_PROTECTION",
+ "FINGERPRINTERS_PROTECTION",
+ "CRYPTOMINERS_PROTECTION",
+ ]);
+ break;
+ case "DISABLE_DOH":
+ Services.prefs.setStringPref(
+ DOH_DOORHANGER_DECISION_PREF,
+ "UIDisabled"
+ );
+ Services.prefs.setIntPref(NETWORK_TRR_MODE_PREF, 5);
+ break;
+ case "ACCEPT_DOH":
+ Services.prefs.setStringPref(DOH_DOORHANGER_DECISION_PREF, "UIOk");
+ break;
+ case "CANCEL":
+ // A no-op used by CFRs that minimizes the notification but does not
+ // trigger a dismiss or block (it keeps the notification around)
+ break;
+ case "CONFIGURE_HOMEPAGE":
+ this.configureHomepage(action.data);
+ break;
+ case "SHOW_SPOTLIGHT":
+ lazy.Spotlight.showSpotlightDialog(browser, action.data);
+ break;
+ case "BLOCK_MESSAGE":
+ await this.blockMessageById(action.data.id);
+ break;
+ case "SET_PREF":
+ this.setPref(action.data.pref);
+ break;
+ case "MULTI_ACTION":
+ await Promise.all(
+ action.data.actions.map(async action => {
+ try {
+ await this.handleAction(action, browser);
+ } catch (err) {
+ throw new Error(`Error in MULTI_ACTION event: ${err.message}`);
+ }
+ })
+ );
+ break;
+ default:
+ throw new Error(
+ `Special message action with type ${action.type} is unsupported.`
+ );
+ case "CLICK_ELEMENT":
+ const clickElement = window.document.querySelector(
+ action.data.selector
+ );
+ clickElement?.click();
+ break;
+ case "RELOAD_BROWSER":
+ browser.reload();
+ break;
+ case "FOCUS_URLBAR":
+ window.gURLBar.focus();
+ window.gURLBar.select();
+ break;
+ }
+ return undefined;
+ },
+};