Bug 1578065 - Port Bug 433238 "Password manager contextual menu password field manual fill" to SeaMonkey. r=frg a=frg
authorIan Neal <iann_cvs@blueyonder.co.uk>
Sun, 15 Sep 2019 12:56:21 +0200
changeset 32304 98612418d9e0fe0db4e79e97e598edd7316f7301
parent 32303 b494c454e78eff609c4ad2b9d8b6706e2fed4eb9
child 32305 901914f5aa1be2c111238e24e807715908abea91
push id218
push userfrgrahl@gmx.net
push dateSun, 15 Sep 2019 11:06:46 +0000
treeherdercomm-esr60@da2c74f53838 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfrg, frg
bugs1578065, 433238
Bug 1578065 - Port Bug 433238 "Password manager contextual menu password field manual fill" to SeaMonkey. r=frg a=frg
suite/base/content/contentAreaContextOverlay.xul
suite/base/content/nsContextMenu.js
suite/locales/en-US/chrome/common/contentAreaCommands.dtd
suite/mailnews/content/mailContextMenus.js
--- a/suite/base/content/contentAreaContextOverlay.xul
+++ b/suite/base/content/contentAreaContextOverlay.xul
@@ -369,16 +369,38 @@
       <menuitem hidden="true" id="context-bidi-text-direction-toggle"
                 label="&bidiSwitchTextDirectionItem.label;"
                 accesskey="&bidiSwitchTextDirectionItem.accesskey;"
                 command="cmd_switchTextDirection"/>
       <menuitem hidden="true" id="context-bidi-page-direction-toggle"
                 label="&bidiSwitchPageDirectionItem.label;"
                 accesskey="&bidiSwitchPageDirectionItem.accesskey;"
                 oncommand="SwitchDocumentDirection(window.content);"/>
+      <menuseparator id="fill-login-separator" hidden="true"/>
+      <menu id="fill-login"
+            label="&fillLoginMenu.label;"
+            label-login="&fillLoginMenu.label;"
+            label-password="&fillPasswordMenu.label;"
+            label-username="&fillUsernameMenu.label;"
+            accesskey="&fillLoginMenu.accesskey;"
+            accesskey-login="&fillLoginMenu.accesskey;"
+            accesskey-password="&fillPasswordMenu.accesskey;"
+            accesskey-username="&fillUsernameMenu.accesskey;"
+            hidden="true">
+        <menupopup id="fill-login-popup">
+          <menuitem id="fill-login-no-logins"
+                    label="&noLoginSuggestions.label;"
+                    disabled="true"
+                    hidden="true"/>
+          <menuseparator id="saved-logins-separator"/>
+          <menuitem id="fill-login-saved-passwords"
+                    label="&viewSavedLogins.label;"
+                    oncommand="gContextMenu.openPasswordManager();"/>
+        </menupopup>
+      </menu>
       <menuseparator id="inspect-separator"
                      hidden="true"/>
       <menuitem id="context-inspect"
                 hidden="true"
                 label="&devtoolsInspect.label;"
                 accesskey="&devtoolsInspect.accesskey;"
                 oncommand="gContextMenu.inspectNode();"/>
     </menupopup>
--- a/suite/base/content/nsContextMenu.js
+++ b/suite/base/content/nsContextMenu.js
@@ -9,38 +9,41 @@
 |                                                                              |
 |   For usage, see references to this class in navigator.xul.                  |
 |                                                                              |
 |   Currently, this code is relatively useless for any other purpose.  In the  |
 |   longer term, this code will be restructured to make it more reusable.      |
 ------------------------------------------------------------------------------*/
 
 ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm");
+ChromeUtils.import("resource://gre/modules/LoginManagerContextMenu.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-var gContextMenuContentData = null;
-
 XPCOMUtils.defineLazyGetter(this, "InlineSpellCheckerUI", function() {
   return new InlineSpellChecker();
 });
 
 XPCOMUtils.defineLazyGetter(this, "PageMenuParent", function() {
   let tmp = {};
   ChromeUtils.import("resource://gre/modules/PageMenu.jsm", tmp);
   return new tmp.PageMenuParent();
 });
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  findCssSelector: "resource://gre/modules/css-selector.js",
+  LoginHelper: "resource://gre/modules/LoginHelper.jsm",
+  LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
   DevToolsShim: "chrome://devtools-shim/content/DevToolsShim.jsm",
-  findCssSelector: "resource://gre/modules/css-selector.js",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   ShellService: "resource:///modules/ShellService.jsm",
 });
 
