forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			2251 lines
		
	
	
	
		
			73 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2251 lines
		
	
	
	
		
			73 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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";
 | 
						|
 | 
						|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
const { XPCOMUtils } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/XPCOMUtils.jsm"
 | 
						|
);
 | 
						|
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 | 
						|
XPCOMUtils.defineLazyModuleGetters(this, {
 | 
						|
  AppConstants: "resource://gre/modules/AppConstants.jsm",
 | 
						|
  OS: "resource://gre/modules/osfile.jsm",
 | 
						|
  BookmarkPanelHub: "resource://activity-stream/lib/BookmarkPanelHub.jsm",
 | 
						|
  SnippetsTestMessageProvider:
 | 
						|
    "resource://activity-stream/lib/SnippetsTestMessageProvider.jsm",
 | 
						|
  PanelTestProvider: "resource://activity-stream/lib/PanelTestProvider.jsm",
 | 
						|
  ToolbarBadgeHub: "resource://activity-stream/lib/ToolbarBadgeHub.jsm",
 | 
						|
  ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm",
 | 
						|
  MomentsPageHub: "resource://activity-stream/lib/MomentsPageHub.jsm",
 | 
						|
  ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm",
 | 
						|
  QueryCache: "resource://activity-stream/lib/ASRouterTargeting.jsm",
 | 
						|
  ASRouterPreferences: "resource://activity-stream/lib/ASRouterPreferences.jsm",
 | 
						|
  TARGETING_PREFERENCES:
 | 
						|
    "resource://activity-stream/lib/ASRouterPreferences.jsm",
 | 
						|
  ASRouterTriggerListeners:
 | 
						|
    "resource://activity-stream/lib/ASRouterTriggerListeners.jsm",
 | 
						|
  CFRMessageProvider: "resource://activity-stream/lib/CFRMessageProvider.jsm",
 | 
						|
  GroupsConfigurationProvider:
 | 
						|
    "resource://activity-stream/lib/GroupsConfigurationProvider.jsm",
 | 
						|
  KintoHttpClient: "resource://services-common/kinto-http-client.js",
 | 
						|
  Downloader: "resource://services-settings/Attachments.jsm",
 | 
						|
  RemoteL10n: "resource://activity-stream/lib/RemoteL10n.jsm",
 | 
						|
  ExperimentAPI: "resource://messaging-system/experiments/ExperimentAPI.jsm",
 | 
						|
  SpecialMessageActions:
 | 
						|
    "resource://messaging-system/lib/SpecialMessageActions.jsm",
 | 
						|
});
 | 
						|
XPCOMUtils.defineLazyServiceGetters(this, {
 | 
						|
  BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
 | 
						|
});
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  this,
 | 
						|
  "multiStageAboutWelcome",
 | 
						|
  "browser.aboutwelcome.overrideContent",
 | 
						|
  ""
 | 
						|
);
 | 
						|
const { actionTypes: at, actionCreators: ac } = ChromeUtils.import(
 | 
						|
  "resource://activity-stream/common/Actions.jsm"
 | 
						|
);
 | 
						|
 | 
						|
const { CFRMessageProvider } = ChromeUtils.import(
 | 
						|
  "resource://activity-stream/lib/CFRMessageProvider.jsm"
 | 
						|
);
 | 
						|
const { OnboardingMessageProvider } = ChromeUtils.import(
 | 
						|
  "resource://activity-stream/lib/OnboardingMessageProvider.jsm"
 | 
						|
);
 | 
						|
const { RemoteSettings } = ChromeUtils.import(
 | 
						|
  "resource://services-settings/remote-settings.js"
 | 
						|
);
 | 
						|
const { CFRPageActions } = ChromeUtils.import(
 | 
						|
  "resource://activity-stream/lib/CFRPageActions.jsm"
 | 
						|
);
 | 
						|
const { AttributionCode } = ChromeUtils.import(
 | 
						|
  "resource:///modules/AttributionCode.jsm"
 | 
						|
);
 | 
						|
 | 
						|
const TRAILHEAD_CONFIG = {
 | 
						|
  DID_SEE_ABOUT_WELCOME_PREF: "trailhead.firstrun.didSeeAboutWelcome",
 | 
						|
  DYNAMIC_TRIPLET_BUNDLE_LENGTH: 3,
 | 
						|
};
 | 
						|
 | 
						|
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
 | 
						|
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
 | 
						|
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
 | 
						|
// List of hosts for endpoints that serve router messages.
 | 
						|
// Key is allowed host, value is a name for the endpoint host.
 | 
						|
const DEFAULT_WHITELIST_HOSTS = {
 | 
						|
  "activity-stream-icons.services.mozilla.com": "production",
 | 
						|
  "snippets-admin.mozilla.org": "preview",
 | 
						|
};
 | 
						|
const SNIPPETS_ENDPOINT_WHITELIST =
 | 
						|
  "browser.newtab.activity-stream.asrouter.whitelistHosts";
 | 
						|
// Max possible impressions cap for any message
 | 
						|
const MAX_MESSAGE_LIFETIME_CAP = 100;
 | 
						|
 | 
						|
const LOCAL_MESSAGE_PROVIDERS = {
 | 
						|
  OnboardingMessageProvider,
 | 
						|
  CFRMessageProvider,
 | 
						|
};
 | 
						|
const STARTPAGE_VERSION = "6";
 | 
						|
 | 
						|
// Remote Settings
 | 
						|
const RS_SERVER_PREF = "services.settings.server";
 | 
						|
const RS_MAIN_BUCKET = "main";
 | 
						|
const RS_COLLECTION_L10N = "ms-language-packs"; // "ms" stands for Messaging System
 | 
						|
const RS_PROVIDERS_WITH_L10N = ["cfr", "cfr-fxa", "whats-new-panel"];
 | 
						|
const RS_FLUENT_VERSION = "v1";
 | 
						|
const RS_FLUENT_RECORD_PREFIX = `cfr-${RS_FLUENT_VERSION}`;
 | 
						|
const RS_DOWNLOAD_MAX_RETRIES = 2;
 | 
						|
// This is the list of providers for which we want to cache the targeting
 | 
						|
// expression result and reuse between calls. Cache duration is defined in
 | 
						|
// ASRouterTargeting where evaluation takes place.
 | 
						|
const JEXL_PROVIDER_CACHE = new Set(["snippets"]);
 | 
						|
 | 
						|
// To observe the app locale change notification.
 | 
						|
const TOPIC_INTL_LOCALE_CHANGED = "intl:app-locales-changed";
 | 
						|
// To observe the pref that controls if ASRouter should use the remote Fluent files for l10n.
 | 
						|
const USE_REMOTE_L10N_PREF =
 | 
						|
  "browser.newtabpage.activity-stream.asrouter.useRemoteL10n";
 | 
						|
 | 
						|
// Experiment groups that need to report the reach event in Messaging-Experiments.
 | 
						|
// If you're adding new groups to it, make sure they're also added in the
 | 
						|
// `messaging_experiments.reach.objects` defined in "toolkit/components/telemetry/Events.yaml"
 | 
						|
const REACH_EVENT_GROUPS = ["cfr", "moments-page"];
 | 
						|
const REACH_EVENT_CATEGORY = "messaging_experiments";
 | 
						|
const REACH_EVENT_METHOD = "reach";
 | 
						|
 | 
						|
