| |
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(); |