gecko-dev/browser/components/newtab/lib/SnippetsFeed.jsm
Mike de Boer 481ae95c00 Bug 1524593 - nsISearchService (aka nsIBrowserSearchService, previously) refactor to be mostly an asynchronous, in preparation of WebExtension engines. r=daleharvey
This is a rollup of all the patches that have landed on the cedar project branch:

891252fdd0
Bug 1492475 - Part 1: Migrate most, if not all nsSearchService consumers to use async APIs. r=florian

79b2eb2367
Bug 1492475 - Part 2: Move nsIBrowserSearchService.idl to toolkit/components/search/nsISearchService.idl and update references. r=florian

a947d3cdf0
Bug 1492475 - Part 3: The search service init() method should simply return a Promise. r=florian

c1e172dfac
Bug 1492475 - Part 4: Remove the synchronous initialization flow. r=florian

cd41189eac
Bug 1492475 - Part 5: Since async initialization of the search service now is implicit behavior, remove the distinctive verbiage used internally. r=florian

2ae7189dfa
Bug 1492475 - Part 6: Update the cache build task to work with an actual Promise and re-initialize only once at the same time - all to fix race conditions here. r=florian

c8ee92973f
Bug 1492475 - Part 7: Make the region fetch not block the init flow, to ensure it's as fast as possible. r=florian

c44e674e16
Bug 1492475 - Part 8: Introduce an init flag, which can only be used privately, that allows to explicitly skip waiting for the region check process to complete. r=florian

6c79eaf1d3
Bug 1492475 - Part 9: Update unit tests to stop using 'currentEngine', in favor of 'defaultEngine'. r=Standard8

21b3aa17ee
Bug 1492475 - Part 10: Update unit tests to be fully aware of the new, async signatures of the search service API and remove sync init flow tests. r=mkaply,florian

ce5ba69019
Bug 1492475 - Part 11: Repair incorrect usage of the `identifier` property of nsISearchEngine instances. r=florian

fd177a7994
Bug 1518543 - Fix up the Android (Fennec) nsISearchService shim to work with the new asynchronous API. r=florian

3653d8ee22
Bug 1523708 - Change the search service interaction in the show-heartbeat action to use the new async API. r=florian

Differential Revision: https://phabricator.services.mozilla.com/D18355

--HG--
rename : netwerk/base/nsIBrowserSearchService.idl => toolkit/components/search/nsISearchService.idl
extra : moz-landing-system : lando
2019-02-02 11:27:21 +00:00

