browser/metro/base/content/contenthandlers/Content.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

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

mercurial