2018-05-07 18:37:47 +03:00
|
|
|
/* 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";
|
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
2018-06-08 21:34:17 +03:00
|
|
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
|
2018-08-28 16:49:23 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
|
|
AddonManager: "resource://gre/modules/AddonManager.jsm",
|
2018-09-14 23:18:00 +03:00
|
|
|
UITour: "resource:///modules/UITour.jsm",
|
2018-10-05 19:14:16 +03:00
|
|
|
FxAccounts: "resource://gre/modules/FxAccounts.jsm",
|
2018-08-28 16:49:23 +03:00
|
|
|
});
|
2018-09-03 18:36:03 +03:00
|
|
|
const {ASRouterActions: ra, actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
|
2018-08-23 17:31:26 +03:00
|
|
|
const {CFRMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/CFRMessageProvider.jsm", {});
|
2018-05-30 16:48:00 +03:00
|
|
|
const {OnboardingMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/OnboardingMessageProvider.jsm", {});
|
2018-10-10 01:49:51 +03:00
|
|
|
const {SnippetsTestMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/SnippetsTestMessageProvider.jsm", {});
|
2018-08-21 23:08:23 +03:00
|
|
|
const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js", {});
|
2018-08-27 23:50:07 +03:00
|
|
|
const {CFRPageActions} = ChromeUtils.import("resource://activity-stream/lib/CFRPageActions.jsm", {});
|
2018-04-30 22:33:31 +03:00
|
|
|
|
2018-09-13 19:21:34 +03:00
|
|
|
ChromeUtils.defineModuleGetter(this, "ASRouterPreferences",
|
|
|
|
"resource://activity-stream/lib/ASRouterPreferences.jsm");
|
2018-05-24 23:37:38 +03:00
|
|
|
ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
|
|
|
|
"resource://activity-stream/lib/ASRouterTargeting.jsm");
|
2018-09-28 20:04:05 +03:00
|
|
|
ChromeUtils.defineModuleGetter(this, "QueryCache",
|
|
|
|
"resource://activity-stream/lib/ASRouterTargeting.jsm");
|
2018-07-31 23:31:23 +03:00
|
|
|
ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
|
|
|
|
"resource://activity-stream/lib/ASRouterTriggerListeners.jsm");
|
2018-05-24 23:37:38 +03:00
|
|
|
|
2018-04-23 21:53:35 +03:00
|
|
|
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
|
|
|
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
|
2018-08-22 19:08:02 +03:00
|
|
|
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
|
2018-06-25 16:44:35 +03:00
|
|
|
// List of hosts for endpoints that serve router messages.
|
|
|
|
// Key is allowed host, value is a name for the endpoint host.
|
2018-07-26 12:33:49 +03:00
|
|
|
const DEFAULT_WHITELIST_HOSTS = {
|
2018-06-25 16:44:35 +03:00
|
|
|
"activity-stream-icons.services.mozilla.com": "production",
|
2018-09-14 23:18:00 +03:00
|
|
|
"snippets-admin.mozilla.org": "preview",
|
2018-06-25 16:44:35 +03:00
|
|
|
};
|
2018-07-26 12:33:49 +03:00
|
|
|
const SNIPPETS_ENDPOINT_WHITELIST = "browser.newtab.activity-stream.asrouter.whitelistHosts";
|
2018-08-22 19:08:02 +03:00
|
|
|
// Max possible impressions cap for any message
|
|
|
|
const MAX_MESSAGE_LIFETIME_CAP = 100;
|
2018-03-19 19:57:23 +03:00
|
|
|
|
2018-10-10 01:49:51 +03:00
|
|
|
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider, CFRMessageProvider, SnippetsTestMessageProvider};
|
2018-09-14 20:45:19 +03:00
|
|
|
const STARTPAGE_VERSION = "6";
|
2018-07-25 00:16:08 +03:00
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
const MessageLoaderUtils = {
|
2018-09-13 21:43:44 +03:00
|
|
|
STARTPAGE_VERSION,
|
|
|
|
REMOTE_LOADER_CACHE_KEY: "RemoteLoaderCache",
|
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
/**
|
|
|
|
* _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;
|
|
|
|
},
|
|
|
|
|
2018-09-13 21:43:44 +03:00
|
|
|
async _remoteLoaderCache(storage) {
|
|
|
|
let allCached;
|
|
|
|
try {
|
|
|
|
allCached = await storage.get(MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY) || {};
|
|
|
|
} catch (e) {
|
|
|
|
// istanbul ignore next
|
|
|
|
Cu.reportError(e);
|
|
|
|
// istanbul ignore next
|
|
|
|
allCached = {};
|
|
|
|
}
|
|
|
|
return allCached;
|
|
|
|
},
|
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
/**
|
|
|
|
* _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
|
2018-09-13 21:43:44 +03:00
|
|
|
* @param {obj} storage A storage object with get() and set() methods for caching.
|
2018-04-30 22:33:31 +03:00
|
|
|
* @returns {Promise} resolves with an array of messages, or an empty array if none could be fetched
|
|
|
|
*/
|
2018-09-13 21:43:44 +03:00
|
|
|
async _remoteLoader(provider, storage) {
|
2018-04-30 22:33:31 +03:00
|
|
|
let remoteMessages = [];
|
|
|
|
if (provider.url) {
|
2018-09-13 21:43:44 +03:00
|
|
|
const allCached = await MessageLoaderUtils._remoteLoaderCache(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);
|
|
|
|
}
|
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
try {
|
2018-09-13 21:43:44 +03:00
|
|
|
const response = await fetch(provider.url, {headers});
|
2018-05-24 21:05:04 +03:00
|
|
|
if (
|
|
|
|
// Empty response
|
|
|
|
response.status !== 204 &&
|
2018-09-13 21:43:44 +03:00
|
|
|
// Not modified
|
|
|
|
response.status !== 304 &&
|
2018-05-24 21:05:04 +03:00
|
|
|
(response.ok || response.status === 302)
|
|
|
|
) {
|
|
|
|
remoteMessages = (await response.json())
|
|
|
|
.messages
|
|
|
|
.map(msg => ({...msg, provider_url: provider.url}));
|
2018-09-13 21:43:44 +03:00
|
|
|
|
|
|
|
// 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(),
|
2018-09-14 23:18:00 +03:00
|
|
|
version: STARTPAGE_VERSION,
|
2018-09-13 21:43:44 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
storage.set(MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY, {...allCached, [provider.id]: cacheInfo});
|
|
|
|
}
|
2018-05-24 21:05:04 +03:00
|
|
|
}
|
2018-04-30 22:33:31 +03:00
|
|
|
} catch (e) {
|
|
|
|
Cu.reportError(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return remoteMessages;
|
|
|
|
},
|
|
|
|
|
2018-08-21 23:08:23 +03:00
|
|
|
/**
|
|
|
|
* _remoteSettingsLoader - Loads messages for a RemoteSettings provider
|
|
|
|
*
|
|
|
|
* @param {obj} provider An AS router provider
|
|
|
|
* @param {string} provider.bucket The name of the Remote Settings bucket
|
|
|
|
* @returns {Promise} resolves with an array of messages, or an empty array if none could be fetched
|
|
|
|
*/
|
|
|
|
async _remoteSettingsLoader(provider) {
|
|
|
|
let messages = [];
|
|
|
|
if (provider.bucket) {
|
|
|
|
try {
|
|
|
|
messages = await MessageLoaderUtils._getRemoteSettingsMessages(provider.bucket);
|
|
|
|
} catch (e) {
|
|
|
|
Cu.reportError(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return messages;
|
|
|
|
},
|
|
|
|
|
|
|
|
_getRemoteSettingsMessages(bucket) {
|
2018-09-24 20:53:01 +03:00
|
|
|
return RemoteSettings(bucket).get({filters: {locale: Services.locale.appLocaleAsLangTag}});
|
2018-08-21 23:08:23 +03:00
|
|
|
},
|
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
/**
|
|
|
|
* _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;
|
2018-08-21 23:08:23 +03:00
|
|
|
case "remote-settings":
|
|
|
|
return this._remoteSettingsLoader;
|
2018-04-30 22:33:31 +03:00
|
|
|
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);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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")
|
2018-09-13 21:43:44 +03:00
|
|
|
* @param {obj} storage A storage object with get() and set() methods for caching.
|
2018-04-30 22:33:31 +03:00
|
|
|
* @returns {obj} Returns an object with .messages (an array of messages) and .lastUpdated (the time the messages were updated)
|
|
|
|
*/
|
2018-09-13 21:43:44 +03:00
|
|
|
async loadMessagesForProvider(provider, storage) {
|
2018-09-13 19:21:34 +03:00
|
|
|
const loader = this._getMessageLoader(provider);
|
2018-09-13 21:43:44 +03:00
|
|
|
let messages = await loader(provider, storage);
|
2018-09-13 19:21:34 +03:00
|
|
|
// istanbul ignore if
|
|
|
|
if (!messages) {
|
|
|
|
messages = [];
|
|
|
|
Cu.reportError(new Error(`Tried to load messages for ${provider.id} but the result was not an Array.`));
|
|
|
|
}
|
2018-04-30 22:33:31 +03:00
|
|
|
const lastUpdated = Date.now();
|
2018-09-20 22:47:40 +03:00
|
|
|
return {
|
|
|
|
messages: messages.map(msg => ({weight: 100, ...msg, provider: provider.id}))
|
|
|
|
.filter(message => message.weight > 0),
|
|
|
|
lastUpdated,
|
|
|
|
};
|
2018-07-30 19:26:45 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
async installAddonFromURL(browser, url) {
|
|
|
|
try {
|
|
|
|
const aUri = Services.io.newURI(url);
|
|
|
|
const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
2018-10-04 14:31:06 +03:00
|
|
|
|
|
|
|
// AddonManager installation source associated to the addons installed from activitystream
|
|
|
|
// (See Bug 1496167 for a rationale).
|
|
|
|
const amTelemetryInfo = {source: "activitystream"};
|
|
|
|
const install = await AddonManager.getInstallForURL(aUri.spec, "application/x-xpinstall", null,
|
|
|
|
null, null, null, null, amTelemetryInfo);
|
2018-07-30 19:26:45 +03:00
|
|
|
await AddonManager.installAddonFromWebpage("application/x-xpinstall", browser,
|
|
|
|
systemPrincipal, install);
|
|
|
|
} catch (e) {}
|
2018-09-13 21:43:44 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
2018-09-14 23:18:00 +03:00
|
|
|
},
|
2018-04-30 22:33:31 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
this.MessageLoaderUtils = MessageLoaderUtils;
|
|
|
|
|
2018-03-19 19:57:23 +03:00
|
|
|
/**
|
2018-04-23 21:53:35 +03:00
|
|
|
* @class _ASRouter - Keeps track of all messages, UI surfaces, and
|
|
|
|
* handles blocking, rotation, etc. Inspecting ASRouter.state will
|
2018-03-19 19:57:23 +03:00
|
|
|
* 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.
|
|
|
|
*/
|
2018-04-23 21:53:35 +03:00
|
|
|
class _ASRouter {
|
2018-09-13 19:21:34 +03:00
|
|
|
constructor(localProviders = LOCAL_MESSAGE_PROVIDERS) {
|
2018-03-19 19:57:23 +03:00
|
|
|
this.initialized = false;
|
|
|
|
this.messageChannel = null;
|
2018-07-26 21:28:28 +03:00
|
|
|
this.dispatchToAS = null;
|
2018-04-24 19:41:36 +03:00
|
|
|
this._storage = null;
|
2018-04-30 22:33:31 +03:00
|
|
|
this._resetInitialization();
|
2018-05-18 20:35:29 +03:00
|
|
|
this._state = {
|
2018-05-19 02:17:32 +03:00
|
|
|
lastMessageId: null,
|
2018-05-18 20:35:29 +03:00
|
|
|
providers: [],
|
2018-08-22 19:08:02 +03:00
|
|
|
messageBlockList: [],
|
|
|
|
providerBlockList: [],
|
|
|
|
messageImpressions: {},
|
|
|
|
providerImpressions: {},
|
2018-09-14 23:18:00 +03:00
|
|
|
messages: [],
|
2018-05-18 20:35:29 +03:00
|
|
|
};
|
2018-07-31 23:31:23 +03:00
|
|
|
this._triggerHandler = this._triggerHandler.bind(this);
|
2018-07-25 00:16:08 +03:00
|
|
|
this._localProviders = localProviders;
|
2018-03-19 19:57:23 +03:00
|
|
|
this.onMessage = this.onMessage.bind(this);
|
2018-07-26 21:28:28 +03:00
|
|
|
this._handleTargetingError = this._handleTargetingError.bind(this);
|
2018-09-13 19:21:34 +03:00
|
|
|
this.onPrefChange = this.onPrefChange.bind(this);
|
2018-03-19 19:57:23 +03:00
|
|
|
}
|
|
|
|
|
2018-09-13 19:21:34 +03:00
|
|
|
// Update message providers and fetch new messages on pref change
|
|
|
|
async onPrefChange() {
|
|
|
|
this._updateMessageProviders();
|
2018-07-16 18:54:55 +03:00
|
|
|
await this.loadMessagesFromAllProviders();
|
2018-09-13 19:21:34 +03:00
|
|
|
this.dispatchToAS(ac.BroadcastToContent({type: at.AS_ROUTER_PREF_CHANGED, data: ASRouterPreferences.specialConditions}));
|
2018-07-16 18:54:55 +03:00
|
|
|
}
|
|
|
|
|
2018-09-21 18:06:54 +03:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-25 00:16:08 +03:00
|
|
|
// Fetch and decode the message provider pref JSON, and update the message providers
|
|
|
|
_updateMessageProviders() {
|
2018-09-13 22:00:58 +03:00
|
|
|
const previousProviders = this.state.providers;
|
2018-09-13 19:21:34 +03:00
|
|
|
const providers = [
|
|
|
|
// If we have added a `preview` provider, hold onto it
|
2018-09-13 22:00:58 +03:00
|
|
|
...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 &&
|
2018-10-05 17:50:46 +03:00
|
|
|
ASRouterPreferences.getUserPreference(p.id) !== false)
|
2018-09-13 22:00:58 +03:00
|
|
|
),
|
2018-09-13 19:21:34 +03:00
|
|
|
].map(_provider => {
|
|
|
|
// make a copy so we don't modify the source of the pref
|
|
|
|
const provider = {..._provider};
|
2018-07-16 18:54:55 +03:00
|
|
|
|
2018-07-25 00:16:08 +03:00
|
|
|
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() : [];
|
|
|
|
}
|
2018-08-08 18:06:36 +03:00
|
|
|
if (provider.type === "remote" && provider.url) {
|
|
|
|
provider.url = provider.url.replace(/%STARTPAGE_VERSION%/g, STARTPAGE_VERSION);
|
|
|
|
provider.url = Services.urlFormatter.formatURL(provider.url);
|
|
|
|
}
|
2018-09-21 18:06:54 +03:00
|
|
|
this.normalizeItemFrequency(provider);
|
2018-07-25 00:16:08 +03:00
|
|
|
// Reset provider update timestamp to force message refresh
|
|
|
|
provider.lastUpdated = undefined;
|
2018-09-13 19:21:34 +03:00
|
|
|
return provider;
|
2018-07-25 00:16:08 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
const providerIDs = providers.map(p => p.id);
|
2018-09-13 22:00:58 +03:00
|
|
|
|
|
|
|
// 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}});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-25 00:16:08 +03:00
|
|
|
this.setState(prevState => ({
|
|
|
|
providers,
|
|
|
|
// Clear any messages from removed providers
|
2018-09-14 23:18:00 +03:00
|
|
|
messages: [...prevState.messages.filter(message => providerIDs.includes(message.provider))],
|
2018-07-25 00:16:08 +03:00
|
|
|
}));
|
2018-07-16 18:54:55 +03:00
|
|
|
}
|
|
|
|
|
2018-03-19 19:57:23 +03:00
|
|
|
get state() {
|
|
|
|
return this._state;
|
|
|
|
}
|
|
|
|
|
|
|
|
set state(value) {
|
|
|
|
throw new Error("Do not modify this.state directy. Instead, call this.setState(newState)");
|
|
|
|
}
|
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
/**
|
|
|
|
* _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();
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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));
|
|
|
|
// 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)) {
|
2018-09-13 21:43:44 +03:00
|
|
|
const {messages, lastUpdated} = await MessageLoaderUtils.loadMessagesForProvider(provider, this._storage);
|
2018-05-18 20:35:29 +03:00
|
|
|
newState.providers.push({...provider, lastUpdated});
|
2018-04-30 22:33:31 +03:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|
2018-07-31 23:31:23 +03:00
|
|
|
|
2018-09-21 18:06:54 +03:00
|
|
|
for (const message of newState.messages) {
|
|
|
|
this.normalizeItemFrequency(message);
|
|
|
|
}
|
|
|
|
|
2018-07-31 23:31:23 +03:00
|
|
|
// 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);
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2018-08-01 15:16:02 +03:00
|
|
|
// We don't want to cache preview endpoints, remove them after messages are fetched
|
|
|
|
await this.setState(this._removePreviewEndpoint(newState));
|
2018-07-10 23:01:52 +03:00
|
|
|
await this.cleanupImpressions();
|
2018-04-30 22:33:31 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-19 19:57:23 +03:00
|
|
|
/**
|
|
|
|
* init - Initializes the MessageRouter.
|
|
|
|
* It is ready when it has been connected to a RemotePageManager instance.
|
|
|
|
*
|
|
|
|
* @param {RemotePageManager} channel a RemotePageManager instance
|
2018-04-30 22:33:31 +03:00
|
|
|
* @param {obj} storage an AS storage instance
|
2018-07-26 21:28:28 +03:00
|
|
|
* @param {func} dispatchToAS dispatch an action the main AS Store
|
2018-04-23 21:53:35 +03:00
|
|
|
* @memberof _ASRouter
|
2018-03-19 19:57:23 +03:00
|
|
|
*/
|
2018-07-26 21:28:28 +03:00
|
|
|
async init(channel, storage, dispatchToAS) {
|
2018-03-19 19:57:23 +03:00
|
|
|
this.messageChannel = channel;
|
|
|
|
this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
|
2018-04-24 19:41:36 +03:00
|
|
|
this._storage = storage;
|
2018-07-26 12:33:49 +03:00
|
|
|
this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
|
2018-07-26 21:28:28 +03:00
|
|
|
this.dispatchToAS = dispatchToAS;
|
2018-08-27 23:50:07 +03:00
|
|
|
this.dispatch = this.dispatch.bind(this);
|
2018-04-24 19:41:36 +03:00
|
|
|
|
2018-09-13 19:21:34 +03:00
|
|
|
ASRouterPreferences.init();
|
|
|
|
ASRouterPreferences.addListener(this.onPrefChange);
|
|
|
|
|
2018-08-22 19:08:02 +03:00
|
|
|
const messageBlockList = await this._storage.get("messageBlockList") || [];
|
|
|
|
const providerBlockList = await this._storage.get("providerBlockList") || [];
|
|
|
|
const messageImpressions = await this._storage.get("messageImpressions") || {};
|
|
|
|
const providerImpressions = await this._storage.get("providerImpressions") || {};
|
2018-09-10 20:56:01 +03:00
|
|
|
const previousSessionEnd = await this._storage.get("previousSessionEnd") || 0;
|
|
|
|
await this.setState({messageBlockList, providerBlockList, messageImpressions, providerImpressions, previousSessionEnd});
|
2018-07-25 00:16:08 +03:00
|
|
|
this._updateMessageProviders();
|
2018-07-10 23:01:52 +03:00
|
|
|
await this.loadMessagesFromAllProviders();
|
2018-09-13 21:43:44 +03:00
|
|
|
await MessageLoaderUtils.cleanupCache(this.state.providers, storage);
|
2018-07-10 23:01:52 +03:00
|
|
|
|
2018-09-13 19:21:34 +03:00
|
|
|
// set necessary state in the rest of AS
|
|
|
|
this.dispatchToAS(ac.BroadcastToContent({type: at.AS_ROUTER_INITIALIZED, data: ASRouterPreferences.specialConditions}));
|
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
// sets .initialized to true and resolves .waitForInitialized promise
|
|
|
|
this._finishInitializing();
|
2018-03-19 19:57:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
uninit() {
|
2018-09-10 20:56:01 +03:00
|
|
|
this._storage.set("previousSessionEnd", Date.now());
|
|
|
|
|
2018-05-19 02:17:32 +03:00
|
|
|
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
|
2018-03-19 19:57:23 +03:00
|
|
|
this.messageChannel.removeMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
|
|
|
|
this.messageChannel = null;
|
2018-07-26 21:28:28 +03:00
|
|
|
this.dispatchToAS = null;
|
2018-09-13 19:21:34 +03:00
|
|
|
|
|
|
|
ASRouterPreferences.removeListener(this.onPrefChange);
|
|
|
|
ASRouterPreferences.uninit();
|
|
|
|
|
2018-07-31 23:31:23 +03:00
|
|
|
// Uninitialise all trigger listeners
|
|
|
|
for (const listener of ASRouterTriggerListeners.values()) {
|
|
|
|
listener.uninit();
|
|
|
|
}
|
2018-08-30 20:47:26 +03:00
|
|
|
// If we added any CFR recommendations, they need to be removed
|
|
|
|
CFRPageActions.clearRecommendations();
|
2018-04-30 22:33:31 +03:00
|
|
|
this._resetInitialization();
|
2018-03-19 19:57:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
setState(callbackOrObj) {
|
|
|
|
const newState = (typeof callbackOrObj === "function") ? callbackOrObj(this.state) : callbackOrObj;
|
2018-05-18 20:35:29 +03:00
|
|
|
this._state = {...this.state, ...newState};
|
2018-03-19 19:57:23 +03:00
|
|
|
return new Promise(resolve => {
|
|
|
|
this._onStateChanged(this.state);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
getMessageById(id) {
|
|
|
|
return this.state.messages.find(message => message.id === id);
|
|
|
|
}
|
|
|
|
|
2018-03-19 19:57:23 +03:00
|
|
|
_onStateChanged(state) {
|
2018-09-13 19:21:34 +03:00
|
|
|
if (ASRouterPreferences.devtoolsEnabled) {
|
2018-09-13 22:00:58 +03:00
|
|
|
this._updateAdminState();
|
2018-09-13 19:21:34 +03:00
|
|
|
}
|
2018-03-19 19:57:23 +03:00
|
|
|
}
|
|
|
|
|
2018-10-29 16:39:40 +03:00
|
|
|
/**
|
|
|
|
* 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) {
|
2018-09-13 22:00:58 +03:00
|
|
|
const channel = target || this.messageChannel;
|
2018-10-18 15:47:03 +03:00
|
|
|
channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
|
|
|
|
type: "ADMIN_SET_STATE",
|
|
|
|
data: {
|
|
|
|
...this.state,
|
|
|
|
providerPrefs: ASRouterPreferences.providers,
|
2018-10-23 01:03:59 +03:00
|
|
|
userPrefs: ASRouterPreferences.getAllUserPreferences(),
|
2018-10-29 16:39:40 +03:00
|
|
|
targetingParameters: await this.getTargetingParameters(ASRouterTargeting.Environment, this._getMessagesContext()),
|
2018-10-18 15:47:03 +03:00
|
|
|
},
|
|
|
|
});
|
2018-09-13 22:00:58 +03:00
|
|
|
}
|
|
|
|
|
2018-07-26 21:28:28 +03:00
|
|
|
_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",
|
2018-09-14 23:18:00 +03:00
|
|
|
value: type,
|
2018-07-26 21:28:28 +03:00
|
|
|
}));
|
2018-06-23 00:10:08 +03:00
|
|
|
}
|
2018-07-26 21:28:28 +03:00
|
|
|
}
|
|
|
|
|
2018-09-10 20:56:01 +03:00
|
|
|
// Return an object containing targeting parameters used to select messages
|
|
|
|
_getMessagesContext() {
|
|
|
|
const {previousSessionEnd} = this.state;
|
|
|
|
return {
|
|
|
|
get previousSessionEnd() {
|
|
|
|
return previousSessionEnd;
|
2018-09-14 23:18:00 +03:00
|
|
|
},
|
2018-09-10 20:56:01 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-08-22 19:08:02 +03:00
|
|
|
_findMessage(candidateMessages, trigger) {
|
|
|
|
const messages = candidateMessages.filter(m => this.isBelowFrequencyCaps(m));
|
2018-09-10 20:56:01 +03:00
|
|
|
const context = this._getMessagesContext();
|
2018-07-26 21:28:28 +03:00
|
|
|
|
|
|
|
// 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.
|
2018-09-10 20:56:01 +03:00
|
|
|
return ASRouterTargeting.findMatchingMessage({messages, trigger, context, onError: this._handleTargetingError});
|
2018-06-23 00:10:08 +03:00
|
|
|
}
|
|
|
|
|
2018-10-29 16:39:40 +03:00
|
|
|
async evaluateExpression(target, {expression, context}) {
|
|
|
|
const channel = target || this.messageChannel;
|
|
|
|
let evaluationStatus;
|
|
|
|
try {
|
|
|
|
evaluationStatus = {result: await ASRouterTargeting.isMatch(expression, context), success: true};
|
|
|
|
} catch (e) {
|
2018-11-02 15:56:44 +03:00
|
|
|
evaluationStatus = {result: e.message, success: false};
|
2018-10-29 16:39:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: {...this.state, evaluationStatus}});
|
|
|
|
}
|
|
|
|
|
2018-07-25 17:33:31 +03:00
|
|
|
_orderBundle(bundle) {
|
|
|
|
return bundle.sort((a, b) => a.order - b.order);
|
|
|
|
}
|
|
|
|
|
2018-08-22 19:08:02 +03:00
|
|
|
// Work out if a message can be shown based on its and its provider's frequency caps.
|
|
|
|
isBelowFrequencyCaps(message) {
|
|
|
|
const {providers, messageImpressions, providerImpressions} = this.state;
|
|
|
|
|
|
|
|
const provider = providers.find(p => p.id === message.provider);
|
|
|
|
const impressionsForMessage = messageImpressions[message.id];
|
|
|
|
const impressionsForProvider = providerImpressions[message.provider];
|
|
|
|
|
|
|
|
return (this._isBelowItemFrequencyCap(message, impressionsForMessage, MAX_MESSAGE_LIFETIME_CAP) &&
|
|
|
|
this._isBelowItemFrequencyCap(provider, impressionsForProvider));
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2018-07-31 23:31:23 +03:00
|
|
|
async _getBundledMessages(originalMessage, target, trigger, force = false) {
|
2018-07-25 17:33:31 +03:00
|
|
|
let result = [{content: originalMessage.content, id: originalMessage.id, order: originalMessage.order || 0}];
|
2018-05-31 23:23:07 +03:00
|
|
|
|
|
|
|
// First, find all messages of same template. These are potential matching targeting candidates
|
|
|
|
let bundledMessagesOfSameTemplate = this._getUnblockedMessages()
|
|
|
|
.filter(msg => msg.bundled && msg.template === originalMessage.template && msg.id !== originalMessage.id);
|
|
|
|
|
|
|
|
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 === originalMessage.bundled) {
|
|
|
|
break;
|
|
|
|
}
|
2018-05-07 18:37:47 +03:00
|
|
|
}
|
2018-05-31 23:23:07 +03:00
|
|
|
} else {
|
|
|
|
while (bundledMessagesOfSameTemplate.length) {
|
|
|
|
// Find a message that matches the targeting context - or break if there are no matching messages
|
2018-08-22 19:08:02 +03:00
|
|
|
const message = await this._findMessage(bundledMessagesOfSameTemplate, trigger);
|
2018-05-31 23:23:07 +03:00
|
|
|
if (!message) {
|
|
|
|
/* istanbul ignore next */ // Code coverage in mochitests
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// Only copy the content of the message (that's what the UI cares about)
|
|
|
|
// Also delete the message we picked so we don't pick it again
|
2018-07-25 17:33:31 +03:00
|
|
|
result.push({content: message.content, id: message.id, order: message.order || 0});
|
2018-05-31 23:23:07 +03:00
|
|
|
bundledMessagesOfSameTemplate.splice(bundledMessagesOfSameTemplate.findIndex(msg => msg.id === message.id), 1);
|
|
|
|
// Stop once we have enough messages to fill a bundle
|
|
|
|
if (result.length === originalMessage.bundled) {
|
|
|
|
break;
|
|
|
|
}
|
2018-05-07 18:37:47 +03:00
|
|
|
}
|
|
|
|
}
|
2018-05-31 21:41:12 +03:00
|
|
|
|
|
|
|
// If we did not find enough messages to fill the bundle, do not send the bundle down
|
2018-05-31 23:23:07 +03:00
|
|
|
if (result.length < originalMessage.bundled) {
|
2018-05-31 21:41:12 +03:00
|
|
|
return null;
|
|
|
|
}
|
2018-07-25 17:33:31 +03:00
|
|
|
|
2018-09-24 23:20:07 +03:00
|
|
|
// 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];
|
2018-05-07 18:37:47 +03:00
|
|
|
}
|
|
|
|
|
2018-05-24 23:37:38 +03:00
|
|
|
_getUnblockedMessages() {
|
|
|
|
let {state} = this;
|
2018-08-22 19:08:02 +03:00
|
|
|
return state.messages.filter(item =>
|
|
|
|
!state.messageBlockList.includes(item.id) &&
|
2018-11-09 22:26:32 +03:00
|
|
|
(!item.campaign || !state.messageBlockList.includes(item.campaign)) &&
|
2018-08-22 19:08:02 +03:00
|
|
|
!state.providerBlockList.includes(item.provider)
|
|
|
|
);
|
2018-05-24 23:37:38 +03:00
|
|
|
}
|
|
|
|
|
2018-07-31 23:31:23 +03:00
|
|
|
async _sendMessageToTarget(message, target, trigger, force = false) {
|
2018-08-27 23:50:07 +03:00
|
|
|
// No message is available, so send CLEAR_ALL.
|
|
|
|
if (!message) {
|
2018-08-28 23:23:58 +03:00
|
|
|
try {
|
|
|
|
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
|
|
|
|
} catch (e) {}
|
2018-08-27 23:50:07 +03:00
|
|
|
|
|
|
|
// 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"};
|
|
|
|
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
|
|
|
|
|
|
|
|
// CFR doorhanger
|
|
|
|
} else if (message.template === "cfr_doorhanger") {
|
2018-08-29 00:34:06 +03:00
|
|
|
if (force) {
|
|
|
|
CFRPageActions.forceRecommendation(target, message, this.dispatch);
|
|
|
|
} else {
|
|
|
|
CFRPageActions.addRecommendation(target, trigger.param, message, this.dispatch);
|
|
|
|
}
|
2018-08-27 23:50:07 +03:00
|
|
|
|
|
|
|
// New tab single messages
|
|
|
|
} else {
|
|
|
|
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_MESSAGE", data: message});
|
2018-03-19 19:57:23 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-10 23:01:52 +03:00
|
|
|
async addImpression(message) {
|
2018-08-22 19:08:02 +03:00
|
|
|
const provider = this.state.providers.find(p => p.id === message.provider);
|
|
|
|
// We only need to store impressions for messages that have frequency, or
|
|
|
|
// that have providers that have frequency
|
|
|
|
if (message.frequency || (provider && provider.frequency)) {
|
|
|
|
const time = Date.now();
|
|
|
|
await this.setState(state => {
|
|
|
|
const messageImpressions = this._addImpressionForItem(state, message, "messageImpressions", time);
|
|
|
|
const providerImpressions = this._addImpressionForItem(state, provider, "providerImpressions", time);
|
|
|
|
return {messageImpressions, providerImpressions};
|
|
|
|
});
|
2018-07-10 23:01:52 +03:00
|
|
|
}
|
2018-08-22 19:08:02 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
2018-07-10 23:01:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* getLongestPeriod
|
|
|
|
*
|
2018-09-21 18:06:54 +03:00
|
|
|
* @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
|
2018-07-10 23:01:52 +03:00
|
|
|
* @memberof _ASRouter
|
|
|
|
*/
|
2018-09-21 18:06:54 +03:00
|
|
|
getLongestPeriod(item) {
|
|
|
|
if (!item.frequency || !item.frequency.custom) {
|
2018-07-10 23:01:52 +03:00
|
|
|
return null;
|
|
|
|
}
|
2018-09-21 18:06:54 +03:00
|
|
|
return item.frequency.custom.sort((a, b) => b.period - a.period)[0].period;
|
2018-07-10 23:01:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* cleanupImpressions - this function cleans up obsolete impressions whenever
|
|
|
|
* messages are refreshed or fetched. It will likely need to be more sophisticated in the future,
|
2018-08-22 19:08:02 +03:00
|
|
|
* but the current behaviour for when both message impressions and provider impressions are
|
|
|
|
* cleared is as follows (where `item` is either `message` or `provider`):
|
2018-07-10 23:01:52 +03:00
|
|
|
*
|
2018-08-22 19:08:02 +03:00
|
|
|
* 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
|
2018-07-10 23:01:52 +03:00
|
|
|
* than the longest time period will be cleared.
|
|
|
|
*/
|
|
|
|
async cleanupImpressions() {
|
|
|
|
await this.setState(state => {
|
2018-08-22 19:08:02 +03:00
|
|
|
const messageImpressions = this._cleanupImpressionsForItems(state, state.messages, "messageImpressions");
|
|
|
|
const providerImpressions = this._cleanupImpressionsForItems(state, state.providers, "providerImpressions");
|
|
|
|
return {messageImpressions, providerImpressions};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helper for cleanupImpressions - calculate the updated impressions object for
|
|
|
|
// the given items, then store it and return it
|
|
|
|
_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;
|
2018-07-10 23:01:52 +03:00
|
|
|
}
|
|
|
|
});
|
2018-08-22 19:08:02 +03:00
|
|
|
if (needsUpdate) {
|
|
|
|
this._storage.set(impressionsString, impressions);
|
|
|
|
}
|
|
|
|
return impressions;
|
2018-07-10 23:01:52 +03:00
|
|
|
}
|
|
|
|
|
2018-07-31 23:31:23 +03:00
|
|
|
async sendNextMessage(target, trigger) {
|
2018-06-08 13:08:11 +03:00
|
|
|
const msgs = this._getUnblockedMessages();
|
2018-06-25 16:44:35 +03:00
|
|
|
let message = null;
|
|
|
|
const previewMsgs = this.state.messages.filter(item => item.provider === "preview");
|
|
|
|
// Always send preview messages when available
|
|
|
|
if (previewMsgs.length) {
|
|
|
|
[message] = previewMsgs;
|
|
|
|
} else {
|
2018-08-22 19:08:02 +03:00
|
|
|
message = await this._findMessage(msgs, trigger);
|
2018-06-25 16:44:35 +03:00
|
|
|
}
|
2018-06-08 13:08:11 +03:00
|
|
|
|
2018-08-01 15:16:02 +03:00
|
|
|
if (previewMsgs.length) {
|
|
|
|
// We don't want to cache preview messages, remove them after we selected the message to show
|
|
|
|
await this.setState(state => ({
|
|
|
|
lastMessageId: message.id,
|
2018-09-14 23:18:00 +03:00
|
|
|
messages: state.messages.filter(m => m.id !== message.id),
|
2018-08-01 15:16:02 +03:00
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
await this.setState({lastMessageId: message ? message.id : null});
|
|
|
|
}
|
2018-07-31 23:31:23 +03:00
|
|
|
await this._sendMessageToTarget(message, target, trigger);
|
2018-06-08 13:08:11 +03:00
|
|
|
}
|
|
|
|
|
2018-06-23 00:10:08 +03:00
|
|
|
async setMessageById(id, target, force = true, action = {}) {
|
2018-05-19 02:17:32 +03:00
|
|
|
await this.setState({lastMessageId: id});
|
2018-04-30 22:33:31 +03:00
|
|
|
const newMessage = this.getMessageById(id);
|
2018-06-08 13:08:11 +03:00
|
|
|
|
2018-08-29 00:34:06 +03:00
|
|
|
await this._sendMessageToTarget(newMessage, target, action.data, force);
|
2018-04-30 22:33:31 +03:00
|
|
|
}
|
|
|
|
|
2018-08-22 19:08:02 +03:00
|
|
|
async blockMessageById(idOrIds) {
|
2018-05-19 02:17:32 +03:00
|
|
|
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
2018-07-10 23:01:52 +03:00
|
|
|
|
2018-05-19 02:17:32 +03:00
|
|
|
await this.setState(state => {
|
2018-11-09 22:26:32 +03:00
|
|
|
const messageBlockList = [...state.messageBlockList];
|
2018-08-22 19:08:02 +03:00
|
|
|
const messageImpressions = {...state.messageImpressions};
|
2018-11-09 22:26:32 +03:00
|
|
|
|
|
|
|
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];
|
|
|
|
});
|
|
|
|
|
2018-08-22 19:08:02 +03:00
|
|
|
this._storage.set("messageBlockList", messageBlockList);
|
|
|
|
return {messageBlockList, messageImpressions};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async blockProviderById(idOrIds) {
|
|
|
|
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
|
|
|
|
|
|
|
await this.setState(state => {
|
|
|
|
const providerBlockList = [...state.providerBlockList, ...idsToBlock];
|
|
|
|
// When a provider is blocked, its impressions should be cleared as well
|
|
|
|
const providerImpressions = {...state.providerImpressions};
|
|
|
|
idsToBlock.forEach(id => delete providerImpressions[id]);
|
|
|
|
this._storage.set("providerBlockList", providerBlockList);
|
|
|
|
return {providerBlockList, providerImpressions};
|
2018-05-19 02:17:32 +03:00
|
|
|
});
|
2018-03-19 19:57:23 +03:00
|
|
|
}
|
|
|
|
|
2018-06-25 16:44:35 +03:00
|
|
|
_validPreviewEndpoint(url) {
|
|
|
|
try {
|
|
|
|
const endpoint = new URL(url);
|
2018-07-26 12:33:49 +03:00
|
|
|
if (!this.WHITELIST_HOSTS[endpoint.host]) {
|
2018-06-25 16:44:35 +03:00
|
|
|
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.");
|
|
|
|
}
|
2018-07-26 12:33:49 +03:00
|
|
|
return (endpoint.protocol === "https:" && this.WHITELIST_HOSTS[endpoint.host]);
|
2018-06-25 16:44:35 +03:00
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-26 12:33:49 +03:00
|
|
|
_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});
|
|
|
|
}
|
|
|
|
|
2018-07-31 23:31:23 +03:00
|
|
|
// To be passed to ASRouterTriggerListeners
|
|
|
|
async _triggerHandler(target, trigger) {
|
2018-08-23 17:31:26 +03:00
|
|
|
await this.onMessage({target, data: {type: "TRIGGER", data: {trigger}}});
|
2018-07-31 23:31:23 +03:00
|
|
|
}
|
|
|
|
|
2018-08-01 15:16:02 +03:00
|
|
|
_removePreviewEndpoint(state) {
|
|
|
|
state.providers = state.providers.filter(p => p.id !== "preview");
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2018-09-03 18:36:03 +03:00
|
|
|
async _addPreviewEndpoint(url, portID) {
|
|
|
|
// When you view a preview snippet we want to hide all real content
|
2018-06-25 16:44:35 +03:00
|
|
|
const providers = [...this.state.providers];
|
|
|
|
if (this._validPreviewEndpoint(url) && !providers.find(p => p.url === url)) {
|
2018-09-03 18:36:03 +03:00
|
|
|
this.dispatchToAS(ac.OnlyToOneContent({type: at.SNIPPETS_PREVIEW_MODE}, portID));
|
2018-06-25 16:44:35 +03:00
|
|
|
providers.push({id: "preview", type: "remote", url, updateCycleInMs: 0});
|
|
|
|
await this.setState({providers});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-17 17:01:14 +03:00
|
|
|
async handleUserAction({data: action, target}) {
|
|
|
|
switch (action.type) {
|
|
|
|
case ra.OPEN_PRIVATE_BROWSER_WINDOW:
|
|
|
|
// Forcefully open about:privatebrowsing
|
|
|
|
target.browser.ownerGlobal.OpenBrowserWindow({private: true});
|
|
|
|
break;
|
|
|
|
case ra.OPEN_URL:
|
2018-11-15 11:54:00 +03:00
|
|
|
target.browser.ownerGlobal.openLinkIn(action.data.args, action.data.where || "current", {
|
2018-08-24 22:31:22 +03:00
|
|
|
private: false,
|
2018-09-14 23:18:00 +03:00
|
|
|
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
|
2018-08-24 22:31:22 +03:00
|
|
|
});
|
2018-08-17 17:01:14 +03:00
|
|
|
break;
|
|
|
|
case ra.OPEN_ABOUT_PAGE:
|
2018-10-05 19:27:00 +03:00
|
|
|
target.browser.ownerGlobal.openTrustedLinkIn(`about:${action.data.args}`, "tab");
|
2018-08-17 17:01:14 +03:00
|
|
|
break;
|
2018-10-20 02:28:16 +03:00
|
|
|
case ra.OPEN_PREFERENCES_PAGE:
|
|
|
|
target.browser.ownerGlobal.openPreferences(action.data.category, {origin: action.data.origin});
|
|
|
|
break;
|
2018-08-17 17:01:14 +03:00
|
|
|
case ra.OPEN_APPLICATIONS_MENU:
|
2018-10-05 19:27:00 +03:00
|
|
|
UITour.showMenu(target.browser.ownerGlobal, action.data.args);
|
2018-08-17 17:01:14 +03:00
|
|
|
break;
|
|
|
|
case ra.INSTALL_ADDON_FROM_URL:
|
2018-10-09 23:10:12 +03:00
|
|
|
await MessageLoaderUtils.installAddonFromURL(target.browser, action.data.url);
|
2018-08-17 17:01:14 +03:00
|
|
|
break;
|
2018-10-05 19:14:16 +03:00
|
|
|
case ra.SHOW_FIREFOX_ACCOUNTS:
|
|
|
|
const url = await FxAccounts.config.promiseSignUpURI("snippets");
|
|
|
|
// We want to replace the current tab.
|
2018-11-15 11:54:00 +03:00
|
|
|
target.browser.ownerGlobal.openLinkIn(url, "current", {
|
2018-10-05 19:14:16 +03:00
|
|
|
private: false,
|
|
|
|
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
|
|
|
|
});
|
|
|
|
break;
|
2018-08-17 17:01:14 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-27 23:50:07 +03:00
|
|
|
dispatch(action, target) {
|
|
|
|
this.onMessage({data: action, target});
|
|
|
|
}
|
|
|
|
|
2018-03-19 19:57:23 +03:00
|
|
|
async onMessage({data: action, target}) {
|
|
|
|
switch (action.type) {
|
2018-08-17 17:01:14 +03:00
|
|
|
case "USER_ACTION":
|
|
|
|
if (action.data.type in ra) {
|
|
|
|
await this.handleUserAction({data: action.data, target});
|
|
|
|
}
|
|
|
|
break;
|
2018-09-13 22:00:58 +03:00
|
|
|
case "SNIPPETS_REQUEST":
|
2018-06-23 00:10:08 +03:00
|
|
|
case "TRIGGER":
|
2018-04-30 22:33:31 +03:00
|
|
|
// Wait for our initial message loading to be done before responding to any UI requests
|
|
|
|
await this.waitForInitialized;
|
2018-06-25 16:44:35 +03:00
|
|
|
if (action.data && action.data.endpoint) {
|
2018-09-03 18:36:03 +03:00
|
|
|
await this._addPreviewEndpoint(action.data.endpoint.url, target.portID);
|
2018-06-25 16:44:35 +03:00
|
|
|
}
|
2018-04-30 22:33:31 +03:00
|
|
|
// Check if any updates are needed first
|
|
|
|
await this.loadMessagesFromAllProviders();
|
2018-07-31 23:31:23 +03:00
|
|
|
await this.sendNextMessage(target, (action.data && action.data.trigger) || {});
|
2018-03-19 19:57:23 +03:00
|
|
|
break;
|
|
|
|
case "BLOCK_MESSAGE_BY_ID":
|
2018-08-22 19:08:02 +03:00
|
|
|
await this.blockMessageById(action.data.id);
|
2018-10-04 17:06:44 +03:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: action.data.id}});
|
|
|
|
break;
|
|
|
|
case "DISMISS_MESSAGE_BY_ID":
|
2018-05-19 02:17:32 +03:00
|
|
|
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: action.data.id}});
|
2018-03-19 19:57:23 +03:00
|
|
|
break;
|
2018-08-22 19:08:02 +03:00
|
|
|
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;
|
2018-05-07 18:37:47 +03:00
|
|
|
case "BLOCK_BUNDLE":
|
2018-08-22 19:08:02 +03:00
|
|
|
await this.blockMessageById(action.data.bundle.map(b => b.id));
|
2018-05-19 02:17:32 +03:00
|
|
|
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
|
2018-05-07 18:37:47 +03:00
|
|
|
break;
|
2018-03-19 19:57:23 +03:00
|
|
|
case "UNBLOCK_MESSAGE_BY_ID":
|
2018-04-30 22:33:31 +03:00
|
|
|
await this.setState(state => {
|
2018-08-22 19:08:02 +03:00
|
|
|
const messageBlockList = [...state.messageBlockList];
|
2018-11-09 22:26:32 +03:00
|
|
|
const message = state.messages.find(m => m.id === action.data.id);
|
|
|
|
const idToUnblock = (message && message.campaign) ? message.campaign : action.data.id;
|
|
|
|
messageBlockList.splice(messageBlockList.indexOf(idToUnblock), 1);
|
2018-08-22 19:08:02 +03:00
|
|
|
this._storage.set("messageBlockList", messageBlockList);
|
|
|
|
return {messageBlockList};
|
|
|
|
});
|
|
|
|
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};
|
2018-03-19 19:57:23 +03:00
|
|
|
});
|
|
|
|
break;
|
2018-05-07 18:37:47 +03:00
|
|
|
case "UNBLOCK_BUNDLE":
|
|
|
|
await this.setState(state => {
|
2018-08-22 19:08:02 +03:00
|
|
|
const messageBlockList = [...state.messageBlockList];
|
2018-05-07 18:37:47 +03:00
|
|
|
for (let message of action.data.bundle) {
|
2018-08-22 19:08:02 +03:00
|
|
|
messageBlockList.splice(messageBlockList.indexOf(message.id), 1);
|
2018-05-07 18:37:47 +03:00
|
|
|
}
|
2018-08-22 19:08:02 +03:00
|
|
|
this._storage.set("messageBlockList", messageBlockList);
|
|
|
|
return {messageBlockList};
|
2018-05-07 18:37:47 +03:00
|
|
|
});
|
|
|
|
break;
|
2018-04-30 22:33:31 +03:00
|
|
|
case "OVERRIDE_MESSAGE":
|
2018-06-23 00:10:08 +03:00
|
|
|
await this.setMessageById(action.data.id, target, true, action);
|
2018-04-30 22:33:31 +03:00
|
|
|
break;
|
2018-03-19 19:57:23 +03:00
|
|
|
case "ADMIN_CONNECT_STATE":
|
2018-06-25 16:44:35 +03:00
|
|
|
if (action.data && action.data.endpoint) {
|
2018-09-03 18:36:03 +03:00
|
|
|
this._addPreviewEndpoint(action.data.endpoint.url, target.portID);
|
2018-06-25 16:44:35 +03:00
|
|
|
await this.loadMessagesFromAllProviders();
|
|
|
|
} else {
|
2018-10-29 16:39:40 +03:00
|
|
|
await this._updateAdminState(target);
|
2018-06-25 16:44:35 +03:00
|
|
|
}
|
2018-03-19 19:57:23 +03:00
|
|
|
break;
|
2018-07-10 23:01:52 +03:00
|
|
|
case "IMPRESSION":
|
2018-08-22 19:08:02 +03:00
|
|
|
await this.addImpression(action.data);
|
2018-07-10 23:01:52 +03:00
|
|
|
break;
|
2018-08-28 01:42:21 +03:00
|
|
|
case "DOORHANGER_TELEMETRY":
|
|
|
|
if (this.dispatchToAS) {
|
|
|
|
this.dispatchToAS(ac.ASRouterUserEvent(action.data));
|
|
|
|
}
|
|
|
|
break;
|
2018-09-28 20:04:05 +03:00
|
|
|
case "EXPIRE_QUERY_CACHE":
|
|
|
|
QueryCache.expireAll();
|
|
|
|
break;
|
2018-10-18 15:47:03 +03:00
|
|
|
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;
|
2018-10-23 01:03:59 +03:00
|
|
|
case "SET_PROVIDER_USER_PREF":
|
|
|
|
ASRouterPreferences.setUserPreference(action.data.id, action.data.value);
|
|
|
|
break;
|
2018-10-29 16:39:40 +03:00
|
|
|
case "EVALUATE_JEXL_EXPRESSION":
|
|
|
|
this.evaluateExpression(target, action.data);
|
2018-03-19 19:57:23 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-23 21:53:35 +03:00
|
|
|
this._ASRouter = _ASRouter;
|
2018-03-19 19:57:23 +03:00
|
|
|
|
|
|
|
/**
|
2018-04-23 21:53:35 +03:00
|
|
|
* ASRouter - singleton instance of _ASRouter that controls all messages
|
2018-03-19 19:57:23 +03:00
|
|
|
* in the new tab page.
|
|
|
|
*/
|
2018-07-25 00:16:08 +03:00
|
|
|
this.ASRouter = new _ASRouter();
|
2018-03-19 19:57:23 +03:00
|
|
|
|
2018-04-30 22:33:31 +03:00
|
|
|
const EXPORTED_SYMBOLS = ["_ASRouter", "ASRouter", "MessageLoaderUtils"];
|