Bug 1210410 Implement search messages for the Remote New Tab r=oyiptong

MozReview-Commit-ID: 8trDqRegP3G

--HG--
extra : rebase_source : 81fa1478d2e51b06a2adb838a322d2c22925253e
This commit is contained in:
Ursula Sarracini 2016-05-10 10:32:31 -04:00
parent 11a106fd9b
commit 2f6e8ea7b8
10 changed files with 635 additions and 142 deletions

View File

@ -2,8 +2,11 @@
NewTabWebChannel,
NewTabPrefsProvider,
PlacesProvider,
PreviewProvider,
NewTabSearchProvider,
Preferences,
XPCOMUtils
XPCOMUtils,
Task
*/
/* exported NewTabMessages */
@ -13,6 +16,7 @@
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesProvider",
"resource:///modules/PlacesProvider.jsm");
@ -20,12 +24,15 @@ XPCOMUtils.defineLazyModuleGetter(this, "PreviewProvider",
"resource:///modules/PreviewProvider.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
"resource:///modules/NewTabPrefsProvider.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabSearchProvider",
"resource:///modules/NewTabSearchProvider.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
"resource:///modules/NewTabWebChannel.jsm");
this.EXPORTED_SYMBOLS = ["NewTabMessages"];
const PREF_ENABLED = "browser.newtabpage.remote";
const CURRENT_ENGINE = "browser-search-engine-modified";
// Action names are from the content's perspective. in from chrome == out from content
// Maybe replace the ACTION objects by a bi-directional Map a bit later?
@ -34,6 +41,13 @@ const ACTIONS = {
"REQUEST_PREFS",
"REQUEST_THUMB",
"REQUEST_FRECENT",
"REQUEST_UISTRINGS",
"REQUEST_SEARCH_SUGGESTIONS",
"REQUEST_MANAGE_ENGINES",
"REQUEST_SEARCH_STATE",
"REQUEST_REMOVE_FORM_HISTORY",
"REQUEST_PERFORM_SEARCH",
"REQUEST_CYCLE_ENGINE",
],
prefs: {
inPrefs: "REQUEST_PREFS",
@ -48,6 +62,23 @@ const ACTIONS = {
outFrecent: "RECEIVE_FRECENT",
outPlacesChange: "RECEIVE_PLACES_CHANGE",
},
search: {
inSearch: {
UIStrings: "REQUEST_UISTRINGS",
suggestions: "REQUEST_SEARCH_SUGGESTIONS",
manageEngines: "REQUEST_MANAGE_ENGINES",
state: "REQUEST_SEARCH_STATE",
removeFormHistory: "REQUEST_REMOVE_FORM_HISTORY",
performSearch: "REQUEST_PERFORM_SEARCH",
cycleEngine: "REQUEST_CYCLE_ENGINE"
},
outSearch: {
UIStrings: "RECEIVE_UISTRINGS",
suggestions: "RECEIVE_SEARCH_SUGGESTIONS",
state: "RECEIVE_SEARCH_STATE",
currentEngine: "RECEIVE_CURRENT_ENGINE"
},
}
};
let NewTabMessages = {
@ -75,6 +106,51 @@ let NewTabMessages = {
NewTabWebChannel.send(ACTIONS.links.outFrecent, links, target);
});
break;
case ACTIONS.search.inSearch.UIStrings:
// Return to the originator all search strings to display
let strings = NewTabSearchProvider.search.searchSuggestionUIStrings;
NewTabWebChannel.send(ACTIONS.search.outSearch.UIStrings, strings, target);
break;
case ACTIONS.search.inSearch.suggestions:
// Return to the originator all search suggestions
Task.spawn(function*() {
try {
let {engineName, searchString} = data;
let suggestions = yield NewTabSearchProvider.search.asyncGetSuggestions(engineName, searchString, target);
NewTabWebChannel.send(ACTIONS.search.outSearch.suggestions, suggestions, target);
} catch (e) {
Cu.reportError(e);
}
});
break;
case ACTIONS.search.inSearch.manageEngines:
// Open about:preferences to manage search state
NewTabSearchProvider.search.manageEngines(target.browser);
break;
case ACTIONS.search.inSearch.state:
// Return the state of the search component (i.e current engine and visible engine details)
Task.spawn(function*() {
try {
let state = yield NewTabSearchProvider.search.asyncGetState();
NewTabWebChannel.broadcast(ACTIONS.search.outSearch.state, state);
} catch (e) {
Cu.reportError(e);
}
});
break;
case ACTIONS.search.inSearch.removeFormHistory:
// Remove a form history entry from the search component
let suggestion = data;
NewTabSearchProvider.search.removeFormHistory(target, suggestion);
break;
case ACTIONS.search.inSearch.performSearch:
// Perform a search
NewTabSearchProvider.search.asyncPerformSearch(target, data).catch(Cu.reportError);
break;
case ACTIONS.search.inSearch.cycleEngine:
// Set the new current engine
NewTabSearchProvider.search.asyncCycleEngine(data).catch(Cu.reportError);
break;
}
},
@ -85,6 +161,14 @@ let NewTabMessages = {
NewTabWebChannel.broadcast(ACTIONS.links.outPlacesChange, {type, data});
},
/*
* Broadcast current engine has changed to all open newtab pages
*/
_handleCurrentEngineChange(name, value) { //jshint unused: false
let engine = value;
NewTabWebChannel.broadcast(ACTIONS.search.outSearch.currentEngine, engine);
},
/*
* Broadcast preference changes to all open newtab pages
*/
@ -107,9 +191,11 @@ let NewTabMessages = {
init() {
this.handleContentRequest = this.handleContentRequest.bind(this);
this._handleEnabledChange = this._handleEnabledChange.bind(this);
this._handleCurrentEngineChange = this._handleCurrentEngineChange.bind(this);
PlacesProvider.links.init();
NewTabPrefsProvider.prefs.init();
NewTabSearchProvider.search.init();
NewTabWebChannel.init();
this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
@ -118,7 +204,9 @@ let NewTabMessages = {
for (let action of ACTIONS.inboundActions) {
NewTabWebChannel.on(action, this.handleContentRequest);
}
NewTabPrefsProvider.prefs.on(PREF_ENABLED, this._handleEnabledChange);
NewTabSearchProvider.search.on(CURRENT_ENGINE, this._handleCurrentEngineChange);
for (let pref of NewTabPrefsProvider.newtabPagePrefSet) {
NewTabPrefsProvider.prefs.on(pref, this.handlePrefChange);
@ -136,6 +224,7 @@ let NewTabMessages = {
if (this._prefs.enabled) {
NewTabPrefsProvider.prefs.off(PREF_ENABLED, this._handleEnabledChange);
NewTabSearchProvider.search.off(CURRENT_ENGINE, this._handleCurrentEngineChange);
for (let action of ACTIONS.inboundActions) {
NewTabWebChannel.off(action, this.handleContentRequest);
@ -148,6 +237,7 @@ let NewTabMessages = {
PlacesProvider.links.uninit();
NewTabPrefsProvider.prefs.uninit();
NewTabSearchProvider.search.uninit();
NewTabWebChannel.uninit();
}
};

View File

@ -28,6 +28,7 @@ const gPrefsMap = new Map([
["browser.newtabpage.blocked", "str"],
["intl.locale.matchOS", "bool"],
["general.useragent.locale", "localized"],
["browser.search.hiddenOneOffs", "str"],
]);
// prefs that are important for the newtab page
@ -37,7 +38,8 @@ const gNewtabPagePrefs = new Set([
"browser.newtabpage.pinned",
"browser.newtabpage.blocked",
"browser.newtabpage.introShown",
"browser.newtabpage.updateIntroShown"
"browser.newtabpage.updateIntroShown",
"browser.search.hiddenOneOffs",
]);
let PrefsProvider = function PrefsProvider() {

View File

@ -0,0 +1,112 @@
/* global XPCOMUtils, ContentSearch, Task, Services, EventEmitter */
/* exported NewTabSearchProvider */
"use strict";
this.EXPORTED_SYMBOLS = ["NewTabSearchProvider"];
const {utils: Cu, interfaces: Ci} = Components;
const CURRENT_ENGINE = "browser-search-engine-modified";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
"resource:///modules/ContentSearch.jsm");
XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
return EventEmitter;
});
function SearchProvider() {
EventEmitter.decorate(this);
}
SearchProvider.prototype = {
observe(subject, topic, data) { // jshint unused:false
switch (data) {
case "engine-current":
if (topic === CURRENT_ENGINE) {
Task.spawn(function* () {
try {
let state = yield ContentSearch.currentStateObj(true);
let engine = state.currentEngine;
this.emit(CURRENT_ENGINE, engine);
} catch (e) {
Cu.reportError(e);
}
}.bind(this));
}
break;
case "engine-default":
// engine-default is always sent with engine-current and isn't
// relevant to content searches.
break;
default:
Cu.reportError(new Error("NewTabSearchProvider observing unknown topic"));
break;
}
},
init() {
try {
Services.obs.addObserver(this, CURRENT_ENGINE, true);
} catch (e) {
Cu.reportError(e);
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference
]),
uninit() {
try {
Services.obs.removeObserver(this, CURRENT_ENGINE, true);
} catch (e) {
Cu.reportError(e);
}
},
get searchSuggestionUIStrings() {
return ContentSearch.searchSuggestionUIStrings;
},
removeFormHistory({browser}, suggestion) {
ContentSearch.removeFormHistoryEntry({target: browser}, suggestion);
},
manageEngines(browser) {
const browserWin = browser.ownerDocument.defaultView;
browserWin.openPreferences("paneSearch");
},
asyncGetState: Task.async(function*() {
let state = yield ContentSearch.currentStateObj(true);
return state;
}),
asyncPerformSearch: Task.async(function*({browser}, searchData) {
ContentSearch.performSearch({target: browser}, searchData);
yield ContentSearch.addFormHistoryEntry({target: browser}, searchData.searchString);
}),
asyncCycleEngine: Task.async(function*(engineName) {
Services.search.currentEngine = Services.search.getEngineByName(engineName);
let state = yield ContentSearch.currentStateObj(true);
let newEngine = state.currentEngine;
this.emit(CURRENT_ENGINE, newEngine);
}),
asyncGetSuggestions: Task.async(function*(engineName, searchString, target) {
let suggestions = ContentSearch.getSuggestions(engineName, searchString, target.browser);
return suggestions;
}),
};
const NewTabSearchProvider = {
search: new SearchProvider(),
};

View File

@ -14,6 +14,7 @@ EXTRA_JS_MODULES += [
'NewTabMessages.jsm',
'NewTabPrefsProvider.jsm',
'NewTabRemoteResources.jsm',
'NewTabSearchProvider.jsm',
'NewTabURL.jsm',
'NewTabWebChannel.jsm',
'PlacesProvider.jsm',

View File

@ -6,6 +6,7 @@ support-files =
newtabmessages_places.html
newtabmessages_prefs.html
newtabmessages_preview.html
newtabmessages_search.html
[browser_PreviewProvider.js]
[browser_remotenewtab_pageloads.js]

View File

@ -1,8 +1,10 @@
/* globals Cu, XPCOMUtils, Preferences, is, registerCleanupFunction, NewTabWebChannel, PlacesTestUtils, Task */
/* globals Cu, XPCOMUtils, Preferences, is, registerCleanupFunction, NewTabWebChannel,
PlacesTestUtils, NewTabMessages, ok, Services, PlacesUtils, NetUtil, Task */
"use strict";
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
"resource:///modules/NewTabWebChannel.jsm");
@ -161,3 +163,60 @@ add_task(function* placesMessages_request() {
});
yield cleanup();
});
/*
* Sanity tests for search messages
*/
add_task(function* searchMessages_request() {
yield setup();
let testURL = "https://example.com/browser/browser/components/newtab/tests/browser/newtabmessages_search.html";
// create dummy test engines
Services.search.addEngineWithDetails("Engine1", "", "", "", "GET",
"http://example.com/?q={searchTerms}");
Services.search.addEngineWithDetails("Engine2", "", "", "", "GET",
"http://example.com/?q={searchTerms}");
let tabOptions = {
gBrowser,
url: testURL
};
let UIStringsResponseAck = new Promise(resolve => {
NewTabWebChannel.once("UIStringsAck", (_, msg) => {
ok(true, "a search request response for UI string has been received");
ok(msg.data, "received the UI Strings");
resolve();
});
});
let suggestionsResponseAck = new Promise(resolve => {
NewTabWebChannel.once("suggestionsAck", (_, msg) => {
ok(true, "a search request response for suggestions has been received");
ok(msg.data, "received the suggestions");
resolve();
});
});
let stateResponseAck = new Promise(resolve => {
NewTabWebChannel.once("stateAck", (_, msg) => {
ok(true, "a search request response for state has been received");
ok(msg.data, "received a state object");
resolve();
});
});
let currentEngineResponseAck = new Promise(resolve => {
NewTabWebChannel.once("currentEngineAck", (_, msg) => {
ok(true, "a search request response for current engine has been received");
ok(msg.data, "received a current engine");
resolve();
});
});
yield BrowserTestUtils.withNewTab(tabOptions, function*() {
yield UIStringsResponseAck;
yield suggestionsResponseAck;
yield stateResponseAck;
yield currentEngineResponseAck;
});
cleanup();
});

View File

@ -0,0 +1,113 @@
<html>
<head>
<meta charset="utf8">
<title>Newtab WebChannel test</title>
</head>
<body>
<script>
let suggestionsData = {
engineName: "Engine1",
searchString: "test",
};
let removeFormHistoryData = "test";
let performSearchData = {
engineName: "Engine1",
healthReportKey: "1",
searchPurpose: "d",
searchString: "test",
};
let cycleEngineData = "Engine2";
window.addEventListener("WebChannelMessageToContent", function(e) {
if (e.detail.message) {
let reply;
switch (e.detail.message.type) {
case "RECEIVE_UISTRINGS":
reply = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "UIStringsAck", data: e.detail.message.data}),
}
});
window.dispatchEvent(reply);
break;
case "RECEIVE_SEARCH_SUGGESTIONS":
reply = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "suggestionsAck", data: e.detail.message.data}),
}
});
window.dispatchEvent(reply);
break;
case "RECEIVE_SEARCH_STATE":
reply = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "stateAck", data: e.detail.message.data}),
}
});
window.dispatchEvent(reply);
break;
case "RECEIVE_CURRENT_ENGINE":
reply = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "currentEngineAck", data: e.detail.message.data}),
}
});
window.dispatchEvent(reply);
break;
}
}
}, true);
document.onreadystatechange = function () {
if (document.readyState === "complete") {
let msg = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "REQUEST_UISTRINGS"}),
}
});
window.dispatchEvent(msg);
msg = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "REQUEST_SEARCH_SUGGESTIONS", data: suggestionsData}),
}
});
window.dispatchEvent(msg);
msg = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "REQUEST_SEARCH_STATE"}),
}
});
window.dispatchEvent(msg);
msg = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "REQUEST_REMOVE_FORM_HISTORY", data: removeFormHistoryData}),
}
});
window.dispatchEvent(msg);
msg = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "REQUEST_PERFORM_SEARCH", data: performSearchData}),
}
});
window.dispatchEvent(msg);
msg = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "newtab",
message: JSON.stringify({type: "REQUEST_CYCLE_ENGINE", data: cycleEngineData}),
}
});
window.dispatchEvent(msg);
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,84 @@
"use strict";
/* global XPCOMUtils, NewTabSearchProvider, run_next_test, ok, equal, do_check_true, do_get_profile, Services */
/* exported run_test */
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabSearchProvider",
"resource:///modules/NewTabSearchProvider.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
"resource:///modules/ContentSearch.jsm");
// ensure a profile exists
do_get_profile();
function run_test() {
run_next_test();
}
function hasProp(obj) {
return function(aProp) {
ok(obj.hasOwnProperty(aProp), `expect to have property ${aProp}`);
};
}
add_task(function* test_search() {
ContentSearch.init();
let observerPromise = new Promise(resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
if (aData === "init-complete" && aTopic === "browser-search-service") {
Services.obs.removeObserver(observer, "browser-search-service");
resolve();
}
}, "browser-search-service", false);
});
Services.search.init();
yield observerPromise;
do_check_true(Services.search.isInitialized);
// get initial state of search and check it has correct properties
let state = yield NewTabSearchProvider.search.asyncGetState();
let stateProps = hasProp(state);
["engines", "currentEngine"].forEach(stateProps);
// check that the current engine is correct and has correct properties
let {currentEngine} = state;
equal(currentEngine.name, Services.search.currentEngine.name, "Current engine has been correctly set");
var engineProps = hasProp(currentEngine);
["name", "placeholder", "iconBuffer"].forEach(engineProps);
//create dummy test engines to test observer
Services.search.addEngineWithDetails("TestSearch1", "", "", "", "GET",
"http://example.com/?q={searchTerms}");
Services.search.addEngineWithDetails("TestSearch2", "", "", "", "GET",
"http://example.com/?q={searchTerms}");
// set one of the dummy test engines to the default engine
Services.search.defaultEngine = Services.search.getEngineByName("TestSearch1");
// test that the event emitter is working by setting a new current engine "TestSearch2"
let engineName = "TestSearch2";
NewTabSearchProvider.search.init();
// event emitter will fire when current engine is changed
let promise = new Promise(resolve => {
NewTabSearchProvider.search.once("browser-search-engine-modified", (name, data) => { // jshint ignore:line
resolve([name, data.name]);
});
});
// set a new current engine
Services.search.currentEngine = Services.search.getEngineByName(engineName);
let expectedEngineName = Services.search.currentEngine.name;
// emitter should fire and return the new engine
let [eventName, actualEngineName] = yield promise;
equal(eventName, "browser-search-engine-modified", `emitter sent the correct event ${eventName}`);
equal(expectedEngineName, actualEngineName, `emitter set the correct engine ${expectedEngineName}`);
NewTabSearchProvider.search.uninit();
});