const MessageLoaderUtils = {
 | 
						|
  STARTPAGE_VERSION,
 | 
						|
  REMOTE_LOADER_CACHE_KEY: "RemoteLoaderCache",
 | 
						|
  _errors: [],
 | 
						|
 | 
						|
  reportError(e) {
 | 
						|
    Cu.reportError(e);
 | 
						|
    this._errors.push({
 | 
						|
      timestamp: new Date(),
 | 
						|
      error: { message: e.toString(), stack: e.stack },
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  get errors() {
 | 
						|
    const errors = this._errors;
 | 
						|
    this._errors = [];
 | 
						|
    return errors;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * _localLoader - Loads messages for a local provider (i.e. one that lives in mozilla central)
 | 
						|
   *
 | 
						|
   * @param {obj} provider An AS router provider
 | 
						|
   * @param {Array} provider.messages An array of messages
 | 
						|
   * @returns {Array} the array of messages
 | 
						|
   */
 | 
						|
  _localLoader(provider) {
 | 
						|
    return provider.messages;
 | 
						|
  },
 | 
						|
 | 
						|
  async _localJsonLoader(provider) {
 | 
						|
    let payload;
 | 
						|
    try {
 | 
						|
      payload = await (
 | 
						|
        await fetch(provider.location, {
 | 
						|
          credentials: "omit",
 | 
						|
        })
 | 
						|
      ).json();
 | 
						|
    } catch (e) {
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
 | 
						|
    return payload.messages;
 | 
						|
  },
 | 
						|
 | 
						|
  async _remoteLoaderCache(storage) {
 | 
						|
    let allCached;
 | 
						|
    try {
 | 
						|
      allCached =
 | 
						|
        (await storage.get(MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY)) || {};
 | 
						|
    } catch (e) {
 | 
						|
      // istanbul ignore next
 | 
						|
      MessageLoaderUtils.reportError(e);
 | 
						|
      // istanbul ignore next
 | 
						|
      allCached = {};
 | 
						|
    }
 | 
						|
    return allCached;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * _remoteLoader - Loads messages for a remote provider
 | 
						|
   *
 | 
						|
   * @param {obj} provider An AS router provider
 | 
						|
   * @param {string} provider.url An endpoint that returns an array of messages as JSON
 | 
						|
   * @param {obj} options.storage A storage object with get() and set() methods for caching.
 | 
						|
   * @returns {Promise} resolves with an array of messages, or an empty array if none could be fetched
 | 
						|
   */
 | 
						|
  async _remoteLoader(provider, options) {
 | 
						|
    let remoteMessages = [];
 | 
						|
    if (provider.url) {
 | 
						|
      const allCached = await MessageLoaderUtils._remoteLoaderCache(
 | 
						|
        options.storage
 | 
						|
      );
 | 
						|
      const cached = allCached[provider.id];
 | 
						|
      let etag;
 | 
						|
 | 
						|
      if (
 | 
						|
        cached &&
 | 
						|
        cached.url === provider.url &&
 | 
						|
        cached.version === STARTPAGE_VERSION
 | 
						|
      ) {
 | 
						|
        const { lastFetched, messages } = cached;
 | 
						|
        if (
 | 
						|
          !MessageLoaderUtils.shouldProviderUpdate({
 | 
						|
            ...provider,
 | 
						|
            lastUpdated: lastFetched,
 | 
						|
          })
 | 
						|
        ) {
 | 
						|
          // Cached messages haven't expired, return early.
 | 
						|
          return messages;
 | 
						|
        }
 | 
						|
        etag = cached.etag;
 | 
						|
        remoteMessages = messages;
 | 
						|
      }
 | 
						|
 | 
						|
      let headers = new Headers();
 | 
						|
      if (etag) {
 | 
						|
        headers.set("If-None-Match", etag);
 | 
						|
      }
 | 
						|
 | 
						|
      let response;
 | 
						|
      try {
 | 
						|
        response = await fetch(provider.url, { headers, credentials: "omit" });
 | 
						|
      } catch (e) {
 | 
						|
        MessageLoaderUtils.reportError(e);
 | 
						|
      }
 | 
						|
      if (
 | 
						|
        response &&
 | 
						|
        response.ok &&
 | 
						|
        response.status >= 200 &&
 | 
						|
        response.status < 400
 | 
						|
      ) {
 | 
						|
        let jsonResponse;
 | 
						|
        try {
 | 
						|
          jsonResponse = await response.json();
 | 
						|
        } catch (e) {
 | 
						|
          MessageLoaderUtils.reportError(e);
 | 
						|
          return remoteMessages;
 | 
						|
        }
 | 
						|
        if (jsonResponse && jsonResponse.messages) {
 | 
						|
          remoteMessages = jsonResponse.messages.map(msg => ({
 | 
						|
            ...msg,
 | 
						|
            provider_url: provider.url,
 | 
						|
          }));
 | 
						|
 | 
						|
          // Cache the results if this isn't a preview URL.
 | 
						|
          if (provider.updateCycleInMs > 0) {
 | 
						|
            etag = response.headers.get("ETag");
 | 
						|
            const cacheInfo = {
 | 
						|
              messages: remoteMessages,
 | 
						|
              etag,
 | 
						|
              lastFetched: Date.now(),
 | 
						|
              version: STARTPAGE_VERSION,
 | 
						|
            };
 | 
						|
 | 
						|
            options.storage.set(MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY, {
 | 
						|
              ...allCached,
 | 
						|
              [provider.id]: cacheInfo,
 | 
						|
            });
 | 
						|
          }
 | 
						|
        } else {
 | 
						|
          MessageLoaderUtils.reportError(
 | 
						|
            `No messages returned from ${provider.url}.`
 | 
						|
          );
 | 
						|
        }
 | 
						|
      } else if (response) {
 | 
						|
        MessageLoaderUtils.reportError(
 | 
						|
          `Invalid response status ${response.status} from ${provider.url}.`
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return remoteMessages;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * _remoteSettingsLoader - Loads messages for a RemoteSettings provider
 | 
						|
   *
 | 
						|
   * Note:
 | 
						|
   * 1). Both "cfr" and "cfr-fxa" require the Fluent file for l10n, so there is
 | 
						|
   * another file downloading phase for those two providers after their messages
 | 
						|
   * are successfully fetched from Remote Settings. Currently, they share the same
 | 
						|
   * attachment of the record "${RS_FLUENT_RECORD_PREFIX}-${locale}" in the
 | 
						|
   * "ms-language-packs" collection. E.g. for "en-US" with version "v1",
 | 
						|
   * the Fluent file is attched to the record with ID "cfr-v1-en-US".
 | 
						|
   *
 | 
						|
   * 2). The Remote Settings downloader is able to detect the duplicate download
 | 
						|
   * requests for the same attachment and ignore the redundent requests automatically.
 | 
						|
   *
 | 
						|
   * @param {obj} provider An AS router provider
 | 
						|
   * @param {string} provider.id The id of the provider
 | 
						|
   * @param {string} provider.bucket The name of the Remote Settings bucket
 | 
						|
   * @param {func} options.dispatchToAS dispatch an action the main AS Store
 | 
						|
   * @returns {Promise} resolves with an array of messages, or an empty array if none could be fetched
 | 
						|
   */
 | 
						|
  async _remoteSettingsLoader(provider, options) {
 | 
						|
    let messages = [];
 | 
						|
    if (provider.bucket) {
 | 
						|
      try {
 | 
						|
        messages = await MessageLoaderUtils._getRemoteSettingsMessages(
 | 
						|
          provider.bucket
 | 
						|
        );
 | 
						|
        if (!messages.length) {
 | 
						|
          MessageLoaderUtils._handleRemoteSettingsUndesiredEvent(
 | 
						|
            "ASR_RS_NO_MESSAGES",
 | 
						|
            provider.id,
 | 
						|
            options.dispatchToAS
 | 
						|
          );
 | 
						|
        } else if (RS_PROVIDERS_WITH_L10N.includes(provider.id)) {
 | 
						|
          const locale = Services.locale.appLocaleAsBCP47;
 | 
						|
          const recordId = `${RS_FLUENT_RECORD_PREFIX}-${locale}`;
 | 
						|
          const kinto = new KintoHttpClient(
 | 
						|
            Services.prefs.getStringPref(RS_SERVER_PREF)
 | 
						|
          );
 | 
						|
          const record = await kinto
 | 
						|
            .bucket(RS_MAIN_BUCKET)
 | 
						|
            .collection(RS_COLLECTION_L10N)
 | 
						|
            .getRecord(recordId);
 | 
						|
          if (record && record.data) {
 | 
						|
            const downloader = new Downloader(
 | 
						|
              RS_MAIN_BUCKET,
 | 
						|
              RS_COLLECTION_L10N
 | 
						|
            );
 | 
						|
            // Await here in order to capture the exceptions for reporting.
 | 
						|
            await downloader.download(record.data, {
 | 
						|
              retries: RS_DOWNLOAD_MAX_RETRIES,
 | 
						|
            });
 | 
						|
            RemoteL10n.reloadL10n();
 | 
						|
          } else {
 | 
						|
            MessageLoaderUtils._handleRemoteSettingsUndesiredEvent(
 | 
						|
              "ASR_RS_NO_MESSAGES",
 | 
						|
              RS_COLLECTION_L10N,
 | 
						|
              options.dispatchToAS
 | 
						|
            );
 | 
						|
          }
 | 
						|
        }
 | 
						|
      } catch (e) {
 | 
						|
        MessageLoaderUtils._handleRemoteSettingsUndesiredEvent(
 | 
						|
          "ASR_RS_ERROR",
 | 
						|
          provider.id,
 | 
						|
          options.dispatchToAS
 | 
						|
        );
 | 
						|
        MessageLoaderUtils.reportError(e);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return messages;
 | 
						|
  },
 | 
						|
 | 
						|
  _getRemoteSettingsMessages(bucket) {
 | 
						|
    return RemoteSettings(bucket).get();
 | 
						|
  },
 | 
						|
 | 
						|
  async _experimentsAPILoader(provider, options) {
 | 
						|
    try {
 | 
						|
      await ExperimentAPI.ready();
 | 
						|
    } catch (e) {
 | 
						|
      MessageLoaderUtils.reportError(e);
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
 | 
						|
    let experiments = [];
 | 
						|
    for (const group of provider.messageGroups) {
 | 
						|
      let experimentData;
 | 
						|
      try {
 | 
						|
        experimentData = ExperimentAPI.getExperiment({ group });
 | 
						|
      } catch (e) {
 | 
						|
        MessageLoaderUtils.reportError(e);
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      if (experimentData && experimentData.branch) {
 | 
						|
        experiments.push(experimentData.branch.value);
 | 
						|
 | 
						|
        if (!REACH_EVENT_GROUPS.includes(group)) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
        // Check other sibling branches for triggers, add them to the return
 | 
						|
        // array if found any. The `forReachEvent` label is used to identify
 | 
						|
        // those branches so that they would only used to record the Reach
 | 
						|
        // event.
 | 
						|
        const branches =
 | 
						|
          (await ExperimentAPI.getAllBranches(experimentData.slug)) || [];
 | 
						|
        for (const branch of branches) {
 | 
						|
          if (
 | 
						|
            branch.slug !== experimentData.branch.slug &&
 | 
						|
            branch.value.trigger
 | 
						|
          ) {
 | 
						|
            experiments.push({
 | 
						|
              group,
 | 
						|
              forReachEvent: { sent: false },
 | 
						|
              experimentSlug: experimentData.slug,
 | 
						|
              branchSlug: branch.slug,
 | 
						|
              ...branch.value,
 | 
						|
            });
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return experiments;
 | 
						|
  },
 | 
						|
 | 
						|
  _handleRemoteSettingsUndesiredEvent(event, providerId, dispatchToAS) {
 | 
						|
    if (dispatchToAS) {
 | 
						|
      dispatchToAS(
 | 
						|
        ac.ASRouterUserEvent({
 | 
						|
          action: "asrouter_undesired_event",
 | 
						|
          event,
 | 
						|
          message_id: "n/a",
 | 
						|
          event_context: providerId,
 | 
						|
        })
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * _getMessageLoader - return the right loading function given the provider's type
 | 
						|
   *
 | 
						|
   * @param {obj} provider An AS Router provider
 | 
						|
   * @returns {func} A loading function
 | 
						|
   */
 | 
						|
  _getMessageLoader(provider) {
 | 
						|
    switch (provider.type) {
 | 
						|
      case "remote":
 | 
						|
        return this._remoteLoader;
 | 
						|
      case "remote-settings":
 | 
						|
        return this._remoteSettingsLoader;
 | 
						|
      case "json":
 | 
						|
        return this._localJsonLoader;
 | 
						|
      case "remote-experiments":
 | 
						|
        return this._experimentsAPILoader;
 | 
						|
      case "local":
 | 
						|
      default:
 | 
						|
        return this._localLoader;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * shouldProviderUpdate - Given the current time, should a provider update its messages?
 | 
						|
   *
 | 
						|
   * @param {any} provider An AS Router provider
 | 
						|
   * @param {int} provider.updateCycleInMs The number of milliseconds we should wait between updates
 | 
						|
   * @param {Date} provider.lastUpdated If the provider has been updated, the time the last update occurred
 | 
						|
   * @param {Date} currentTime The time we should check against. (defaults to Date.now())
 | 
						|
   * @returns {bool} Should an update happen?
 | 
						|
   */
 | 
						|
  shouldProviderUpdate(provider, currentTime = Date.now()) {
 | 
						|
    return (
 | 
						|
      !(provider.lastUpdated >= 0) ||
 | 
						|
      currentTime - provider.lastUpdated > provider.updateCycleInMs
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async _loadDataForProvider(provider, options) {
 | 
						|
    const loader = this._getMessageLoader(provider);
 | 
						|
    let messages = await loader(provider, options);
 | 
						|
    // istanbul ignore if
 | 
						|
    if (!messages) {
 | 
						|
      messages = [];
 | 
						|
      MessageLoaderUtils.reportError(
 | 
						|
        new Error(
 | 
						|
          `Tried to load messages for ${provider.id} but the result was not an Array.`
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return { messages };
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * loadMessagesForProvider - Load messages for a provider, given the provider's type.
 | 
						|
   *
 | 
						|
   * @param {obj} provider An AS Router provider
 | 
						|
   * @param {string} provider.type An AS Router provider type (defaults to "local")
 | 
						|
   * @param {obj} options.storage A storage object with get() and set() methods for caching.
 | 
						|
   * @param {func} options.dispatchToAS dispatch an action the main AS Store
 | 
						|
   * @returns {obj} Returns an object with .messages (an array of messages) and .lastUpdated (the time the messages were updated)
 | 
						|
   */
 | 
						|
  async loadMessagesForProvider(provider, options) {
 | 
						|
    let { messages } = await this._loadDataForProvider(provider, options);
 | 
						|
    // Filter out messages we temporarily want to exclude
 | 
						|
    if (provider.exclude && provider.exclude.length) {
 | 
						|
      messages = messages.filter(
 | 
						|
        message => !provider.exclude.includes(message.id)
 | 
						|
      );
 | 
						|
    }
 | 
						|
    const lastUpdated = Date.now();
 | 
						|
    return {
 | 
						|
      messages: messages
 | 
						|
        .map(messageData => {
 | 
						|
          const message = {
 | 
						|
            weight: 100,
 | 
						|
            ...messageData,
 | 
						|
            groups: [...(messageData.groups || []), provider.id],
 | 
						|
            provider: provider.id,
 | 
						|
          };
 | 
						|
 | 
						|
          // This is to support a personalization experiment
 | 
						|
          if (provider.personalized) {
 | 
						|
            const score = ASRouterPreferences.personalizedCfrScores[message.id];
 | 
						|
            if (score) {
 | 
						|
              message.score = score;
 | 
						|
            }
 | 
						|
            message.personalizedModelVersion =
 | 
						|
              provider.personalizedModelVersion;
 | 
						|
          }
 | 
						|
 | 
						|
          return message;
 | 
						|
        })
 | 
						|
        .filter(message => message.weight > 0),
 | 
						|
      lastUpdated,
 | 
						|
      errors: MessageLoaderUtils.errors,
 | 
						|
    };
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * cleanupCache - Removes cached data of removed providers.
 | 
						|
   *
 | 
						|
   * @param {Array} providers A list of activer AS Router providers
 | 
						|
   */
 | 
						|
  async cleanupCache(providers, storage) {
 | 
						|
    const ids = providers.filter(p => p.type === "remote").map(p => p.id);
 | 
						|
    const cache = await MessageLoaderUtils._remoteLoaderCache(storage);
 | 
						|
    let dirty = false;
 | 
						|
    for (let id in cache) {
 | 
						|
      if (!ids.includes(id)) {
 | 
						|
        delete cache[id];
 | 
						|
        dirty = true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (dirty) {
 | 
						|
      await storage.set(MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY, cache);
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
this.MessageLoaderUtils = MessageLoaderUtils;
 | 
						|
 | 
						|
/**
 | 
						|
 * @class _ASRouter - Keeps track of all messages, UI surfaces, and
 | 
						|
 * handles blocking, rotation, etc. Inspecting ASRouter.state will
 | 
						|
 * tell you what the current displayed message is in all UI surfaces.
 | 
						|
 *
 | 
						|
 * Note: This is written as a constructor rather than just a plain object
 | 
						|
 * so that it can be more easily unit tested.
 | 
						|
 */
 | 
						|
class _ASRouter {
 | 
						|
  constructor(localProviders = LOCAL_MESSAGE_PROVIDERS) {
 | 
						|
    this.initialized = false;
 | 
						|
    this.messageChannel = null;
 | 
						|
    this.dispatchToAS = null;
 | 
						|
    this._storage = null;
 | 
						|
    this._resetInitialization();
 | 
						|
    this._state = {
 | 
						|
      providers: [],
 | 
						|
      messageBlockList: [],
 | 
						|
      groupBlockList: [],
 | 
						|
      providerBlockList: [],
 | 
						|
      messageImpressions: {},
 | 
						|
      trailheadInitialized: false,
 | 
						|
      messages: [],
 | 
						|
      groups: [],
 | 
						|
      errors: [],
 | 
						|
      localeInUse: Services.locale.appLocaleAsBCP47,
 | 
						|
    };
 | 
						|
    this._triggerHandler = this._triggerHandler.bind(this);
 | 
						|
    this._localProviders = localProviders;
 | 
						|
    this.blockMessageById = this.blockMessageById.bind(this);
 | 
						|
    this.unblockMessageById = this.unblockMessageById.bind(this);
 | 
						|
    this.onMessage = this.onMessage.bind(this);
 | 
						|
    this.handleMessageRequest = this.handleMessageRequest.bind(this);
 | 
						|
    this.addImpression = this.addImpression.bind(this);
 | 
						|
    this._handleTargetingError = this._handleTargetingError.bind(this);
 | 
						|
    this.onPrefChange = this.onPrefChange.bind(this);
 | 
						|
    this.dispatch = this.dispatch.bind(this);
 | 
						|
    this._onLocaleChanged = this._onLocaleChanged.bind(this);
 | 
						|
    this.isUnblockedMessage = this.isUnblockedMessage.bind(this);
 | 
						|
    this.renderWNMessages = this.renderWNMessages.bind(this);
 | 
						|
    this.forceWNPanel = this.forceWNPanel.bind(this);
 | 
						|
    Services.telemetry.setEventRecordingEnabled(REACH_EVENT_CATEGORY, true);
 | 
						|
  }
 | 
						|
 | 
						|
  async onPrefChange(prefName) {
 | 
						|
    if (TARGETING_PREFERENCES.includes(prefName)) {
 | 
						|
      // Notify all tabs of messages that have become invalid after pref change
 | 
						|
      const invalidMessages = [];
 | 
						|
      const context = this._getMessagesContext();
 | 
						|
 | 
						|
      for (const msg of this.state.messages.filter(this.isUnblockedMessage)) {
 | 
						|
        if (!msg.targeting) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
        const isMatch = await ASRouterTargeting.isMatch(msg.targeting, context);
 | 
						|
        if (!isMatch) {
 | 
						|
          invalidMessages.push(msg.id);
 | 
						|
        }
 | 
						|
      }
 | 
						|
      this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
        type: at.AS_ROUTER_TARGETING_UPDATE,
 | 
						|
        data: invalidMessages,
 | 
						|
      });
 | 
						|
    } else {
 | 
						|
      // Update message providers and fetch new messages on pref change
 | 
						|
      this._loadLocalProviders();
 | 
						|
      this._updateMessageProviders();
 | 
						|
      await this.loadMessagesFromAllProviders();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Replace all frequency time period aliases with their millisecond values
 | 
						|
  // This allows us to avoid accounting for special cases later on
 | 
						|
  normalizeItemFrequency({ frequency }) {
 | 
						|
    if (frequency && frequency.custom) {
 | 
						|
      for (const setting of frequency.custom) {
 | 
						|
        if (setting.period === "daily") {
 | 
						|
          setting.period = ONE_DAY_IN_MS;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Fetch and decode the message provider pref JSON, and update the message providers
 | 
						|
  _updateMessageProviders() {
 | 
						|
    const previousProviders = this.state.providers;
 | 
						|
    const providers = [
 | 
						|
      // If we have added a `preview` provider, hold onto it
 | 
						|
      ...previousProviders.filter(p => p.id === "preview"),
 | 
						|
      // The provider should be enabled and not have a user preference set to false
 | 
						|
      ...ASRouterPreferences.providers.filter(
 | 
						|
        p =>
 | 
						|
          p.enabled &&
 | 
						|
          ASRouterPreferences.getUserPreference(p.id) !== false &&
 | 
						|
          // Provider is enabled or if provider has multiple categories
 | 
						|
          // check that at least one category is enabled
 | 
						|
          (!p.categories ||
 | 
						|
            p.categories.some(
 | 
						|
              c => ASRouterPreferences.getUserPreference(c) !== false
 | 
						|
            ))
 | 
						|
      ),
 | 
						|
    ].map(_provider => {
 | 
						|
      // make a copy so we don't modify the source of the pref
 | 
						|
      const provider = { ..._provider };
 | 
						|
 | 
						|
      if (provider.type === "local" && !provider.messages) {
 | 
						|
        // Get the messages from the local message provider
 | 
						|
        const localProvider = this._localProviders[provider.localProvider];
 | 
						|
        provider.messages = localProvider ? localProvider.getMessages() : [];
 | 
						|
      }
 | 
						|
      if (provider.type === "remote" && provider.url) {
 | 
						|
        provider.url = provider.url.replace(
 | 
						|
          /%STARTPAGE_VERSION%/g,
 | 
						|
          STARTPAGE_VERSION
 | 
						|
        );
 | 
						|
        provider.url = Services.urlFormatter.formatURL(provider.url);
 | 
						|
      }
 | 
						|
      this.normalizeItemFrequency(provider);
 | 
						|
      // Reset provider update timestamp to force message refresh
 | 
						|
      provider.lastUpdated = undefined;
 | 
						|
      return provider;
 | 
						|
    });
 | 
						|
 | 
						|
    const providerIDs = providers.map(p => p.id);
 | 
						|
 | 
						|
    // Clear old messages for providers that are no longer enabled
 | 
						|
    for (const prevProvider of previousProviders) {
 | 
						|
      if (!providerIDs.includes(prevProvider.id)) {
 | 
						|
        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
          type: "CLEAR_PROVIDER",
 | 
						|
          data: { id: prevProvider.id },
 | 
						|
        });
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return this.setState(prevState => ({
 | 
						|
      providers,
 | 
						|
      // Clear any messages from removed providers
 | 
						|
      messages: [
 | 
						|
        ...prevState.messages.filter(message =>
 | 
						|
          providerIDs.includes(message.provider)
 | 
						|
        ),
 | 
						|
      ],
 | 
						|
    }));
 | 
						|
  }
 | 
						|
 | 
						|
  get state() {
 | 
						|
    return this._state;
 | 
						|
  }
 | 
						|
 | 
						|
  set state(value) {
 | 
						|
    throw new Error(
 | 
						|
      "Do not modify this.state directy. Instead, call this.setState(newState)"
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * _resetInitialization - adds the following to the instance:
 | 
						|
   *  .initialized {bool}            Has AS Router been initialized?
 | 
						|
   *  .waitForInitialized {Promise}  A promise that resolves when initializion is complete
 | 
						|
   *  ._finishInitializing {func}    A function that, when called, resolves the .waitForInitialized
 | 
						|
   *                                 promise and sets .initialized to true.
 | 
						|
   * @memberof _ASRouter
 | 
						|
   */
 | 
						|
  _resetInitialization() {
 | 
						|
    this.initialized = false;
 | 
						|
    this.waitForInitialized = new Promise(resolve => {
 | 
						|
      this._finishInitializing = () => {
 | 
						|
        this.initialized = true;
 | 
						|
        resolve();
 | 
						|
      };
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Check all provided groups are enabled
 | 
						|
   * @param groups Set of groups to verify
 | 
						|
   * @returns bool
 | 
						|
   */
 | 
						|
  hasGroupsEnabled(groups = []) {
 | 
						|
    return this.state.groups
 | 
						|
      .filter(({ id }) => groups.includes(id))
 | 
						|
      .every(({ enabled }) => enabled);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Verify that the provider block the message through the `exclude` field
 | 
						|
   * @param message Message to verify
 | 
						|
   * @returns bool
 | 
						|
   */
 | 
						|
  isExcludedByProvider(message) {
 | 
						|
    // preview snippets are never excluded
 | 
						|
    if (message.provider === "preview") {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    const provider = this.state.providers.find(p => p.id === message.provider);
 | 
						|
    if (!provider) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    if (provider.exclude) {
 | 
						|
      return provider.exclude.includes(message.id);
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Fetch all message groups and update Router.state.groups
 | 
						|
   * There are 3 types of groups:
 | 
						|
   * - auto generated groups based on existing providers
 | 
						|
   * - locally defined groups
 | 
						|
   * - remotely defined groups
 | 
						|
   * The override logic is as follows:
 | 
						|
   * 1. Auto generated groups can be overriden by local or remote group configs.
 | 
						|
   *    When generating a default group we check local and remote and merge all the options.
 | 
						|
   * 2. Locally defined groups can be overriden by remotely defined group configs.
 | 
						|
   *    When generating groups based on remote messages we merge with the local
 | 
						|
   *    configuration.
 | 
						|
   * @param provider RS messages provider for message groups
 | 
						|
   */
 | 
						|
  async loadAllMessageGroups() {
 | 
						|
    const LOCAL_GROUP_CONFIGURATIONS = GroupsConfigurationProvider.getMessages();
 | 
						|
    const [provider] = this.state.providers.filter(
 | 
						|
      p =>
 | 
						|
        p.id === "message-groups" && MessageLoaderUtils.shouldProviderUpdate(p)
 | 
						|
    );
 | 
						|
    let remoteMessages = [];
 | 
						|
    if (provider) {
 | 
						|
      const { messages } = await MessageLoaderUtils._loadDataForProvider(
 | 
						|
        provider,
 | 
						|
        {
 | 
						|
          storage: this._storage,
 | 
						|
          dispatchToAS: this.dispatchToAS,
 | 
						|
        }
 | 
						|
      );
 | 
						|
      if (messages && messages.length) {
 | 
						|
        remoteMessages = messages;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    const providerGroups = this.state.providers.map(
 | 
						|
      ({ id, frequency = null, enabled }) => {
 | 
						|
        const defaultGroup = { id, enabled, type: "default" };
 | 
						|
        if (frequency) {
 | 
						|
          defaultGroup.frequency = frequency;
 | 
						|
        }
 | 
						|
        const localGroup =
 | 
						|
          LOCAL_GROUP_CONFIGURATIONS.find(g => g.id === id) || {};
 | 
						|
        const remoteGroup = remoteMessages.find(g => g.id === id) || {};
 | 
						|
        return { ...defaultGroup, ...localGroup, ...remoteGroup };
 | 
						|
      }
 | 
						|
    );
 | 
						|
    const messageGroups = remoteMessages
 | 
						|
      .filter(m => !providerGroups.find(g => g.id === m.id))
 | 
						|
      .map(remoteGroup => {
 | 
						|
        const localGroup =
 | 
						|
          LOCAL_GROUP_CONFIGURATIONS.find(g => g.id === remoteGroup.id) || {};
 | 
						|
        return { ...localGroup, ...remoteGroup };
 | 
						|
      });
 | 
						|
    const localGroups = LOCAL_GROUP_CONFIGURATIONS.filter(
 | 
						|
      local =>
 | 
						|
        !providerGroups.find(g => g.id === local.id) &&
 | 
						|
        !messageGroups.find(g => g.id === local.id)
 | 
						|
    );
 | 
						|
    // Groups consist of automatically generated groups based on each message provider
 | 
						|
    // merged with message defined groups fetched from Remote Settings.
 | 
						|
    // A message defined group can override a provider group is it has the same name.
 | 
						|
    await this.setState(state => ({
 | 
						|
      groups: [...providerGroups, ...messageGroups, ...localGroups].map(
 | 
						|
        group => ({
 | 
						|
          ...group,
 | 
						|
          enabled:
 | 
						|
            group.enabled &&
 | 
						|
            // Enabled if the group is not preset in the block list
 | 
						|
            !state.groupBlockList.includes(group.id) &&
 | 
						|
            (Array.isArray(group.userPreferences)
 | 
						|
              ? group.userPreferences.every(
 | 
						|
                  ASRouterPreferences.getUserPreference
 | 
						|
                )
 | 
						|
              : true),
 | 
						|
        })
 | 
						|
      ),
 | 
						|
    }));
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * loadMessagesFromAllProviders - Loads messages from all providers if they require updates.
 | 
						|
   *                                Checks the .lastUpdated field on each provider to see if updates are needed
 | 
						|
   * @memberof _ASRouter
 | 
						|
   */
 | 
						|
  async loadMessagesFromAllProviders() {
 | 
						|
    const needsUpdate = this.state.providers.filter(provider =>
 | 
						|
      MessageLoaderUtils.shouldProviderUpdate(provider)
 | 
						|
    );
 | 
						|
    await this.loadAllMessageGroups();
 | 
						|
    // Don't do extra work if we don't need any updates
 | 
						|
    if (needsUpdate.length) {
 | 
						|
      let newState = { messages: [], providers: [] };
 | 
						|
      for (const provider of this.state.providers) {
 | 
						|
        if (needsUpdate.includes(provider)) {
 | 
						|
          const {
 | 
						|
            messages,
 | 
						|
            lastUpdated,
 | 
						|
            errors,
 | 
						|
          } = await MessageLoaderUtils.loadMessagesForProvider(provider, {
 | 
						|
            storage: this._storage,
 | 
						|
            dispatchToAS: this.dispatchToAS,
 | 
						|
          });
 | 
						|
          newState.providers.push({ ...provider, lastUpdated, errors });
 | 
						|
          newState.messages = [...newState.messages, ...messages];
 | 
						|
        } else {
 | 
						|
          // Skip updating this provider's messages if no update is required
 | 
						|
          let messages = this.state.messages.filter(
 | 
						|
            msg => msg.provider === provider.id
 | 
						|
          );
 | 
						|
          newState.providers.push(provider);
 | 
						|
          newState.messages = [...newState.messages, ...messages];
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      for (const message of newState.messages) {
 | 
						|
        this.normalizeItemFrequency(message);
 | 
						|
      }
 | 
						|
 | 
						|
      // Some messages have triggers that require us to initalise trigger listeners
 | 
						|
      const unseenListeners = new Set(ASRouterTriggerListeners.keys());
 | 
						|
      for (const { trigger } of newState.messages) {
 | 
						|
        if (trigger && ASRouterTriggerListeners.has(trigger.id)) {
 | 
						|
          ASRouterTriggerListeners.get(trigger.id).init(
 | 
						|
            this._triggerHandler,
 | 
						|
            trigger.params,
 | 
						|
            trigger.patterns
 | 
						|
          );
 | 
						|
          unseenListeners.delete(trigger.id);
 | 
						|
        }
 | 
						|
      }
 | 
						|
      // We don't need these listeners, but they may have previously been
 | 
						|
      // initialised, so uninitialise them
 | 
						|
      for (const triggerID of unseenListeners) {
 | 
						|
        ASRouterTriggerListeners.get(triggerID).uninit();
 | 
						|
      }
 | 
						|
 | 
						|
      // We don't want to cache preview endpoints, remove them after messages are fetched
 | 
						|
      await this.setState(this._removePreviewEndpoint(newState));
 | 
						|
      await this.cleanupImpressions();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async _maybeUpdateL10nAttachment() {
 | 
						|
    const { localeInUse } = this.state.localeInUse;
 | 
						|
    const newLocale = Services.locale.appLocaleAsBCP47;
 | 
						|
    if (newLocale !== localeInUse) {
 | 
						|
      const providers = [...this.state.providers];
 | 
						|
      let needsUpdate = false;
 | 
						|
      providers.forEach(provider => {
 | 
						|
        if (RS_PROVIDERS_WITH_L10N.includes(provider.id)) {
 | 
						|
          // Force to refresh the messages as well as the attachment.
 | 
						|
          provider.lastUpdated = undefined;
 | 
						|
          needsUpdate = true;
 | 
						|
        }
 | 
						|
      });
 | 
						|
      if (needsUpdate) {
 | 
						|
        await this.setState({
 | 
						|
          localeInUse: newLocale,
 | 
						|
          providers,
 | 
						|
        });
 | 
						|
        await this.loadMessagesFromAllProviders();
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async _onLocaleChanged(subject, topic, data) {
 | 
						|
    await this._maybeUpdateL10nAttachment();
 | 
						|
  }
 | 
						|
 | 
						|
  observe(aSubject, aTopic, aPrefName) {
 | 
						|
    switch (aPrefName) {
 | 
						|
      case USE_REMOTE_L10N_PREF:
 | 
						|
        CFRPageActions.reloadL10n();
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * init - Initializes the MessageRouter.
 | 
						|
   * It is ready when it has been connected to a RemotePageManager instance.
 | 
						|
   *
 | 
						|
   * @param {RemotePageManager} channel a RemotePageManager instance
 | 
						|
   * @param {obj} storage an AS storage instance
 | 
						|
   * @param {func} dispatchToAS dispatch an action the main AS Store
 | 
						|
   * @memberof _ASRouter
 | 
						|
   */
 | 
						|
  async init(channel, storage, dispatchToAS) {
 | 
						|
    this.messageChannel = channel;
 | 
						|
    this.messageChannel.addMessageListener(
 | 
						|
      INCOMING_MESSAGE_NAME,
 | 
						|
      this.onMessage
 | 
						|
    );
 | 
						|
    this._storage = storage;
 | 
						|
    this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
 | 
						|
    this.dispatchToAS = dispatchToAS;
 | 
						|
 | 
						|
    ASRouterPreferences.init();
 | 
						|
    ASRouterPreferences.addListener(this.onPrefChange);
 | 
						|
    BookmarkPanelHub.init(
 | 
						|
      this.handleMessageRequest,
 | 
						|
      this.addImpression,
 | 
						|
      this.dispatch
 | 
						|
    );
 | 
						|
    ToolbarBadgeHub.init(this.waitForInitialized, {
 | 
						|
      handleMessageRequest: this.handleMessageRequest,
 | 
						|
      addImpression: this.addImpression,
 | 
						|
      blockMessageById: this.blockMessageById,
 | 
						|
      unblockMessageById: this.unblockMessageById,
 | 
						|
      dispatch: this.dispatch,
 | 
						|
    });
 | 
						|
    ToolbarPanelHub.init(this.waitForInitialized, {
 | 
						|
      getMessages: this.handleMessageRequest,
 | 
						|
      dispatch: this.dispatch,
 | 
						|
    });
 | 
						|
    MomentsPageHub.init(this.waitForInitialized, {
 | 
						|
      handleMessageRequest: this.handleMessageRequest,
 | 
						|
      addImpression: this.addImpression,
 | 
						|
      blockMessageById: this.blockMessageById,
 | 
						|
      dispatch: this.dispatch,
 | 
						|
    });
 | 
						|
 | 
						|
    this._loadLocalProviders();
 | 
						|
 | 
						|
    const messageBlockList =
 | 
						|
      (await this._storage.get("messageBlockList")) || [];
 | 
						|
    const providerBlockList =
 | 
						|
      (await this._storage.get("providerBlockList")) || [];
 | 
						|
    const messageImpressions =
 | 
						|
      (await this._storage.get("messageImpressions")) || {};
 | 
						|
    const groupImpressions =
 | 
						|
      (await this._storage.get("groupImpressions")) || {};
 | 
						|
    // Combine the existing providersBlockList into the groupBlockList
 | 
						|
    const groupBlockList = (
 | 
						|
      (await this._storage.get("groupBlockList")) || []
 | 
						|
    ).concat(providerBlockList);
 | 
						|
 | 
						|
    const previousSessionEnd =
 | 
						|
      (await this._storage.get("previousSessionEnd")) || 0;
 | 
						|
    await this.setState({
 | 
						|
      messageBlockList,
 | 
						|
      groupBlockList,
 | 
						|
      providerBlockList,
 | 
						|
      groupImpressions,
 | 
						|
      messageImpressions,
 | 
						|
      previousSessionEnd,
 | 
						|
    });
 | 
						|
    this._updateMessageProviders();
 | 
						|
    await this.loadMessagesFromAllProviders();
 | 
						|
    await MessageLoaderUtils.cleanupCache(this.state.providers, storage);
 | 
						|
 | 
						|
    // set necessary state in the rest of AS
 | 
						|
    this.dispatchToAS(
 | 
						|
      ac.BroadcastToContent({
 | 
						|
        type: at.AS_ROUTER_INITIALIZED,
 | 
						|
        data: ASRouterPreferences.specialConditions,
 | 
						|
        meta: {
 | 
						|
          isStartup: true,
 | 
						|
        },
 | 
						|
      })
 | 
						|
    );
 | 
						|
 | 
						|
    SpecialMessageActions.blockMessageById = this.blockMessageById;
 | 
						|
    Services.obs.addObserver(this._onLocaleChanged, TOPIC_INTL_LOCALE_CHANGED);
 | 
						|
    Services.prefs.addObserver(USE_REMOTE_L10N_PREF, this);
 | 
						|
    // sets .initialized to true and resolves .waitForInitialized promise
 | 
						|
    this._finishInitializing();
 | 
						|
  }
 | 
						|
 | 
						|
  uninit() {
 | 
						|
    this._storage.set("previousSessionEnd", Date.now());
 | 
						|
 | 
						|
    this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
      type: "CLEAR_ALL",
 | 
						|
    });
 | 
						|
    this.messageChannel.removeMessageListener(
 | 
						|
      INCOMING_MESSAGE_NAME,
 | 
						|
      this.onMessage
 | 
						|
    );
 | 
						|
    this.messageChannel = null;
 | 
						|
    this.dispatchToAS = null;
 | 
						|
 | 
						|
    ASRouterPreferences.removeListener(this.onPrefChange);
 | 
						|
    ASRouterPreferences.uninit();
 | 
						|
    BookmarkPanelHub.uninit();
 | 
						|
    ToolbarPanelHub.uninit();
 | 
						|
    ToolbarBadgeHub.uninit();
 | 
						|
    MomentsPageHub.uninit();
 | 
						|
 | 
						|
    // Uninitialise all trigger listeners
 | 
						|
    for (const listener of ASRouterTriggerListeners.values()) {
 | 
						|
      listener.uninit();
 | 
						|
    }
 | 
						|
    Services.obs.removeObserver(
 | 
						|
      this._onLocaleChanged,
 | 
						|
      TOPIC_INTL_LOCALE_CHANGED
 | 
						|
    );
 | 
						|
    Services.prefs.removeObserver(USE_REMOTE_L10N_PREF, this);
 | 
						|
    // If we added any CFR recommendations, they need to be removed
 | 
						|
    CFRPageActions.clearRecommendations();
 | 
						|
    this._resetInitialization();
 | 
						|
  }
 | 
						|
 | 
						|
  setState(callbackOrObj) {
 | 
						|
    const newState =
 | 
						|
      typeof callbackOrObj === "function"
 | 
						|
        ? callbackOrObj(this.state)
 | 
						|
        : callbackOrObj;
 | 
						|
    this._state = { ...this.state, ...newState };
 | 
						|
    return new Promise(resolve => {
 | 
						|
      this._onStateChanged(this.state);
 | 
						|
      resolve();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  getMessageById(id) {
 | 
						|
    return this.state.messages.find(message => message.id === id);
 | 
						|
  }
 | 
						|
 | 
						|
  _onStateChanged(state) {
 | 
						|
    if (ASRouterPreferences.devtoolsEnabled) {
 | 
						|
      this._updateAdminState();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _loadLocalProviders() {
 | 
						|
    // If we're in ASR debug mode add the local test providers
 | 
						|
    if (ASRouterPreferences.devtoolsEnabled) {
 | 
						|
      this._localProviders = {
 | 
						|
        ...this._localProviders,
 | 
						|
        SnippetsTestMessageProvider,
 | 
						|
        PanelTestProvider,
 | 
						|
      };
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Used by ASRouter Admin returns all ASRouterTargeting.Environment
 | 
						|
   * and ASRouter._getMessagesContext parameters and values
 | 
						|
   */
 | 
						|
  async getTargetingParameters(environment, localContext) {
 | 
						|
    const targetingParameters = {};
 | 
						|
    for (const param of Object.keys(environment)) {
 | 
						|
      targetingParameters[param] = await environment[param];
 | 
						|
    }
 | 
						|
    for (const param of Object.keys(localContext)) {
 | 
						|
      targetingParameters[param] = await localContext[param];
 | 
						|
    }
 | 
						|
 | 
						|
    return targetingParameters;
 | 
						|
  }
 | 
						|
 | 
						|
  async _updateAdminState(target) {
 | 
						|
    const channel = target || this.messageChannel;
 | 
						|
    channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
      type: "ADMIN_SET_STATE",
 | 
						|
      data: {
 | 
						|
        ...this.state,
 | 
						|
        providerPrefs: ASRouterPreferences.providers,
 | 
						|
        userPrefs: ASRouterPreferences.getAllUserPreferences(),
 | 
						|
        targetingParameters: await this.getTargetingParameters(
 | 
						|
          ASRouterTargeting.Environment,
 | 
						|
          this._getMessagesContext()
 | 
						|
        ),
 | 
						|
        trailhead: ASRouterPreferences.trailhead,
 | 
						|
        errors: this.errors,
 | 
						|
      },
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  _handleTargetingError(type, error, message) {
 | 
						|
    Cu.reportError(error);
 | 
						|
    if (this.dispatchToAS) {
 | 
						|
      this.dispatchToAS(
 | 
						|
        ac.ASRouterUserEvent({
 | 
						|
          message_id: message.id,
 | 
						|
          action: "asrouter_undesired_event",
 | 
						|
          event: "TARGETING_EXPRESSION_ERROR",
 | 
						|
          event_context: type,
 | 
						|
        })
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async setTrailHeadMessageSeen() {
 | 
						|
    if (!this.state.trailheadInitialized) {
 | 
						|
      Services.prefs.setBoolPref(
 | 
						|
        TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF,
 | 
						|
        true
 | 
						|
      );
 | 
						|
      await this.setState({
 | 
						|
        trailheadInitialized: true,
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Return an object containing targeting parameters used to select messages
 | 
						|
  _getMessagesContext() {
 | 
						|
    const { messageImpressions, previousSessionEnd } = this.state;
 | 
						|
 | 
						|
    return {
 | 
						|
      get messageImpressions() {
 | 
						|
        return messageImpressions;
 | 
						|
      },
 | 
						|
      get previousSessionEnd() {
 | 
						|
        return previousSessionEnd;
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  async evaluateExpression(target, { expression, context }) {
 | 
						|
    const channel = target || this.messageChannel;
 | 
						|
    let evaluationStatus;
 | 
						|
    try {
 | 
						|
      evaluationStatus = {
 | 
						|
        result: await ASRouterTargeting.isMatch(expression, context),
 | 
						|
        success: true,
 | 
						|
      };
 | 
						|
    } catch (e) {
 | 
						|
      evaluationStatus = { result: e.message, success: false };
 | 
						|
    }
 | 
						|
 | 
						|
    channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
      type: "ADMIN_SET_STATE",
 | 
						|
      data: {
 | 
						|
        ...this.state,
 | 
						|
        evaluationStatus,
 | 
						|
      },
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  _orderBundle(bundle) {
 | 
						|
    return bundle.sort((a, b) => a.order - b.order);
 | 
						|
  }
 | 
						|
 | 
						|
  isUnblockedMessage(message) {
 | 
						|
    let { state } = this;
 | 
						|
    return (
 | 
						|
      !state.messageBlockList.includes(message.id) &&
 | 
						|
      (!message.campaign ||
 | 
						|
        !state.messageBlockList.includes(message.campaign)) &&
 | 
						|
      !state.providerBlockList.includes(message.provider) &&
 | 
						|
      this.hasGroupsEnabled(message.groups) &&
 | 
						|
      !this.isExcludedByProvider(message)
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  // Work out if a message can be shown based on its and its provider's frequency caps.
 | 
						|
  isBelowFrequencyCaps(message) {
 | 
						|
    const { messageImpressions, groupImpressions } = this.state;
 | 
						|
    const impressionsForMessage = messageImpressions[message.id];
 | 
						|
 | 
						|
    return (
 | 
						|
      this._isBelowItemFrequencyCap(
 | 
						|
        message,
 | 
						|
        impressionsForMessage,
 | 
						|
        MAX_MESSAGE_LIFETIME_CAP
 | 
						|
      ) &&
 | 
						|
      message.groups.every(messageGroup =>
 | 
						|
        this._isBelowItemFrequencyCap(
 | 
						|
          this.state.groups.find(({ id }) => id === messageGroup),
 | 
						|
          groupImpressions[messageGroup]
 | 
						|
        )
 | 
						|
      )
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  // Helper for isBelowFrecencyCaps - work out if the frequency cap for the given
 | 
						|
  //                                  item has been exceeded or not
 | 
						|
  _isBelowItemFrequencyCap(item, impressions, maxLifetimeCap = Infinity) {
 | 
						|
    if (item && item.frequency && impressions && impressions.length) {
 | 
						|
      if (
 | 
						|
        item.frequency.lifetime &&
 | 
						|
        impressions.length >= Math.min(item.frequency.lifetime, maxLifetimeCap)
 | 
						|
      ) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      if (item.frequency.custom) {
 | 
						|
        const now = Date.now();
 | 
						|
        for (const setting of item.frequency.custom) {
 | 
						|
          let { period } = setting;
 | 
						|
          const impressionsInPeriod = impressions.filter(t => now - t < period);
 | 
						|
          if (impressionsInPeriod.length >= setting.cap) {
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  async _getBundledMessages(originalMessage, target, trigger, force = false) {
 | 
						|
    let result = [];
 | 
						|
    let bundleLength;
 | 
						|
    let bundleTemplate;
 | 
						|
    let originalId;
 | 
						|
 | 
						|
    if (originalMessage.includeBundle) {
 | 
						|
      // The original message is not part of the bundle, so don't include it
 | 
						|
      bundleLength = originalMessage.includeBundle.length;
 | 
						|
      bundleTemplate = originalMessage.includeBundle.template;
 | 
						|
    } else {
 | 
						|
      // The original message is part of the bundle
 | 
						|
      bundleLength = originalMessage.bundled;
 | 
						|
      bundleTemplate = originalMessage.template;
 | 
						|
      originalId = originalMessage.id;
 | 
						|
      // Add in a copy of the first message
 | 
						|
      result.push({
 | 
						|
        content: originalMessage.content,
 | 
						|
        id: originalMessage.id,
 | 
						|
        order: originalMessage.order || 0,
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    // First, find all messages of same template. These are potential matching targeting candidates
 | 
						|
    let bundledMessagesOfSameTemplate = this.state.messages.filter(
 | 
						|
      msg =>
 | 
						|
        msg.bundled &&
 | 
						|
        msg.template === bundleTemplate &&
 | 
						|
        msg.id !== originalId &&
 | 
						|
        this.isUnblockedMessage(msg)
 | 
						|
    );
 | 
						|
 | 
						|
    if (force) {
 | 
						|
      // Forcefully show the messages without targeting matching - this is for about:newtab#asrouter to show the messages
 | 
						|
      for (const message of bundledMessagesOfSameTemplate) {
 | 
						|
        result.push({ content: message.content, id: message.id });
 | 
						|
        // Stop once we have enough messages to fill a bundle
 | 
						|
        if (result.length === bundleLength) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      // Find all messages that matches the targeting context
 | 
						|
      const allMessages = await this.handleMessageRequest({
 | 
						|
        messages: bundledMessagesOfSameTemplate,
 | 
						|
        triggerId: trigger && trigger.id,
 | 
						|
        triggerContext: trigger && trigger.context,
 | 
						|
        triggerParam: trigger && trigger.param,
 | 
						|
        ordered: true,
 | 
						|
        returnAll: true,
 | 
						|
      });
 | 
						|
 | 
						|
      if (allMessages && allMessages.length) {
 | 
						|
        // Retrieve enough messages needed to fill a bundle
 | 
						|
        // Only copy the content of the message (that's what the UI cares about)
 | 
						|
        result = result.concat(
 | 
						|
          allMessages.slice(0, bundleLength).map(message => ({
 | 
						|
            content: message.content,
 | 
						|
            id: message.id,
 | 
						|
            order: message.order || 0,
 | 
						|
            // This is used to determine whether to block when action is triggered
 | 
						|
            // Only block for dynamic triplets experiment and when there are more messages available
 | 
						|
            blockOnClick:
 | 
						|
              ASRouterPreferences.trailhead.trailheadTriplet.startsWith(
 | 
						|
                "dynamic"
 | 
						|
              ) &&
 | 
						|
              allMessages.length >
 | 
						|
                TRAILHEAD_CONFIG.DYNAMIC_TRIPLET_BUNDLE_LENGTH,
 | 
						|
          }))
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // If we did not find enough messages to fill the bundle, do not send the bundle down
 | 
						|
    if (result.length < bundleLength) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    // The bundle may have some extra attributes, like a header, or a dismiss button, so attempt to get those strings now
 | 
						|
    // This is a temporary solution until we can use Fluent strings in the content process, in which case the content can
 | 
						|
    // handle finding these strings on its own. See bug 1488973
 | 
						|
    const extraTemplateStrings = await this._extraTemplateStrings(
 | 
						|
      originalMessage
 | 
						|
    );
 | 
						|
 | 
						|
    return {
 | 
						|
      bundle: this._orderBundle(result),
 | 
						|
      ...(extraTemplateStrings && { extraTemplateStrings }),
 | 
						|
      provider: originalMessage.provider,
 | 
						|
      template: originalMessage.template,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  async _extraTemplateStrings(originalMessage) {
 | 
						|
    let extraTemplateStrings;
 | 
						|
    let localProvider = this._findProvider(originalMessage.provider);
 | 
						|
    if (localProvider && localProvider.getExtraAttributes) {
 | 
						|
      extraTemplateStrings = await localProvider.getExtraAttributes();
 | 
						|
    }
 | 
						|
 | 
						|
    return extraTemplateStrings;
 | 
						|
  }
 | 
						|
 | 
						|
  _findProvider(providerID) {
 | 
						|
    return this._localProviders[
 | 
						|
      this.state.providers.find(i => i.id === providerID).localProvider
 | 
						|
    ];
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Route messages based on template to the correct module that can display them
 | 
						|
   */
 | 
						|
  routeMessageToTarget(message, target, trigger, force = false) {
 | 
						|
    switch (message.template) {
 | 
						|
      case "whatsnew_panel_message":
 | 
						|
        if (force) {
 | 
						|
          ToolbarPanelHub.forceShowMessage(target, message);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case "cfr_doorhanger":
 | 
						|
      case "milestone_message":
 | 
						|
        if (force) {
 | 
						|
          CFRPageActions.forceRecommendation(target, message, this.dispatch);
 | 
						|
        } else {
 | 
						|
          CFRPageActions.addRecommendation(
 | 
						|
            target,
 | 
						|
            trigger.param && trigger.param.host,
 | 
						|
            message,
 | 
						|
            this.dispatch
 | 
						|
          );
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case "cfr_urlbar_chiclet":
 | 
						|
        if (force) {
 | 
						|
          CFRPageActions.forceRecommendation(target, message, this.dispatch);
 | 
						|
        } else {
 | 
						|
          CFRPageActions.addRecommendation(
 | 
						|
            target,
 | 
						|
            null,
 | 
						|
            message,
 | 
						|
            this.dispatch
 | 
						|
          );
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case "fxa_bookmark_panel":
 | 
						|
        if (force) {
 | 
						|
          BookmarkPanelHub._forceShowMessage(target, message);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case "toolbar_badge":
 | 
						|
        ToolbarBadgeHub.registerBadgeNotificationListener(message, { force });
 | 
						|
        break;
 | 
						|
      case "update_action":
 | 
						|
        MomentsPageHub.executeAction(message);
 | 
						|
        break;
 | 
						|
      default:
 | 
						|
        try {
 | 
						|
          target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
            type: "SET_MESSAGE",
 | 
						|
            data: message,
 | 
						|
          });
 | 
						|
        } catch (e) {}
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async _sendMessageToTarget(message, target, trigger, force = false) {
 | 
						|
    // No message is available, so send CLEAR_ALL.
 | 
						|
    if (!message) {
 | 
						|
      try {
 | 
						|
        target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, { type: "CLEAR_ALL" });
 | 
						|
      } catch (e) {}
 | 
						|
 | 
						|
      // For bundled messages, look for the rest of the bundle or else send CLEAR_ALL
 | 
						|
    } else if (message.bundled) {
 | 
						|
      const bundledMessages = await this._getBundledMessages(
 | 
						|
        message,
 | 
						|
        target,
 | 
						|
        trigger,
 | 
						|
        force
 | 
						|
      );
 | 
						|
      const action = bundledMessages
 | 
						|
        ? { type: "SET_BUNDLED_MESSAGES", data: bundledMessages }
 | 
						|
        : { type: "CLEAR_ALL" };
 | 
						|
      try {
 | 
						|
        target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
 | 
						|
      } catch (e) {}
 | 
						|
 | 
						|
      // For nested bundled messages, look for the desired bundle
 | 
						|
    } else if (message.includeBundle) {
 | 
						|
      const bundledMessages = await this._getBundledMessages(
 | 
						|
        message,
 | 
						|
        target,
 | 
						|
        message.includeBundle.trigger,
 | 
						|
        force
 | 
						|
      );
 | 
						|
      try {
 | 
						|
        target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
          type: "SET_MESSAGE",
 | 
						|
          data: {
 | 
						|
            ...message,
 | 
						|
            trailheadTriplet:
 | 
						|
              ASRouterPreferences.trailhead.trailheadTriplet || "",
 | 
						|
            bundle: bundledMessages && bundledMessages.bundle,
 | 
						|
          },
 | 
						|
        });
 | 
						|
      } catch (e) {}
 | 
						|
    } else {
 | 
						|
      this.routeMessageToTarget(message, target, trigger, force);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async addImpression(message) {
 | 
						|
    const groupsWithFrequency = this.state.groups.filter(
 | 
						|
      ({ frequency, id }) => frequency && message.groups.includes(id)
 | 
						|
    );
 | 
						|
    // We only need to store impressions for messages that have frequency, or
 | 
						|
    // that have providers that have frequency
 | 
						|
    if (message.frequency || groupsWithFrequency.length) {
 | 
						|
      const time = Date.now();
 | 
						|
      await this.setState(state => {
 | 
						|
        const messageImpressions = this._addImpressionForItem(
 | 
						|
          state,
 | 
						|
          message,
 | 
						|
          "messageImpressions",
 | 
						|
          time
 | 
						|
        );
 | 
						|
        let { groupImpressions } = this.state;
 | 
						|
        for (const group of groupsWithFrequency) {
 | 
						|
          groupImpressions = this._addImpressionForItem(
 | 
						|
            state,
 | 
						|
            group,
 | 
						|
            "groupImpressions",
 | 
						|
            time
 | 
						|
          );
 | 
						|
        }
 | 
						|
        return { messageImpressions, groupImpressions };
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Helper for addImpression - calculate the updated impressions object for the given
 | 
						|
  //                            item, then store it and return it
 | 
						|
  _addImpressionForItem(state, item, impressionsString, time) {
 | 
						|
    // The destructuring here is to avoid mutating existing objects in state as in redux
 | 
						|
    // (see https://redux.js.org/recipes/structuring-reducers/prerequisite-concepts#immutable-data-management)
 | 
						|
    const impressions = { ...state[impressionsString] };
 | 
						|
    if (item.frequency) {
 | 
						|
      impressions[item.id] = impressions[item.id]
 | 
						|
        ? [...impressions[item.id]]
 | 
						|
        : [];
 | 
						|
      impressions[item.id].push(time);
 | 
						|
      this._storage.set(impressionsString, impressions);
 | 
						|
    }
 | 
						|
    return impressions;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * getLongestPeriod
 | 
						|
   *
 | 
						|
   * @param {obj} item Either an ASRouter message or an ASRouter provider
 | 
						|
   * @returns {int|null} if the item has custom frequency caps, the longest period found in the list of caps.
 | 
						|
                         if the item has no custom frequency caps, null
 | 
						|
   * @memberof _ASRouter
 | 
						|
   */
 | 
						|
  getLongestPeriod(item) {
 | 
						|
    if (!item.frequency || !item.frequency.custom) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
    return item.frequency.custom.sort((a, b) => b.period - a.period)[0].period;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * cleanupImpressions - this function cleans up obsolete impressions whenever
 | 
						|
   * messages are refreshed or fetched. It will likely need to be more sophisticated in the future,
 | 
						|
   * but the current behaviour for when both message impressions and provider impressions are
 | 
						|
   * cleared is as follows (where `item` is either `message` or `provider`):
 | 
						|
   *
 | 
						|
   * 1. If the item id for a list of item impressions no longer exists in the ASRouter state, it
 | 
						|
   *    will be cleared.
 | 
						|
   * 2. If the item has time-bound frequency caps but no lifetime cap, any item impressions older
 | 
						|
   *    than the longest time period will be cleared.
 | 
						|
   */
 | 
						|
  async cleanupImpressions() {
 | 
						|
    await this.setState(state => {
 | 
						|
      const messageImpressions = this._cleanupImpressionsForItems(
 | 
						|
        state,
 | 
						|
        state.messages,
 | 
						|
        "messageImpressions"
 | 
						|
      );
 | 
						|
      const groupImpressions = this._cleanupImpressionsForItems(
 | 
						|
        state,
 | 
						|
        state.groups,
 | 
						|
        "groupImpressions"
 | 
						|
      );
 | 
						|
      return { messageImpressions, groupImpressions };
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /** _cleanupImpressionsForItems - Helper for cleanupImpressions - calculate the updated
 | 
						|
  /*                                impressions object for the given items, then store it and return it
 | 
						|
   *
 | 
						|
   * @param {obj} state Reference to ASRouter internal state
 | 
						|
   * @param {array} items Can be messages, providers or groups that we count impressions for
 | 
						|
   * @param {string} impressionsString Key name for entry in state where impressions are stored
 | 
						|
   */
 | 
						|
  _cleanupImpressionsForItems(state, items, impressionsString) {
 | 
						|
    const impressions = { ...state[impressionsString] };
 | 
						|
    let needsUpdate = false;
 | 
						|
    Object.keys(impressions).forEach(id => {
 | 
						|
      const [item] = items.filter(x => x.id === id);
 | 
						|
      // Don't keep impressions for items that no longer exist
 | 
						|
      if (!item || !item.frequency || !Array.isArray(impressions[id])) {
 | 
						|
        delete impressions[id];
 | 
						|
        needsUpdate = true;
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      if (!impressions[id].length) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // If we don't want to store impressions older than the longest period
 | 
						|
      if (item.frequency.custom && !item.frequency.lifetime) {
 | 
						|
        const now = Date.now();
 | 
						|
        impressions[id] = impressions[id].filter(
 | 
						|
          t => now - t < this.getLongestPeriod(item)
 | 
						|
        );
 | 
						|
        needsUpdate = true;
 | 
						|
      }
 | 
						|
    });
 | 
						|
    if (needsUpdate) {
 | 
						|
      this._storage.set(impressionsString, impressions);
 | 
						|
    }
 | 
						|
    return impressions;
 | 
						|
  }
 | 
						|
 | 
						|
  handleMessageRequest({
 | 
						|
    messages: candidates,
 | 
						|
    triggerId,
 | 
						|
    triggerParam,
 | 
						|
    triggerContext,
 | 
						|
    template,
 | 
						|
    provider,
 | 
						|
    ordered = false,
 | 
						|
    returnAll = false,
 | 
						|
  }) {
 | 
						|
    let shouldCache;
 | 
						|
    const messages =
 | 
						|
      candidates ||
 | 
						|
      this.state.messages.filter(m => {
 | 
						|
        if (provider && m.provider !== provider) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        if (template && m.template !== template) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        if (triggerId && !m.trigger) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        if (triggerId && m.trigger.id !== triggerId) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        if (!this.isUnblockedMessage(m)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        if (!this.isBelowFrequencyCaps(m)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (shouldCache !== false) {
 | 
						|
          shouldCache = JEXL_PROVIDER_CACHE.has(m.provider);
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
      });
 | 
						|
 | 
						|
    if (!messages.length) {
 | 
						|
      return returnAll ? messages : null;
 | 
						|
    }
 | 
						|
 | 
						|
    const context = this._getMessagesContext();
 | 
						|
 | 
						|
    // Find a message that matches the targeting context as well as the trigger context (if one is provided)
 | 
						|
    // If no trigger is provided, we should find a message WITHOUT a trigger property defined.
 | 
						|
    return ASRouterTargeting.findMatchingMessage({
 | 
						|
      messages,
 | 
						|
      trigger: triggerId && {
 | 
						|
        id: triggerId,
 | 
						|
        param: triggerParam,
 | 
						|
        context: triggerContext,
 | 
						|
      },
 | 
						|
      context,
 | 
						|
      onError: this._handleTargetingError,
 | 
						|
      ordered,
 | 
						|
      shouldCache,
 | 
						|
      returnAll,
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  async modifyMessageJson(content, target, force = true, action = {}) {
 | 
						|
    await this._sendMessageToTarget(content, target, action.data, force);
 | 
						|
  }
 | 
						|
 | 
						|
  async setMessageById(id, target, force = true, action = {}) {
 | 
						|
    const newMessage = this.getMessageById(id);
 | 
						|
 | 
						|
    await this._sendMessageToTarget(newMessage, target, action.data, force);
 | 
						|
  }
 | 
						|
 | 
						|
  async blockMessageById(idOrIds) {
 | 
						|
    const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
 | 
						|
 | 
						|
    await this.setState(state => {
 | 
						|
      const messageBlockList = [...state.messageBlockList];
 | 
						|
      const messageImpressions = { ...state.messageImpressions };
 | 
						|
 | 
						|
      idsToBlock.forEach(id => {
 | 
						|
        const message = state.messages.find(m => m.id === id);
 | 
						|
        const idToBlock = message && message.campaign ? message.campaign : id;
 | 
						|
        if (!messageBlockList.includes(idToBlock)) {
 | 
						|
          messageBlockList.push(idToBlock);
 | 
						|
        }
 | 
						|
 | 
						|
        // When a message is blocked, its impressions should be cleared as well
 | 
						|
        delete messageImpressions[id];
 | 
						|
      });
 | 
						|
 | 
						|
      this._storage.set("messageBlockList", messageBlockList);
 | 
						|
      this._storage.set("messageImpressions", messageImpressions);
 | 
						|
      return { messageBlockList, messageImpressions };
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  unblockMessageById(idOrIds) {
 | 
						|
    const idsToUnblock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
 | 
						|
 | 
						|
    return this.setState(state => {
 | 
						|
      const messageBlockList = [...state.messageBlockList];
 | 
						|
      idsToUnblock
 | 
						|
        .map(id => state.messages.find(m => m.id === id))
 | 
						|
        // Remove all `id`s (or `campaign`s for snippets) from the message
 | 
						|
        // block list
 | 
						|
        .forEach(message => {
 | 
						|
          const idToUnblock =
 | 
						|
            message && message.campaign ? message.campaign : message.id;
 | 
						|
          messageBlockList.splice(messageBlockList.indexOf(idToUnblock), 1);
 | 
						|
        });
 | 
						|
 | 
						|
      this._storage.set("messageBlockList", messageBlockList);
 | 
						|
      return { messageBlockList };
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets `group.enabled` to false, blocks associated messages and persists
 | 
						|
   * the information in indexedDB
 | 
						|
   * @param id {string} - identifier for group
 | 
						|
   */
 | 
						|
  blockGroupById(id) {
 | 
						|
    if (!id) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    const groupBlockList = [...this.state.groupBlockList, id];
 | 
						|
    this._storage.set("groupBlockList", groupBlockList);
 | 
						|
    return this.setGroupState({ id, value: false });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets `group.enabled` to true, unblocks associated messages and persists
 | 
						|
   * the information in indexedDB
 | 
						|
   * @param id {string} - identifier for group
 | 
						|
   */
 | 
						|
  unblockGroupById(id) {
 | 
						|
    if (!id) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    const groupBlockList = [
 | 
						|
      ...this.state.groupBlockList.filter(groupId => groupId !== id),
 | 
						|
    ];
 | 
						|
    this._storage.set("groupBlockList", groupBlockList);
 | 
						|
    return this.setGroupState({ id, value: true });
 | 
						|
  }
 | 
						|
 | 
						|
  async blockProviderById(idOrIds) {
 | 
						|
    const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
 | 
						|
 | 
						|
    await this.setState(state => {
 | 
						|
      const providerBlockList = [...state.providerBlockList, ...idsToBlock];
 | 
						|
      this._storage.set("providerBlockList", providerBlockList);
 | 
						|
      return { providerBlockList };
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  setGroupState({ id, value }) {
 | 
						|
    const newGroupState = {
 | 
						|
      ...this.state.groups.find(group => group.id === id),
 | 
						|
      enabled: value,
 | 
						|
    };
 | 
						|
    const newGroupImpressions = { ...this.state.groupImpressions };
 | 
						|
    delete newGroupImpressions[id];
 | 
						|
    return this.setState(({ groups }) => ({
 | 
						|
      groups: [...groups.filter(group => group.id !== id), newGroupState],
 | 
						|
      groupImpressions: newGroupImpressions,
 | 
						|
    }));
 | 
						|
  }
 | 
						|
 | 
						|
  _validPreviewEndpoint(url) {
 | 
						|
    try {
 | 
						|
      const endpoint = new URL(url);
 | 
						|
      if (!this.WHITELIST_HOSTS[endpoint.host]) {
 | 
						|
        Cu.reportError(
 | 
						|
          `The preview URL host ${endpoint.host} is not in the whitelist.`
 | 
						|
        );
 | 
						|
      }
 | 
						|
      if (endpoint.protocol !== "https:") {
 | 
						|
        Cu.reportError("The URL protocol is not https.");
 | 
						|
      }
 | 
						|
      return (
 | 
						|
        endpoint.protocol === "https:" && this.WHITELIST_HOSTS[endpoint.host]
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Ensure we switch to the Onboarding message after RTAMO addon was installed
 | 
						|
  _updateOnboardingState() {
 | 
						|
    let addonInstallObs = (subject, topic) => {
 | 
						|
      Services.obs.removeObserver(
 | 
						|
        addonInstallObs,
 | 
						|
        "webextension-install-notify"
 | 
						|
      );
 | 
						|
      this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
        type: "CLEAR_INTERRUPT",
 | 
						|
      });
 | 
						|
    };
 | 
						|
    Services.obs.addObserver(addonInstallObs, "webextension-install-notify");
 | 
						|
  }
 | 
						|
 | 
						|
  _loadSnippetsWhitelistHosts() {
 | 
						|
    let additionalHosts = [];
 | 
						|
    const whitelistPrefValue = Services.prefs.getStringPref(
 | 
						|
      SNIPPETS_ENDPOINT_WHITELIST,
 | 
						|
      ""
 | 
						|
    );
 | 
						|
    try {
 | 
						|
      additionalHosts = JSON.parse(whitelistPrefValue);
 | 
						|
    } catch (e) {
 | 
						|
      if (whitelistPrefValue) {
 | 
						|
        Cu.reportError(
 | 
						|
          `Pref ${SNIPPETS_ENDPOINT_WHITELIST} value is not valid JSON`
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!additionalHosts.length) {
 | 
						|
      return DEFAULT_WHITELIST_HOSTS;
 | 
						|
    }
 | 
						|
 | 
						|
    // If there are additional hosts we want to whitelist, add them as
 | 
						|
    // `preview` so that the updateCycle is 0
 | 
						|
    return additionalHosts.reduce(
 | 
						|
      (whitelist_hosts, host) => {
 | 
						|
        whitelist_hosts[host] = "preview";
 | 
						|
        Services.console.logStringMessage(`Adding ${host} to whitelist hosts.`);
 | 
						|
        return whitelist_hosts;
 | 
						|
      },
 | 
						|
      { ...DEFAULT_WHITELIST_HOSTS }
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  // To be passed to ASRouterTriggerListeners
 | 
						|
  async _triggerHandler(target, trigger) {
 | 
						|
    // Disable ASRouterTriggerListeners in kiosk mode.
 | 
						|
    if (BrowserHandler.kiosk) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    await this.onMessage({
 | 
						|
      target,
 | 
						|
      data: { type: "TRIGGER", data: { trigger } },
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  _removePreviewEndpoint(state) {
 | 
						|
    state.providers = state.providers.filter(p => p.id !== "preview");
 | 
						|
    return state;
 | 
						|
  }
 | 
						|
 | 
						|
  async _addPreviewEndpoint(url, portID) {
 | 
						|
    // When you view a preview snippet we want to hide all real content
 | 
						|
    const providers = [...this.state.providers];
 | 
						|
    if (
 | 
						|
      this._validPreviewEndpoint(url) &&
 | 
						|
      !providers.find(p => p.url === url)
 | 
						|
    ) {
 | 
						|
      this.dispatchToAS(
 | 
						|
        ac.OnlyToOneContent({ type: at.SNIPPETS_PREVIEW_MODE }, portID)
 | 
						|
      );
 | 
						|
      providers.push({
 | 
						|
        id: "preview",
 | 
						|
        type: "remote",
 | 
						|
        enabled: true,
 | 
						|
        url,
 | 
						|
        updateCycleInMs: 0,
 | 
						|
      });
 | 
						|
      await this.setState({ providers });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Windows specific calls to write attribution data
 | 
						|
  // Used by `forceAttribution` to set required targeting attributes for
 | 
						|
  // RTAMO messages. This should only be called from within about:newtab#asrouter
 | 
						|
  /* istanbul ignore next */
 | 
						|
  async _writeAttributionFile(data) {
 | 
						|
    let appDir = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
 | 
						|
    let file = appDir.clone();
 | 
						|
    file.append(Services.appinfo.vendor || "mozilla");
 | 
						|
    file.append(AppConstants.MOZ_APP_NAME);
 | 
						|
 | 
						|
    await OS.File.makeDir(file.path, {
 | 
						|
      from: appDir.path,
 | 
						|
      ignoreExisting: true,
 | 
						|
    });
 | 
						|
 | 
						|
    file.append("postSigningData");
 | 
						|
    await OS.File.writeAtomic(file.path, data);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * forceAttribution - this function should only be called from within about:newtab#asrouter.
 | 
						|
   * It forces the browser attribution to be set to something specified in asrouter admin
 | 
						|
   * tools, and reloads the providers in order to get messages that are dependant on this
 | 
						|
   * attribution data (see Return to AMO flow in bug 1475354 for example). Note - OSX and Windows only
 | 
						|
   * @param {data} Object an object containing the attribtion data that came from asrouter admin page
 | 
						|
   */
 | 
						|
  async forceAttribution(data) {
 | 
						|
    // Extract the parameters from data that will make up the referrer url
 | 
						|
    const attributionData = AttributionCode.allowedCodeKeys
 | 
						|
      .map(key => `${key}=${encodeURIComponent(data[key] || "")}`)
 | 
						|
      .join("&");
 | 
						|
    if (AppConstants.platform === "win") {
 | 
						|
      // The whole attribution data is encoded (again) for windows
 | 
						|
      this._writeAttributionFile(encodeURIComponent(attributionData));
 | 
						|
    } else if (AppConstants.platform === "macosx") {
 | 
						|
      let appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path;
 | 
						|
      let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
 | 
						|
        Ci.nsIMacAttributionService
 | 
						|
      );
 | 
						|
 | 
						|
      // The attribution data is treated as a url query for mac
 | 
						|
      let referrer = `https://www.mozilla.org/anything/?${attributionData}`;
 | 
						|
 | 
						|
      // This sets the Attribution to be the referrer
 | 
						|
      attributionSvc.setReferrerUrl(appPath, referrer, true);
 | 
						|
    }
 | 
						|
 | 
						|
    // Clear cache call is only possible in a testing environment
 | 
						|
    let env = Cc["@mozilla.org/process/environment;1"].getService(
 | 
						|
      Ci.nsIEnvironment
 | 
						|
    );
 | 
						|
    env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
 | 
						|
 | 
						|
    // Clear and refresh Attribution, and then fetch the messages again to update
 | 
						|
    AttributionCode._clearCache();
 | 
						|
    await AttributionCode.getAttrDataAsync();
 | 
						|
    this._updateMessageProviders();
 | 
						|
    await this.loadMessagesFromAllProviders();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * sendAsyncMessageToPreloaded - Sends an action to each preloaded browser, if any
 | 
						|
   *
 | 
						|
   * @param  {obj} action An action to be sent to content
 | 
						|
   */
 | 
						|
  sendAsyncMessageToPreloaded(action) {
 | 
						|
    const preloadedBrowsers = this.getPreloadedBrowser();
 | 
						|
    if (preloadedBrowsers) {
 | 
						|
      for (let preloadedBrowser of preloadedBrowsers) {
 | 
						|
        try {
 | 
						|
          preloadedBrowser.sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
 | 
						|
        } catch (e) {
 | 
						|
          // The preloaded page is no longer available, so just ignore.
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * getPreloadedBrowser - Retrieve the port of any preloaded browsers
 | 
						|
   *
 | 
						|
   * @return {Array|null} An array of ports belonging to the preloaded browsers, or null
 | 
						|
   *                      if there aren't any preloaded browsers
 | 
						|
   */
 | 
						|
  getPreloadedBrowser() {
 | 
						|
    let preloadedPorts = [];
 | 
						|
    for (let port of this.messageChannel.messagePorts) {
 | 
						|
      if (this.isPreloadedBrowser(port.browser)) {
 | 
						|
        preloadedPorts.push(port);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return preloadedPorts.length ? preloadedPorts : null;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * isPreloadedBrowser - Returns true if the passed browser has been preloaded
 | 
						|
   *                      for faster rendering of new tabs.
 | 
						|
   *
 | 
						|
   * @param {<browser>} A <browser> to check.
 | 
						|
   * @return {boolean} True if the browser is preloaded.
 | 
						|
   *                   False if there aren't any preloaded browsers
 | 
						|
   */
 | 
						|
  isPreloadedBrowser(browser) {
 | 
						|
    return browser.getAttribute("preloadedState") === "preloaded";
 | 
						|
  }
 | 
						|
 | 
						|
  dispatch(action, target) {
 | 
						|
    this.onMessage({ data: action, target });
 | 
						|
  }
 | 
						|
 | 
						|
  hasMultiStageAboutWelcome() {
 | 
						|
    // Verify if user has onboarded using multistage about:welcome by
 | 
						|
    // checking overridecontent pref has content or aboutwelcome group experiment value
 | 
						|
    // has template as multistage
 | 
						|
    let experimentData;
 | 
						|
    try {
 | 
						|
      experimentData = ExperimentAPI.getExperiment({
 | 
						|
        group: "aboutwelcome",
 | 
						|
      });
 | 
						|
    } catch (e) {
 | 
						|
      Cu.reportError(e);
 | 
						|
    }
 | 
						|
 | 
						|
    return !!(
 | 
						|
      multiStageAboutWelcome ||
 | 
						|
      (experimentData &&
 | 
						|
        experimentData.branch &&
 | 
						|
        experimentData.branch.value &&
 | 
						|
        experimentData.branch.value.template === "multistage")
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  async sendNewTabMessage(target, options = {}) {
 | 
						|
    const { endpoint } = options;
 | 
						|
    let message;
 | 
						|
 | 
						|
    // Load preview endpoint for snippets if one is sent
 | 
						|
    if (endpoint) {
 | 
						|
      await this._addPreviewEndpoint(endpoint.url, target.portID);
 | 
						|
    }
 | 
						|
 | 
						|
    // Load all messages
 | 
						|
    await this.loadMessagesFromAllProviders();
 | 
						|
 | 
						|
    if (endpoint) {
 | 
						|
      message = await this.handleMessageRequest({ provider: "preview" });
 | 
						|
 | 
						|
      // We don't want to cache preview messages, remove them after we selected the message to show
 | 
						|
      if (message) {
 | 
						|
        await this.setState(state => ({
 | 
						|
          messages: state.messages.filter(m => m.id !== message.id),
 | 
						|
        }));
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      const telemetryObject = { port: target.portID };
 | 
						|
      TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
 | 
						|
      // On new tab, send cards if they match and not part of multistage onboarding experiment;
 | 
						|
      // othwerise send a snippet
 | 
						|
      if (!this.hasMultiStageAboutWelcome()) {
 | 
						|
        message = await this.handleMessageRequest({
 | 
						|
          template: "extended_triplets",
 | 
						|
        });
 | 
						|
      }
 | 
						|
      // If no extended triplets message was returned, show snippets instead
 | 
						|
      if (!message) {
 | 
						|
        message = await this.handleMessageRequest({ provider: "snippets" });
 | 
						|
      }
 | 
						|
      TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
 | 
						|
    }
 | 
						|
 | 
						|
    await this._sendMessageToTarget(message, target);
 | 
						|
  }
 | 
						|
 | 
						|
  _recordReachEvent(message) {
 | 
						|
    // Events telemetry only accepts understores for the event `object`
 | 
						|
    const underscored = message.group.split("-").join("_");
 | 
						|
    const extra = { branches: message.branchSlug };
 | 
						|
    Services.telemetry.recordEvent(
 | 
						|
      REACH_EVENT_CATEGORY,
 | 
						|
      REACH_EVENT_METHOD,
 | 
						|
      underscored,
 | 
						|
      message.experimentSlug,
 | 
						|
      extra
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  async sendTriggerMessage(target, trigger) {
 | 
						|
    await this.loadMessagesFromAllProviders();
 | 
						|
 | 
						|
    if (trigger.id === "firstRun") {
 | 
						|
      // On about welcome, set trailhead message seen on receiving firstrun trigger
 | 
						|
      await this.setTrailHeadMessageSeen();
 | 
						|
    }
 | 
						|
 | 
						|
    const telemetryObject = { port: target.portID };
 | 
						|
    TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
 | 
						|
    // Return all the messages so that it can record the Reach event
 | 
						|
    const messages =
 | 
						|
      (await this.handleMessageRequest({
 | 
						|
        triggerId: trigger.id,
 | 
						|
        triggerParam: trigger.param,
 | 
						|
        triggerContext: trigger.context,
 | 
						|
        returnAll: true,
 | 
						|
      })) || [];
 | 
						|
    TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
 | 
						|
 | 
						|
    // Record the Reach event for all the messages with `forReachEvent`,
 | 
						|
    // only send the first message without forReachEvent to the target
 | 
						|
    const nonReachMessages = [];
 | 
						|
    for (const message of messages) {
 | 
						|
      if (message.forReachEvent) {
 | 
						|
        if (!message.forReachEvent.sent) {
 | 
						|
          this._recordReachEvent(message);
 | 
						|
          message.forReachEvent.sent = true;
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        nonReachMessages.push(message);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    await this._sendMessageToTarget(
 | 
						|
      nonReachMessages[0] || null,
 | 
						|
      target,
 | 
						|
      trigger
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  renderWNMessages(browserWindow, messageIds) {
 | 
						|
    let messages = messageIds.map(msgId => this.getMessageById(msgId));
 | 
						|
 | 
						|
    ToolbarPanelHub.forceShowMessage(browserWindow, messages);
 | 
						|
  }
 | 
						|
 | 
						|
  async forceWNPanel(browserWindow) {
 | 
						|
    await ToolbarPanelHub.enableToolbarButton();
 | 
						|
 | 
						|
    browserWindow.PanelUI.showSubView(
 | 
						|
      "PanelUI-whatsNew",
 | 
						|
      browserWindow.document.getElementById("whats-new-menu-button")
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  /* eslint-disable complexity */
 | 
						|
  async onMessage({ data: action, target }) {
 | 
						|
    switch (action.type) {
 | 
						|
      case "USER_ACTION":
 | 
						|
        // This is to support ReturnToAMO
 | 
						|
        if (action.data.type === "INSTALL_ADDON_FROM_URL") {
 | 
						|
          this._updateOnboardingState();
 | 
						|
        }
 | 
						|
        await SpecialMessageActions.handleAction(action.data, target.browser);
 | 
						|
        break;
 | 
						|
      case "NEWTAB_MESSAGE_REQUEST":
 | 
						|
        await this.waitForInitialized;
 | 
						|
        await this.sendNewTabMessage(target, action.data);
 | 
						|
        break;
 | 
						|
      case "TRIGGER":
 | 
						|
        await this.waitForInitialized;
 | 
						|
        await this.sendTriggerMessage(
 | 
						|
          target,
 | 
						|
          action.data && action.data.trigger
 | 
						|
        );
 | 
						|
        break;
 | 
						|
      case "BLOCK_MESSAGE_BY_ID":
 | 
						|
        await this.blockMessageById(action.data.id);
 | 
						|
        // Block the message but don't dismiss it in case the action taken has
 | 
						|
        // another state that needs to be visible
 | 
						|
        if (action.data.preventDismiss) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
 | 
						|
        const outgoingMessage = {
 | 
						|
          type: "CLEAR_MESSAGE",
 | 
						|
          data: { id: action.data.id },
 | 
						|
        };
 | 
						|
        if (action.data.preloadedOnly) {
 | 
						|
          this.sendAsyncMessageToPreloaded(outgoingMessage);
 | 
						|
        } else {
 | 
						|
          this.messageChannel.sendAsyncMessage(
 | 
						|
            OUTGOING_MESSAGE_NAME,
 | 
						|
            outgoingMessage
 | 
						|
          );
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case "MODIFY_MESSAGE_JSON":
 | 
						|
        await this.modifyMessageJson(action.data.content, target, true, action);
 | 
						|
        break;
 | 
						|
      case "DISMISS_MESSAGE_BY_ID":
 | 
						|
        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
          type: "CLEAR_MESSAGE",
 | 
						|
          data: { id: action.data.id },
 | 
						|
        });
 | 
						|
        break;
 | 
						|
      case "BLOCK_PROVIDER_BY_ID":
 | 
						|
        await this.blockProviderById(action.data.id);
 | 
						|
        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
          type: "CLEAR_PROVIDER",
 | 
						|
          data: { id: action.data.id },
 | 
						|
        });
 | 
						|
        break;
 | 
						|
      case "BLOCK_BUNDLE":
 | 
						|
        await this.blockMessageById(action.data.bundle.map(b => b.id));
 | 
						|
        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
 | 
						|
          type: "CLEAR_BUNDLE",
 | 
						|
        });
 | 
						|
        break;
 | 
						|
      case "UNBLOCK_MESSAGE_BY_ID":
 | 
						|
        this.unblockMessageById(action.data.id);
 | 
						|
        break;
 | 
						|
      case "UNBLOCK_PROVIDER_BY_ID":
 | 
						|
        await this.setState(state => {
 | 
						|
          const providerBlockList = [...state.providerBlockList];
 | 
						|
          providerBlockList.splice(
 | 
						|
            providerBlockList.indexOf(action.data.id),
 | 
						|
            1
 | 
						|
          );
 | 
						|
          this._storage.set("providerBlockList", providerBlockList);
 | 
						|
          return { providerBlockList };
 | 
						|
        });
 | 
						|
        break;
 | 
						|
      case "UNBLOCK_BUNDLE":
 | 
						|
        await this.setState(state => {
 | 
						|
          const messageBlockList = [...state.messageBlockList];
 | 
						|
          for (let message of action.data.bundle) {
 | 
						|
            messageBlockList.splice(messageBlockList.indexOf(message.id), 1);
 | 
						|
          }
 | 
						|
          this._storage.set("messageBlockList", messageBlockList);
 | 
						|
          return { messageBlockList };
 | 
						|
        });
 | 
						|
        break;
 | 
						|
      case "OVERRIDE_MESSAGE":
 | 
						|
        await this.setMessageById(action.data.id, target, true, action);
 | 
						|
        break;
 | 
						|
      case "ADMIN_CONNECT_STATE":
 | 
						|
        if (action.data && action.data.endpoint) {
 | 
						|
          this._addPreviewEndpoint(action.data.endpoint.url, target.portID);
 | 
						|
          await this.loadMessagesFromAllProviders();
 | 
						|
        } else {
 | 
						|
          await this._updateAdminState(target);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case "IMPRESSION":
 | 
						|
        await this.addImpression(action.data);
 | 
						|
        break;
 | 
						|
      case "DOORHANGER_TELEMETRY":
 | 
						|
      case "TOOLBAR_BADGE_TELEMETRY":
 | 
						|
      case "TOOLBAR_PANEL_TELEMETRY":
 | 
						|
      case "MOMENTS_PAGE_TELEMETRY":
 | 
						|
        if (this.dispatchToAS) {
 | 
						|
          this.dispatchToAS(ac.ASRouterUserEvent(action.data));
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case "EXPIRE_QUERY_CACHE":
 | 
						|
        QueryCache.expireAll();
 | 
						|
        break;
 | 
						|
      case "ENABLE_PROVIDER":
 | 
						|
        ASRouterPreferences.enableOrDisableProvider(action.data, true);
 | 
						|
        break;
 | 
						|
      case "DISABLE_PROVIDER":
 | 
						|
        ASRouterPreferences.enableOrDisableProvider(action.data, false);
 | 
						|
        break;
 | 
						|
      case "RESET_PROVIDER_PREF":
 | 
						|
        ASRouterPreferences.resetProviderPref();
 | 
						|
        break;
 | 
						|
      case "SET_PROVIDER_USER_PREF":
 | 
						|
        ASRouterPreferences.setUserPreference(
 | 
						|
          action.data.id,
 | 
						|
          action.data.value
 | 
						|
        );
 | 
						|
        break;
 | 
						|
      case "SET_GROUP_STATE":
 | 
						|
        await this.setGroupState(action.data);
 | 
						|
        await this.loadMessagesFromAllProviders();
 | 
						|
        break;
 | 
						|
      case "BLOCK_GROUP_BY_ID":
 | 
						|
        await this.blockGroupById(action.data.id);
 | 
						|
        await this.loadMessagesFromAllProviders();
 | 
						|
        break;
 | 
						|
      case "UNBLOCK_GROUP_BY_ID":
 | 
						|
        await this.unblockGroupById(action.data.id);
 | 
						|
        await this.loadMessagesFromAllProviders();
 | 
						|
        break;
 | 
						|
      case "EVALUATE_JEXL_EXPRESSION":
 | 
						|
        this.evaluateExpression(target, action.data);
 | 
						|
        break;
 | 
						|
      case "FORCE_ATTRIBUTION":
 | 
						|
        this.forceAttribution(action.data);
 | 
						|
        break;
 | 
						|
      case "FORCE_WHATSNEW_PANEL":
 | 
						|
        this.forceWNPanel(target.browser.ownerGlobal);
 | 
						|
        break;
 | 
						|
      case "RENDER_WHATSNEW_MESSAGES":
 | 
						|
        this.renderWNMessages(target.browser.ownerGlobal, action.data);
 | 
						|
        break;
 | 
						|
      default:
 | 
						|
        Cu.reportError("Unknown message received");
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
this._ASRouter = _ASRouter;
 | 
						|
this.TRAILHEAD_CONFIG = TRAILHEAD_CONFIG;
 | 
						|
 | 
						|
/**
 | 
						|
 * ASRouter - singleton instance of _ASRouter that controls all messages
 | 
						|
 * in the new tab page.
 | 
						|
 */
 | 
						|
this.ASRouter = new _ASRouter();
 | 
						|
 | 
						|
const EXPORTED_SYMBOLS = [
 | 
						|
  "_ASRouter",
 | 
						|
  "ASRouter",
 | 
						|
  "MessageLoaderUtils",
 | 
						|
  "TRAILHEAD_CONFIG",
 | 
						|
];
 |