+var gContextMenuContentData = null;
+
 function nsContextMenu(aXulMenu, aIsShift, aEvent) {
   this.shouldDisplay = true;
   this.initMenu(aXulMenu, aIsShift, aEvent);
 }
 
 // Prototype for nsContextMenu "class."
 nsContextMenu.prototype = {
   initMenu: function(aXulMenu, aIsShift, aEvent) {
@@ -56,21 +59,21 @@ nsContextMenu.prototype = {
         Services.prefs.getBoolPref("javascript.enabled"))
       this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target, aXulMenu);
 
     this.isTextSelected = this.isTextSelection();
     this.isContentSelected = this.isContentSelection();
 
     this.initPopupPrincipal();
 
-    // Initialize (disable/remove) menu items.
-    this.initItems();
     // Initialize gContextMenuContentData.
     if (aEvent)
       this.initContentData(aEvent);
+    // Initialize (disable/remove) menu items.
+    this.initItems();
   },
 
   initContentData: function(aEvent) {
     var addonInfo = {};
     var subject = {
       event: aEvent,
       addonInfo: addonInfo,
     };
@@ -113,37 +116,40 @@ nsContextMenu.prototype = {
       charSet: doc.characterSet,
       referrer: doc.referrer,
       referrerPolicy: doc.referrerPolicy,
       contentType: contentType,
       contentDisposition: contentDisposition,
       frameOuterWindowID: doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
                                          .getInterface(Ci.nsIDOMWindowUtils)
                                          .outerWindowID,
+      loginFillInfo: LoginManagerContent.getFieldContext(popupNode),
     };
   },
 
   hiding: function () {
     gContextMenuContentData = null;
     InlineSpellCheckerUI.clearSuggestionsFromMenu();
     InlineSpellCheckerUI.clearDictionaryListFromMenu();
     InlineSpellCheckerUI.uninit();
+    LoginManagerContextMenu.clearLoginsFromMenu(document);
   },
 
   initItems: function() {
     this.initPageMenuSeparator();
     this.initOpenItems();
     this.initNavigationItems();
     this.initViewItems();
     this.initMiscItems();
     this.initSpellingItems();
     this.initSaveItems();
     this.initClipboardItems();
     this.initMetadataItems();
     this.initMediaPlayerItems();
+    this.initPasswordManagerItems();
   },
 
   initPageMenuSeparator: function() {
     this.showItem("page-menu-separator", this.hasPageMenu);
   },
 
   initOpenItems: function() {
     var showOpen = this.onSaveableLink || (this.inDirList && this.onLink);
@@ -495,16 +501,70 @@ nsContextMenu.prototype = {
         this.setItemAttr("context-video-fullscreen", "disabled", hasError);
         this.setItemAttr("context-video-showstats", "disabled", hasError);
         this.setItemAttr("context-video-hidestats", "disabled", hasError);
       }
     }
     this.showItem("context-media-sep-commands", onMedia);
   },
 