View File

@ -6,5 +6,6 @@ skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_AboutNewTabService.js]
[test_NewTabPrefsProvider.js]
[test_NewTabSearchProvider.js]
[test_NewTabURL.js]
[test_PlacesProvider.js]

View File

@ -1,7 +1,7 @@
/* 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/. */
/* globals XPCOMUtils, Services, Task, Promise, SearchSuggestionController, FormHistory, PrivateBrowsingUtils */
"use strict";
this.EXPORTED_SYMBOLS = [
@ -73,10 +73,10 @@ const MAX_SUGGESTIONS = 6;
* data: see _currentEngineObj
* CurrentState
* Broadcast when the current search state changes.
* data: see _currentStateObj
* data: see currentStateObj
* State
* Sent in reply to GetState.
* data: see _currentStateObj
* data: see currentStateObj
* Strings
* Sent in reply to GetStrings
* data: Object containing string names and values for the current locale.
@ -126,6 +126,7 @@ this.ContentSearch = {
let searchBundle = Services.strings.createBundle("chrome://browser/locale/search.properties");
let stringNames = ["searchHeader", "searchPlaceholder", "searchForSomethingWith",
"searchWithHeader", "searchSettings"];
for (let name of stringNames) {
this._searchSuggestionUIStrings[name] = searchBundle.GetStringFromName(name);
}
@ -144,7 +145,8 @@ this.ContentSearch = {
Services.obs.removeObserver(this, "shutdown-leaks-before-check");
this._eventQueue.length = 0;
return this._destroyedPromise = Promise.resolve(this._currentEventPromise);
this._destroyedPromise = Promise.resolve(this._currentEventPromise);
return this._destroyedPromise;
},
/**
@ -177,7 +179,7 @@ this.ContentSearch = {
// Search requests cause cancellation of all Suggestion requests from the
// same browser.
if (msg.data.type == "Search") {
if (msg.data.type === "Search") {
this._cancelSuggestions(msg);
}
@ -205,6 +207,155 @@ this.ContentSearch = {
}
},
removeFormHistoryEntry: function (msg, entry) {
let browserData = this._suggestionDataForBrowser(msg.target);
if (browserData && browserData.previousFormHistoryResult) {
let { previousFormHistoryResult } = browserData;
for (let i = 0; i < previousFormHistoryResult.matchCount; i++) {
if (previousFormHistoryResult.getValueAt(i) === entry) {
previousFormHistoryResult.removeValueAt(i, true);
break;
}
}
}
},
performSearch: function (msg, data) {
this._ensureDataHasProperties(data, [
"engineName",
"searchString",
"healthReportKey",
"searchPurpose",
]);
let engine = Services.search.getEngineByName(data.engineName);
let submission = engine.getSubmission(data.searchString, "", data.searchPurpose);
let browser = msg.target;
let win;
try {
win = browser.ownerDocument.defaultView;
}
catch (err) {
// The browser may have been closed between the time its content sent the
// message and the time we handle it. In that case, trying to call any
// method on it will throw.
return;
}
let where = win.whereToOpenLink(data.originalEvent);
// There is a chance that by the time we receive the search message, the user
// has switched away from the tab that triggered the search. If, based on the
// event, we need to load the search in the same tab that triggered it (i.e.
// where === "current"), openUILinkIn will not work because that tab is no
// longer the current one. For this case we manually load the URI.
if (where === "current") {
browser.loadURIWithFlags(submission.uri.spec,
Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null,
submission.postData);
} else {
let params = {
postData: submission.postData,
inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
};
win.openUILinkIn(submission.uri.spec, where, params);
}
win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
data.selection || null);
return;
},
getSuggestions: Task.async(function* (engineName, searchString, browser, remoteTimeout=null) {
let engine = Services.search.getEngineByName(engineName);
if (!engine) {
throw new Error("Unknown engine name: " + engineName);
}
let browserData = this._suggestionDataForBrowser(browser, true);
let { controller } = browserData;
let ok = SearchSuggestionController.engineOffersSuggestions(engine);
controller.maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS;
controller.maxRemoteResults = ok ? MAX_SUGGESTIONS : 0;
controller.remoteTimeout = remoteTimeout || undefined;
let priv = PrivateBrowsingUtils.isBrowserPrivate(browser);
// fetch() rejects its promise if there's a pending request, but since we
// process our event queue serially, there's never a pending request.
this._currentSuggestion = { controller: controller, target: browser };
let suggestions = yield controller.fetch(searchString, priv, engine);
this._currentSuggestion = null;
// suggestions will be null if the request was cancelled
let result = {};
if (!suggestions) {
return result;
}
// Keep the form history result so RemoveFormHistoryEntry can remove entries
// from it. Keeping only one result isn't foolproof because the client may
// try to remove an entry from one set of suggestions after it has requested
// more but before it's received them. In that case, the entry may not
// appear in the new suggestions. But that should happen rarely.
browserData.previousFormHistoryResult = suggestions.formHistoryResult;
result = {
engineName,
term: suggestions.term,
local: suggestions.local,
remote: suggestions.remote,
};
return result;
}),
addFormHistoryEntry: Task.async(function* (browser, entry="") {
let isPrivate = false;
try {
// isBrowserPrivate assumes that the passed-in browser has all the normal
// properties, which won't be true if the browser has been destroyed.
// That may be the case here due to the asynchronous nature of messaging.
isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser.target);
} catch (err) {
return false;
}
if (isPrivate || entry === "") {
return false;
}
let browserData = this._suggestionDataForBrowser(browser.target, true);
FormHistory.update({
op: "bump",
fieldname: browserData.controller.formHistoryParam,
value: entry,
}, {
handleCompletion: () => {},
handleError: err => {
Cu.reportError("Error adding form history entry: " + err);
},
});
return true;
}),
currentStateObj: Task.async(function* (uriFlag=false) {
let state = {
engines: [],
currentEngine: yield this._currentEngineObj(),
};
if (uriFlag) {
state.currentEngine.iconBuffer = Services.search.currentEngine.getIconURLBySize(16, 16);
}
let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs");
let hiddenList = pref ? pref.split(",") : [];
for (let engine of Services.search.getVisibleEngines()) {
let uri = engine.getIconURLBySize(16, 16);
let iconBuffer = uri;
if (!uriFlag) {
iconBuffer = yield this._arrayBufferFromDataURI(uri);
}
state.engines.push({
name: engine.name,
iconBuffer,
hidden: hiddenList.indexOf(engine.name) !== -1,
});
}
return state;
}),
_processEventQueue: function () {
if (this._currentEventPromise || !this._eventQueue.length) {
return;
@ -227,14 +378,14 @@ this.ContentSearch = {
_cancelSuggestions: function (msg) {
let cancelled = false;
// cancel active suggestion request
if (this._currentSuggestion && this._currentSuggestion.target == msg.target) {
if (this._currentSuggestion && this._currentSuggestion.target === msg.target) {
this._currentSuggestion.controller.stop();
cancelled = true;
}
// cancel queued suggestion requests
for (let i = 0; i < this._eventQueue.length; i++) {
let m = this._eventQueue[i].data;
if (msg.target == m.target && m.data.type == "GetSuggestions") {
if (msg.target === m.target && m.data.type === "GetSuggestions") {
this._eventQueue.splice(i, 1);
cancelled = true;
i--;
@ -255,7 +406,7 @@ this.ContentSearch = {
}),
_onMessageGetState: function (msg, data) {
return this._currentStateObj().then(state => {
return this.currentStateObj().then(state => {
this._reply(msg, "State", state);
});
},
@ -265,58 +416,16 @@ this.ContentSearch = {
},
_onMessageSearch: function (msg, data) {
this._ensureDataHasProperties(data, [
"engineName",
"searchString",
"healthReportKey",
"searchPurpose",
]);
let engine = Services.search.getEngineByName(data.engineName);
let submission = engine.getSubmission(data.searchString, "", data.searchPurpose);
let browser = msg.target;
let win;
try {
win = browser.ownerDocument.defaultView;
}
catch (err) {
// The browser may have been closed between the time its content sent the
// message and the time we handle it. In that case, trying to call any
// method on it will throw.
return Promise.resolve();
}
let where = win.whereToOpenLink(data.originalEvent);
// There is a chance that by the time we receive the search message, the user
// has switched away from the tab that triggered the search. If, based on the
// event, we need to load the search in the same tab that triggered it (i.e.
// where == "current"), openUILinkIn will not work because that tab is no
// longer the current one. For this case we manually load the URI.
if (where == "current") {
browser.loadURIWithFlags(submission.uri.spec,
Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null,
submission.postData);
} else {
let params = {
postData: submission.postData,
inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
};
win.openUILinkIn(submission.uri.spec, where, params);
}
win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
data.selection || null);
return Promise.resolve();
this.performSearch(msg, data);
},
_onMessageSetCurrentEngine: function (msg, data) {
Services.search.currentEngine = Services.search.getEngineByName(data);
return Promise.resolve();
},
_onMessageManageEngines: function (msg, data) {
let browserWin = msg.target.ownerDocument.defaultView;
browserWin.openPreferences("paneSearch");
return Promise.resolve();
},
_onMessageGetSuggestions: Task.async(function* (msg, data) {
@ -324,36 +433,8 @@ this.ContentSearch = {
"engineName",
"searchString",
]);
let engine = Services.search.getEngineByName(data.engineName);
if (!engine) {
throw new Error("Unknown engine name: " + data.engineName);
}
let browserData = this._suggestionDataForBrowser(msg.target, true);
let { controller } = browserData;
let ok = SearchSuggestionController.engineOffersSuggestions(engine);
controller.maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS;
controller.maxRemoteResults = ok ? MAX_SUGGESTIONS : 0;
controller.remoteTimeout = data.remoteTimeout || undefined;
let priv = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
// fetch() rejects its promise if there's a pending request, but since we
// process our event queue serially, there's never a pending request.
this._currentSuggestion = { controller: controller, target: msg.target };
let suggestions = yield controller.fetch(data.searchString, priv, engine);
this._currentSuggestion = null;
// suggestions will be null if the request was cancelled
if (!suggestions) {
return;
}
// Keep the form history result so RemoveFormHistoryEntry can remove entries
// from it. Keeping only one result isn't foolproof because the client may
// try to remove an entry from one set of suggestions after it has requested
// more but before it's received them. In that case, the entry may not
// appear in the new suggestions. But that should happen rarely.
browserData.previousFormHistoryResult = suggestions.formHistoryResult;
let {engineName, searchString} = data;
let suggestions = yield this.getSuggestions(engineName, searchString, msg.target);
this._reply(msg, "Suggestions", {
engineName: data.engineName,
@ -363,45 +444,12 @@ this.ContentSearch = {
});
}),
_onMessageAddFormHistoryEntry: function (msg, entry) {
let isPrivate = true;
try {
// isBrowserPrivate assumes that the passed-in browser has all the normal
// properties, which won't be true if the browser has been destroyed.
// That may be the case here due to the asynchronous nature of messaging.
isPrivate = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
} catch (err) {}
if (isPrivate || entry === "") {
return Promise.resolve();
}
let browserData = this._suggestionDataForBrowser(msg.target, true);
if (FormHistory.enabled) {
FormHistory.update({
op: "bump",
fieldname: browserData.controller.formHistoryParam,
value: entry,
}, {
handleCompletion: () => {},
handleError: err => {
Cu.reportError("Error adding form history entry: " + err);
},
});
}
return Promise.resolve();
},
_onMessageAddFormHistoryEntry: Task.async(function* (msg, entry) {
yield this.addFormHistoryEntry(msg, entry);
}),
_onMessageRemoveFormHistoryEntry: function (msg, entry) {
let browserData = this._suggestionDataForBrowser(msg.target);
if (browserData && browserData.previousFormHistoryResult) {
let { previousFormHistoryResult } = browserData;
for (let i = 0; i < previousFormHistoryResult.matchCount; i++) {
if (previousFormHistoryResult.getValueAt(i) == entry) {
previousFormHistoryResult.removeValueAt(i, true);
break;
}
}
}
return Promise.resolve();
this.removeFormHistoryEntry(msg, entry);
},
_onMessageSpeculativeConnect: function (msg, engineName) {
@ -417,14 +465,14 @@ this.ContentSearch = {
},
_onObserve: Task.async(function* (data) {
if (data == "engine-current") {
if (data === "engine-current") {
let engine = yield this._currentEngineObj();
this._broadcast("CurrentEngine", engine);
}
else if (data != "engine-default") {
else if (data !== "engine-default") {
// engine-default is always sent with engine-current and isn't otherwise
// relevant to content searches.
let state = yield this._currentStateObj();
let state = yield this.currentStateObj();
this._broadcast("CurrentState", state);
}
}),
@ -464,24 +512,6 @@ this.ContentSearch = {
}];
},
_currentStateObj: Task.async(function* () {
let state = {
engines: [],
currentEngine: yield this._currentEngineObj(),
};
let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs");
let hiddenList = pref ? pref.split(",") : [];
for (let engine of Services.search.getVisibleEngines()) {
let uri = engine.getIconURLBySize(16, 16);
state.engines.push({
name: engine.name,
iconBuffer: yield this._arrayBufferFromDataURI(uri),
hidden: hiddenList.indexOf(engine.name) != -1,
});
}
return state;
}),
_currentEngineObj: Task.async(function* () {
let engine = Services.search.currentEngine;
let favicon = engine.getIconURLBySize(16, 16);