tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ASRouterTargeting.sys.mjs (48148B)


      1 /* This Source Code Form is subject to the terms of the Mozilla PublicddonMa
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 const FXA_ENABLED_PREF = "identity.fxaccounts.enabled";
      5 const TOPIC_SELECTION_MODAL_LAST_DISPLAYED_PREF =
      6  "browser.newtabpage.activity-stream.discoverystream.topicSelection.onboarding.lastDisplayed";
      7 const NOTIFICATION_INTERVAL_AFTER_TOPIC_MODAL_MS = 60000; // Assuming avoid notification up to 1 minute after newtab Topic Notification Modal
      8 
      9 // We use importESModule here instead of static import so that
     10 // the Karma test environment won't choke on this module. This
     11 // is because the Karma test environment already stubs out
     12 // XPCOMUtils, AppConstants, NewTabUtils and ShellService, and
     13 // overrides importESModule to be a no-op (which can't be done
     14 // for a static import statement).
     15 
     16 // eslint-disable-next-line mozilla/use-static-import
     17 const { XPCOMUtils } = ChromeUtils.importESModule(
     18  "resource://gre/modules/XPCOMUtils.sys.mjs"
     19 );
     20 
     21 // eslint-disable-next-line mozilla/use-static-import
     22 const { AppConstants } = ChromeUtils.importESModule(
     23  "resource://gre/modules/AppConstants.sys.mjs"
     24 );
     25 
     26 // eslint-disable-next-line mozilla/use-static-import
     27 const { NewTabUtils } = ChromeUtils.importESModule(
     28  "resource://gre/modules/NewTabUtils.sys.mjs"
     29 );
     30 
     31 // eslint-disable-next-line mozilla/use-static-import
     32 const { ShellService } = ChromeUtils.importESModule(
     33  "moz-src:///browser/components/shell/ShellService.sys.mjs"
     34 );
     35 
     36 // eslint-disable-next-line mozilla/use-static-import
     37 const { ClientID } = ChromeUtils.importESModule(
     38  "resource://gre/modules/ClientID.sys.mjs"
     39 );
     40 
     41 // eslint-disable-next-line mozilla/use-static-import
     42 const { PlacesUtils } = ChromeUtils.importESModule(
     43  "resource://gre/modules/PlacesUtils.sys.mjs"
     44 );
     45 
     46 const lazy = {};
     47 
     48 ChromeUtils.defineESModuleGetters(lazy, {
     49  AboutNewTabResourceMapping:
     50    "resource:///modules/AboutNewTabResourceMapping.sys.mjs",
     51  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     52  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
     53  ASRouterPreferences:
     54    "resource:///modules/asrouter/ASRouterPreferences.sys.mjs",
     55  AttributionCode:
     56    "moz-src:///browser/components/attribution/AttributionCode.sys.mjs",
     57  BackupService: "resource:///modules/backup/BackupService.sys.mjs",
     58  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     59  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     60  ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs",
     61  CustomizableUI:
     62    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     63  ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
     64  FeatureCalloutBroker:
     65    "resource:///modules/asrouter/FeatureCalloutBroker.sys.mjs",
     66  HomePage: "resource:///modules/HomePage.sys.mjs",
     67  ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
     68  Region: "resource://gre/modules/Region.sys.mjs",
     69  // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
     70  SelectableProfileService:
     71    "resource:///modules/profiles/SelectableProfileService.sys.mjs",
     72  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
     73  TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs",
     74  TaskbarTabs: "resource:///modules/taskbartabs/TaskbarTabs.sys.mjs",
     75  TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
     76  TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
     77  WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs",
     78 });
     79 
     80 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
     81  return ChromeUtils.importESModule(
     82    "resource://gre/modules/FxAccounts.sys.mjs"
     83  ).getFxAccountsSingleton();
     84 });
     85 
     86 XPCOMUtils.defineLazyPreferenceGetter(
     87  lazy,
     88  "cfrFeaturesUserPref",
     89  "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
     90  true
     91 );
     92 XPCOMUtils.defineLazyPreferenceGetter(
     93  lazy,
     94  "cfrAddonsUserPref",
     95  "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
     96  true
     97 );
     98 XPCOMUtils.defineLazyPreferenceGetter(
     99  lazy,
    100  "hasAccessedFxAPanel",
    101  "identity.fxaccounts.toolbar.accessed",
    102  false
    103 );
    104 XPCOMUtils.defineLazyPreferenceGetter(
    105  lazy,
    106  "clientsDevicesDesktop",
    107  "services.sync.clients.devices.desktop",
    108  0
    109 );
    110 XPCOMUtils.defineLazyPreferenceGetter(
    111  lazy,
    112  "clientsDevicesMobile",
    113  "services.sync.clients.devices.mobile",
    114  0
    115 );
    116 XPCOMUtils.defineLazyPreferenceGetter(
    117  lazy,
    118  "syncNumClients",
    119  "services.sync.numClients",
    120  0
    121 );
    122 XPCOMUtils.defineLazyPreferenceGetter(
    123  lazy,
    124  "devtoolsSelfXSSCount",
    125  "devtools.selfxss.count",
    126  0
    127 );
    128 XPCOMUtils.defineLazyPreferenceGetter(
    129  lazy,
    130  "isFxAEnabled",
    131  FXA_ENABLED_PREF,
    132  true
    133 );
    134 XPCOMUtils.defineLazyPreferenceGetter(
    135  lazy,
    136  "isXPIInstallEnabled",
    137  "xpinstall.enabled",
    138  true
    139 );
    140 XPCOMUtils.defineLazyPreferenceGetter(
    141  lazy,
    142  "hasMigratedBookmarks",
    143  "browser.migrate.interactions.bookmarks",
    144  false
    145 );
    146 XPCOMUtils.defineLazyPreferenceGetter(
    147  lazy,
    148  "hasMigratedCSVPasswords",
    149  "browser.migrate.interactions.csvpasswords",
    150  false
    151 );
    152 XPCOMUtils.defineLazyPreferenceGetter(
    153  lazy,
    154  "hasMigratedHistory",
    155  "browser.migrate.interactions.history",
    156  false
    157 );
    158 XPCOMUtils.defineLazyPreferenceGetter(
    159  lazy,
    160  "hasMigratedPasswords",
    161  "browser.migrate.interactions.passwords",
    162  false
    163 );
    164 XPCOMUtils.defineLazyPreferenceGetter(
    165  lazy,
    166  "useEmbeddedMigrationWizard",
    167  "browser.migrate.content-modal.about-welcome-behavior",
    168  "default",
    169  null,
    170  behaviorString => {
    171    return behaviorString === "embedded";
    172  }
    173 );
    174 XPCOMUtils.defineLazyPreferenceGetter(
    175  lazy,
    176  "totalSearches",
    177  "browser.search.totalSearches",
    178  0
    179 );
    180 XPCOMUtils.defineLazyPreferenceGetter(
    181  lazy,
    182  "newTabTopicModalLastSeen",
    183  TOPIC_SELECTION_MODAL_LAST_DISPLAYED_PREF,
    184  null,
    185  lastSeenString => {
    186    return Number.isInteger(parseInt(lastSeenString, 10))
    187      ? parseInt(lastSeenString, 10)
    188      : 0;
    189  }
    190 );
    191 XPCOMUtils.defineLazyPreferenceGetter(
    192  lazy,
    193  "profilesCreated",
    194  "browser.profiles.created",
    195  false
    196 );
    197 XPCOMUtils.defineLazyPreferenceGetter(
    198  lazy,
    199  "didHandleCampaignAction",
    200  "trailhead.firstrun.didHandleCampaignAction",
    201  false
    202 );
    203 
    204 XPCOMUtils.defineLazyServiceGetters(lazy, {
    205  AUS: [
    206    "@mozilla.org/updates/update-service;1",
    207    Ci.nsIApplicationUpdateService,
    208  ],
    209  BrowserHandler: ["@mozilla.org/browser/clh;1", Ci.nsIBrowserHandler],
    210  ScreenManager: ["@mozilla.org/gfx/screenmanager;1", Ci.nsIScreenManager],
    211  TrackingDBService: [
    212    "@mozilla.org/tracking-db-service;1",
    213    Ci.nsITrackingDBService,
    214  ],
    215  UpdateCheckSvc: [
    216    "@mozilla.org/updates/update-checker;1",
    217    Ci.nsIUpdateChecker,
    218  ],
    219 });
    220 
    221 const FXA_USERNAME_PREF = "services.sync.username";
    222 
    223 const { activityStreamProvider: asProvider } = NewTabUtils;
    224 
    225 const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours
    226 const FRECENT_SITES_IGNORE_BLOCKED = false;
    227 const FRECENT_SITES_NUM_ITEMS = 25;
    228 // 2 visits, 30 days ago.
    229 const FRECENT_SITES_MIN_FRECENCY = PlacesUtils.history.pageFrecencyThreshold(
    230  30,
    231  2,
    232  false
    233 );
    234 
    235 const CACHE_EXPIRATION = 5 * 60 * 1000;
    236 const jexlEvaluationCache = new Map();
    237 
    238 /**
    239 * CachedTargetingGetter
    240 *
    241 * @param property {string} Name of the method
    242 * @param options {any=} Options passed to the method
    243 * @param updateInterval {number?} Update interval for query. Defaults to FRECENT_SITES_UPDATE_INTERVAL
    244 */
    245 export function CachedTargetingGetter(
    246  property,
    247  options = null,
    248  updateInterval = FRECENT_SITES_UPDATE_INTERVAL,
    249  getter = asProvider
    250 ) {
    251  return {
    252    _lastUpdated: 0,
    253    _value: null,
    254    // For testing
    255    expire() {
    256      this._lastUpdated = 0;
    257      this._value = null;
    258    },
    259    async get() {
    260      const now = Date.now();
    261      if (now - this._lastUpdated >= updateInterval) {
    262        this._value = await getter[property](options);
    263        this._lastUpdated = now;
    264      }
    265      return this._value;
    266    },
    267  };
    268 }
    269 
    270 function CacheUnhandledCampaignAction() {
    271  return {
    272    _lastUpdated: 0,
    273    _value: null,
    274    expire() {
    275      this._lastUpdated = 0;
    276      this._value = null;
    277    },
    278    get() {
    279      const now = Date.now();
    280      // Don't get cached value until the action has been handled to ensure
    281      // proper screen targeting in about:welcome
    282      if (
    283        now - this._lastUpdated >= FRECENT_SITES_UPDATE_INTERVAL ||
    284        !lazy.didHandleCampaignAction
    285      ) {
    286        this._value = null;
    287        if (!lazy.didHandleCampaignAction) {
    288          const attributionData =
    289            lazy.AttributionCode.getCachedAttributionData();
    290          const ALLOWED_CAMPAIGN_ACTIONS = [
    291            "PIN_AND_DEFAULT",
    292            "PIN_FIREFOX_TO_TASKBAR",
    293            "SET_DEFAULT_BROWSER",
    294          ];
    295          const campaign = attributionData?.campaign?.toUpperCase();
    296          if (campaign && ALLOWED_CAMPAIGN_ACTIONS.includes(campaign)) {
    297            this._value = campaign;
    298          }
    299        }
    300        this._lastUpdated = now;
    301      }
    302      return this._value;
    303    },
    304  };
    305 }
    306 
    307 function CheckBrowserNeedsUpdate(
    308  updateInterval = FRECENT_SITES_UPDATE_INTERVAL
    309 ) {
    310  const checker = {
    311    _lastUpdated: 0,
    312    _value: null,
    313    // For testing. Avoid update check network call.
    314    setUp(value) {
    315      this._lastUpdated = Date.now();
    316      this._value = value;
    317    },
    318    expire() {
    319      this._lastUpdated = 0;
    320      this._value = null;
    321    },
    322    async get() {
    323      const now = Date.now();
    324      if (
    325        !AppConstants.MOZ_UPDATER ||
    326        now - this._lastUpdated < updateInterval
    327      ) {
    328        return this._value;
    329      }
    330      if (!lazy.AUS.canCheckForUpdates) {
    331        return false;
    332      }
    333      this._lastUpdated = now;
    334      let check = lazy.UpdateCheckSvc.checkForUpdates(
    335        lazy.UpdateCheckSvc.FOREGROUND_CHECK
    336      );
    337      let result = await check.result;
    338      if (!result.succeeded) {
    339        lazy.ASRouterPreferences.console.error(
    340          "CheckBrowserNeedsUpdate failed :>> ",
    341          result.request
    342        );
    343        return false;
    344      }
    345      checker._value = !!result.updates.length;
    346      return checker._value;
    347    },
    348  };
    349 
    350  return checker;
    351 }
    352 
    353 export const QueryCache = {
    354  expireAll() {
    355    Object.keys(this.queries).forEach(query => {
    356      this.queries[query].expire();
    357    });
    358    Object.keys(this.getters).forEach(key => {
    359      this.getters[key].expire();
    360    });
    361  },
    362  queries: {
    363    TopFrecentSites: new CachedTargetingGetter("getTopFrecentSites", {
    364      ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,
    365      numItems: FRECENT_SITES_NUM_ITEMS,
    366      topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
    367      onePerDomain: true,
    368      includeFavicon: false,
    369    }),
    370    TotalBookmarksCount: new CachedTargetingGetter("getTotalBookmarksCount"),
    371    CheckBrowserNeedsUpdate: new CheckBrowserNeedsUpdate(),
    372    RecentBookmarks: new CachedTargetingGetter("getRecentBookmarks"),
    373    UserMonthlyActivity: new CachedTargetingGetter("getUserMonthlyActivity"),
    374    UnhandledCampaignAction: new CacheUnhandledCampaignAction(),
    375  },
    376  getters: {
    377    doesAppNeedPin: new CachedTargetingGetter(
    378      "doesAppNeedPin",
    379      null,
    380      FRECENT_SITES_UPDATE_INTERVAL,
    381      ShellService
    382    ),
    383    doesAppNeedPrivatePin: new CachedTargetingGetter(
    384      "doesAppNeedPin",
    385      true,
    386      FRECENT_SITES_UPDATE_INTERVAL,
    387      ShellService
    388    ),
    389    doesAppNeedStartMenuPin: new CachedTargetingGetter(
    390      "doesAppNeedStartMenuPin",
    391      null,
    392      FRECENT_SITES_UPDATE_INTERVAL,
    393      ShellService
    394    ),
    395    isDefaultBrowser: new CachedTargetingGetter(
    396      "isDefaultBrowser",
    397      null,
    398      FRECENT_SITES_UPDATE_INTERVAL,
    399      ShellService
    400    ),
    401    currentThemes: new CachedTargetingGetter(
    402      "getAddonsByTypes",
    403      ["theme"],
    404      FRECENT_SITES_UPDATE_INTERVAL,
    405      lazy.AddonManager // eslint-disable-line mozilla/valid-lazy
    406    ),
    407    isDefaultHTMLHandler: new CachedTargetingGetter(
    408      "isDefaultHandlerFor",
    409      [".html"],
    410      FRECENT_SITES_UPDATE_INTERVAL,
    411      ShellService
    412    ),
    413    isDefaultPDFHandler: new CachedTargetingGetter(
    414      "isDefaultHandlerFor",
    415      [".pdf"],
    416      FRECENT_SITES_UPDATE_INTERVAL,
    417      ShellService
    418    ),
    419    defaultPDFHandler: new CachedTargetingGetter(
    420      "getDefaultPDFHandler",
    421      null,
    422      FRECENT_SITES_UPDATE_INTERVAL,
    423      ShellService
    424    ),
    425    profileGroupId: new CachedTargetingGetter(
    426      "getCachedProfileGroupID",
    427      null,
    428      FRECENT_SITES_UPDATE_INTERVAL,
    429      ClientID
    430    ),
    431    profileGroupProfileCount: new CachedTargetingGetter(
    432      "getProfileGroupProfileCount",
    433      null,
    434      FRECENT_SITES_UPDATE_INTERVAL,
    435      {
    436        getProfileGroupProfileCount() {
    437          if (
    438            !Services.prefs.getBoolPref("browser.profiles.enabled", false) ||
    439            !Services.prefs.getBoolPref("browser.profiles.created", false)
    440          ) {
    441            return 0;
    442          }
    443 
    444          return lazy.SelectableProfileService.getProfileCount();
    445        },
    446      }
    447    ),
    448    backupsInfo: new CachedTargetingGetter(
    449      "findBackupsInWellKnownLocations",
    450      null,
    451      FRECENT_SITES_UPDATE_INTERVAL,
    452      {
    453        async findBackupsInWellKnownLocations() {
    454          let bs;
    455          try {
    456            bs = lazy.BackupService.get();
    457          } catch {
    458            bs = lazy.BackupService.init();
    459          }
    460          return bs.findBackupsInWellKnownLocations();
    461        },
    462      }
    463    ),
    464  },
    465 };
    466 
    467 /**
    468 * sortMessagesByWeightedRank
    469 *
    470 * Each message has an associated weight, which is guaranteed to be strictly
    471 * positive. Sort the messages so that higher weighted messages are more likely
    472 * to come first.
    473 *
    474 * Specifically, sort them so that the probability of message x_1 with weight
    475 * w_1 appearing before message x_2 with weight w_2 is (w_1 / (w_1 + w_2)).
    476 *
    477 * This is equivalent to requiring that x_1 appearing before x_2 is (w_1 / w_2)
    478 * "times" as likely as x_2 appearing before x_1.
    479 *
    480 * See Bug 1484996, Comment 2 for a justification of the method.
    481 *
    482 * @param {Array} messages - A non-empty array of messages to sort, all with
    483 *                           strictly positive weights
    484 * @returns the sorted array
    485 */
    486 function sortMessagesByWeightedRank(messages) {
    487  return messages
    488    .map(message => ({
    489      message,
    490      rank: Math.pow(Math.random(), 1 / message.weight),
    491    }))
    492    .sort((a, b) => b.rank - a.rank)
    493    .map(({ message }) => message);
    494 }
    495 
    496 /**
    497 * getSortedMessages - Given an array of Messages, applies sorting and filtering rules
    498 *                     in expected order.
    499 *
    500 * @param {Array<Message>} messages
    501 * @param {{}} options
    502 * @param {boolean} options.ordered - Should .order be used instead of random weighted sorting?
    503 * @returns {Array<Message>}
    504 */
    505 export function getSortedMessages(messages, options = {}) {
    506  let { ordered } = { ordered: false, ...options };
    507  let result = messages;
    508 
    509  if (!ordered) {
    510    result = sortMessagesByWeightedRank(result);
    511  }
    512 
    513  result.sort((a, b) => {
    514    // Next, sort by priority
    515    if (a.priority > b.priority || (!isNaN(a.priority) && isNaN(b.priority))) {
    516      return -1;
    517    }
    518    if (a.priority < b.priority || (isNaN(a.priority) && !isNaN(b.priority))) {
    519      return 1;
    520    }
    521 
    522    // Sort messages with targeting expressions higher than those with none
    523    if (a.targeting && !b.targeting) {
    524      return -1;
    525    }
    526    if (!a.targeting && b.targeting) {
    527      return 1;
    528    }
    529 
    530    // Next, sort by order *ascending* if ordered = true
    531    if (ordered) {
    532      if (a.order > b.order || (!isNaN(a.order) && isNaN(b.order))) {
    533        return 1;
    534      }
    535      if (a.order < b.order || (isNaN(a.order) && !isNaN(b.order))) {
    536        return -1;
    537      }
    538    }
    539 
    540    return 0;
    541  });
    542 
    543  return result;
    544 }
    545 
    546 /**
    547 * parseAboutPageURL - Parse a URL string retrieved from about:home and about:new, returns
    548 *                    its type (web extenstion or custom url) and the parsed url(s)
    549 *
    550 * @param {string} url - A URL string for home page or newtab page
    551 * @returns  {{isWebExt: boolean, isCustomUrl: boolean, urls: {url: string, host: string}[]}}
    552 */
    553 function parseAboutPageURL(url) {
    554  let ret = {
    555    isWebExt: false,
    556    isCustomUrl: false,
    557    urls: [],
    558  };
    559  if (lazy.ExtensionUtils.isExtensionUrl(url)) {
    560    ret.isWebExt = true;
    561    ret.urls.push({ url, host: "" });
    562  } else {
    563    // The home page URL could be either a single URL or a list of "|" separated URLs.
    564    // Note that it should work with "about:home" and "about:blank", in which case the
    565    // "host" is set as an empty string.
    566    for (const _url of url.split("|")) {
    567      if (!["about:home", "about:newtab", "about:blank"].includes(_url)) {
    568        ret.isCustomUrl = true;
    569      }
    570      try {
    571        const parsedURL = new URL(_url);
    572        const host = parsedURL.hostname.replace(/^www\./i, "");
    573        ret.urls.push({ url: _url, host });
    574      } catch (e) {}
    575    }
    576    // If URL parsing failed, just return the given url with an empty host
    577    if (!ret.urls.length) {
    578      ret.urls.push({ url, host: "" });
    579    }
    580  }
    581 
    582  return ret;
    583 }
    584 
    585 /**
    586 * Get the number of records in autofill storage, e.g. credit cards/addresses.
    587 *
    588 * @param  {object} [data]
    589 * @param  {string} [data.collectionName]
    590 *         The name used to specify which collection to retrieve records.
    591 * @param  {string} [data.searchString]
    592 *         The typed string for filtering out the matched records.
    593 * @param  {string} [data.info]
    594 *         The input autocomplete property's information.
    595 * @returns {Promise<number>} The number of matched records.
    596 * @see FormAutofillParent._getRecords
    597 */
    598 async function getAutofillRecords(data) {
    599  let actor;
    600  try {
    601    const win = Services.wm.getMostRecentBrowserWindow();
    602    actor =
    603      win.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
    604        "FormAutofill"
    605      );
    606  } catch (error) {
    607    // If the actor is not available, we can't get the records. We could import
    608    // the records directly from FormAutofillStorage to avoid the messiness of
    609    // JSActors, but that would import a lot of code for a targeting attribute.
    610    return 0;
    611  }
    612  let records = await actor?.receiveMessage({
    613    name: "FormAutofill:GetRecords",
    614    data,
    615  });
    616  return records?.records?.length ?? 0;
    617 }
    618 
    619 // Attribution data can be encoded multiple times so we need this function to
    620 // get a cleartext value.
    621 function decodeAttributionValue(value) {
    622  if (!value) {
    623    return null;
    624  }
    625 
    626  let decodedValue = value;
    627 
    628  while (decodedValue.includes("%")) {
    629    try {
    630      const result = decodeURIComponent(decodedValue);
    631      if (result === decodedValue) {
    632        break;
    633      }
    634      decodedValue = result;
    635    } catch (e) {
    636      break;
    637    }
    638  }
    639 
    640  return decodedValue;
    641 }
    642 
    643 async function getPinStatus() {
    644  return await ShellService.doesAppNeedPin();
    645 }
    646 
    647 const TargetingGetters = {
    648  get locale() {
    649    return Services.locale.appLocaleAsBCP47;
    650  },
    651  get localeLanguageCode() {
    652    return (
    653      Services.locale.appLocaleAsBCP47 &&
    654      Services.locale.appLocaleAsBCP47.substr(0, 2)
    655    );
    656  },
    657  get browserSettings() {
    658    const { settings } = lazy.TelemetryEnvironment.currentEnvironment;
    659    return {
    660      update: settings.update,
    661    };
    662  },
    663  get attributionData() {
    664    // Attribution is determined at startup - so we can use the cached attribution at this point
    665    return lazy.AttributionCode.getCachedAttributionData();
    666  },
    667  get currentDate() {
    668    return new Date();
    669  },
    670  get canCreateSelectableProfiles() {
    671    if (!AppConstants.MOZ_SELECTABLE_PROFILES) {
    672      return false;
    673    }
    674    return lazy.SelectableProfileService?.isEnabled ?? false;
    675  },
    676  get hasSelectableProfiles() {
    677    return lazy.profilesCreated;
    678  },
    679  get profileAgeCreated() {
    680    return lazy.ProfileAge().then(times => times.created);
    681  },
    682  get profileAgeReset() {
    683    return lazy.ProfileAge().then(times => times.reset);
    684  },
    685  get usesFirefoxSync() {
    686    return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF);
    687  },
    688  get isFxAEnabled() {
    689    return lazy.isFxAEnabled;
    690  },
    691  get isFxASignedIn() {
    692    return new Promise(resolve => {
    693      if (!lazy.isFxAEnabled) {
    694        resolve(false);
    695      }
    696      if (Services.prefs.getStringPref(FXA_USERNAME_PREF, "")) {
    697        resolve(true);
    698      }
    699      lazy.fxAccounts
    700        .getSignedInUser()
    701        .then(data => resolve(!!data))
    702        .catch(() => resolve(false));
    703    });
    704  },
    705  get sync() {
    706    return {
    707      desktopDevices: lazy.clientsDevicesDesktop,
    708      mobileDevices: lazy.clientsDevicesMobile,
    709      totalDevices: lazy.syncNumClients,
    710    };
    711  },
    712  get xpinstallEnabled() {
    713    // This is needed for all add-on recommendations, to know if we allow xpi installs in the first place
    714    return lazy.isXPIInstallEnabled;
    715  },
    716  get addonsInfo() {
    717    let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
    718      Ci.nsIBackgroundTasks
    719    );
    720    if (bts?.isBackgroundTaskMode) {
    721      return { addons: {}, isFullData: true };
    722    }
    723 
    724    return lazy.AddonManager.getActiveAddons(["extension", "service"]).then(
    725      ({ addons, fullData }) => {
    726        const info = {};
    727        let hasInstalledAddons = false;
    728        for (const addon of addons) {
    729          info[addon.id] = {
    730            version: addon.version,
    731            type: addon.type,
    732            isSystem: addon.isSystem,
    733            isWebExtension: addon.isWebExtension,
    734            hidden: addon.hidden,
    735            isBuiltin: addon.isBuiltin,
    736          };
    737          if (fullData) {
    738            Object.assign(info[addon.id], {
    739              name: addon.name,
    740              userDisabled: addon.userDisabled,
    741              installDate: addon.installDate,
    742            });
    743          }
    744          // special-powers and mochikit are addons installed in tests that
    745          // are not "isSystem" or "isBuiltin"
    746          const testAddons = [
    747            "special-powers@mozilla.org",
    748            "mochikit@mozilla.org",
    749          ];
    750          if (
    751            !addon.isSystem &&
    752            !addon.isBuiltin &&
    753            !testAddons.includes(addon.id)
    754          ) {
    755            hasInstalledAddons = true;
    756          }
    757        }
    758        return { addons: info, isFullData: fullData, hasInstalledAddons };
    759      }
    760    );
    761  },
    762  get searchEngines() {
    763    const NONE = { installed: [], current: "" };
    764    let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
    765      Ci.nsIBackgroundTasks
    766    );
    767    if (bts?.isBackgroundTaskMode) {
    768      return Promise.resolve(NONE);
    769    }
    770    return new Promise(resolve => {
    771      // Note: calling getAppProvidedEngines, calls Services.search.init which
    772      // ensures this code is only executed after Search has been initialized.
    773      Services.search
    774        .getAppProvidedEngines()
    775        .then(engines => {
    776          let { defaultEngine } = Services.search;
    777          resolve({
    778            // Skip reporting the id for third party engines.
    779            current: defaultEngine.isAppProvided ? defaultEngine.id : null,
    780            // We don't need to filter the id here, as getAppProvidedEngines has
    781            // already done that for us.
    782            installed: engines.map(engine => engine.id),
    783          });
    784        })
    785        .catch(() => resolve(NONE));
    786    });
    787  },
    788  get isDefaultBrowser() {
    789    return QueryCache.getters.isDefaultBrowser.get().catch(() => null);
    790  },
    791  get isDefaultBrowserUncached() {
    792    return ShellService.isDefaultBrowser();
    793  },
    794  get devToolsOpenedCount() {
    795    return lazy.devtoolsSelfXSSCount;
    796  },
    797  get topFrecentSites() {
    798    return QueryCache.queries.TopFrecentSites.get().then(sites =>
    799      sites.map(site => ({
    800        url: site.url,
    801        host: new URL(site.url).hostname,
    802        frecency: site.frecency,
    803        lastVisitDate: site.lastVisitDate,
    804      }))
    805    );
    806  },
    807  get recentBookmarks() {
    808    return QueryCache.queries.RecentBookmarks.get();
    809  },
    810  get pinnedSites() {
    811    return NewTabUtils.pinnedLinks.links.map(site =>
    812      site
    813        ? {
    814            url: site.url,
    815            host: new URL(site.url).hostname,
    816            searchTopSite: site.searchTopSite,
    817          }
    818        : {}
    819    );
    820  },
    821  get providerCohorts() {
    822    return lazy.ASRouterPreferences.providers.reduce((prev, current) => {
    823      prev[current.id] = current.cohort || "";
    824      return prev;
    825    }, {});
    826  },
    827  get totalBookmarksCount() {
    828    return QueryCache.queries.TotalBookmarksCount.get();
    829  },
    830  get firefoxVersion() {
    831    return parseInt(AppConstants.MOZ_APP_VERSION.match(/\d+/), 10);
    832  },
    833  get region() {
    834    return lazy.Region.home || "";
    835  },
    836  get needsUpdate() {
    837    return QueryCache.queries.CheckBrowserNeedsUpdate.get();
    838  },
    839  get savedTabGroups() {
    840    return lazy.SessionStore.getSavedTabGroups().length;
    841  },
    842  get currentTabGroups() {
    843    let win = lazy.BrowserWindowTracker.getTopWindow({
    844      allowFromInactiveWorkspace: true,
    845    });
    846    // If there's no window, there can't be any current tab groups.
    847    if (!win) {
    848      return 0;
    849    }
    850    let totalTabGroups = win.gBrowser.getAllTabGroups().length;
    851    return totalTabGroups;
    852  },
    853  get currentTabInstalledAsWebApp() {
    854    let win = lazy.BrowserWindowTracker.getTopWindow({
    855      allowFromInactiveWorkspace: true,
    856    });
    857    if (!win) {
    858      // There is no active tab, so it isn't a web app.
    859      return false;
    860    }
    861 
    862    // Note: this is a promise!
    863    return (
    864      lazy.TaskbarTabs.findTaskbarTab(
    865        win.gBrowser.selectedBrowser.currentURI,
    866        win.gBrowser.selectedTab.userContextId
    867      )
    868        .then(aTaskbarTab => aTaskbarTab !== null)
    869        // If this is not an nsIURL (e.g. if it's about:blank), then this will
    870        // throw; in that case there isn't a matching web app.
    871        .catch(() => false)
    872    );
    873  },
    874  get hasPinnedTabs() {
    875    for (let win of Services.wm.getEnumerator("navigator:browser")) {
    876      if (win.closed || !win.ownerGlobal.gBrowser) {
    877        continue;
    878      }
    879      if (win.ownerGlobal.gBrowser.visibleTabs.filter(t => t.pinned).length) {
    880        return true;
    881      }
    882    }
    883 
    884    return false;
    885  },
    886  get hasAccessedFxAPanel() {
    887    return lazy.hasAccessedFxAPanel;
    888  },
    889  get userPrefs() {
    890    return {
    891      cfrFeatures: lazy.cfrFeaturesUserPref,
    892      cfrAddons: lazy.cfrAddonsUserPref,
    893    };
    894  },
    895  get totalBlockedCount() {
    896    return lazy.TrackingDBService.sumAllEvents();
    897  },
    898  get blockedCountByType() {
    899    const idToTextMap = new Map([
    900      [Ci.nsITrackingDBService.TRACKERS_ID, "trackerCount"],
    901      [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookieCount"],
    902      [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominerCount"],
    903      [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinterCount"],
    904      [Ci.nsITrackingDBService.SOCIAL_ID, "socialCount"],
    905    ]);
    906 
    907    const dateTo = new Date();
    908    const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000);
    909    return lazy.TrackingDBService.getEventsByDateRange(dateFrom, dateTo).then(
    910      eventsByDate => {
    911        let totalEvents = {};
    912        for (let blockedType of idToTextMap.values()) {
    913          totalEvents[blockedType] = 0;
    914        }
    915 
    916        return eventsByDate.reduce((acc, day) => {
    917          const type = day.getResultByName("type");
    918          const count = day.getResultByName("count");
    919          acc[idToTextMap.get(type)] = acc[idToTextMap.get(type)] + count;
    920          return acc;
    921        }, totalEvents);
    922      }
    923    );
    924  },
    925  get attachedFxAOAuthClients() {
    926    return this.usesFirefoxSync
    927      ? new Promise(resolve =>
    928          lazy.fxAccounts
    929            .listAttachedOAuthClients()
    930            .then(clients => resolve(clients))
    931            .catch(() => resolve([]))
    932        )
    933      : [];
    934  },
    935  get platformName() {
    936    return AppConstants.platform;
    937  },
    938  get isChinaRepack() {
    939    return lazy.BrowserUtils.isChinaRepack();
    940  },
    941  get userId() {
    942    return lazy.ClientEnvironment.userId;
    943  },
    944  get profileRestartCount() {
    945    let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
    946      Ci.nsIBackgroundTasks
    947    );
    948    if (bts?.isBackgroundTaskMode) {
    949      return 0;
    950    }
    951    // Counter starts at 1 when a profile is created, substract 1 so the value
    952    // returned matches expectations
    953    return (
    954      lazy.TelemetrySession.getMetadata("targeting").profileSubsessionCounter -
    955      1
    956    );
    957  },
    958  get homePageSettings() {
    959    const url = lazy.HomePage.get();
    960    const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url);
    961 
    962    return {
    963      isWebExt,
    964      isCustomUrl,
    965      urls,
    966      isDefault: lazy.HomePage.isDefault,
    967      isLocked: lazy.HomePage.locked,
    968    };
    969  },
    970  get newtabSettings() {
    971    const url = lazy.AboutNewTab.newTabURL;
    972    const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url);
    973 
    974    return {
    975      isWebExt,
    976      isCustomUrl,
    977      isDefault: lazy.AboutNewTab.activityStreamEnabled,
    978      url: urls[0].url,
    979      host: urls[0].host,
    980    };
    981  },
    982  get activeNotifications() {
    983    let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
    984      Ci.nsIBackgroundTasks
    985    );
    986    if (bts?.isBackgroundTaskMode) {
    987      // This might need to hook into the alert service to enumerate relevant
    988      // persistent native notifications.
    989      return false;
    990    }
    991 
    992    let window = lazy.BrowserWindowTracker.getTopWindow({
    993      allowFromInactiveWorkspace: true,
    994    });
    995 
    996    // Technically this doesn't mean we have active notifications,
    997    // but because we use !activeNotifications to check for conflicts, this should return true
    998    if (!window) {
    999      return true;
   1000    }
   1001 
   1002    let duration = Date.now() - lazy.newTabTopicModalLastSeen;
   1003    let isDialogShowing =
   1004      window.gBrowser?.selectedBrowser.hasAttribute("tabDialogShowing") ||
   1005      window.gDialogBox?.isOpen;
   1006    let isFeatureCalloutShowing = lazy.FeatureCalloutBroker.isCalloutShowing;
   1007 
   1008    if (
   1009      isDialogShowing ||
   1010      isFeatureCalloutShowing ||
   1011      window.gURLBar?.view.isOpen ||
   1012      window.gNotificationBox?.currentNotification ||
   1013      window.gBrowser.readNotificationBox()?.currentNotification ||
   1014      // Avoid showing messages if the newtab Topic selection modal was shown in
   1015      // the past 1 minute
   1016      duration <= NOTIFICATION_INTERVAL_AFTER_TOPIC_MODAL_MS
   1017    ) {
   1018      return true;
   1019    }
   1020    // use observer service to query Newtab
   1021    const subjectWithBrowser = {
   1022      browser: window.gBrowser,
   1023      activeNewtabMessage: false,
   1024    };
   1025    Services.obs.notifyObservers(subjectWithBrowser, "newtab-message-query");
   1026    if (subjectWithBrowser.activeNewtabMessage) {
   1027      return true;
   1028    }
   1029    return false;
   1030  },
   1031 
   1032  get isMajorUpgrade() {
   1033    return lazy.BrowserHandler.majorUpgrade;
   1034  },
   1035 
   1036  get hasActiveEnterprisePolicies() {
   1037    return Services.policies.status === Services.policies.ACTIVE;
   1038  },
   1039 
   1040  get userMonthlyActivity() {
   1041    return QueryCache.queries.UserMonthlyActivity.get();
   1042  },
   1043 
   1044  get doesAppNeedPin() {
   1045    return (async () => {
   1046      return (
   1047        (await QueryCache.getters.doesAppNeedPin.get()) ||
   1048        (await QueryCache.getters.doesAppNeedStartMenuPin.get())
   1049      );
   1050    })();
   1051  },
   1052 
   1053  get doesAppNeedPinUncached() {
   1054    return getPinStatus();
   1055  },
   1056 
   1057  get doesAppNeedPrivatePin() {
   1058    return QueryCache.getters.doesAppNeedPrivatePin.get();
   1059  },
   1060 
   1061  get launchOnLoginEnabled() {
   1062    if (AppConstants.platform !== "win") {
   1063      return false;
   1064    }
   1065    return lazy.WindowsLaunchOnLogin.getLaunchOnLoginEnabled();
   1066  },
   1067 
   1068  get isMSIX() {
   1069    if (AppConstants.platform !== "win") {
   1070      return false;
   1071    }
   1072    // While we can write registry keys using external programs, we have no
   1073    // way of cleanup on uninstall. If we are on an MSIX build
   1074    // launch on login should never be enabled.
   1075    // Default to false so that the feature isn't unnecessarily
   1076    // disabled.
   1077    // See Bug 1888263.
   1078    return Services.sysinfo.getProperty("hasWinPackageId", false);
   1079  },
   1080 
   1081  get packageFamilyName() {
   1082    if (AppConstants.platform !== "win") {
   1083      // PackageFamilyNames are an MSIX feature, so they won't be available on non-Windows platforms.
   1084      return null;
   1085    }
   1086 
   1087    let packageFamilyName = Services.sysinfo.getProperty(
   1088      "winPackageFamilyName"
   1089    );
   1090    if (packageFamilyName === "") {
   1091      return null;
   1092    }
   1093 
   1094    return packageFamilyName;
   1095  },
   1096 
   1097  /**
   1098   * Is this invocation running in background task mode?
   1099   *
   1100   * @return {boolean} `true` if running in background task mode.
   1101   */
   1102  get isBackgroundTaskMode() {
   1103    let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
   1104      Ci.nsIBackgroundTasks
   1105    );
   1106    return !!bts?.isBackgroundTaskMode;
   1107  },
   1108 
   1109  /**
   1110   * A non-empty task name if this invocation is running in background
   1111   * task mode, or `null` if this invocation is not running in
   1112   * background task mode.
   1113   *
   1114   * @return {string|null} background task name or `null`.
   1115   */
   1116  get backgroundTaskName() {
   1117    let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
   1118      Ci.nsIBackgroundTasks
   1119    );
   1120    return bts?.backgroundTaskName();
   1121  },
   1122 
   1123  get userPrefersReducedMotion() {
   1124    return Services.appinfo.prefersReducedMotion;
   1125  },
   1126 
   1127  /**
   1128   * The distribution id, if any.
   1129   *
   1130   * @return {string}
   1131   */
   1132  get distributionId() {
   1133    return Services.prefs
   1134      .getDefaultBranch(null)
   1135      .getCharPref("distribution.id", "");
   1136  },
   1137 
   1138  /**
   1139   * Where the Firefox View button is shown, if at all.
   1140   *
   1141   * @return {string} container of the button if it is shown in the toolbar/overflow menu
   1142   * @return {string} `null` if the button has been removed
   1143   */
   1144  get fxViewButtonAreaType() {
   1145    let button = lazy.CustomizableUI.getWidget("firefox-view-button");
   1146    return button.areaType;
   1147  },
   1148 
   1149  get alltabsButtonAreaType() {
   1150    let button = lazy.CustomizableUI.getWidget("alltabs-button");
   1151    return button.areaType;
   1152  },
   1153 
   1154  isDefaultHandler: {
   1155    get html() {
   1156      return QueryCache.getters.isDefaultHTMLHandler.get();
   1157    },
   1158    get pdf() {
   1159      return QueryCache.getters.isDefaultPDFHandler.get();
   1160    },
   1161  },
   1162 
   1163  get defaultPDFHandler() {
   1164    return QueryCache.getters.defaultPDFHandler.get();
   1165  },
   1166 
   1167  get creditCardsSaved() {
   1168    return getAutofillRecords({ collectionName: "creditCards" });
   1169  },
   1170 
   1171  get addressesSaved() {
   1172    return getAutofillRecords({ collectionName: "addresses" });
   1173  },
   1174 
   1175  /**
   1176   * Has the user ever used the Migration Wizard to migrate bookmarks?
   1177   *
   1178   * @return {boolean} `true` if bookmark migration has occurred.
   1179   */
   1180  get hasMigratedBookmarks() {
   1181    return lazy.hasMigratedBookmarks;
   1182  },
   1183 
   1184  /**
   1185   * Has the user ever used the Migration Wizard to migrate passwords from
   1186   * a CSV file?
   1187   *
   1188   * @return {boolean} `true` if CSV passwords have been imported via the
   1189   *   migration wizard.
   1190   */
   1191  get hasMigratedCSVPasswords() {
   1192    return lazy.hasMigratedCSVPasswords;
   1193  },
   1194 
   1195  /**
   1196   * Has the user ever used the Migration Wizard to migrate history?
   1197   *
   1198   * @return {boolean} `true` if history migration has occurred.
   1199   */
   1200  get hasMigratedHistory() {
   1201    return lazy.hasMigratedHistory;
   1202  },
   1203 
   1204  /**
   1205   * Has the user ever used the Migration Wizard to migrate passwords?
   1206   *
   1207   * @return {boolean} `true` if password migration has occurred.
   1208   */
   1209  get hasMigratedPasswords() {
   1210    return lazy.hasMigratedPasswords;
   1211  },
   1212 
   1213  /**
   1214   * Returns true if the user is configured to use the embedded migration
   1215   * wizard in about:welcome by having
   1216   * "browser.migrate.content-modal.about-welcome-behavior" be equal to
   1217   * "embedded".
   1218   *
   1219   * @return {boolean} `true` if the embedded migration wizard is enabled.
   1220   */
   1221  get useEmbeddedMigrationWizard() {
   1222    return lazy.useEmbeddedMigrationWizard;
   1223  },
   1224 
   1225  /**
   1226   * Returns the version number of the New Tab built-in addon being used
   1227   * by the build.
   1228   *
   1229   * @return {string}
   1230   */
   1231  get newtabAddonVersion() {
   1232    return lazy.AboutNewTabResourceMapping.addonVersion;
   1233  },
   1234 
   1235  /**
   1236   * Whether the user installed Firefox via the RTAMO flow.
   1237   *
   1238   * @return {boolean} `true` when RTAMO has been used to download Firefox,
   1239   * `false` otherwise.
   1240   */
   1241  get isRTAMO() {
   1242    const { attributionData } = this;
   1243 
   1244    return (
   1245      attributionData?.source === "addons.mozilla.org" &&
   1246      !!decodeAttributionValue(attributionData?.content)?.startsWith("rta:")
   1247    );
   1248  },
   1249 
   1250  /**
   1251   * Whether the user installed via the device migration flow.
   1252   *
   1253   * @return {boolean} `true` when the link to download the browser was part
   1254   * of guidance for device migration. `false` otherwise.
   1255   */
   1256  get isDeviceMigration() {
   1257    const { attributionData } = this;
   1258 
   1259    return attributionData?.campaign === "migration";
   1260  },
   1261 
   1262  /**
   1263   * Whether the user opted into a special message action represented by an
   1264   * installer attribution campaign and this choice still needs to be honored.
   1265   *
   1266   * @return {string} A special message action to be executed on first-run. For
   1267   * example, `"SET_DEFAULT_BROWSER"` when the user selected to set as default
   1268   * via the install marketing page and set default has not yet been
   1269   * automatically triggered, 'null' otherwise.
   1270   */
   1271  get unhandledCampaignAction() {
   1272    return QueryCache.queries.UnhandledCampaignAction.get();
   1273  },
   1274  /**
   1275   * The values of the height and width available to the browser to display
   1276   * web content. The available height and width are each calculated taking
   1277   * into account the presence of menu bars, docks, and other similar OS elements
   1278   *
   1279   * @returns {object} resolution The resolution object containing width and height
   1280   * @returns {number} resolution.width The available width of the primary monitor
   1281   * @returns {number} resolution.height The available height of the primary monitor
   1282   */
   1283  get primaryResolution() {
   1284    const { primaryScreen } = lazy.ScreenManager;
   1285    const { defaultCSSScaleFactor } = primaryScreen;
   1286    let availDeviceLeft = {};
   1287    let availDeviceTop = {};
   1288    let availDeviceWidth = {};
   1289    let availDeviceHeight = {};
   1290    primaryScreen.GetAvailRect(
   1291      availDeviceLeft,
   1292      availDeviceTop,
   1293      availDeviceWidth,
   1294      availDeviceHeight
   1295    );
   1296    return {
   1297      width: Math.floor(availDeviceWidth.value / defaultCSSScaleFactor),
   1298      height: Math.floor(availDeviceHeight.value / defaultCSSScaleFactor),
   1299    };
   1300  },
   1301 
   1302  get archBits() {
   1303    let bits = null;
   1304    try {
   1305      bits = Services.sysinfo.getProperty("archbits", null);
   1306    } catch (_e) {
   1307      // getProperty can throw if the memsize does not exist
   1308    }
   1309    if (bits) {
   1310      bits = Number(bits);
   1311    }
   1312    return bits;
   1313  },
   1314 
   1315  get systemArch() {
   1316    try {
   1317      return Services.sysinfo.get("arch");
   1318    } catch (_e) {
   1319      return null;
   1320    }
   1321  },
   1322 
   1323  get memoryMB() {
   1324    let memory = null;
   1325    try {
   1326      memory = Services.sysinfo.getProperty("memsize", null);
   1327    } catch (_e) {
   1328      // getProperty can throw if the memsize does not exist
   1329    }
   1330    if (memory) {
   1331      memory = Number(memory) / 1024 / 1024;
   1332    }
   1333    return memory;
   1334  },
   1335 
   1336  get totalSearches() {
   1337    return lazy.totalSearches;
   1338  },
   1339 
   1340  get profileGroupId() {
   1341    return QueryCache.getters.profileGroupId.get();
   1342  },
   1343 
   1344  get currentProfileId() {
   1345    if (!lazy.SelectableProfileService.currentProfile) {
   1346      return "";
   1347    }
   1348    return lazy.SelectableProfileService.currentProfile.id.toString();
   1349  },
   1350 
   1351  get profileGroupProfileCount() {
   1352    return QueryCache.getters.profileGroupProfileCount.get();
   1353  },
   1354 
   1355  get buildId() {
   1356    return parseInt(AppConstants.MOZ_BUILDID, 10);
   1357  },
   1358 
   1359  get backupsInfo() {
   1360    return QueryCache.getters.backupsInfo.get().catch(() => null);
   1361  },
   1362 
   1363  get backupArchiveEnabled() {
   1364    let bs;
   1365    try {
   1366      bs = lazy.BackupService.get();
   1367    } catch {
   1368      bs = lazy.BackupService.init();
   1369    }
   1370    return bs.archiveEnabledStatus.enabled;
   1371  },
   1372 
   1373  get backupRestoreEnabled() {
   1374    let bs;
   1375    try {
   1376      bs = lazy.BackupService.get();
   1377    } catch {
   1378      bs = lazy.BackupService.init();
   1379    }
   1380    return bs.restoreEnabledStatus.enabled;
   1381  },
   1382 
   1383  get isEncryptedBackup() {
   1384    const isEncryptedBackup =
   1385      Services.prefs.getStringPref(
   1386        "messaging-system-action.backupChooser",
   1387        null
   1388      ) === "full";
   1389    return isEncryptedBackup;
   1390  },
   1391 };
   1392 
   1393 function addAIWindowTargeting(targeting) {
   1394  if (!targeting || targeting === "true") {
   1395    // Default behavior: Classic-only if no targeting is specified
   1396    return `!isAIWindow`;
   1397  }
   1398 
   1399  if (/\bisAIWindow\b/.test(targeting)) {
   1400    return targeting;
   1401  }
   1402 
   1403  return `((${targeting}) && !isAIWindow)`;
   1404 }
   1405 
   1406 export const ASRouterTargeting = {
   1407  Environment: TargetingGetters,
   1408 
   1409  /**
   1410   * Snapshot the current targeting environment.
   1411   *
   1412   * Asynchronous getters are handled.  Getters that throw or reject
   1413   * are ignored.
   1414   *
   1415   * Leftward (earlier) targets supercede rightward (later) targets, just like
   1416   * `TargetingContext.combineContexts`.
   1417   *
   1418   * @param {object} options - object containing:
   1419   * @param {Array<object>|null} options.targets -
   1420   *        targeting environments to snapshot; (default: `[ASRouterTargeting.Environment]`)
   1421   * @return {object} snapshot of target with `environment` object and `version` integer.
   1422   */
   1423  async getEnvironmentSnapshot({
   1424    targets = [ASRouterTargeting.Environment],
   1425  } = {}) {
   1426    async function resolve(object) {
   1427      if (typeof object === "object" && object !== null) {
   1428        if (Array.isArray(object)) {
   1429          return Promise.all(object.map(async item => resolve(await item)));
   1430        }
   1431 
   1432        if (object instanceof Date) {
   1433          return object;
   1434        }
   1435 
   1436        // One promise for each named property. Label promises with property name.
   1437        const promises = Object.keys(object).map(async key => {
   1438          // Each promise needs to check if we're shutting down when it is evaluated.
   1439          if (Services.startup.shuttingDown) {
   1440            throw new Error(
   1441              "shutting down, so not querying targeting environment"
   1442            );
   1443          }
   1444 
   1445          const value = await resolve(await object[key]);
   1446 
   1447          return [key, value];
   1448        });
   1449 
   1450        const resolved = {};
   1451        for (const result of await Promise.allSettled(promises)) {
   1452          // Ignore properties that are rejected.
   1453          if (result.status === "fulfilled") {
   1454            const [key, value] = result.value;
   1455            resolved[key] = value;
   1456          }
   1457        }
   1458 
   1459        return resolved;
   1460      }
   1461 
   1462      return object;
   1463    }
   1464 
   1465    // We would like to use `TargetingContext.combineContexts`, but `Proxy`
   1466    // instances complicate iterating with `Object.keys`.  Instead, merge by
   1467    // hand after resolving.
   1468    const environment = {};
   1469    for (let target of targets.toReversed()) {
   1470      Object.assign(environment, await resolve(target));
   1471    }
   1472 
   1473    // Should we need to migrate in the future.
   1474    const snapshot = { environment, version: 1 };
   1475 
   1476    return snapshot;
   1477  },
   1478 
   1479  isTriggerMatch(trigger = {}, candidateMessageTrigger = {}) {
   1480    if (trigger.id !== candidateMessageTrigger.id) {
   1481      return false;
   1482    } else if (
   1483      !candidateMessageTrigger.params &&
   1484      !candidateMessageTrigger.patterns
   1485    ) {
   1486      return true;
   1487    }
   1488 
   1489    if (!trigger.param) {
   1490      return false;
   1491    }
   1492 
   1493    return (
   1494      (candidateMessageTrigger.params &&
   1495        trigger.param.host &&
   1496        candidateMessageTrigger.params.includes(trigger.param.host)) ||
   1497      (candidateMessageTrigger.params &&
   1498        trigger.param.type &&
   1499        candidateMessageTrigger.params.filter(t => t === trigger.param.type)
   1500          .length) ||
   1501      (candidateMessageTrigger.params &&
   1502        trigger.param.type &&
   1503        candidateMessageTrigger.params.filter(
   1504          t => (t & trigger.param.type) === t
   1505        ).length) ||
   1506      (candidateMessageTrigger.patterns &&
   1507        trigger.param.url &&
   1508        new MatchPatternSet(candidateMessageTrigger.patterns).matches(
   1509          trigger.param.url
   1510        ))
   1511    );
   1512  },
   1513 
   1514  /**
   1515   * getCachedEvaluation - Return a cached jexl evaluation if available
   1516   *
   1517   * @param {string} targeting JEXL expression to lookup
   1518   * @returns {obj|null} Object with value result or null if not available
   1519   */
   1520  getCachedEvaluation(targeting) {
   1521    if (jexlEvaluationCache.has(targeting)) {
   1522      const { timestamp, value } = jexlEvaluationCache.get(targeting);
   1523      if (Date.now() - timestamp <= CACHE_EXPIRATION) {
   1524        return { value };
   1525      }
   1526      jexlEvaluationCache.delete(targeting);
   1527    }
   1528 
   1529    return null;
   1530  },
   1531 
   1532  /**
   1533   * checkMessageTargeting - Checks is a message's targeting parameters are satisfied
   1534   *
   1535   * @param {*} message An AS router message
   1536   * @param {obj} targetingContext a TargetingContext instance complete with eval environment
   1537   * @param {func} onError A function to handle errors (takes two params; error, message)
   1538   * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused.
   1539   * @returns
   1540   */
   1541  async checkMessageTargeting(message, targetingContext, onError, shouldCache) {
   1542    lazy.ASRouterPreferences.console.debug(
   1543      "in checkMessageTargeting, arguments = ",
   1544      Array.from(arguments) // eslint-disable-line prefer-rest-params
   1545    );
   1546 
   1547    let { targeting } = message;
   1548    targeting = addAIWindowTargeting(targeting);
   1549 
   1550    let result;
   1551    try {
   1552      if (shouldCache) {
   1553        result = this.getCachedEvaluation(targeting);
   1554        if (result) {
   1555          return result.value;
   1556        }
   1557      }
   1558      // Used to report the source of the targeting error in the case of
   1559      // undesired events
   1560      targetingContext.setTelemetrySource(message.id);
   1561      result = await targetingContext.evalWithDefault(targeting);
   1562      if (shouldCache) {
   1563        jexlEvaluationCache.set(targeting, {
   1564          timestamp: Date.now(),
   1565          value: result,
   1566        });
   1567      }
   1568    } catch (error) {
   1569      if (onError) {
   1570        onError(error, message);
   1571      }
   1572      console.error(error);
   1573      result = false;
   1574    }
   1575    return result;
   1576  },
   1577 
   1578  _isMessageMatch(
   1579    message,
   1580    trigger,
   1581    targetingContext,
   1582    onError,
   1583    shouldCache = false
   1584  ) {
   1585    return (
   1586      message &&
   1587      (trigger
   1588        ? this.isTriggerMatch(trigger, message.trigger)
   1589        : !message.trigger) &&
   1590      // If a trigger expression was passed to this function, the message should match it.
   1591      // Otherwise, we should choose a message with no trigger property (i.e. a message that can show up at any time)
   1592      this.checkMessageTargeting(
   1593        message,
   1594        targetingContext,
   1595        onError,
   1596        shouldCache
   1597      )
   1598    );
   1599  },
   1600 
   1601  /**
   1602   * findMatchingMessage - Given an array of messages, returns one message
   1603   *                       whos targeting expression evaluates to true
   1604   *
   1605   * @param {Array<Message>} messages An array of AS router messages
   1606   * @param {trigger} string A trigger expression if a message for that trigger is desired
   1607   * @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
   1608   * @param {func} onError A function to handle errors (takes two params; error, message)
   1609   * @param {func} ordered An optional param when true sort message by order specified in message
   1610   * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused.
   1611   * @param {boolean} returnAll Should we return all matching messages, not just the first one found.
   1612   * @returns {obj|Array<Message>} If returnAll is false, a single message. If returnAll is true, an array of messages.
   1613   */
   1614  async findMatchingMessage({
   1615    messages,
   1616    trigger = {},
   1617    context = {},
   1618    onError,
   1619    ordered = false,
   1620    shouldCache = false,
   1621    returnAll = false,
   1622  }) {
   1623    const sortedMessages = getSortedMessages(messages, { ordered });
   1624    lazy.ASRouterPreferences.console.debug(
   1625      "in findMatchingMessage, sortedMessages = ",
   1626      sortedMessages
   1627    );
   1628    const matching = returnAll ? [] : null;
   1629    const targetingContext = new lazy.TargetingContext(
   1630      lazy.TargetingContext.combineContexts(
   1631        context,
   1632        this.Environment,
   1633        trigger.context || {}
   1634      )
   1635    );
   1636 
   1637    const isMatch = candidate =>
   1638      this._isMessageMatch(
   1639        candidate,
   1640        trigger,
   1641        targetingContext,
   1642        onError,
   1643        shouldCache
   1644      );
   1645 
   1646    for (const candidate of sortedMessages) {
   1647      if (await isMatch(candidate)) {
   1648        // If not returnAll, we should return the first message we find that matches.
   1649        if (!returnAll) {
   1650          return candidate;
   1651        }
   1652 
   1653        matching.push(candidate);
   1654      }
   1655    }
   1656    return matching;
   1657  },
   1658 };