Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
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/. */
6 // This stays here because otherwise it's hard to tell if there's a parsing error
7 dump("### Content.js loaded\n");
9 let Cc = Components.classes;
10 let Ci = Components.interfaces;
11 let Cu = Components.utils;
12 let Cr = Components.results;
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
16 XPCOMUtils.defineLazyGetter(this, "Services", function() {
17 Cu.import("resource://gre/modules/Services.jsm");
18 return Services;
19 });
21 XPCOMUtils.defineLazyGetter(this, "Rect", function() {
22 Cu.import("resource://gre/modules/Geometry.jsm");
23 return Rect;
24 });
26 XPCOMUtils.defineLazyGetter(this, "Point", function() {
27 Cu.import("resource://gre/modules/Geometry.jsm");
28 return Point;
29 });
31 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
32 "resource://gre/modules/LoginManagerContent.jsm");
34 XPCOMUtils.defineLazyServiceGetter(this, "gFocusManager",
35 "@mozilla.org/focus-manager;1", "nsIFocusManager");
37 XPCOMUtils.defineLazyServiceGetter(this, "gDOMUtils",
38 "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
40 this.XULDocument = Ci.nsIDOMXULDocument;
41 this.HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
42 this.HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
43 this.HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
44 this.HTMLFrameSetElement = Ci.nsIDOMHTMLFrameSetElement;
45 this.HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
46 this.HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
48 const kReferenceDpi = 240; // standard "pixel" size used in some preferences
50 const kStateActive = 0x00000001; // :active pseudoclass for elements
52 const kZoomToElementMargin = 16; // in px
54 /*
55 * getBoundingContentRect
56 *
57 * @param aElement
58 * @return Bounding content rect adjusted for scroll and frame offsets.
59 */
60 function getBoundingContentRect(aElement) {
61 if (!aElement)
62 return new Rect(0, 0, 0, 0);
64 let document = aElement.ownerDocument;
65 while(document.defaultView.frameElement)
66 document = document.defaultView.frameElement.ownerDocument;
68 let offset = ContentScroll.getScrollOffset(content);
69 offset = new Point(offset.x, offset.y);
71 let r = aElement.getBoundingClientRect();
73 // step out of iframes and frames, offsetting scroll values
74 let view = aElement.ownerDocument.defaultView;
75 for (let frame = view; frame != content; frame = frame.parent) {
76 // adjust client coordinates' origin to be top left of iframe viewport
77 let rect = frame.frameElement.getBoundingClientRect();
78 let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
79 let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
80 offset.add(rect.left + parseInt(left), rect.top + parseInt(top));
81 }
83 return new Rect(r.left + offset.x, r.top + offset.y, r.width, r.height);
84 }
85 this.getBoundingContentRect = getBoundingContentRect;
87 /*
88 * getOverflowContentBoundingRect
89 *
90 * @param aElement
91 * @return Bounding content rect adjusted for scroll and frame offsets.
92 */
94 function getOverflowContentBoundingRect(aElement) {
95 let r = getBoundingContentRect(aElement);
97 // If the overflow is hidden don't bother calculating it
98 let computedStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement);
99 let blockDisplays = ["block", "inline-block", "list-item"];
100 if ((blockDisplays.indexOf(computedStyle.getPropertyValue("display")) != -1 &&
101 computedStyle.getPropertyValue("overflow") == "hidden") ||
102 aElement instanceof HTMLSelectElement) {
103 return r;
104 }
106 for (let i = 0; i < aElement.childElementCount; i++) {
107 r = r.union(getBoundingContentRect(aElement.children[i]));
108 }
110 return r;
111 }
112 this.getOverflowContentBoundingRect = getOverflowContentBoundingRect;
114 /*
115 * Content
116 *
117 * Browser event receiver for content.
118 */
119 let Content = {
120 _debugEvents: false,
122 get formAssistant() {
123 delete this.formAssistant;
124 return this.formAssistant = new FormAssistant();
125 },
127 init: function init() {
128 // Asyncronous messages sent from the browser
129 addMessageListener("Browser:Blur", this);
130 addMessageListener("Browser:SaveAs", this);
131 addMessageListener("Browser:MozApplicationCache:Fetch", this);
132 addMessageListener("Browser:SetCharset", this);
133 addMessageListener("Browser:CanUnload", this);
134 addMessageListener("Browser:PanBegin", this);
135 addMessageListener("Gesture:SingleTap", this);
136 addMessageListener("Gesture:DoubleTap", this);
138 addEventListener("touchstart", this, false);
139 addEventListener("click", this, true);
140 addEventListener("keydown", this);
141 addEventListener("keyup", this);
143 // Synchronous events caught during the bubbling phase
144 addEventListener("MozApplicationManifest", this, false);
145 addEventListener("DOMContentLoaded", this, false);
146 addEventListener("DOMAutoComplete", this, false);
147 addEventListener("DOMFormHasPassword", this, false);
148 addEventListener("blur", this, false);
149 // Attach a listener to watch for "click" events bubbling up from error
150 // pages and other similar page. This lets us fix bugs like 401575 which
151 // require error page UI to do privileged things, without letting error
152 // pages have any privilege themselves.
153 addEventListener("click", this, false);
155 docShell.useGlobalHistory = true;
156 },
158 /*******************************************
159 * Events
160 */
162 handleEvent: function handleEvent(aEvent) {
163 if (this._debugEvents) Util.dumpLn("Content:", aEvent.type);
164 switch (aEvent.type) {
165 case "MozApplicationManifest": {
166 let doc = aEvent.originalTarget;
167 sendAsyncMessage("Browser:MozApplicationManifest", {
168 location: doc.documentURIObject.spec,
169 manifest: doc.documentElement.getAttribute("manifest"),
170 charset: doc.characterSet
171 });
172 break;
173 }
175 case "keyup":
176 // If after a key is pressed we still have no input, then close
177 // the autocomplete. Perhaps the user used backspace or delete.
178 // Allow down arrow to trigger autofill popup on empty input.
179 if ((!aEvent.target.value && aEvent.keyCode != aEvent.DOM_VK_DOWN)
180 || aEvent.keyCode == aEvent.DOM_VK_ESCAPE)
181 this.formAssistant.close();
182 else
183 this.formAssistant.open(aEvent.target, aEvent);
184 break;
186 case "click":
187 // Workaround for bug 925457: we sometimes don't recognize the
188 // correct tap target or are unable to identify if it's editable.
189 // Instead always save tap co-ordinates for the keyboard to look for
190 // when it is up.
191 SelectionHandler.onClickCoords(aEvent.clientX, aEvent.clientY);
193 if (aEvent.eventPhase == aEvent.BUBBLING_PHASE)
194 this._onClickBubble(aEvent);
195 else
196 this._onClickCapture(aEvent);
197 break;
199 case "DOMFormHasPassword":
200 LoginManagerContent.onFormPassword(aEvent);
201 break;
203 case "DOMContentLoaded":
204 this._maybeNotifyErrorPage();
205 break;
207 case "DOMAutoComplete":
208 case "blur":
209 LoginManagerContent.onUsernameInput(aEvent);
210 break;
212 case "touchstart":
213 this._onTouchStart(aEvent);
214 break;
215 }
216 },
218 receiveMessage: function receiveMessage(aMessage) {
219 if (this._debugEvents) Util.dumpLn("Content:", aMessage.name);
220 let json = aMessage.json;
221 let x = json.x;
222 let y = json.y;
224 switch (aMessage.name) {
225 case "Browser:Blur":
226 gFocusManager.clearFocus(content);
227 break;
229 case "Browser:CanUnload":
230 let canUnload = docShell.contentViewer.permitUnload();
231 sendSyncMessage("Browser:CanUnload:Return", { permit: canUnload });
232 break;
234 case "Browser:SaveAs":
235 break;
237 case "Browser:MozApplicationCache:Fetch": {
238 let currentURI = Services.io.newURI(json.location, json.charset, null);
239 let manifestURI = Services.io.newURI(json.manifest, json.charset, currentURI);
240 let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]
241 .getService(Ci.nsIOfflineCacheUpdateService);
242 updateService.scheduleUpdate(manifestURI, currentURI, content);
243 break;
244 }
246 case "Browser:SetCharset": {
247 docShell.gatherCharsetMenuTelemetry();
248 docShell.charset = json.charset;
250 let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
251 webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
252 break;
253 }
255 case "Browser:PanBegin":
256 this._cancelTapHighlight();
257 break;
259 case "Gesture:SingleTap":
260 this._onSingleTap(json.x, json.y, json.modifiers);
261 break;
263 case "Gesture:DoubleTap":
264 this._onDoubleTap(json.x, json.y);
265 break;
266 }
267 },
269 /******************************************************
270 * Event handlers
271 */
273 _onTouchStart: function _onTouchStart(aEvent) {
274 let element = aEvent.target;
276 // There is no need to have a feedback for disabled element
277 let isDisabled = element instanceof HTMLOptionElement ?
278 (element.disabled || element.parentNode.disabled) : element.disabled;
279 if (isDisabled)
280 return;
282 // Set the target element to active
283 this._doTapHighlight(element);
284 },
286 _onClickCapture: function _onClickCapture(aEvent) {
287 let element = aEvent.target;
289 ContextMenuHandler.reset();
291 // Only show autocomplete after the item is clicked
292 if (!this.lastClickElement || this.lastClickElement != element) {
293 this.lastClickElement = element;
294 if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE &&
295 !(element instanceof HTMLSelectElement)) {
296 return;
297 }
298 }
300 this.formAssistant.focusSync = true;
301 this.formAssistant.open(element, aEvent);
302 this._cancelTapHighlight();
303 this.formAssistant.focusSync = false;
305 // A tap on a form input triggers touch input caret selection
306 if (Util.isEditable(element) &&
307 aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
308 let { offsetX, offsetY } = Util.translateToTopLevelWindow(element);
309 sendAsyncMessage("Content:SelectionCaret", {
310 xPos: aEvent.clientX + offsetX,
311 yPos: aEvent.clientY + offsetY
312 });
313 } else {
314 SelectionHandler.closeSelection();
315 }
316 },
318 // Checks clicks we care about - events bubbling up from about pages.
319 _onClickBubble: function _onClickBubble(aEvent) {
320 // Don't trust synthetic events
321 if (!aEvent.isTrusted)
322 return;
324 let ot = aEvent.originalTarget;
325 let errorDoc = ot.ownerDocument;
326 if (!errorDoc)
327 return;
329 // If the event came from an ssl error page, it is probably either
330 // "Add Exception…" or "Get me out of here!" button.
331 if (/^about:certerror\?e=nssBadCert/.test(errorDoc.documentURI)) {
332 let perm = errorDoc.getElementById("permanentExceptionButton");
333 let temp = errorDoc.getElementById("temporaryExceptionButton");
334 if (ot == temp || ot == perm) {
335 let action = (ot == perm ? "permanent" : "temporary");
336 sendAsyncMessage("Browser:CertException",
337 { url: errorDoc.location.href, action: action });
338 } else if (ot == errorDoc.getElementById("getMeOutOfHereButton")) {
339 sendAsyncMessage("Browser:CertException",
340 { url: errorDoc.location.href, action: "leave" });
341 }
342 } else if (/^about:blocked/.test(errorDoc.documentURI)) {
343 // The event came from a button on a malware/phishing block page
344 // First check whether it's malware or phishing, so that we can
345 // use the right strings/links.
346 let isMalware = /e=malwareBlocked/.test(errorDoc.documentURI);
348 if (ot == errorDoc.getElementById("getMeOutButton")) {
349 sendAsyncMessage("Browser:BlockedSite",
350 { url: errorDoc.location.href, action: "leave" });
351 } else if (ot == errorDoc.getElementById("reportButton")) {
352 // This is the "Why is this site blocked" button. For malware,
353 // we can fetch a site-specific report, for phishing, we redirect
354 // to the generic page describing phishing protection.
355 let action = isMalware ? "report-malware" : "report-phishing";
356 sendAsyncMessage("Browser:BlockedSite",
357 { url: errorDoc.location.href, action: action });
358 } else if (ot == errorDoc.getElementById("ignoreWarningButton")) {
359 // Allow users to override and continue through to the site,
360 // but add a notify bar as a reminder, so that they don't lose
361 // track after, e.g., tab switching.
362 let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
363 webNav.loadURI(content.location,
364 Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
365 null, null, null);
366 }
367 }
368 },
370 _onSingleTap: function (aX, aY, aModifiers) {
371 let utils = Util.getWindowUtils(content);
372 for (let type of ["mousemove", "mousedown", "mouseup"]) {
373 utils.sendMouseEventToWindow(type, aX, aY, 0, 1, aModifiers, true, 1.0,
374 Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
375 }
376 },
378 _onDoubleTap: function (aX, aY) {
379 let { element } = Content.getCurrentWindowAndOffset(aX, aY);
380 while (element && !this._shouldZoomToElement(element)) {
381 element = element.parentNode;
382 }
384 if (!element) {
385 this._zoomOut();
386 } else {
387 this._zoomToElement(element);
388 }
389 },
391 /******************************************************
392 * Zoom utilities
393 */
394 _zoomOut: function() {
395 let rect = new Rect(0,0,0,0);
396 this._zoomToRect(rect);
397 },
399 _zoomToElement: function(aElement) {
400 let rect = getBoundingContentRect(aElement);
401 this._inflateRect(rect, kZoomToElementMargin);
402 this._zoomToRect(rect);
403 },
405 _inflateRect: function(aRect, aMargin) {
406 aRect.left -= aMargin;
407 aRect.top -= aMargin;
408 aRect.bottom += aMargin;
409 aRect.right += aMargin;
410 },
412 _zoomToRect: function (aRect) {
413 let utils = Util.getWindowUtils(content);
414 let viewId = utils.getViewId(content.document.documentElement);
415 let presShellId = {};
416 utils.getPresShellId(presShellId);
417 sendAsyncMessage("Content:ZoomToRect", {
418 rect: aRect,
419 presShellId: presShellId.value,
420 viewId: viewId,
421 });
422 },
424 _shouldZoomToElement: function(aElement) {
425 let win = aElement.ownerDocument.defaultView;
426 if (win.getComputedStyle(aElement, null).display == "inline") {
427 return false;
428 }
429 else if (aElement instanceof Ci.nsIDOMHTMLLIElement) {
430 return false;
431 }
432 else if (aElement instanceof Ci.nsIDOMHTMLQuoteElement) {
433 return false;
434 }
435 else {
436 return true;
437 }
438 },
441 /******************************************************
442 * General utilities
443 */
445 /*
446 * Retrieve the total offset from the window's origin to the sub frame
447 * element including frame and scroll offsets. The resulting offset is
448 * such that:
449 * sub frame coords + offset = root frame position
450 */
451 getCurrentWindowAndOffset: function(x, y) {
452 // If the element at the given point belongs to another document (such
453 // as an iframe's subdocument), the element in the calling document's
454 // DOM (e.g. the iframe) is returned.
455 let utils = Util.getWindowUtils(content);
456 let element = utils.elementFromPoint(x, y, true, false);
457 let offset = { x:0, y:0 };
459 while (element && (element instanceof HTMLIFrameElement ||
460 element instanceof HTMLFrameElement)) {
461 // get the child frame position in client coordinates
462 let rect = element.getBoundingClientRect();
464 // calculate offsets for digging down into sub frames
465 // using elementFromPoint:
467 // Get the content scroll offset in the child frame
468 scrollOffset = ContentScroll.getScrollOffset(element.contentDocument.defaultView);
469 // subtract frame and scroll offset from our elementFromPoint coordinates
470 x -= rect.left + scrollOffset.x;
471 y -= rect.top + scrollOffset.y;
473 // calculate offsets we'll use to translate to client coords:
475 // add frame client offset to our total offset result
476 offset.x += rect.left;
477 offset.y += rect.top;
479 // get the frame's nsIDOMWindowUtils
480 utils = element.contentDocument
481 .defaultView
482 .QueryInterface(Ci.nsIInterfaceRequestor)
483 .getInterface(Ci.nsIDOMWindowUtils);
485 // retrieve the target element in the sub frame at x, y
486 element = utils.elementFromPoint(x, y, true, false);
487 }
489 if (!element)
490 return {};
492 return {
493 element: element,
494 contentWindow: element.ownerDocument.defaultView,
495 offset: offset,
496 utils: utils
497 };
498 },
501 _maybeNotifyErrorPage: function _maybeNotifyErrorPage() {
502 // Notify browser that an error page is being shown instead
503 // of the target location. Necessary to get proper thumbnail
504 // updates on chrome for error pages.
505 if (content.location.href !== content.document.documentURI)
506 sendAsyncMessage("Browser:ErrorPage", null);
507 },
509 _highlightElement: null,
511 _doTapHighlight: function _doTapHighlight(aElement) {
512 gDOMUtils.setContentState(aElement, kStateActive);
513 this._highlightElement = aElement;
514 },
516 _cancelTapHighlight: function _cancelTapHighlight(aElement) {
517 gDOMUtils.setContentState(content.document.documentElement, kStateActive);
518 this._highlightElement = null;
519 },
520 };
522 Content.init();
524 var FormSubmitObserver = {
525 init: function init(){
526 addMessageListener("Browser:TabOpen", this);
527 addMessageListener("Browser:TabClose", this);
529 addEventListener("pageshow", this, false);
531 Services.obs.addObserver(this, "invalidformsubmit", false);
532 },
534 handleEvent: function handleEvent(aEvent) {
535 let target = aEvent.originalTarget;
536 let isRootDocument = (target == content.document || target.ownerDocument == content.document);
537 if (!isRootDocument)
538 return;
540 // Reset invalid submit state on each pageshow
541 if (aEvent.type == "pageshow")
542 Content.formAssistant.invalidSubmit = false;
543 },
545 receiveMessage: function receiveMessage(aMessage) {
546 let json = aMessage.json;
547 switch (aMessage.name) {
548 case "Browser:TabOpen":
549 Services.obs.addObserver(this, "formsubmit", false);
550 break;
551 case "Browser:TabClose":
552 Services.obs.removeObserver(this, "formsubmit");
553 break;
554 }
555 },
557 notify: function notify(aFormElement, aWindow, aActionURI, aCancelSubmit) {
558 // Do not notify unless this is the window where the submit occurred
559 if (aWindow == content)
560 // We don't need to send any data along
561 sendAsyncMessage("Browser:FormSubmit", {});
562 },
564 notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
565 if (!aInvalidElements.length)
566 return;
568 let element = aInvalidElements.queryElementAt(0, Ci.nsISupports);
569 if (!(element instanceof HTMLInputElement ||
570 element instanceof HTMLTextAreaElement ||
571 element instanceof HTMLSelectElement ||
572 element instanceof HTMLButtonElement)) {
573 return;
574 }
576 Content.formAssistant.invalidSubmit = true;
577 Content.formAssistant.open(element);
578 },
580 QueryInterface : function(aIID) {
581 if (!aIID.equals(Ci.nsIFormSubmitObserver) &&
582 !aIID.equals(Ci.nsISupportsWeakReference) &&
583 !aIID.equals(Ci.nsISupports))
584 throw Cr.NS_ERROR_NO_INTERFACE;
585 return this;
586 }
587 };
588 this.Content = Content;
590 FormSubmitObserver.init();