|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 let Cc = Components.classes; |
|
7 let Ci = Components.interfaces; |
|
8 let Cu = Components.utils; |
|
9 |
|
10 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
11 Cu.import("resource://gre/modules/Services.jsm"); |
|
12 |
|
13 XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler", |
|
14 "resource:///modules/ContentLinkHandler.jsm"); |
|
15 XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector", |
|
16 "resource:///modules/translation/LanguageDetector.jsm"); |
|
17 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", |
|
18 "resource://gre/modules/LoginManagerContent.jsm"); |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils", |
|
20 "resource://gre/modules/InsecurePasswordUtils.jsm"); |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", |
|
22 "resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
23 XPCOMUtils.defineLazyModuleGetter(this, "UITour", |
|
24 "resource:///modules/UITour.jsm"); |
|
25 |
|
26 // Creates a new nsIURI object. |
|
27 function makeURI(uri, originCharset, baseURI) { |
|
28 return Services.io.newURI(uri, originCharset, baseURI); |
|
29 } |
|
30 |
|
31 addMessageListener("Browser:HideSessionRestoreButton", function (message) { |
|
32 // Hide session restore button on about:home |
|
33 let doc = content.document; |
|
34 let container; |
|
35 if (doc.documentURI.toLowerCase() == "about:home" && |
|
36 (container = doc.getElementById("sessionRestoreContainer"))){ |
|
37 container.hidden = true; |
|
38 } |
|
39 }); |
|
40 |
|
41 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { |
|
42 addEventListener("contextmenu", function (event) { |
|
43 sendAsyncMessage("contextmenu", {}, { event: event }); |
|
44 }, false); |
|
45 } else { |
|
46 addEventListener("DOMFormHasPassword", function(event) { |
|
47 InsecurePasswordUtils.checkForInsecurePasswords(event.target); |
|
48 LoginManagerContent.onFormPassword(event); |
|
49 }); |
|
50 addEventListener("DOMAutoComplete", function(event) { |
|
51 LoginManagerContent.onUsernameInput(event); |
|
52 }); |
|
53 addEventListener("blur", function(event) { |
|
54 LoginManagerContent.onUsernameInput(event); |
|
55 }); |
|
56 |
|
57 addEventListener("mozUITour", function(event) { |
|
58 if (!Services.prefs.getBoolPref("browser.uitour.enabled")) |
|
59 return; |
|
60 |
|
61 let handled = UITour.onPageEvent(event); |
|
62 if (handled) |
|
63 addEventListener("pagehide", UITour); |
|
64 }, false, true); |
|
65 } |
|
66 |
|
67 let AboutHomeListener = { |
|
68 init: function(chromeGlobal) { |
|
69 chromeGlobal.addEventListener('AboutHomeLoad', () => this.onPageLoad(), false, true); |
|
70 }, |
|
71 |
|
72 handleEvent: function(aEvent) { |
|
73 switch (aEvent.type) { |
|
74 case "AboutHomeLoad": |
|
75 this.onPageLoad(); |
|
76 break; |
|
77 } |
|
78 }, |
|
79 |
|
80 receiveMessage: function(aMessage) { |
|
81 switch (aMessage.name) { |
|
82 case "AboutHome:Update": |
|
83 this.onUpdate(aMessage.data); |
|
84 break; |
|
85 } |
|
86 }, |
|
87 |
|
88 onUpdate: function(aData) { |
|
89 let doc = content.document; |
|
90 if (doc.documentURI.toLowerCase() != "about:home") |
|
91 return; |
|
92 |
|
93 if (aData.showRestoreLastSession && !PrivateBrowsingUtils.isWindowPrivate(content)) |
|
94 doc.getElementById("launcher").setAttribute("session", "true"); |
|
95 |
|
96 // Inject search engine and snippets URL. |
|
97 let docElt = doc.documentElement; |
|
98 // set the following attributes BEFORE searchEngineName, which triggers to |
|
99 // show the snippets when it's set. |
|
100 docElt.setAttribute("snippetsURL", aData.snippetsURL); |
|
101 if (aData.showKnowYourRights) |
|
102 docElt.setAttribute("showKnowYourRights", "true"); |
|
103 docElt.setAttribute("snippetsVersion", aData.snippetsVersion); |
|
104 docElt.setAttribute("searchEngineName", aData.defaultEngineName); |
|
105 }, |
|
106 |
|
107 onPageLoad: function() { |
|
108 let doc = content.document; |
|
109 if (doc.documentURI.toLowerCase() != "about:home" || |
|
110 doc.documentElement.hasAttribute("hasBrowserHandlers")) { |
|
111 return; |
|
112 } |
|
113 |
|
114 doc.documentElement.setAttribute("hasBrowserHandlers", "true"); |
|
115 let self = this; |
|
116 addMessageListener("AboutHome:Update", self); |
|
117 addEventListener("click", this.onClick, true); |
|
118 addEventListener("pagehide", function onPageHide(event) { |
|
119 if (event.target.defaultView.frameElement) |
|
120 return; |
|
121 removeMessageListener("AboutHome:Update", self); |
|
122 removeEventListener("click", self.onClick, true); |
|
123 removeEventListener("pagehide", onPageHide, true); |
|
124 if (event.target.documentElement) |
|
125 event.target.documentElement.removeAttribute("hasBrowserHandlers"); |
|
126 }, true); |
|
127 |
|
128 // XXX bug 738646 - when Marketplace is launched, remove this statement and |
|
129 // the hidden attribute set on the apps button in aboutHome.xhtml |
|
130 if (Services.prefs.getPrefType("browser.aboutHome.apps") == Services.prefs.PREF_BOOL && |
|
131 Services.prefs.getBoolPref("browser.aboutHome.apps")) |
|
132 doc.getElementById("apps").removeAttribute("hidden"); |
|
133 |
|
134 sendAsyncMessage("AboutHome:RequestUpdate"); |
|
135 |
|
136 doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) { |
|
137 sendAsyncMessage("AboutHome:Search", { searchData: e.detail }); |
|
138 }, true, true); |
|
139 }, |
|
140 |
|
141 onClick: function(aEvent) { |
|
142 if (!aEvent.isTrusted || // Don't trust synthetic events |
|
143 aEvent.button == 2 || aEvent.target.localName != "button") { |
|
144 return; |
|
145 } |
|
146 |
|
147 let originalTarget = aEvent.originalTarget; |
|
148 let ownerDoc = originalTarget.ownerDocument; |
|
149 if (ownerDoc.documentURI != "about:home") { |
|
150 // This shouldn't happen, but we're being defensive. |
|
151 return; |
|
152 } |
|
153 |
|
154 let elmId = originalTarget.getAttribute("id"); |
|
155 |
|
156 switch (elmId) { |
|
157 case "restorePreviousSession": |
|
158 sendAsyncMessage("AboutHome:RestorePreviousSession"); |
|
159 ownerDoc.getElementById("launcher").removeAttribute("session"); |
|
160 break; |
|
161 |
|
162 case "downloads": |
|
163 sendAsyncMessage("AboutHome:Downloads"); |
|
164 break; |
|
165 |
|
166 case "bookmarks": |
|
167 sendAsyncMessage("AboutHome:Bookmarks"); |
|
168 break; |
|
169 |
|
170 case "history": |
|
171 sendAsyncMessage("AboutHome:History"); |
|
172 break; |
|
173 |
|
174 case "apps": |
|
175 sendAsyncMessage("AboutHome:Apps"); |
|
176 break; |
|
177 |
|
178 case "addons": |
|
179 sendAsyncMessage("AboutHome:Addons"); |
|
180 break; |
|
181 |
|
182 case "sync": |
|
183 sendAsyncMessage("AboutHome:Sync"); |
|
184 break; |
|
185 |
|
186 case "settings": |
|
187 sendAsyncMessage("AboutHome:Settings"); |
|
188 break; |
|
189 } |
|
190 }, |
|
191 }; |
|
192 AboutHomeListener.init(this); |
|
193 |
|
194 |
|
195 let ContentSearchMediator = { |
|
196 |
|
197 whitelist: new Set([ |
|
198 "about:newtab", |
|
199 ]), |
|
200 |
|
201 init: function (chromeGlobal) { |
|
202 chromeGlobal.addEventListener("ContentSearchClient", this, true, true); |
|
203 addMessageListener("ContentSearch", this); |
|
204 }, |
|
205 |
|
206 handleEvent: function (event) { |
|
207 if (this._contentWhitelisted) { |
|
208 this._sendMsg(event.detail.type, event.detail.data); |
|
209 } |
|
210 }, |
|
211 |
|
212 receiveMessage: function (msg) { |
|
213 if (msg.data.type == "AddToWhitelist") { |
|
214 for (let uri of msg.data.data) { |
|
215 this.whitelist.add(uri); |
|
216 } |
|
217 this._sendMsg("AddToWhitelistAck"); |
|
218 return; |
|
219 } |
|
220 if (this._contentWhitelisted) { |
|
221 this._fireEvent(msg.data.type, msg.data.data); |
|
222 } |
|
223 }, |
|
224 |
|
225 get _contentWhitelisted() { |
|
226 return this.whitelist.has(content.document.documentURI.toLowerCase()); |
|
227 }, |
|
228 |
|
229 _sendMsg: function (type, data=null) { |
|
230 sendAsyncMessage("ContentSearch", { |
|
231 type: type, |
|
232 data: data, |
|
233 }); |
|
234 }, |
|
235 |
|
236 _fireEvent: function (type, data=null) { |
|
237 content.dispatchEvent(new content.CustomEvent("ContentSearchService", { |
|
238 detail: { |
|
239 type: type, |
|
240 data: data, |
|
241 }, |
|
242 })); |
|
243 }, |
|
244 }; |
|
245 ContentSearchMediator.init(this); |
|
246 |
|
247 |
|
248 var global = this; |
|
249 |
|
250 // Lazily load the finder code |
|
251 addMessageListener("Finder:Initialize", function () { |
|
252 let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {}); |
|
253 new RemoteFinderListener(global); |
|
254 }); |
|
255 |
|
256 |
|
257 let ClickEventHandler = { |
|
258 init: function init() { |
|
259 Cc["@mozilla.org/eventlistenerservice;1"] |
|
260 .getService(Ci.nsIEventListenerService) |
|
261 .addSystemEventListener(global, "click", this, true); |
|
262 }, |
|
263 |
|
264 handleEvent: function(event) { |
|
265 // Bug 903016: Most of this code is an unfortunate duplication from |
|
266 // contentAreaClick in browser.js. |
|
267 if (!event.isTrusted || event.defaultPrevented || event.button == 2) |
|
268 return; |
|
269 |
|
270 let [href, node] = this._hrefAndLinkNodeForClickEvent(event); |
|
271 |
|
272 let json = { button: event.button, shiftKey: event.shiftKey, |
|
273 ctrlKey: event.ctrlKey, metaKey: event.metaKey, |
|
274 altKey: event.altKey, href: null, title: null, |
|
275 bookmark: false }; |
|
276 |
|
277 if (href) { |
|
278 json.href = href; |
|
279 if (node) { |
|
280 json.title = node.getAttribute("title"); |
|
281 |
|
282 if (event.button == 0 && !event.ctrlKey && !event.shiftKey && |
|
283 !event.altKey && !event.metaKey) { |
|
284 json.bookmark = node.getAttribute("rel") == "sidebar"; |
|
285 if (json.bookmark) |
|
286 event.preventDefault(); // Need to prevent the pageload. |
|
287 } |
|
288 } |
|
289 |
|
290 sendAsyncMessage("Content:Click", json); |
|
291 return; |
|
292 } |
|
293 |
|
294 // This might be middle mouse navigation. |
|
295 if (event.button == 1) |
|
296 sendAsyncMessage("Content:Click", json); |
|
297 }, |
|
298 |
|
299 /** |
|
300 * Extracts linkNode and href for the current click target. |
|
301 * |
|
302 * @param event |
|
303 * The click event. |
|
304 * @return [href, linkNode]. |
|
305 * |
|
306 * @note linkNode will be null if the click wasn't on an anchor |
|
307 * element (or XLink). |
|
308 */ |
|
309 _hrefAndLinkNodeForClickEvent: function(event) { |
|
310 function isHTMLLink(aNode) { |
|
311 // Be consistent with what nsContextMenu.js does. |
|
312 return ((aNode instanceof content.HTMLAnchorElement && aNode.href) || |
|
313 (aNode instanceof content.HTMLAreaElement && aNode.href) || |
|
314 aNode instanceof content.HTMLLinkElement); |
|
315 } |
|
316 |
|
317 let node = event.target; |
|
318 while (node && !isHTMLLink(node)) { |
|
319 node = node.parentNode; |
|
320 } |
|
321 |
|
322 if (node) |
|
323 return [node.href, node]; |
|
324 |
|
325 // If there is no linkNode, try simple XLink. |
|
326 let href, baseURI; |
|
327 node = event.target; |
|
328 while (node && !href) { |
|
329 if (node.nodeType == content.Node.ELEMENT_NODE) { |
|
330 href = node.getAttributeNS("http://www.w3.org/1999/xlink", "href"); |
|
331 if (href) |
|
332 baseURI = node.ownerDocument.baseURIObject; |
|
333 } |
|
334 node = node.parentNode; |
|
335 } |
|
336 |
|
337 // In case of XLink, we don't return the node we got href from since |
|
338 // callers expect <a>-like elements. |
|
339 // Note: makeURI() will throw if aUri is not a valid URI. |
|
340 return [href ? makeURI(href, null, baseURI).spec : null, null]; |
|
341 } |
|
342 }; |
|
343 ClickEventHandler.init(); |
|
344 |
|
345 ContentLinkHandler.init(this); |
|
346 |
|
347 addEventListener("DOMWebNotificationClicked", function(event) { |
|
348 sendAsyncMessage("DOMWebNotificationClicked", {}); |
|
349 }, false); |
|
350 |
|
351 let PageStyleHandler = { |
|
352 init: function() { |
|
353 addMessageListener("PageStyle:Switch", this); |
|
354 addMessageListener("PageStyle:Disable", this); |
|
355 |
|
356 // Send a CPOW to the parent so that it can synchronously request |
|
357 // the list of style sheets. |
|
358 sendSyncMessage("PageStyle:SetSyncHandler", {}, {syncHandler: this}); |
|
359 }, |
|
360 |
|
361 get markupDocumentViewer() { |
|
362 return docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); |
|
363 }, |
|
364 |
|
365 // Called synchronously via CPOW from the parent. |
|
366 getStyleSheetInfo: function() { |
|
367 let styleSheets = this._filterStyleSheets(this.getAllStyleSheets()); |
|
368 return { |
|
369 styleSheets: styleSheets, |
|
370 authorStyleDisabled: this.markupDocumentViewer.authorStyleDisabled, |
|
371 preferredStyleSheetSet: content.document.preferredStyleSheetSet |
|
372 }; |
|
373 }, |
|
374 |
|
375 // Called synchronously via CPOW from the parent. |
|
376 getAllStyleSheets: function(frameset = content) { |
|
377 let selfSheets = Array.slice(frameset.document.styleSheets); |
|
378 let subSheets = Array.map(frameset.frames, frame => this.getAllStyleSheets(frame)); |
|
379 return selfSheets.concat(...subSheets); |
|
380 }, |
|
381 |
|
382 receiveMessage: function(msg) { |
|
383 switch (msg.name) { |
|
384 case "PageStyle:Switch": |
|
385 this.markupDocumentViewer.authorStyleDisabled = false; |
|
386 this._stylesheetSwitchAll(content, msg.data.title); |
|
387 break; |
|
388 |
|
389 case "PageStyle:Disable": |
|
390 this.markupDocumentViewer.authorStyleDisabled = true; |
|
391 break; |
|
392 } |
|
393 }, |
|
394 |
|
395 _stylesheetSwitchAll: function (frameset, title) { |
|
396 if (!title || this._stylesheetInFrame(frameset, title)) { |
|
397 this._stylesheetSwitchFrame(frameset, title); |
|
398 } |
|
399 |
|
400 for (let i = 0; i < frameset.frames.length; i++) { |
|
401 // Recurse into sub-frames. |
|
402 this._stylesheetSwitchAll(frameset.frames[i], title); |
|
403 } |
|
404 }, |
|
405 |
|
406 _stylesheetSwitchFrame: function (frame, title) { |
|
407 var docStyleSheets = frame.document.styleSheets; |
|
408 |
|
409 for (let i = 0; i < docStyleSheets.length; ++i) { |
|
410 let docStyleSheet = docStyleSheets[i]; |
|
411 if (docStyleSheet.title) { |
|
412 docStyleSheet.disabled = (docStyleSheet.title != title); |
|
413 } else if (docStyleSheet.disabled) { |
|
414 docStyleSheet.disabled = false; |
|
415 } |
|
416 } |
|
417 }, |
|
418 |
|
419 _stylesheetInFrame: function (frame, title) { |
|
420 return Array.some(frame.document.styleSheets, (styleSheet) => styleSheet.title == title); |
|
421 }, |
|
422 |
|
423 _filterStyleSheets: function(styleSheets) { |
|
424 let result = []; |
|
425 |
|
426 for (let currentStyleSheet of styleSheets) { |
|
427 if (!currentStyleSheet.title) |
|
428 continue; |
|
429 |
|
430 // Skip any stylesheets that don't match the screen media type. |
|
431 if (currentStyleSheet.media.length > 0) { |
|
432 let mediaQueryList = currentStyleSheet.media.mediaText; |
|
433 if (!content.matchMedia(mediaQueryList).matches) { |
|
434 continue; |
|
435 } |
|
436 } |
|
437 |
|
438 result.push({title: currentStyleSheet.title, |
|
439 disabled: currentStyleSheet.disabled}); |
|
440 } |
|
441 |
|
442 return result; |
|
443 }, |
|
444 }; |
|
445 PageStyleHandler.init(); |
|
446 |
|
447 let TranslationHandler = { |
|
448 init: function() { |
|
449 let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) |
|
450 .getInterface(Ci.nsIWebProgress); |
|
451 webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); |
|
452 }, |
|
453 |
|
454 /* nsIWebProgressListener implementation */ |
|
455 onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { |
|
456 if (!aWebProgress.isTopLevel || |
|
457 !(aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)) |
|
458 return; |
|
459 |
|
460 let url = aRequest.name; |
|
461 if (!url.startsWith("http://") && !url.startsWith("https://")) |
|
462 return; |
|
463 |
|
464 // Grab a 60k sample of text from the page. |
|
465 let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"] |
|
466 .createInstance(Ci.nsIDocumentEncoder); |
|
467 encoder.init(content.document, "text/plain", encoder.SkipInvisibleContent); |
|
468 let string = encoder.encodeToStringWithMaxLength(60 * 1024); |
|
469 |
|
470 // Language detection isn't reliable on very short strings. |
|
471 if (string.length < 100) |
|
472 return; |
|
473 |
|
474 LanguageDetector.detectLanguage(string).then(result => { |
|
475 if (result.confident) |
|
476 sendAsyncMessage("LanguageDetection:Result", result.language); |
|
477 }); |
|
478 }, |
|
479 |
|
480 // Unused methods. |
|
481 onProgressChange: function() {}, |
|
482 onLocationChange: function() {}, |
|
483 onStatusChange: function() {}, |
|
484 onSecurityChange: function() {}, |
|
485 |
|
486 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, |
|
487 Ci.nsISupportsWeakReference]) |
|
488 }; |
|
489 |
|
490 if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) |
|
491 TranslationHandler.init(); |