michael@0: // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // This stays here because otherwise it's hard to tell if there's a parsing error michael@0: dump("### Content.js loaded\n"); michael@0: michael@0: let Cc = Components.classes; michael@0: let Ci = Components.interfaces; michael@0: let Cu = Components.utils; michael@0: let Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "Services", function() { michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: return Services; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "Rect", function() { michael@0: Cu.import("resource://gre/modules/Geometry.jsm"); michael@0: return Rect; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "Point", function() { michael@0: Cu.import("resource://gre/modules/Geometry.jsm"); michael@0: return Point; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", michael@0: "resource://gre/modules/LoginManagerContent.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gFocusManager", michael@0: "@mozilla.org/focus-manager;1", "nsIFocusManager"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gDOMUtils", michael@0: "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); michael@0: michael@0: this.XULDocument = Ci.nsIDOMXULDocument; michael@0: this.HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement; michael@0: this.HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; michael@0: this.HTMLFrameElement = Ci.nsIDOMHTMLFrameElement; michael@0: this.HTMLFrameSetElement = Ci.nsIDOMHTMLFrameSetElement; michael@0: this.HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; michael@0: this.HTMLOptionElement = Ci.nsIDOMHTMLOptionElement; michael@0: michael@0: const kReferenceDpi = 240; // standard "pixel" size used in some preferences michael@0: michael@0: const kStateActive = 0x00000001; // :active pseudoclass for elements michael@0: michael@0: const kZoomToElementMargin = 16; // in px michael@0: michael@0: /* michael@0: * getBoundingContentRect michael@0: * michael@0: * @param aElement michael@0: * @return Bounding content rect adjusted for scroll and frame offsets. michael@0: */ michael@0: function getBoundingContentRect(aElement) { michael@0: if (!aElement) michael@0: return new Rect(0, 0, 0, 0); michael@0: michael@0: let document = aElement.ownerDocument; michael@0: while(document.defaultView.frameElement) michael@0: document = document.defaultView.frameElement.ownerDocument; michael@0: michael@0: let offset = ContentScroll.getScrollOffset(content); michael@0: offset = new Point(offset.x, offset.y); michael@0: michael@0: let r = aElement.getBoundingClientRect(); michael@0: michael@0: // step out of iframes and frames, offsetting scroll values michael@0: let view = aElement.ownerDocument.defaultView; michael@0: for (let frame = view; frame != content; frame = frame.parent) { michael@0: // adjust client coordinates' origin to be top left of iframe viewport michael@0: let rect = frame.frameElement.getBoundingClientRect(); michael@0: let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; michael@0: let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; michael@0: offset.add(rect.left + parseInt(left), rect.top + parseInt(top)); michael@0: } michael@0: michael@0: return new Rect(r.left + offset.x, r.top + offset.y, r.width, r.height); michael@0: } michael@0: this.getBoundingContentRect = getBoundingContentRect; michael@0: michael@0: /* michael@0: * getOverflowContentBoundingRect michael@0: * michael@0: * @param aElement michael@0: * @return Bounding content rect adjusted for scroll and frame offsets. michael@0: */ michael@0: michael@0: function getOverflowContentBoundingRect(aElement) { michael@0: let r = getBoundingContentRect(aElement); michael@0: michael@0: // If the overflow is hidden don't bother calculating it michael@0: let computedStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement); michael@0: let blockDisplays = ["block", "inline-block", "list-item"]; michael@0: if ((blockDisplays.indexOf(computedStyle.getPropertyValue("display")) != -1 && michael@0: computedStyle.getPropertyValue("overflow") == "hidden") || michael@0: aElement instanceof HTMLSelectElement) { michael@0: return r; michael@0: } michael@0: michael@0: for (let i = 0; i < aElement.childElementCount; i++) { michael@0: r = r.union(getBoundingContentRect(aElement.children[i])); michael@0: } michael@0: michael@0: return r; michael@0: } michael@0: this.getOverflowContentBoundingRect = getOverflowContentBoundingRect; michael@0: michael@0: /* michael@0: * Content michael@0: * michael@0: * Browser event receiver for content. michael@0: */ michael@0: let Content = { michael@0: _debugEvents: false, michael@0: michael@0: get formAssistant() { michael@0: delete this.formAssistant; michael@0: return this.formAssistant = new FormAssistant(); michael@0: }, michael@0: michael@0: init: function init() { michael@0: // Asyncronous messages sent from the browser michael@0: addMessageListener("Browser:Blur", this); michael@0: addMessageListener("Browser:SaveAs", this); michael@0: addMessageListener("Browser:MozApplicationCache:Fetch", this); michael@0: addMessageListener("Browser:SetCharset", this); michael@0: addMessageListener("Browser:CanUnload", this); michael@0: addMessageListener("Browser:PanBegin", this); michael@0: addMessageListener("Gesture:SingleTap", this); michael@0: addMessageListener("Gesture:DoubleTap", this); michael@0: michael@0: addEventListener("touchstart", this, false); michael@0: addEventListener("click", this, true); michael@0: addEventListener("keydown", this); michael@0: addEventListener("keyup", this); michael@0: michael@0: // Synchronous events caught during the bubbling phase michael@0: addEventListener("MozApplicationManifest", this, false); michael@0: addEventListener("DOMContentLoaded", this, false); michael@0: addEventListener("DOMAutoComplete", this, false); michael@0: addEventListener("DOMFormHasPassword", this, false); michael@0: addEventListener("blur", this, false); michael@0: // Attach a listener to watch for "click" events bubbling up from error michael@0: // pages and other similar page. This lets us fix bugs like 401575 which michael@0: // require error page UI to do privileged things, without letting error michael@0: // pages have any privilege themselves. michael@0: addEventListener("click", this, false); michael@0: michael@0: docShell.useGlobalHistory = true; michael@0: }, michael@0: michael@0: /******************************************* michael@0: * Events michael@0: */ michael@0: michael@0: handleEvent: function handleEvent(aEvent) { michael@0: if (this._debugEvents) Util.dumpLn("Content:", aEvent.type); michael@0: switch (aEvent.type) { michael@0: case "MozApplicationManifest": { michael@0: let doc = aEvent.originalTarget; michael@0: sendAsyncMessage("Browser:MozApplicationManifest", { michael@0: location: doc.documentURIObject.spec, michael@0: manifest: doc.documentElement.getAttribute("manifest"), michael@0: charset: doc.characterSet michael@0: }); michael@0: break; michael@0: } michael@0: michael@0: case "keyup": michael@0: // If after a key is pressed we still have no input, then close michael@0: // the autocomplete. Perhaps the user used backspace or delete. michael@0: // Allow down arrow to trigger autofill popup on empty input. michael@0: if ((!aEvent.target.value && aEvent.keyCode != aEvent.DOM_VK_DOWN) michael@0: || aEvent.keyCode == aEvent.DOM_VK_ESCAPE) michael@0: this.formAssistant.close(); michael@0: else michael@0: this.formAssistant.open(aEvent.target, aEvent); michael@0: break; michael@0: michael@0: case "click": michael@0: // Workaround for bug 925457: we sometimes don't recognize the michael@0: // correct tap target or are unable to identify if it's editable. michael@0: // Instead always save tap co-ordinates for the keyboard to look for michael@0: // when it is up. michael@0: SelectionHandler.onClickCoords(aEvent.clientX, aEvent.clientY); michael@0: michael@0: if (aEvent.eventPhase == aEvent.BUBBLING_PHASE) michael@0: this._onClickBubble(aEvent); michael@0: else michael@0: this._onClickCapture(aEvent); michael@0: break; michael@0: michael@0: case "DOMFormHasPassword": michael@0: LoginManagerContent.onFormPassword(aEvent); michael@0: break; michael@0: michael@0: case "DOMContentLoaded": michael@0: this._maybeNotifyErrorPage(); michael@0: break; michael@0: michael@0: case "DOMAutoComplete": michael@0: case "blur": michael@0: LoginManagerContent.onUsernameInput(aEvent); michael@0: break; michael@0: michael@0: case "touchstart": michael@0: this._onTouchStart(aEvent); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function receiveMessage(aMessage) { michael@0: if (this._debugEvents) Util.dumpLn("Content:", aMessage.name); michael@0: let json = aMessage.json; michael@0: let x = json.x; michael@0: let y = json.y; michael@0: michael@0: switch (aMessage.name) { michael@0: case "Browser:Blur": michael@0: gFocusManager.clearFocus(content); michael@0: break; michael@0: michael@0: case "Browser:CanUnload": michael@0: let canUnload = docShell.contentViewer.permitUnload(); michael@0: sendSyncMessage("Browser:CanUnload:Return", { permit: canUnload }); michael@0: break; michael@0: michael@0: case "Browser:SaveAs": michael@0: break; michael@0: michael@0: case "Browser:MozApplicationCache:Fetch": { michael@0: let currentURI = Services.io.newURI(json.location, json.charset, null); michael@0: let manifestURI = Services.io.newURI(json.manifest, json.charset, currentURI); michael@0: let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"] michael@0: .getService(Ci.nsIOfflineCacheUpdateService); michael@0: updateService.scheduleUpdate(manifestURI, currentURI, content); michael@0: break; michael@0: } michael@0: michael@0: case "Browser:SetCharset": { michael@0: docShell.gatherCharsetMenuTelemetry(); michael@0: docShell.charset = json.charset; michael@0: michael@0: let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); michael@0: webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); michael@0: break; michael@0: } michael@0: michael@0: case "Browser:PanBegin": michael@0: this._cancelTapHighlight(); michael@0: break; michael@0: michael@0: case "Gesture:SingleTap": michael@0: this._onSingleTap(json.x, json.y, json.modifiers); michael@0: break; michael@0: michael@0: case "Gesture:DoubleTap": michael@0: this._onDoubleTap(json.x, json.y); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /****************************************************** michael@0: * Event handlers michael@0: */ michael@0: michael@0: _onTouchStart: function _onTouchStart(aEvent) { michael@0: let element = aEvent.target; michael@0: michael@0: // There is no need to have a feedback for disabled element michael@0: let isDisabled = element instanceof HTMLOptionElement ? michael@0: (element.disabled || element.parentNode.disabled) : element.disabled; michael@0: if (isDisabled) michael@0: return; michael@0: michael@0: // Set the target element to active michael@0: this._doTapHighlight(element); michael@0: }, michael@0: michael@0: _onClickCapture: function _onClickCapture(aEvent) { michael@0: let element = aEvent.target; michael@0: michael@0: ContextMenuHandler.reset(); michael@0: michael@0: // Only show autocomplete after the item is clicked michael@0: if (!this.lastClickElement || this.lastClickElement != element) { michael@0: this.lastClickElement = element; michael@0: if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE && michael@0: !(element instanceof HTMLSelectElement)) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: this.formAssistant.focusSync = true; michael@0: this.formAssistant.open(element, aEvent); michael@0: this._cancelTapHighlight(); michael@0: this.formAssistant.focusSync = false; michael@0: michael@0: // A tap on a form input triggers touch input caret selection michael@0: if (Util.isEditable(element) && michael@0: aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) { michael@0: let { offsetX, offsetY } = Util.translateToTopLevelWindow(element); michael@0: sendAsyncMessage("Content:SelectionCaret", { michael@0: xPos: aEvent.clientX + offsetX, michael@0: yPos: aEvent.clientY + offsetY michael@0: }); michael@0: } else { michael@0: SelectionHandler.closeSelection(); michael@0: } michael@0: }, michael@0: michael@0: // Checks clicks we care about - events bubbling up from about pages. michael@0: _onClickBubble: function _onClickBubble(aEvent) { michael@0: // Don't trust synthetic events michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: let ot = aEvent.originalTarget; michael@0: let errorDoc = ot.ownerDocument; michael@0: if (!errorDoc) michael@0: return; michael@0: michael@0: // If the event came from an ssl error page, it is probably either michael@0: // "Add Exception…" or "Get me out of here!" button. michael@0: if (/^about:certerror\?e=nssBadCert/.test(errorDoc.documentURI)) { michael@0: let perm = errorDoc.getElementById("permanentExceptionButton"); michael@0: let temp = errorDoc.getElementById("temporaryExceptionButton"); michael@0: if (ot == temp || ot == perm) { michael@0: let action = (ot == perm ? "permanent" : "temporary"); michael@0: sendAsyncMessage("Browser:CertException", michael@0: { url: errorDoc.location.href, action: action }); michael@0: } else if (ot == errorDoc.getElementById("getMeOutOfHereButton")) { michael@0: sendAsyncMessage("Browser:CertException", michael@0: { url: errorDoc.location.href, action: "leave" }); michael@0: } michael@0: } else if (/^about:blocked/.test(errorDoc.documentURI)) { michael@0: // The event came from a button on a malware/phishing block page michael@0: // First check whether it's malware or phishing, so that we can michael@0: // use the right strings/links. michael@0: let isMalware = /e=malwareBlocked/.test(errorDoc.documentURI); michael@0: michael@0: if (ot == errorDoc.getElementById("getMeOutButton")) { michael@0: sendAsyncMessage("Browser:BlockedSite", michael@0: { url: errorDoc.location.href, action: "leave" }); michael@0: } else if (ot == errorDoc.getElementById("reportButton")) { michael@0: // This is the "Why is this site blocked" button. For malware, michael@0: // we can fetch a site-specific report, for phishing, we redirect michael@0: // to the generic page describing phishing protection. michael@0: let action = isMalware ? "report-malware" : "report-phishing"; michael@0: sendAsyncMessage("Browser:BlockedSite", michael@0: { url: errorDoc.location.href, action: action }); michael@0: } else if (ot == errorDoc.getElementById("ignoreWarningButton")) { michael@0: // Allow users to override and continue through to the site, michael@0: // but add a notify bar as a reminder, so that they don't lose michael@0: // track after, e.g., tab switching. michael@0: let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); michael@0: webNav.loadURI(content.location, michael@0: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, michael@0: null, null, null); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _onSingleTap: function (aX, aY, aModifiers) { michael@0: let utils = Util.getWindowUtils(content); michael@0: for (let type of ["mousemove", "mousedown", "mouseup"]) { michael@0: utils.sendMouseEventToWindow(type, aX, aY, 0, 1, aModifiers, true, 1.0, michael@0: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH); michael@0: } michael@0: }, michael@0: michael@0: _onDoubleTap: function (aX, aY) { michael@0: let { element } = Content.getCurrentWindowAndOffset(aX, aY); michael@0: while (element && !this._shouldZoomToElement(element)) { michael@0: element = element.parentNode; michael@0: } michael@0: michael@0: if (!element) { michael@0: this._zoomOut(); michael@0: } else { michael@0: this._zoomToElement(element); michael@0: } michael@0: }, michael@0: michael@0: /****************************************************** michael@0: * Zoom utilities michael@0: */ michael@0: _zoomOut: function() { michael@0: let rect = new Rect(0,0,0,0); michael@0: this._zoomToRect(rect); michael@0: }, michael@0: michael@0: _zoomToElement: function(aElement) { michael@0: let rect = getBoundingContentRect(aElement); michael@0: this._inflateRect(rect, kZoomToElementMargin); michael@0: this._zoomToRect(rect); michael@0: }, michael@0: michael@0: _inflateRect: function(aRect, aMargin) { michael@0: aRect.left -= aMargin; michael@0: aRect.top -= aMargin; michael@0: aRect.bottom += aMargin; michael@0: aRect.right += aMargin; michael@0: }, michael@0: michael@0: _zoomToRect: function (aRect) { michael@0: let utils = Util.getWindowUtils(content); michael@0: let viewId = utils.getViewId(content.document.documentElement); michael@0: let presShellId = {}; michael@0: utils.getPresShellId(presShellId); michael@0: sendAsyncMessage("Content:ZoomToRect", { michael@0: rect: aRect, michael@0: presShellId: presShellId.value, michael@0: viewId: viewId, michael@0: }); michael@0: }, michael@0: michael@0: _shouldZoomToElement: function(aElement) { michael@0: let win = aElement.ownerDocument.defaultView; michael@0: if (win.getComputedStyle(aElement, null).display == "inline") { michael@0: return false; michael@0: } michael@0: else if (aElement instanceof Ci.nsIDOMHTMLLIElement) { michael@0: return false; michael@0: } michael@0: else if (aElement instanceof Ci.nsIDOMHTMLQuoteElement) { michael@0: return false; michael@0: } michael@0: else { michael@0: return true; michael@0: } michael@0: }, michael@0: michael@0: michael@0: /****************************************************** michael@0: * General utilities michael@0: */ michael@0: michael@0: /* michael@0: * Retrieve the total offset from the window's origin to the sub frame michael@0: * element including frame and scroll offsets. The resulting offset is michael@0: * such that: michael@0: * sub frame coords + offset = root frame position michael@0: */ michael@0: getCurrentWindowAndOffset: function(x, y) { michael@0: // If the element at the given point belongs to another document (such michael@0: // as an iframe's subdocument), the element in the calling document's michael@0: // DOM (e.g. the iframe) is returned. michael@0: let utils = Util.getWindowUtils(content); michael@0: let element = utils.elementFromPoint(x, y, true, false); michael@0: let offset = { x:0, y:0 }; michael@0: michael@0: while (element && (element instanceof HTMLIFrameElement || michael@0: element instanceof HTMLFrameElement)) { michael@0: // get the child frame position in client coordinates michael@0: let rect = element.getBoundingClientRect(); michael@0: michael@0: // calculate offsets for digging down into sub frames michael@0: // using elementFromPoint: michael@0: michael@0: // Get the content scroll offset in the child frame michael@0: scrollOffset = ContentScroll.getScrollOffset(element.contentDocument.defaultView); michael@0: // subtract frame and scroll offset from our elementFromPoint coordinates michael@0: x -= rect.left + scrollOffset.x; michael@0: y -= rect.top + scrollOffset.y; michael@0: michael@0: // calculate offsets we'll use to translate to client coords: michael@0: michael@0: // add frame client offset to our total offset result michael@0: offset.x += rect.left; michael@0: offset.y += rect.top; michael@0: michael@0: // get the frame's nsIDOMWindowUtils michael@0: utils = element.contentDocument michael@0: .defaultView michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: michael@0: // retrieve the target element in the sub frame at x, y michael@0: element = utils.elementFromPoint(x, y, true, false); michael@0: } michael@0: michael@0: if (!element) michael@0: return {}; michael@0: michael@0: return { michael@0: element: element, michael@0: contentWindow: element.ownerDocument.defaultView, michael@0: offset: offset, michael@0: utils: utils michael@0: }; michael@0: }, michael@0: michael@0: michael@0: _maybeNotifyErrorPage: function _maybeNotifyErrorPage() { michael@0: // Notify browser that an error page is being shown instead michael@0: // of the target location. Necessary to get proper thumbnail michael@0: // updates on chrome for error pages. michael@0: if (content.location.href !== content.document.documentURI) michael@0: sendAsyncMessage("Browser:ErrorPage", null); michael@0: }, michael@0: michael@0: _highlightElement: null, michael@0: michael@0: _doTapHighlight: function _doTapHighlight(aElement) { michael@0: gDOMUtils.setContentState(aElement, kStateActive); michael@0: this._highlightElement = aElement; michael@0: }, michael@0: michael@0: _cancelTapHighlight: function _cancelTapHighlight(aElement) { michael@0: gDOMUtils.setContentState(content.document.documentElement, kStateActive); michael@0: this._highlightElement = null; michael@0: }, michael@0: }; michael@0: michael@0: Content.init(); michael@0: michael@0: var FormSubmitObserver = { michael@0: init: function init(){ michael@0: addMessageListener("Browser:TabOpen", this); michael@0: addMessageListener("Browser:TabClose", this); michael@0: michael@0: addEventListener("pageshow", this, false); michael@0: michael@0: Services.obs.addObserver(this, "invalidformsubmit", false); michael@0: }, michael@0: michael@0: handleEvent: function handleEvent(aEvent) { michael@0: let target = aEvent.originalTarget; michael@0: let isRootDocument = (target == content.document || target.ownerDocument == content.document); michael@0: if (!isRootDocument) michael@0: return; michael@0: michael@0: // Reset invalid submit state on each pageshow michael@0: if (aEvent.type == "pageshow") michael@0: Content.formAssistant.invalidSubmit = false; michael@0: }, michael@0: michael@0: receiveMessage: function receiveMessage(aMessage) { michael@0: let json = aMessage.json; michael@0: switch (aMessage.name) { michael@0: case "Browser:TabOpen": michael@0: Services.obs.addObserver(this, "formsubmit", false); michael@0: break; michael@0: case "Browser:TabClose": michael@0: Services.obs.removeObserver(this, "formsubmit"); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: notify: function notify(aFormElement, aWindow, aActionURI, aCancelSubmit) { michael@0: // Do not notify unless this is the window where the submit occurred michael@0: if (aWindow == content) michael@0: // We don't need to send any data along michael@0: sendAsyncMessage("Browser:FormSubmit", {}); michael@0: }, michael@0: michael@0: notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) { michael@0: if (!aInvalidElements.length) michael@0: return; michael@0: michael@0: let element = aInvalidElements.queryElementAt(0, Ci.nsISupports); michael@0: if (!(element instanceof HTMLInputElement || michael@0: element instanceof HTMLTextAreaElement || michael@0: element instanceof HTMLSelectElement || michael@0: element instanceof HTMLButtonElement)) { michael@0: return; michael@0: } michael@0: michael@0: Content.formAssistant.invalidSubmit = true; michael@0: Content.formAssistant.open(element); michael@0: }, michael@0: michael@0: QueryInterface : function(aIID) { michael@0: if (!aIID.equals(Ci.nsIFormSubmitObserver) && michael@0: !aIID.equals(Ci.nsISupportsWeakReference) && michael@0: !aIID.equals(Ci.nsISupports)) michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: return this; michael@0: } michael@0: }; michael@0: this.Content = Content; michael@0: michael@0: FormSubmitObserver.init();