1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 this.EXPORTED_SYMBOLS = [
11 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
13 Cu.import("resource://gre/modules/Services.jsm");
14 Cu.import("resource://gre/modules/Promise.jsm");
15 Cu.import("resource://gre/modules/Task.jsm");
16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
18 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
19 "resource://gre/modules/FormHistory.jsm");
20 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
21 "resource://gre/modules/PrivateBrowsingUtils.jsm");
22 XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController",
23 "resource://gre/modules/SearchSuggestionController.jsm");
25 const INBOUND_MESSAGE = "ContentSearch";
26 const OUTBOUND_MESSAGE = INBOUND_MESSAGE;
29 * ContentSearch receives messages named INBOUND_MESSAGE and sends messages
30 * named OUTBOUND_MESSAGE. The data of each message is expected to look like
31 * { type, data }. type is the message's type (or subtype if you consider the
32 * type of the message itself to be INBOUND_MESSAGE), and data is data that is
33 * specific to the type.
35 * Inbound messages have the following types:
38 * Adds an entry to the search form history.
39 * data: the entry, a string
41 * Retrieves an array of search suggestions given a search string.
42 * data: { engineName, searchString, [remoteTimeout] }
44 * Retrieves the current search engine state.
47 * Opens the search engine management window.
49 * RemoveFormHistoryEntry
50 * Removes an entry from the search form history.
51 * data: the entry, a string
54 * data: { engineName, searchString, whence }
56 * Sets the current engine.
57 * data: the name of the engine
59 * Speculatively connects to an engine.
60 * data: the name of the engine
62 * Outbound messages have the following types:
65 * Broadcast when the current engine changes.
66 * data: see _currentEngineObj
68 * Broadcast when the current search state changes.
69 * data: see _currentStateObj
71 * Sent in reply to GetState.
72 * data: see _currentStateObj
74 * Sent in reply to GetSuggestions.
75 * data: see _onMessageGetSuggestions
78 this.ContentSearch = {
80 // Inbound events are queued and processed in FIFO order instead of handling
81 // them immediately, which would result in non-FIFO responses due to the
82 // asynchrononicity added by converting image data URIs to ArrayBuffers.
84 _currentEventPromise: null,
86 // This is used to handle search suggestions. It maps xul:browsers to objects
87 // { controller, previousFormHistoryResult }. See _onMessageGetSuggestions.
88 _suggestionMap: new WeakMap(),
90 // Resolved when we finish shutting down.
91 _destroyedPromise: null,
94 Cc["@mozilla.org/globalmessagemanager;1"].
95 getService(Ci.nsIMessageListenerManager).
96 addMessageListener(INBOUND_MESSAGE, this);
97 Services.obs.addObserver(this, "browser-search-engine-modified", false);
98 Services.obs.addObserver(this, "shutdown-leaks-before-check", false);
99 this._stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
102 destroy: function () {
103 if (this._destroyedPromise) {
104 return this._destroyedPromise;
107 Cc["@mozilla.org/globalmessagemanager;1"].
108 getService(Ci.nsIMessageListenerManager).
109 removeMessageListener(INBOUND_MESSAGE, this);
110 Services.obs.removeObserver(this, "browser-search-engine-modified");
111 Services.obs.removeObserver(this, "shutdown-leaks-before-check");
113 this._eventQueue.length = 0;
114 return this._destroyedPromise = Promise.resolve(this._currentEventPromise);
118 * Focuses the search input in the page with the given message manager.
119 * @param messageManager
120 * The MessageManager object of the selected browser.
122 focusInput: function (messageManager) {
123 messageManager.sendAsyncMessage(OUTBOUND_MESSAGE, {
128 receiveMessage: function (msg) {
129 // Add a temporary event handler that exists only while the message is in
130 // the event queue. If the message's source docshell changes browsers in
131 // the meantime, then we need to update msg.target. event.detail will be
132 // the docshell's new parent <xul:browser> element.
133 msg.handleEvent = event => {
134 let browserData = this._suggestionMap.get(msg.target);
136 this._suggestionMap.delete(msg.target);
137 this._suggestionMap.set(event.detail, browserData);
139 msg.target.removeEventListener("SwapDocShells", msg, true);
140 msg.target = event.detail;
141 msg.target.addEventListener("SwapDocShells", msg, true);
143 msg.target.addEventListener("SwapDocShells", msg, true);
145 this._eventQueue.push({
149 this._processEventQueue();
152 observe: function (subj, topic, data) {
154 case "browser-search-engine-modified":
155 this._eventQueue.push({
159 this._processEventQueue();
161 case "shutdown-leaks-before-check":
162 subj.wrappedJSObject.client.addBlocker(
163 "ContentSearch: Wait until the service is destroyed", () => this.destroy());
168 _processEventQueue: function () {
169 if (this._currentEventPromise || !this._eventQueue.length) {
173 let event = this._eventQueue.shift();
175 return this._currentEventPromise = Task.spawn(function* () {
177 yield this["_on" + event.type](event.data);
181 this._currentEventPromise = null;
182 this._processEventQueue();
187 _onMessage: Task.async(function* (msg) {
188 let methodName = "_onMessage" + msg.data.type;
189 if (methodName in this) {
190 yield this._initService();
191 yield this[methodName](msg, msg.data.data);
192 msg.target.removeEventListener("SwapDocShells", msg, true);
196 _onMessageGetState: function (msg, data) {
197 return this._currentStateObj().then(state => {
198 this._reply(msg, "State", state);
202 _onMessageSearch: function (msg, data) {
203 this._ensureDataHasProperties(data, [
208 let engine = Services.search.getEngineByName(data.engineName);
209 let submission = engine.getSubmission(data.searchString, "", data.whence);
210 let browser = msg.target;
212 browser.loadURIWithFlags(submission.uri.spec,
213 Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null,
214 submission.postData);
217 // The browser may have been closed between the time its content sent the
218 // message and the time we handle it. In that case, trying to call any
219 // method on it will throw.
220 return Promise.resolve();
222 let win = browser.ownerDocument.defaultView;
223 win.BrowserSearch.recordSearchInHealthReport(engine, data.whence,
224 data.selection || null);
225 return Promise.resolve();
228 _onMessageSetCurrentEngine: function (msg, data) {
229 Services.search.currentEngine = Services.search.getEngineByName(data);
230 return Promise.resolve();
233 _onMessageManageEngines: function (msg, data) {
234 let browserWin = msg.target.ownerDocument.defaultView;
236 if (Services.prefs.getBoolPref("browser.search.showOneOffButtons")) {
237 browserWin.openPreferences("paneSearch");
238 return Promise.resolve();
241 let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
242 getService(Components.interfaces.nsIWindowMediator);
243 let window = wm.getMostRecentWindow("Browser:SearchManager");
249 browserWin.setTimeout(function () {
250 browserWin.openDialog("chrome://browser/content/search/engineManager.xul",
251 "_blank", "chrome,dialog,modal,centerscreen,resizable");
254 return Promise.resolve();
257 _onMessageGetSuggestions: Task.async(function* (msg, data) {
258 this._ensureDataHasProperties(data, [
263 let engine = Services.search.getEngineByName(data.engineName);
265 throw new Error("Unknown engine name: " + data.engineName);
268 let browserData = this._suggestionDataForBrowser(msg.target, true);
269 let { controller } = browserData;
270 let ok = SearchSuggestionController.engineOffersSuggestions(engine);
271 controller.maxLocalResults = ok ? 2 : 6;
272 controller.maxRemoteResults = ok ? 6 : 0;
273 controller.remoteTimeout = data.remoteTimeout || undefined;
274 let priv = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
275 // fetch() rejects its promise if there's a pending request, but since we
276 // process our event queue serially, there's never a pending request.
277 let suggestions = yield controller.fetch(data.searchString, priv, engine);
279 // Keep the form history result so RemoveFormHistoryEntry can remove entries
280 // from it. Keeping only one result isn't foolproof because the client may
281 // try to remove an entry from one set of suggestions after it has requested
282 // more but before it's received them. In that case, the entry may not
283 // appear in the new suggestions. But that should happen rarely.
284 browserData.previousFormHistoryResult = suggestions.formHistoryResult;
286 this._reply(msg, "Suggestions", {
287 engineName: data.engineName,
288 searchString: suggestions.term,
289 formHistory: suggestions.local,
290 remote: suggestions.remote,
294 _onMessageAddFormHistoryEntry: function (msg, entry) {
295 let isPrivate = true;
297 // isBrowserPrivate assumes that the passed-in browser has all the normal
298 // properties, which won't be true if the browser has been destroyed.
299 // That may be the case here due to the asynchronous nature of messaging.
300 isPrivate = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
302 if (isPrivate || entry === "") {
303 return Promise.resolve();
305 let browserData = this._suggestionDataForBrowser(msg.target, true);
308 fieldname: browserData.controller.formHistoryParam,
311 handleCompletion: () => {},
312 handleError: err => {
313 Cu.reportError("Error adding form history entry: " + err);
316 return Promise.resolve();
319 _onMessageRemoveFormHistoryEntry: function (msg, entry) {
320 let browserData = this._suggestionDataForBrowser(msg.target);
321 if (browserData && browserData.previousFormHistoryResult) {
322 let { previousFormHistoryResult } = browserData;
323 for (let i = 0; i < previousFormHistoryResult.matchCount; i++) {
324 if (previousFormHistoryResult.getValueAt(i) == entry) {
325 previousFormHistoryResult.removeValueAt(i, true);
330 return Promise.resolve();
333 _onMessageSpeculativeConnect: function (msg, engineName) {
334 let engine = Services.search.getEngineByName(engineName);
336 throw new Error("Unknown engine name: " + engineName);
338 if (msg.target.contentWindow) {
339 engine.speculativeConnect({
340 window: msg.target.contentWindow,
345 _onObserve: Task.async(function* (data) {
346 if (data == "engine-current") {
347 let engine = yield this._currentEngineObj();
348 this._broadcast("CurrentEngine", engine);
350 else if (data != "engine-default") {
351 // engine-default is always sent with engine-current and isn't otherwise
352 // relevant to content searches.
353 let state = yield this._currentStateObj();
354 this._broadcast("CurrentState", state);
358 _suggestionDataForBrowser: function (browser, create=false) {
359 let data = this._suggestionMap.get(browser);
360 if (!data && create) {
361 // Since one SearchSuggestionController instance is meant to be used per
362 // autocomplete widget, this means that we assume each xul:browser has at
363 // most one such widget.
365 controller: new SearchSuggestionController(),
367 this._suggestionMap.set(browser, data);
372 _reply: function (msg, type, data) {
373 // We reply asyncly to messages, and by the time we reply the browser we're
374 // responding to may have been destroyed. messageManager is null then.
375 if (msg.target.messageManager) {
376 msg.target.messageManager.sendAsyncMessage(...this._msgArgs(type, data));
380 _broadcast: function (type, data) {
381 Cc["@mozilla.org/globalmessagemanager;1"].
382 getService(Ci.nsIMessageListenerManager).
383 broadcastAsyncMessage(...this._msgArgs(type, data));
386 _msgArgs: function (type, data) {
387 return [OUTBOUND_MESSAGE, {
393 _currentStateObj: Task.async(function* () {
396 currentEngine: yield this._currentEngineObj(),
398 for (let engine of Services.search.getVisibleEngines()) {
399 let uri = engine.getIconURLBySize(16, 16);
402 iconBuffer: yield this._arrayBufferFromDataURI(uri),
408 _currentEngineObj: Task.async(function* () {
409 let engine = Services.search.currentEngine;
410 let favicon = engine.getIconURLBySize(16, 16);
411 let uri1x = engine.getIconURLBySize(65, 26);
412 let uri2x = engine.getIconURLBySize(130, 52);
413 let placeholder = this._stringBundle.formatStringFromName(
414 "searchWithEngine", [engine.name], 1);
417 placeholder: placeholder,
418 iconBuffer: yield this._arrayBufferFromDataURI(favicon),
419 logoBuffer: yield this._arrayBufferFromDataURI(uri1x),
420 logo2xBuffer: yield this._arrayBufferFromDataURI(uri2x),
425 _arrayBufferFromDataURI: function (uri) {
427 return Promise.resolve(null);
429 let deferred = Promise.defer();
430 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
431 createInstance(Ci.nsIXMLHttpRequest);
432 xhr.open("GET", uri, true);
433 xhr.responseType = "arraybuffer";
434 xhr.onloadend = () => {
435 deferred.resolve(xhr.response);
438 // This throws if the URI is erroneously encoded.
442 return Promise.resolve(null);
444 return deferred.promise;
447 _ensureDataHasProperties: function (data, requiredProperties) {
448 for (let prop of requiredProperties) {
449 if (!(prop in data)) {
450 throw new Error("Message data missing required property: " + prop);
455 _initService: function () {
456 if (!this._initServicePromise) {
457 let deferred = Promise.defer();
458 this._initServicePromise = deferred.promise;
459 Services.search.init(() => deferred.resolve());
461 return this._initServicePromise;