browser/metro/components/LoginManagerPrompter.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 const Cc = Components.classes;
     7 const Ci = Components.interfaces;
     8 const Cr = Components.results;
    10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    11 Components.utils.import("resource://gre/modules/Services.jsm");
    13 /* ==================== LoginManagerPrompter ==================== */
    14 /*
    15  * LoginManagerPrompter
    16  *
    17  * Implements interfaces for prompting the user to enter/save/change auth info.
    18  *
    19  * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
    20  * found in HTML forms.
    21  */
    22 function LoginManagerPrompter() {
    23 }
    25 LoginManagerPrompter.prototype = {
    27     classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"),
    28     QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerPrompter]),
    30     _factory       : null,
    31     _window        : null,
    32     _debug         : false, // mirrors signon.debug
    34     __pwmgr : null, // Password Manager service
    35     get _pwmgr() {
    36         if (!this.__pwmgr)
    37             this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
    38                            getService(Ci.nsILoginManager);
    39         return this.__pwmgr;
    40     },
    42     __promptService : null, // Prompt service for user interaction
    43     get _promptService() {
    44         if (!this.__promptService)
    45             this.__promptService =
    46                 Cc["@mozilla.org/embedcomp/prompt-service;1"].
    47                 getService(Ci.nsIPromptService2);
    48         return this.__promptService;
    49     },
    51     __strBundle : null, // String bundle for L10N
    52     get _strBundle() {
    53         if (!this.__strBundle) {
    54             var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
    55                              getService(Ci.nsIStringBundleService);
    56             this.__strBundle = bunService.createBundle(
    57                         "chrome://browser/locale/passwordmgr.properties");
    58             if (!this.__strBundle)
    59                 throw "String bundle for Login Manager not present!";
    60         }
    62         return this.__strBundle;
    63     },
    65     __brandBundle : null, // String bundle for L10N
    66     get _brandBundle() {
    67         if (!this.__brandBundle) {
    68             var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
    69                              getService(Ci.nsIStringBundleService);
    70             this.__brandBundle = bunService.createBundle(
    71                         "chrome://branding/locale/brand.properties");
    72             if (!this.__brandBundle)
    73                 throw "Branding string bundle not present!";
    74         }
    76         return this.__brandBundle;
    77     },
    80     __ellipsis : null,
    81     get _ellipsis() {
    82         if (!this.__ellipsis) {
    83             this.__ellipsis = "\u2026";
    84             try {
    85                 this.__ellipsis = Services.prefs.getComplexValue(
    86                                     "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
    87             } catch (e) { }
    88         }
    89         return this.__ellipsis;
    90     },
    93     /*
    94      * log
    95      *
    96      * Internal function for logging debug messages to the Error Console window.
    97      */
    98     log : function (message) {
    99         if (!this._debug)
   100             return;
   102         dump("Pwmgr Prompter: " + message + "\n");
   103         Services.console.logStringMessage("Pwmgr Prompter: " + message);
   104     },
   107     /* ---------- nsILoginManagerPrompter prompts ---------- */
   112     /*
   113      * init
   114      *
   115      */
   116     init : function (aWindow, aFactory) {
   117         this._window = aWindow;
   118         this._factory = aFactory || null;
   120         var prefBranch = Services.prefs.getBranch("signon.");
   121         this._debug = prefBranch.getBoolPref("debug");
   122         this.log("===== initialized =====");
   123     },
   126     /*
   127      * promptToSavePassword
   128      *
   129      */
   130     promptToSavePassword : function (aLogin) {
   131         var notifyBox = this._getNotifyBox();
   132         if (notifyBox)
   133             this._showSaveLoginNotification(notifyBox, aLogin);
   134     },
   137     /*
   138      * _showLoginNotification
   139      *
   140      * Displays a notification bar.
   141      *
   142      */
   143     _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
   144         var oldBar = aNotifyBox.getNotificationWithValue(aName);
   145         const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
   147         this.log("Adding new " + aName + " notification bar");
   148         var newBar = aNotifyBox.appendNotification(
   149                                 aText, aName,
   150                                 "chrome://browser/skin/images/infobar-key.png",
   151                                 priority, aButtons);
   153         // The page we're going to hasn't loaded yet, so we want to persist
   154         // across the first location change.
   155         newBar.persistence++;
   157         // Sites like Gmail perform a funky redirect dance before you end up
   158         // at the post-authentication page. I don't see a good way to
   159         // heuristically determine when to ignore such location changes, so
   160         // we'll try ignoring location changes based on a time interval.
   161         newBar.timeout = Date.now() + 20000; // 20 seconds
   163         if (oldBar) {
   164             this.log("(...and removing old " + aName + " notification bar)");
   165             aNotifyBox.removeNotification(oldBar);
   166         }
   167     },
   170     /*
   171      * _showSaveLoginNotification
   172      *
   173      * Displays a notification bar (rather than a popup), to allow the user to
   174      * save the specified login. This allows the user to see the results of
   175      * their login, and only save a login which they know worked.
   176      *
   177      */
   178     _showSaveLoginNotification : function (aNotifyBox, aLogin) {
   179         // Ugh. We can't use the strings from the popup window, because they
   180         // have the access key marked in the string (eg "Mo&zilla"), along
   181         // with some weird rules for handling access keys that do not occur
   182         // in the string, for L10N. See commonDialog.js's setLabelForNode().
   183         var neverButtonText =
   184               this._getLocalizedString("notifyBarNotForThisSiteButtonText");
   185         var neverButtonAccessKey =
   186               this._getLocalizedString("notifyBarNotForThisSiteButtonAccessKey");
   187         var rememberButtonText =
   188               this._getLocalizedString("notifyBarRememberPasswordButtonText");
   189         var rememberButtonAccessKey =
   190               this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
   192         var brandShortName =
   193               this._brandBundle.GetStringFromName("brandShortName");
   194         var displayHost = this._getShortDisplayHost(aLogin.hostname);
   195         var notificationText;
   196         if (aLogin.username) {
   197             var displayUser = this._sanitizeUsername(aLogin.username);
   198             notificationText  = this._getLocalizedString(
   199                                         "saveLoginText",
   200                                         [brandShortName, displayUser, displayHost]);
   201         } else {
   202             notificationText  = this._getLocalizedString(
   203                                         "saveLoginTextNoUsername",
   204                                         [brandShortName, displayHost]);
   205         }
   207         // The callbacks in |buttons| have a closure to access the variables
   208         // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
   209         // without a getService() call.
   210         var pwmgr = this._pwmgr;
   213         var buttons = [
   214             // "Remember" button
   215             {
   216                 label:     rememberButtonText,
   217                 accessKey: rememberButtonAccessKey,
   218                 popup:     null,
   219                 callback: function(aNotificationBar, aButton) {
   220                     pwmgr.addLogin(aLogin);
   221                 }
   222             },
   224             // "Never for this site" button
   225             {
   226                 label:     neverButtonText,
   227                 accessKey: neverButtonAccessKey,
   228                 popup:     null,
   229                 callback: function(aNotificationBar, aButton) {
   230                     pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
   231                 }
   232             }
   233         ];
   235         this._showLoginNotification(aNotifyBox, "password-save",
   236              notificationText, buttons);
   237     },
   240     /*
   241      * promptToChangePassword
   242      *
   243      * Called when we think we detect a password change for an existing
   244      * login, when the form being submitted contains multiple password
   245      * fields.
   246      *
   247      */
   248     promptToChangePassword : function (aOldLogin, aNewLogin) {
   249         var notifyBox = this._getNotifyBox();
   250         if (notifyBox)
   251             this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin.password);
   252     },
   254     /*
   255      * _showChangeLoginNotification
   256      *
   257      * Shows the Change Password notification bar.
   258      *
   259      */
   260     _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewPassword) {
   261         var notificationText;
   262         if (aOldLogin.username)
   263             notificationText  = this._getLocalizedString(
   264                                           "passwordChangeText",
   265                                           [aOldLogin.username]);
   266         else
   267             notificationText  = this._getLocalizedString(
   268                                           "passwordChangeTextNoUser");
   270         var changeButtonText =
   271               this._getLocalizedString("notifyBarChangeButtonText");
   272         var changeButtonAccessKey =
   273               this._getLocalizedString("notifyBarChangeButtonAccessKey");
   274         var dontChangeButtonText =
   275               this._getLocalizedString("notifyBarDontChangeButtonText2");
   276         var dontChangeButtonAccessKey =
   277               this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
   279         // The callbacks in |buttons| have a closure to access the variables
   280         // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
   281         // without a getService() call.
   282         var self = this;
   284         var buttons = [
   285             // "Yes" button
   286             {
   287                 label:     changeButtonText,
   288                 accessKey: changeButtonAccessKey,
   289                 popup:     null,
   290                 callback:  function(aNotificationBar, aButton) {
   291                     self._updateLogin(aOldLogin, aNewPassword);
   292                 }
   293             },
   295             // "No" button
   296             {
   297                 label:     dontChangeButtonText,
   298                 accessKey: dontChangeButtonAccessKey,
   299                 popup:     null,
   300                 callback:  function(aNotificationBar, aButton) {
   301                     // do nothing
   302                 }
   303             }
   304         ];
   306         this._showLoginNotification(aNotifyBox, "password-change",
   307              notificationText, buttons);
   308     },
   310     /*
   311      * promptToChangePasswordWithUsernames
   312      *
   313      * Called when we detect a password change in a form submission, but we
   314      * don't know which existing login (username) it's for. Asks the user
   315      * to select a username and confirm the password change.
   316      *
   317      * Note: The caller doesn't know the username for aNewLogin, so this
   318      *       function fills in .username and .usernameField with the values
   319      *       from the login selected by the user.
   320      * 
   321      * Note; XPCOM stupidity: |count| is just |logins.length|.
   322      */
   323     promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
   324         const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
   326         var usernames = logins.map(function (l) l.username);
   327         var dialogText  = this._getLocalizedString("userSelectText");
   328         var dialogTitle = this._getLocalizedString("passwordChangeTitle");
   329         var selectedIndex = { value: null };
   331         // If user selects ok, outparam.value is set to the index
   332         // of the selected username.
   333         var ok = this._promptService.select(null,
   334                                 dialogTitle, dialogText,
   335                                 usernames.length, usernames,
   336                                 selectedIndex);
   337         if (ok) {
   338             // Now that we know which login to use, modify its password.
   339             var selectedLogin = logins[selectedIndex.value];
   340             this.log("Updating password for user " + selectedLogin.username);
   341             this._updateLogin(selectedLogin, aNewLogin.password);
   342         }
   343     },
   346     /* ---------- Internal Methods ---------- */
   348     /*
   349      * _updateLogin
   350      */
   351     _updateLogin : function (login, newPassword) {
   352         var now = Date.now();
   353         var propBag = Cc["@mozilla.org/hash-property-bag;1"].
   354                       createInstance(Ci.nsIWritablePropertyBag);
   355         if (newPassword) {
   356             propBag.setProperty("password", newPassword);
   357             // Explicitly set the password change time here (even though it would
   358             // be changed automatically), to ensure that it's exactly the same
   359             // value as timeLastUsed.
   360             propBag.setProperty("timePasswordChanged", now);
   361         }
   362         propBag.setProperty("timeLastUsed", now);
   363         propBag.setProperty("timesUsedIncrement", 1);
   364         this._pwmgr.modifyLogin(login, propBag);
   365     },
   367     /*
   368      * _getNotifyWindow
   369      */
   370     _getNotifyWindow: function () {
   371         try {
   372             // Get topmost window, in case we're in a frame.
   373             var notifyWin = this._window.top;
   375             // Some sites pop up a temporary login window, when disappears
   376             // upon submission of credentials. We want to put the notification
   377             // bar in the opener window if this seems to be happening.
   378             if (notifyWin.opener) {
   379                 var chromeDoc = this._getChromeWindow(notifyWin).
   380                                      document.documentElement;
   381                 var webnav = notifyWin.
   382                              QueryInterface(Ci.nsIInterfaceRequestor).
   383                              getInterface(Ci.nsIWebNavigation);
   385                 // Check to see if the current window was opened with chrome
   386                 // disabled, and if so use the opener window. But if the window
   387                 // has been used to visit other pages (ie, has a history),
   388                 // assume it'll stick around and *don't* use the opener.
   389                 if (chromeDoc.getAttribute("chromehidden") &&
   390                     webnav.sessionHistory.count == 1) {
   391                     this.log("Using opener window for notification bar.");
   392                     notifyWin = notifyWin.opener;
   393                 }
   394             }
   396             return notifyWin;
   398         } catch (e) {
   399             // If any errors happen, just assume no notification box.
   400             this.log("Unable to get notify window");
   401             return null;
   402         }
   403     },
   405     /*
   406      * _getChromeWindow
   407      *
   408      * Given a content DOM window, returns the chrome window it's in.
   409      */
   410     _getChromeWindow: function (aWindow) {
   411         var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   412                                .getInterface(Ci.nsIWebNavigation)
   413                                .QueryInterface(Ci.nsIDocShell)
   414                                .chromeEventHandler.ownerDocument.defaultView;
   415         return chromeWin;
   416     },
   418     /*
   419      * _getNotifyBox
   420      *
   421      * Returns the notification box to this prompter, or null if there isn't
   422      * a notification box available.
   423      */
   424     _getNotifyBox : function () {
   425         let notifyBox = null;
   427         try {
   428             let notifyWin = this._getNotifyWindow();
   429             let windowID = notifyWin.QueryInterface(Ci.nsIInterfaceRequestor)
   430                                     .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
   432             // Get the chrome window for the content window we're using.
   433             // .wrappedJSObject needed here -- see bug 422974 comment 5.
   434             let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
   435             let browser = chromeWin.Browser.getBrowserForWindowId(windowID);
   437             notifyBox = chromeWin.getNotificationBox(browser);
   438         } catch (e) {
   439             Cu.reportError(e);
   440         }
   442         return notifyBox;
   443     },
   445     /*
   446      * _getLocalizedString
   447      *
   448      * Can be called as:
   449      *   _getLocalizedString("key1");
   450      *   _getLocalizedString("key2", ["arg1"]);
   451      *   _getLocalizedString("key3", ["arg1", "arg2"]);
   452      *   (etc)
   453      *
   454      * Returns the localized string for the specified key,
   455      * formatted if required.
   456      *
   457      */ 
   458     _getLocalizedString : function (key, formatArgs) {
   459         if (formatArgs)
   460             return this._strBundle.formatStringFromName(
   461                                         key, formatArgs, formatArgs.length);
   462         else
   463             return this._strBundle.GetStringFromName(key);
   464     },
   467     /*
   468      * _sanitizeUsername
   469      *
   470      * Sanitizes the specified username, by stripping quotes and truncating if
   471      * it's too long. This helps prevent an evil site from messing with the
   472      * "save password?" prompt too much.
   473      */
   474     _sanitizeUsername : function (username) {
   475         if (username.length > 30) {
   476             username = username.substring(0, 30);
   477             username += this._ellipsis;
   478         }
   479         return username.replace(/['"]/g, "");
   480     },
   483     /*
   484      * _getShortDisplayHost
   485      *
   486      * Converts a login's hostname field (a URL) to a short string for
   487      * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
   488      * "ftp://www.site.co.uk" --> "site.co.uk".
   489      */
   490     _getShortDisplayHost: function (aURIString) {
   491         var displayHost;
   493         var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
   494                           getService(Ci.nsIEffectiveTLDService);
   495         var idnService = Cc["@mozilla.org/network/idn-service;1"].
   496                          getService(Ci.nsIIDNService);
   497         try {
   498             var uri = Services.io.newURI(aURIString, null, null);
   499             var baseDomain = eTLDService.getBaseDomain(uri);
   500             displayHost = idnService.convertToDisplayIDN(baseDomain, {});
   501         } catch (e) {
   502             this.log("_getShortDisplayHost couldn't process " + aURIString);
   503         }
   505         if (!displayHost)
   506             displayHost = aURIString;
   508         return displayHost;
   509     },
   511 }; // end of LoginManagerPrompter implementation
   514 var component = [LoginManagerPrompter];
   515 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial