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(),
91 Cc["@mozilla.org/globalmessagemanager;1"].
92 getService(Ci.nsIMessageListenerManager).
93 addMessageListener(INBOUND_MESSAGE, this);
94 Services.obs.addObserver(this, "browser-search-engine-modified", false);
97 destroy: function () {
98 Cc["@mozilla.org/globalmessagemanager;1"].
99 getService(Ci.nsIMessageListenerManager).
100 removeMessageListener(INBOUND_MESSAGE, this);
101 Services.obs.removeObserver(this, "browser-search-engine-modified");
103 this._eventQueue.length = 0;
104 return Promise.resolve(this._currentEventPromise);
108 * Focuses the search input in the page with the given message manager.
109 * @param messageManager
110 * The MessageManager object of the selected browser.
112 focusInput: function (messageManager) {
113 messageManager.sendAsyncMessage(OUTBOUND_MESSAGE, {
118 receiveMessage: function (msg) {
119 // Add a temporary event handler that exists only while the message is in
120 // the event queue. If the message's source docshell changes browsers in
121 // the meantime, then we need to update msg.target. event.detail will be
122 // the docshell's new parent <xul:browser> element.
123 msg.handleEvent = event => {
124 let browserData = this._suggestionMap.get(msg.target);
126 this._suggestionMap.delete(msg.target);
127 this._suggestionMap.set(event.detail, browserData);
129 msg.target.removeEventListener("SwapDocShells", msg, true);
130 msg.target = event.detail;
131 msg.target.addEventListener("SwapDocShells", msg, true);
133 msg.target.addEventListener("SwapDocShells", msg, true);
135 this._eventQueue.push({
139 this._processEventQueue();
142 observe: function (subj, topic, data) {
144 case "browser-search-engine-modified":
145 this._eventQueue.push({
149 this._processEventQueue();
154 _processEventQueue: function () {
155 if (this._currentEventPromise || !this._eventQueue.length) {
159 let event = this._eventQueue.shift();
161 return this._currentEventPromise = Task.spawn(function* () {
163 yield this["_on" + event.type](event.data);
167 this._currentEventPromise = null;
168 this._processEventQueue();
173 _onMessage: Task.async(function* (msg) {
174 let methodName = "_onMessage" + msg.data.type;
175 if (methodName in this) {
176 yield this._initService();
177 yield this[methodName](msg, msg.data.data);
178 msg.target.removeEventListener("SwapDocShells", msg, true);
182 _onMessageGetState: function (msg, data) {
183 return this._currentStateObj().then(state => {
184 this._reply(msg, "State", state);
188 _onMessageSearch: function (msg, data) {
189 this._ensureDataHasProperties(data, [
194 let browserWin = msg.target.ownerDocument.defaultView;
195 let engine = Services.search.getEngineByName(data.engineName);
196 browserWin.BrowserSearch.recordSearchInHealthReport(engine, data.whence);
197 let submission = engine.getSubmission(data.searchString, "", data.whence);
198 browserWin.loadURI(submission.uri.spec, null, submission.postData);
199 return Promise.resolve();
202 _onMessageSetCurrentEngine: function (msg, data) {
203 Services.search.currentEngine = Services.search.getEngineByName(data);
204 return Promise.resolve();
207 _onMessageManageEngines: function (msg, data) {
208 let browserWin = msg.target.ownerDocument.defaultView;
210 if (Services.prefs.getBoolPref("browser.search.showOneOffButtons")) {
211 browserWin.openPreferences("paneSearch");
212 return Promise.resolve();
215 let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
216 getService(Components.interfaces.nsIWindowMediator);
217 let window = wm.getMostRecentWindow("Browser:SearchManager");
223 browserWin.setTimeout(function () {
224 browserWin.openDialog("chrome://browser/content/search/engineManager.xul",
225 "_blank", "chrome,dialog,modal,centerscreen,resizable");
228 return Promise.resolve();
231 _onMessageGetSuggestions: Task.async(function* (msg, data) {
232 this._ensureDataHasProperties(data, [
237 let engine = Services.search.getEngineByName(data.engineName);
239 throw new Error("Unknown engine name: " + data.engineName);
242 let browserData = this._suggestionDataForBrowser(msg.target, true);
243 let { controller } = browserData;
244 let ok = SearchSuggestionController.engineOffersSuggestions(engine);
245 controller.maxLocalResults = ok ? 2 : 6;
246 controller.maxRemoteResults = ok ? 6 : 0;
247 controller.remoteTimeout = data.remoteTimeout || undefined;
248 let priv = PrivateBrowsingUtils.isWindowPrivate(msg.target.contentWindow);
249 // fetch() rejects its promise if there's a pending request, but since we
250 // process our event queue serially, there's never a pending request.
251 let suggestions = yield controller.fetch(data.searchString, priv, engine);
253 // Keep the form history result so RemoveFormHistoryEntry can remove entries
254 // from it. Keeping only one result isn't foolproof because the client may
255 // try to remove an entry from one set of suggestions after it has requested
256 // more but before it's received them. In that case, the entry may not
257 // appear in the new suggestions. But that should happen rarely.
258 browserData.previousFormHistoryResult = suggestions.formHistoryResult;
260 this._reply(msg, "Suggestions", {
261 engineName: data.engineName,
262 searchString: suggestions.term,
263 formHistory: suggestions.local,
264 remote: suggestions.remote,
268 _onMessageAddFormHistoryEntry: function (msg, entry) {
269 let isPrivate = true;
271 // isBrowserPrivate assumes that the passed-in browser has all the normal
272 // properties, which won't be true if the browser has been destroyed.
273 // That may be the case here due to the asynchronous nature of messaging.
274 isPrivate = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
276 if (isPrivate || entry === "") {
277 return Promise.resolve();
279 let browserData = this._suggestionDataForBrowser(msg.target, true);
282 fieldname: browserData.controller.formHistoryParam,
285 handleCompletion: () => {},
286 handleError: err => {
287 Cu.reportError("Error adding form history entry: " + err);
290 return Promise.resolve();
293 _onMessageRemoveFormHistoryEntry: function (msg, entry) {
294 let browserData = this._suggestionDataForBrowser(msg.target);
295 if (browserData && browserData.previousFormHistoryResult) {
296 let { previousFormHistoryResult } = browserData;
297 for (let i = 0; i < previousFormHistoryResult.matchCount; i++) {
298 if (previousFormHistoryResult.getValueAt(i) == entry) {
299 previousFormHistoryResult.removeValueAt(i, true);
304 return Promise.resolve();
307 _onMessageSpeculativeConnect: function (msg, engineName) {
308 let engine = Services.search.getEngineByName(engineName);
310 throw new Error("Unknown engine name: " + engineName);
312 if (msg.target.contentWindow) {
313 engine.speculativeConnect({
314 window: msg.target.contentWindow,
319 _onObserve: Task.async(function* (data) {
320 if (data == "engine-current") {
321 let engine = yield this._currentEngineObj();
322 this._broadcast("CurrentEngine", engine);
324 else if (data != "engine-default") {
325 // engine-default is always sent with engine-current and isn't otherwise
326 // relevant to content searches.
327 let state = yield this._currentStateObj();
328 this._broadcast("CurrentState", state);
332 _suggestionDataForBrowser: function (browser, create=false) {
333 let data = this._suggestionMap.get(browser);
334 if (!data && create) {
335 // Since one SearchSuggestionController instance is meant to be used per
336 // autocomplete widget, this means that we assume each xul:browser has at
337 // most one such widget.
339 controller: new SearchSuggestionController(),
341 this._suggestionMap.set(browser, data);
346 _reply: function (msg, type, data) {
347 // We reply asyncly to messages, and by the time we reply the browser we're
348 // responding to may have been destroyed. messageManager is null then.
349 if (msg.target.messageManager) {
350 msg.target.messageManager.sendAsyncMessage(...this._msgArgs(type, data));
354 _broadcast: function (type, data) {
355 Cc["@mozilla.org/globalmessagemanager;1"].
356 getService(Ci.nsIMessageListenerManager).
357 broadcastAsyncMessage(...this._msgArgs(type, data));
360 _msgArgs: function (type, data) {
361 return [OUTBOUND_MESSAGE, {
367 _currentStateObj: Task.async(function* () {
370 currentEngine: yield this._currentEngineObj(),
372 for (let engine of Services.search.getVisibleEngines()) {
373 let uri = engine.getIconURLBySize(16, 16);
376 iconBuffer: yield this._arrayBufferFromDataURI(uri),
382 _currentEngineObj: Task.async(function* () {
383 let engine = Services.search.currentEngine;
384 let uri1x = engine.getIconURLBySize(65, 26);
385 let uri2x = engine.getIconURLBySize(130, 52);
388 logoBuffer: yield this._arrayBufferFromDataURI(uri1x),
389 logo2xBuffer: yield this._arrayBufferFromDataURI(uri2x),
394 _arrayBufferFromDataURI: function (uri) {
396 return Promise.resolve(null);
398 let deferred = Promise.defer();
399 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
400 createInstance(Ci.nsIXMLHttpRequest);
401 xhr.open("GET", uri, true);
402 xhr.responseType = "arraybuffer";
403 xhr.onloadend = () => {
404 deferred.resolve(xhr.response);
407 // This throws if the URI is erroneously encoded.
411 return Promise.resolve(null);
413 return deferred.promise;
416 _ensureDataHasProperties: function (data, requiredProperties) {
417 for (let prop of requiredProperties) {
418 if (!(prop in data)) {
419 throw new Error("Message data missing required property: " + prop);
424 _initService: function () {
425 if (!this._initServicePromise) {
426 let deferred = Promise.defer();
427 this._initServicePromise = deferred.promise;
428 Services.search.init(() => deferred.resolve());
430 return this._initServicePromise;