215 lines
7.5 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 {actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
ChromeUtils.defineModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
ChromeUtils.defineModuleGetter(this, "ShellService",
"resource:///modules/ShellService.jsm");
ChromeUtils.defineModuleGetter(this, "ProfileAge",
"resource://gre/modules/ProfileAge.jsm");
ChromeUtils.defineModuleGetter(this, "FxAccounts",
"resource://gre/modules/FxAccounts.jsm");
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
// Url to fetch snippets, in the urlFormatter service format.
const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
const TELEMETRY_PREF = "datareporting.healthreport.uploadEnabled";
const FXA_USERNAME_PREF = "services.sync.username";
// Prefix for any target matching a search engine.
const TARGET_SEARCHENGINE_PREFIX = "searchEngine-";
const SEARCH_ENGINE_OBSERVER_TOPIC = "browser-search-engine-modified";
// Should be bumped up if the snippets content format changes.
const STARTPAGE_VERSION = 5;
const ONE_DAY = 24 * 60 * 60 * 1000;
const ONE_WEEK = 7 * ONE_DAY;
this.SnippetsFeed = class SnippetsFeed {
constructor() {
this._refresh = this._refresh.bind(this);
this._totalBookmarks = null;
this._totalBookmarksLastUpdated = null;
}
get snippetsURL() {
const updateURL = Services
.prefs.getStringPref(SNIPPETS_URL_PREF)
.replace("%STARTPAGE_VERSION%", STARTPAGE_VERSION);
return Services.urlFormatter.formatURL(updateURL);
}
isDefaultBrowser() {
try {
return ShellService.isDefaultBrowser();
} catch (e) {}
// istanbul ignore next
return null;
}
isDevtoolsUser() {
return Services.prefs.getIntPref("devtools.selfxss.count") >= 5;
}
async getProfileInfo() {
const profileAge = await ProfileAge();
const createdDate = await profileAge.created;
const resetDate = await profileAge.reset;
return {
createdWeeksAgo: Math.floor((Date.now() - createdDate) / ONE_WEEK),
resetWeeksAgo: resetDate ? Math.floor((Date.now() - resetDate) / ONE_WEEK) : null,
};
}
getSelectedSearchEngine() {
return new Promise(resolve => {
// Note: calling init ensures this code is only executed after Search has been initialized
Services.search.getVisibleEngines().then(engines => {
resolve({
searchEngineIdentifier: Services.search.defaultEngine.identifier,
engines: engines
.filter(engine => engine.identifier)
.map(engine => `${TARGET_SEARCHENGINE_PREFIX}${engine.identifier}`),
});
}).catch(() => resolve({engines: [], searchEngineIdentifier: ""}));
});
}
async getAddonsInfo(target) {
const {addons, fullData} = await AddonManager.getActiveAddons(["extension", "service"]);
const info = {};
for (const addon of addons) {
info[addon.id] = {
version: addon.version,
type: addon.type,
isSystem: addon.isSystem,
isWebExtension: addon.isWebExtension,
};
if (fullData) {
Object.assign(info[addon.id], {
name: addon.name,
userDisabled: addon.userDisabled,
installDate: addon.installDate,
});
}
}
const data = {addons: info, isFullData: fullData};
this.store.dispatch(ac.OnlyToOneContent({type: at.ADDONS_INFO_RESPONSE, data}, target));
}
async getTotalBookmarksCount(target) {
if (!this._totalBookmarks || (Date.now() - this._totalBookmarksLastUpdated > ONE_DAY)) {
this._totalBookmarksLastUpdated = Date.now();
try {
this._totalBookmarks = await NewTabUtils.activityStreamProvider.getTotalBookmarksCount();
} catch (e) {
Cu.reportError(e);
}
}
this.store.dispatch(ac.OnlyToOneContent({type: at.TOTAL_BOOKMARKS_RESPONSE, data: this._totalBookmarks}, target));
}
_dispatchChanges(data) {
this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_DATA, data}));
}
async _saveBlockedSnippet(snippetId) {
const blockList = await this._getBlockList() || [];
return this._storage.set("blockList", blockList.concat([snippetId]));
}
_getBlockList() {
return this._storage.get("blockList");
}
_clearBlockList() {
return this._storage.set("blockList", []);
}
async _refresh() {
const profileInfo = await this.getProfileInfo();
const data = {
profileCreatedWeeksAgo: profileInfo.createdWeeksAgo,
profileResetWeeksAgo: profileInfo.resetWeeksAgo,
snippetsURL: this.snippetsURL,
version: STARTPAGE_VERSION,
telemetryEnabled: Services.prefs.getBoolPref(TELEMETRY_PREF),
onboardingFinished: true,
fxaccount: Services.prefs.prefHasUserValue(FXA_USERNAME_PREF),
selectedSearchEngine: await this.getSelectedSearchEngine(),
defaultBrowser: this.isDefaultBrowser(),
isDevtoolsUser: this.isDevtoolsUser(),
blockList: await this._getBlockList() || [],
previousSessionEnd: this._previousSessionEnd,
};
this._dispatchChanges(data);
}
async observe(subject, topic, data) {
if (topic === SEARCH_ENGINE_OBSERVER_TOPIC) {
const selectedSearchEngine = await this.getSelectedSearchEngine();
this._dispatchChanges({selectedSearchEngine});
}
}
async init() {
this._storage = this.store.dbStorage.getDbTable("snippets");
Services.obs.addObserver(this, SEARCH_ENGINE_OBSERVER_TOPIC);
this._previousSessionEnd = await this._storage.get("previousSessionEnd");
await this._refresh();
Services.prefs.addObserver(SNIPPETS_URL_PREF, this._refresh);
Services.prefs.addObserver(TELEMETRY_PREF, this._refresh);
Services.prefs.addObserver(FXA_USERNAME_PREF, this._refresh);
}
uninit() {
this._storage.set("previousSessionEnd", Date.now());
Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._refresh);
Services.prefs.removeObserver(TELEMETRY_PREF, this._refresh);
Services.prefs.removeObserver(FXA_USERNAME_PREF, this._refresh);
Services.obs.removeObserver(this, SEARCH_ENGINE_OBSERVER_TOPIC);
this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_RESET}));
}
async showFirefoxAccounts(browser) {
const url = await FxAccounts.config.promiseSignUpURI("snippets");
// We want to replace the current tab.
browser.loadURI(url, {triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})});
}
async onAction(action) {
switch (action.type) {
case at.INIT:
this.init();
break;
case at.UNINIT:
this.uninit();
break;
case at.SHOW_FIREFOX_ACCOUNTS:
this.showFirefoxAccounts(action._target.browser);
break;
case at.SNIPPETS_BLOCKLIST_UPDATED:
this._saveBlockedSnippet(action.data);
this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPET_BLOCKED, data: action.data}));
break;
case at.SNIPPETS_BLOCKLIST_CLEARED:
this._clearBlockList();
break;
case at.TOTAL_BOOKMARKS_REQUEST:
this.getTotalBookmarksCount(action.meta.fromTarget);
break;
case at.ADDONS_INFO_REQUEST:
await this.getAddonsInfo(action.meta.fromTarget);
break;
}
}
};
const EXPORTED_SYMBOLS = ["SnippetsFeed"];