Bug 1294413 - Bounding client validation popup to selected browser. draft
authorJonathan Kingston <jkt@mozilla.com>
Fri, 09 Mar 2018 02:21:00 -0800
changeset 804010 e4636691c27335412205c0bb68db7b26893e56c0
parent 803604 c71b1bbac9050b6ab00895f0b28725cbffc9f5bf
push id112269
push userbmo:jkt@mozilla.com
push dateTue, 05 Jun 2018 10:49:52 +0000
bugs1294413
milestone62.0a1
Bug 1294413 - Bounding client validation popup to selected browser. MozReview-Commit-ID: 5cnAV9MTGRY
browser/modules/FormValidationHandler.jsm
dom/html/test/browser.ini
dom/html/test/browser_prevent_error_spoofing.js
toolkit/modules/DateTimePickerHelper.jsm
--- a/browser/modules/FormValidationHandler.jsm
+++ b/browser/modules/FormValidationHandler.jsm
@@ -36,22 +36,17 @@ var FormValidationHandler =
 
   // Listeners are added in nsBrowserGlue.js
   receiveMessage(aMessage) {
     let window = aMessage.target.ownerGlobal;
     let json = aMessage.json;
     let tabBrowser = window.gBrowser;
     switch (aMessage.name) {
       case "FormValidation:ShowPopup":
-        // target is the <browser>, make sure we're receiving a message
-        // from the foreground tab.
-        if (tabBrowser && aMessage.target != tabBrowser.selectedBrowser) {
-          return;
-        }
-        this._showPopup(window, json);
+        this._showPopup(aMessage.target, json);
         break;
       case "FormValidation:HidePopup":
         this._hidePopup();
         break;
     }
   },
 
   observe(aSubject, aTopic, aData) {
@@ -89,39 +84,70 @@ var FormValidationHandler =
     this._anchor.hidden = true;
     this._anchor = null;
   },
 
   /*
    * Shows the form validation popup at a specified position or updates the
    * messaging and position if the popup is already displayed.
    *
-   * @aWindow - the chrome window
+   * @aBrowser - the browser instance
    * @aPanelData - Object that contains popup information
    *  aPanelData stucture detail:
    *   contentRect - the bounding client rect of the target element. If
    *    content is remote, this is relative to the browser, otherwise its
    *    relative to the window.
    *   position - popup positional string constants.
    *   message - the form element validation message text.
    */