+  initPasswordManagerItems: function() {
+    let fillMenu = document.getElementById("fill-login");
+    // If no fill Menu, probably mailContext so nothing to set up.
+    if (!fillMenu)
+      return;
+
+    let loginFillInfo = gContextMenuContentData && gContextMenuContentData.loginFillInfo;
+
+    // If we could not find a password field we
+    // don't want to show the form fill option.
+    let showFill = loginFillInfo && loginFillInfo.passwordField.found;
+
+    // Disable the fill option if the user has set a master password
+    // or if the password field or target field are disabled.
+    let disableFill = !loginFillInfo ||
+                      !Services.logins ||
+                      !Services.logins.isLoggedIn ||
+                      loginFillInfo.passwordField.disabled ||
+                      (!this.onPassword && loginFillInfo.usernameField.disabled);
+
+    this.showItem("fill-login-separator", showFill);
+    this.showItem("fill-login", showFill);
+    this.setItemAttr("fill-login", "disabled", disableFill);
+
+    // Set the correct label for the fill menu
+    if (this.onPassword) {
+      fillMenu.setAttribute("label", fillMenu.getAttribute("label-password"));
+      fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-password"));
+    } else {
+      fillMenu.setAttribute("label", fillMenu.getAttribute("label-login"));
+      fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-login"));
+    }
+
+    if (!showFill || disableFill) {
+      return;
+    }
+    let documentURI = gContextMenuContentData.documentURIObject;
+    let fragment = LoginManagerContextMenu.addLoginsToMenu(this.target, this.browser, documentURI);
+
+    this.showItem("fill-login-no-logins", !fragment);
+
+    if (!fragment) {
+      return;
+    }
+    let popup = document.getElementById("fill-login-popup");
+    let insertBeforeElement = document.getElementById("fill-login-no-logins");
+    popup.insertBefore(fragment, insertBeforeElement);
+  },
+
+  openPasswordManager: function() {
+    // LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
+    toDataManager(gContextMenuContentData.documentURIObject.host + '|passwords');
+  },
+
   /**
    * Retrieve the array of CSS selectors corresponding to the provided node. The first item
    * of the array is the selector of the node in its owner document. Additional items are
    * used if the node is inside a frame, each representing the CSS selector for finding the
    * frame element in its parent document.
    *
    * This format is expected by DevTools in order to handle the Inspect Node context menu
    * item.
@@ -530,22 +590,16 @@ nsContextMenu.prototype = {
   },
 
   // Set various context menu attributes based on the state of the world.
   setTarget: function(aNode, aRangeParent, aRangeOffset) {
     // Currently "isRemote" is always false.
     //this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote;
 
     const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    if (aNode.nodeType == Node.DOCUMENT_NODE ||
-        // Not display on XUL element but relax for <label class="text-link">
-        (aNode.namespaceURI == xulNS && !isXULTextLinkLabel(aNode))) {
-      this.shouldDisplay = false;
-      return;
-    }
 
     // Initialize contextual info.
     this.onImage               = false;
     this.onLoadedImage         = false;
     this.onCompletedImage      = false;
     this.onStandaloneImage     = false;
     this.onCanvas              = false;
     this.onVideo               = false;
@@ -569,20 +623,28 @@ nsContextMenu.prototype = {
     this.hasBGImage            = false;
     this.bgImageURL            = "";
     this.popupPrincipal        = null;
     this.autoDownload          = false;
     this.isTextSelected        = false;
     this.isContentSelected     = false;
     this.onEditableArea        = false;
     this.canSpellCheck         = false;
+    this.onPassword            = false;
 
     // Remember the node that was clicked.
     this.target = aNode;
 
+    if (aNode.nodeType == Node.DOCUMENT_NODE ||
+        // Not display on XUL element but relax for <label class="text-link">
+        (aNode.namespaceURI == xulNS && !isXULTextLinkLabel(aNode))) {
+      this.shouldDisplay = false;
+      return;
+    }
+
     let editFlags = SpellCheckHelper.isEditable(this.target, window);
     this.browser = this.target.ownerDocument.defaultView
                               .QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebNavigation)
                               .QueryInterface(Ci.nsIDocShell)
                               .chromeEventHandler;
 
     this.autoDownload = Services.prefs.getBoolPref("browser.download.useDownloadDir");
@@ -629,16 +691,17 @@ nsContextMenu.prototype = {
       else if (this.target instanceof HTMLAudioElement) {
         this.onAudio = true;
         this.mediaURL = this.target.currentSrc || this.target.src;
       }
       else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
         this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
         this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
         this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
+        this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
         if (this.onEditableArea) {
           InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
           InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
         }
         this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
       }
       else if ( this.target instanceof HTMLHtmlElement ) {
         // pages with multiple <body>s are lame. we'll teach them a lesson.
--- a/suite/locales/en-US/chrome/common/contentAreaCommands.dtd
+++ b/suite/locales/en-US/chrome/common/contentAreaCommands.dtd
@@ -150,11 +150,21 @@ items are mutually exclusive. -->
 <!ENTITY spellIgnoreWord.accesskey      "W">
 <!ENTITY spellCheckEnable.label         "Check Spelling">
 <!ENTITY spellCheckEnable.accesskey     "S">
 <!ENTITY spellNoSuggestions.label       "(No Spelling Suggestions)">
 <!ENTITY spellDictionaries.label        "Languages">
 <!ENTITY spellDictionaries.accesskey    "L">
 <!ENTITY spellAddDictionaries.label     "Download More Dictionaries…">
 <!ENTITY spellAddDictionaries.accesskey "D">
+
+<!ENTITY fillLoginMenu.label          "Fill Login">
+<!ENTITY fillLoginMenu.accesskey      "F">
+<!ENTITY fillPasswordMenu.label       "Fill Password">
+<!ENTITY fillPasswordMenu.accesskey   "F">
+<!ENTITY fillUsernameMenu.label       "Fill Username">
+<!ENTITY fillUsernameMenu.accesskey   "F">
+<!ENTITY noLoginSuggestions.label     "(No Login Suggestions)">
+<!ENTITY viewSavedLogins.label        "View Saved Logins">
+
 <!-- Developer Tools -->
 <!ENTITY devtoolsInspect.label          "Inspect Element">
 <!ENTITY devtoolsInspect.accesskey      "n">
--- a/suite/mailnews/content/mailContextMenus.js
+++ b/suite/mailnews/content/mailContextMenus.js
@@ -99,23 +99,23 @@ function InThreadPane(aTarget)
  * @param aTarget the target of the popup event
  * @return true always
  */
 function FillMailContextMenu(aTarget, aEvent)
 {
   var inThreadPane = InThreadPane(aTarget);
   gContextMenu = new nsContextMenu(aTarget);
 
-  // Need to call nsContextMenu's initItems to hide what is not used.
-  gContextMenu.initItems();
-
   // Initialize gContextMenuContentData.
   if (aEvent)
     gContextMenu.initContentData(aEvent);
 
+  // Need to call nsContextMenu's initItems to hide what is not used.
+  gContextMenu.initItems();
+
   var numSelected = GetNumSelectedMessages();
   var oneOrMore = (numSelected > 0);
   var single = (numSelected == 1);
 
   var isNewsgroup = gFolderDisplay.selectedMessageIsNews;
 
   // Clear the global var used to keep track if a 'Delete Message' or 'Move
   // To' command has been triggered via the thread pane context menu.