-  _showPopup(aWindow, aPanelData) {
-    let previouslyShown = !!this._panel;
-    this._panel = aWindow.document.getElementById("invalid-form-popup");
-    this._panel.firstChild.textContent = aPanelData.message;
-    this._panel.hidden = false;
+  _showPopup(aBrowser, aPanelData) {
+    let window = aBrowser.ownerGlobal;
+    let tabBrowser = window.gBrowser;
+
+    /* first ignore validation messages from non visible forms */
+    if (Services.focus.activeWindow != window ||
+        tabBrowser.selectedBrowser != aBrowser) {
+      return;
+    }
 
-    let tabBrowser = aWindow.gBrowser;
+    let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils);
+    let docRect = winUtils.getBoundsWithoutFlushing(tabBrowser.selectedBrowser);
+   /*
+     Instead of using the cache of the anchor element winUtils.getBoundsWithoutFlushing(this._anchor);
+     as the anchors parent is the document we can calc the rect from the position
+   */
+    let anchorRect = {};
+    anchorRect.left = docRect.left + aPanelData.contentRect.left;
+    anchorRect.top = docRect.top + aPanelData.contentRect.top;
+    anchorRect.bottom = anchorRect.top + aPanelData.contentRect.height;
+    anchorRect.right = anchorRect.left + aPanelData.contentRect.width;
+
+    /* if the anchor is outside of the document don't display */
+    if (!(anchorRect.left >= docRect.left &&
+        anchorRect.right <= docRect.right &&
+        anchorRect.top >= docRect.top &&
+        anchorRect.bottom <= docRect.bottom)) {
+      // As the page might have scrolled from the previously calculated placement, we should hide the popup if showing
+      this._hidePopup();
+      return;
+    }
+
     this._anchor = tabBrowser.popupAnchor;
     this._anchor.left = aPanelData.contentRect.left;
     this._anchor.top = aPanelData.contentRect.top;
     this._anchor.width = aPanelData.contentRect.width;
     this._anchor.height = aPanelData.contentRect.height;
     this._anchor.hidden = false;
 
+    let previouslyShown = !!this._panel;
+    this._panel = window.document.getElementById("invalid-form-popup");
+    this._panel.firstChild.textContent = aPanelData.message;
+    this._panel.hidden = false;
+
     // Display the panel if it isn't already visible.
     if (!previouslyShown) {
       // Cleanup after the popup is hidden
       this._panel.addEventListener("popuphiding", this, true);
 
       // Hide if the user scrolls the page
       tabBrowser.selectedBrowser.addEventListener("scroll", this, true);
       tabBrowser.selectedBrowser.addEventListener("FullZoomChange", this);
--- a/dom/html/test/browser.ini
+++ b/dom/html/test/browser.ini
@@ -27,9 +27,10 @@ support-files =
 [browser_form_post_from_file_to_http.js]
 [browser_fullscreen-api-keys.js]
 tags = fullscreen
 [browser_fullscreen-contextmenu-esc.js]
 tags = fullscreen
 [browser_submission_flush.js]
 [browser_refresh_wyciwyg_url.js]
 support-files =
-  file_refresh_wyciwyg_url.html
\ No newline at end of file
+  file_refresh_wyciwyg_url.html
+[browser_prevent_error_spoofing.js]
new file mode 100644
--- /dev/null
+++ b/dom/html/test/browser_prevent_error_spoofing.js
@@ -0,0 +1,38 @@
+let testId = 0;
+const gInvalidFormPopup = document.getElementById("invalid-form-popup");
+function checkPopupShow() {
+  ok(gInvalidFormPopup.state == "showing" || gInvalidFormPopup.state == "open",
+     "[Test " + testId + "] The invalid form popup should be shown");
+}
+
+function checkPopupHide() {
+  ok(gInvalidFormPopup.state != "showing" && gInvalidFormPopup.state != "open",
+     "[Test " + testId + "] The invalid form popup should not be shown");
+}
+
+add_task(async function test_error_loads() {
+  const dataUrl = "data:text/html;charset=utf-8,";
+  const testPage = `${dataUrl}
+  <form>
+      <input id="qaz" required="" x-moz-errormessage="I am a spoof" type="text">
+      <input id="q" value="Submit" type="submit">
+  </form>
+  <style>
+  #qaz{
+    position: absolute;
+    border: 1px solid red;
+    top: -74px;
+    left: 95px;
+  }
+  #q{
+    position: absolute;
+    top: -100px;
+  }
+  </style>`;
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPage);
+  checkPopupHide();
+  await BrowserTestUtils.synthesizeMouse("#q", 0, 0, {}, gBrowser.selectedBrowser);
+  checkPopupHide();
+  BrowserTestUtils.removeTab(tab);
+});
--- a/toolkit/modules/DateTimePickerHelper.jsm
+++ b/toolkit/modules/DateTimePickerHelper.jsm
@@ -50,21 +50,17 @@ var DateTimePickerHelper = {
   receiveMessage(aMessage) {
     debug("receiveMessage: " + aMessage.name);
     switch (aMessage.name) {
       case "FormDateTime:OpenPicker": {
         this.showPicker(aMessage.target, aMessage.data);
         break;
       }
       case "FormDateTime:ClosePicker": {
-        if (!this.picker) {
-          return;
-        }
-        this.picker.closePicker();
-        this.close();
+        this._closePicker();
         break;
       }
       case "FormDateTime:UpdatePicker": {
         if (!this.picker) {
           return;
         }
         this.picker.setPopupValue(aMessage.data);
         break;
@@ -79,61 +75,99 @@ var DateTimePickerHelper = {
     debug("handleEvent: " + aEvent.type);
     switch (aEvent.type) {
       case "DateTimePickerValueChanged": {
         this.updateInputBoxValue(aEvent);
         break;
       }
       case "popuphidden": {
         let browser = this.weakBrowser ? this.weakBrowser.get() : null;
-        if (browser) {
-          browser.messageManager.sendAsyncMessage("FormDateTime:PickerClosed");
-        }
-        this.picker.closePicker();
-        this.close();
+        this._sendCloseEvent(browser);
+        this._closePicker();
         break;
       }
       default:
         break;
     }
   },
 
+  // Close UI and cleanup
+  _closePicker() {
+    if (!this.picker) {
+      return;
+    }
+    this.picker.closePicker();
+    this.close();
+  },
+
+  // Send close event to browser-content.js
+  _sendCloseEvent(browser) {
+    if (browser) {
+      browser.messageManager.sendAsyncMessage("FormDateTime:PickerClosed");
+    }
+  },
+
   // Called when picker value has changed, notify input box about it.
   updateInputBoxValue(aEvent) {
     let browser = this.weakBrowser ? this.weakBrowser.get() : null;
     if (browser) {
       browser.messageManager.sendAsyncMessage(
         "FormDateTime:PickerValueChanged", aEvent.detail);
     }
   },
 
   // Get picker from browser and show it anchored to the input box.
   async showPicker(aBrowser, aData) {
     let rect = aData.rect;
     let type = aData.type;
     let detail = aData.detail;
 
-    this._anchor = aBrowser.ownerGlobal.gBrowser.popupAnchor;
-    this._anchor.left = rect.left;
-    this._anchor.top = rect.top;
-    this._anchor.width = rect.width;
-    this._anchor.height = rect.height;
-    this._anchor.hidden = false;
-
-    debug("Opening picker with details: " + JSON.stringify(detail));
-
     let window = aBrowser.ownerGlobal;
     let tabbrowser = window.gBrowser;
     if (Services.focus.activeWindow != window ||
         tabbrowser.selectedBrowser != aBrowser) {
       // We were sent a message from a window or tab that went into the
       // background, so we'll ignore it for now.
+      this._sendCloseEvent(aBrowser);
+      this._closePicker();
       return;
     }
 
+    let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils);
+    let docRect = winUtils.getBoundsWithoutFlushing(tabbrowser.selectedBrowser);
+
+    let anchorRect = {};
+    anchorRect.left = docRect.left + rect.left;
+    anchorRect.top = docRect.top + rect.top;
+    anchorRect.height = rect.height;
+    anchorRect.width = rect.width;
+    anchorRect.bottom = anchorRect.top + rect.height;
+    anchorRect.right = anchorRect.left + rect.width;
+
+    /* if the anchor is outside of the document don't display */
+    if (!(anchorRect.left >= docRect.left &&
+        anchorRect.right <= docRect.right &&
+        anchorRect.top >= docRect.top &&
+        anchorRect.bottom <= docRect.bottom)) {
+      // As the page might have scrolled from the previously calculated placement, we should hide the popup if showing
+      this._sendCloseEvent(aBrowser);
+      this._closePicker();
+      return;
+    }
+
+    this._anchor = aBrowser.ownerGlobal.gBrowser.popupAnchor;
+    this._anchor.left = anchorRect.left;
+    this._anchor.top = anchorRect.top;
+    this._anchor.width = anchorRect.width;
+    this._anchor.height = anchorRect.height;
+    this._anchor.hidden = false;
+
+    debug("Opening picker with details: " + JSON.stringify(detail));
+
     this.weakBrowser = Cu.getWeakReference(aBrowser);
     this.picker = aBrowser.dateTimePicker;
     if (!this.picker) {
       debug("aBrowser.dateTimePicker not found, exiting now.");
       return;
     }
     // The datetimepopup binding is only attached when it is needed.
     // Check if openPicker method is present to determine if binding has