toolkit/components/passwordmgr/LoginManagerContent.jsm

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 /* 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/. */
     5 this.EXPORTED_SYMBOLS = ["LoginManagerContent"];
     7 const Ci = Components.interfaces;
     8 const Cr = Components.results;
     9 const Cc = Components.classes;
    10 const Cu = Components.utils;
    12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    13 Cu.import("resource://gre/modules/Services.jsm");
    14 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
    16 // These mirror signon.* prefs.
    17 var gEnabled, gDebug, gAutofillForms, gStoreWhenAutocompleteOff;
    19 function log(...pieces) {
    20     function generateLogMessage(args) {
    21         let strings = ['Login Manager (content):'];
    23         args.forEach(function(arg) {
    24             if (typeof arg === 'string') {
    25                 strings.push(arg);
    26             } else if (typeof arg === 'undefined') {
    27                 strings.push('undefined');
    28             } else if (arg === null) {
    29                 strings.push('null');
    30             } else {
    31                 try {
    32                   strings.push(JSON.stringify(arg, null, 2));
    33                 } catch(err) {
    34                   strings.push("<<something>>");
    35                 }
    36             }
    37         });
    38         return strings.join(' ');
    39     }
    41     if (!gDebug)
    42         return;
    44     let message = generateLogMessage(pieces);
    45     dump(message + "\n");
    46     Services.console.logStringMessage(message);
    47 }
    50 var observer = {
    51     QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
    52                                             Ci.nsIFormSubmitObserver,
    53                                             Ci.nsISupportsWeakReference]),
    55     // nsIFormSubmitObserver
    56     notify : function (formElement, aWindow, actionURI) {
    57         log("observer notified for form submission.");
    59         // We're invoked before the content's |onsubmit| handlers, so we
    60         // can grab form data before it might be modified (see bug 257781).
    62         try {
    63             LoginManagerContent._onFormSubmit(formElement);
    64         } catch (e) {
    65             log("Caught error in onFormSubmit:", e);
    66         }
    68         return true; // Always return true, or form submit will be canceled.
    69     },
    71     onPrefChange : function() {
    72         gDebug = Services.prefs.getBoolPref("signon.debug");
    73         gEnabled = Services.prefs.getBoolPref("signon.rememberSignons");
    74         gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms");
    75         gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff");
    76     },
    77 };
    79 Services.obs.addObserver(observer, "earlyformsubmit", false);
    80 var prefBranch = Services.prefs.getBranch("signon.");
    81 prefBranch.addObserver("", observer.onPrefChange, false);
    83 observer.onPrefChange(); // read initial values
    86 var LoginManagerContent = {
    88     __formFillService : null, // FormFillController, for username autocompleting
    89     get _formFillService() {
    90         if (!this.__formFillService)
    91             this.__formFillService =
    92                             Cc["@mozilla.org/satchel/form-fill-controller;1"].
    93                             getService(Ci.nsIFormFillController);
    94         return this.__formFillService;
    95     },
    98     onFormPassword: function (event) {
    99       if (!event.isTrusted)
   100           return;
   102       if (!gEnabled)
   103           return;
   105       let form = event.target;
   106       let doc = form.ownerDocument;
   108       log("onFormPassword for", doc.documentURI);
   110       // If there are no logins for this site, bail out now.
   111       let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
   112       if (!Services.logins.countLogins(formOrigin, "", null))
   113           return;
   115       // If we're currently displaying a master password prompt, defer
   116       // processing this form until the user handles the prompt.
   117       if (Services.logins.uiBusy) {
   118         log("deferring onFormPassword for", doc.documentURI);
   119         let self = this;
   120         let observer = {
   121             QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
   123             observe: function (subject, topic, data) {
   124                 log("Got deferred onFormPassword notification:", topic);
   125                 // Only run observer once.
   126                 Services.obs.removeObserver(this, "passwordmgr-crypto-login");
   127                 Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
   128                 if (topic == "passwordmgr-crypto-loginCanceled")
   129                     return;
   130                 self.onFormPassword(event);
   131             },
   132             handleEvent : function (event) {
   133                 // Not expected to be called
   134             }
   135         };
   136         // Trickyness follows: We want an observer, but don't want it to
   137         // cause leaks. So add the observer with a weak reference, and use
   138         // a dummy event listener (a strong reference) to keep it alive
   139         // until the form is destroyed.
   140         Services.obs.addObserver(observer, "passwordmgr-crypto-login", true);
   141         Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true);
   142         form.addEventListener("mozCleverClosureHack", observer);
   143         return;
   144       }
   146       let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isWindowPrivate(doc.defaultView);
   148       this._fillForm(form, autofillForm, false, false, null);
   149     },
   152     /*
   153      * onUsernameInput
   154      *
   155      * Listens for DOMAutoComplete and blur events on an input field.
   156      */
   157     onUsernameInput : function(event) {
   158         if (!event.isTrusted)
   159             return;
   161         if (!gEnabled)
   162             return;
   164         var acInputField = event.target;
   166         // This is probably a bit over-conservatative.
   167         if (!(acInputField.ownerDocument instanceof Ci.nsIDOMHTMLDocument))
   168             return;
   170         if (!this._isUsernameFieldType(acInputField))
   171             return;
   173         var acForm = acInputField.form;
   174         if (!acForm)
   175             return;
   177         // If the username is blank, bail out now -- we don't want
   178         // fillForm() to try filling in a login without a username
   179         // to filter on (bug 471906).
   180         if (!acInputField.value)
   181             return;
   183         log("onUsernameInput from", event.type);
   185         // Make sure the username field fillForm will use is the
   186         // same field as the autocomplete was activated on.
   187         var [usernameField, passwordField, ignored] =
   188             this._getFormFields(acForm, false);
   189         if (usernameField == acInputField && passwordField) {
   190             // If the user has a master password but itsn't logged in, bail
   191             // out now to prevent annoying prompts.
   192             if (!Services.logins.isLoggedIn)
   193                 return;
   195             this._fillForm(acForm, true, true, true, null);
   196         } else {
   197             // Ignore the event, it's for some input we don't care about.
   198         }
   199     },
   202     /*
   203      * _getPasswordFields
   204      *
   205      * Returns an array of password field elements for the specified form.
   206      * If no pw fields are found, or if more than 3 are found, then null
   207      * is returned.
   208      *
   209      * skipEmptyFields can be set to ignore password fields with no value.
   210      */
   211     _getPasswordFields : function (form, skipEmptyFields) {
   212         // Locate the password fields in the form.
   213         var pwFields = [];
   214         for (var i = 0; i < form.elements.length; i++) {
   215             var element = form.elements[i];
   216             if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
   217                 element.type != "password")
   218                 continue;
   220             if (skipEmptyFields && !element.value)
   221                 continue;
   223             pwFields[pwFields.length] = {
   224                                             index   : i,
   225                                             element : element
   226                                         };
   227         }
   229         // If too few or too many fields, bail out.
   230         if (pwFields.length == 0) {
   231             log("(form ignored -- no password fields.)");
   232             return null;
   233         } else if (pwFields.length > 3) {
   234             log("(form ignored -- too many password fields. [ got ",
   235                         pwFields.length, "])");
   236             return null;
   237         }
   239         return pwFields;
   240     },
   243     _isUsernameFieldType: function(element) {
   244         if (!(element instanceof Ci.nsIDOMHTMLInputElement))
   245             return false;
   247         let fieldType = (element.hasAttribute("type") ?
   248                          element.getAttribute("type").toLowerCase() :
   249                          element.type);
   250         if (fieldType == "text"  ||
   251             fieldType == "email" ||
   252             fieldType == "url"   ||
   253             fieldType == "tel"   ||
   254             fieldType == "number") {
   255             return true;
   256         }
   257         return false;
   258     },
   261     /*
   262      * _getFormFields
   263      *
   264      * Returns the username and password fields found in the form.
   265      * Can handle complex forms by trying to figure out what the
   266      * relevant fields are.
   267      *
   268      * Returns: [usernameField, newPasswordField, oldPasswordField]
   269      *
   270      * usernameField may be null.
   271      * newPasswordField will always be non-null.
   272      * oldPasswordField may be null. If null, newPasswordField is just
   273      * "theLoginField". If not null, the form is apparently a
   274      * change-password field, with oldPasswordField containing the password
   275      * that is being changed.
   276      */
   277     _getFormFields : function (form, isSubmission) {
   278         var usernameField = null;
   280         // Locate the password field(s) in the form. Up to 3 supported.
   281         // If there's no password field, there's nothing for us to do.
   282         var pwFields = this._getPasswordFields(form, isSubmission);
   283         if (!pwFields)
   284             return [null, null, null];
   287         // Locate the username field in the form by searching backwards
   288         // from the first passwordfield, assume the first text field is the
   289         // username. We might not find a username field if the user is
   290         // already logged in to the site.
   291         for (var i = pwFields[0].index - 1; i >= 0; i--) {
   292             var element = form.elements[i];
   293             if (this._isUsernameFieldType(element)) {
   294                 usernameField = element;
   295                 break;
   296             }
   297         }
   299         if (!usernameField)
   300             log("(form -- no username field found)");
   303         // If we're not submitting a form (it's a page load), there are no
   304         // password field values for us to use for identifying fields. So,
   305         // just assume the first password field is the one to be filled in.
   306         if (!isSubmission || pwFields.length == 1)
   307             return [usernameField, pwFields[0].element, null];
   310         // Try to figure out WTF is in the form based on the password values.
   311         var oldPasswordField, newPasswordField;
   312         var pw1 = pwFields[0].element.value;
   313         var pw2 = pwFields[1].element.value;
   314         var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
   316         if (pwFields.length == 3) {
   317             // Look for two identical passwords, that's the new password
   319             if (pw1 == pw2 && pw2 == pw3) {
   320                 // All 3 passwords the same? Weird! Treat as if 1 pw field.
   321                 newPasswordField = pwFields[0].element;
   322                 oldPasswordField = null;
   323             } else if (pw1 == pw2) {
   324                 newPasswordField = pwFields[0].element;
   325                 oldPasswordField = pwFields[2].element;
   326             } else if (pw2 == pw3) {
   327                 oldPasswordField = pwFields[0].element;
   328                 newPasswordField = pwFields[2].element;
   329             } else  if (pw1 == pw3) {
   330                 // A bit odd, but could make sense with the right page layout.
   331                 newPasswordField = pwFields[0].element;
   332                 oldPasswordField = pwFields[1].element;
   333             } else {
   334                 // We can't tell which of the 3 passwords should be saved.
   335                 log("(form ignored -- all 3 pw fields differ)");
   336                 return [null, null, null];
   337             }
   338         } else { // pwFields.length == 2
   339             if (pw1 == pw2) {
   340                 // Treat as if 1 pw field
   341                 newPasswordField = pwFields[0].element;
   342                 oldPasswordField = null;
   343             } else {
   344                 // Just assume that the 2nd password is the new password
   345                 oldPasswordField = pwFields[0].element;
   346                 newPasswordField = pwFields[1].element;
   347             }
   348         }
   350         return [usernameField, newPasswordField, oldPasswordField];
   351     },
   354     /*
   355      * _isAutoCompleteDisabled
   356      *
   357      * Returns true if the page requests autocomplete be disabled for the
   358      * specified form input.
   359      */
   360     _isAutocompleteDisabled :  function (element) {
   361         if (element && element.hasAttribute("autocomplete") &&
   362             element.getAttribute("autocomplete").toLowerCase() == "off")
   363             return true;
   365         return false;
   366     },
   369     /*
   370      * _onFormSubmit
   371      *
   372      * Called by the our observer when notified of a form submission.
   373      * [Note that this happens before any DOM onsubmit handlers are invoked.]
   374      * Looks for a password change in the submitted form, so we can update
   375      * our stored password.
   376      */
   377     _onFormSubmit : function (form) {
   379         // For E10S this will need to move.
   380         function getPrompter(aWindow) {
   381             var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
   382                               createInstance(Ci.nsILoginManagerPrompter);
   383             prompterSvc.init(aWindow);
   384             return prompterSvc;
   385         }
   387         var doc = form.ownerDocument;
   388         var win = doc.defaultView;
   390         if (PrivateBrowsingUtils.isWindowPrivate(win)) {
   391             // We won't do anything in private browsing mode anyway,
   392             // so there's no need to perform further checks.
   393             log("(form submission ignored in private browsing mode)");
   394             return;
   395         }
   397         // If password saving is disabled (globally or for host), bail out now.
   398         if (!gEnabled)
   399             return;
   401         var hostname = LoginUtils._getPasswordOrigin(doc.documentURI);
   402         if (!hostname) {
   403             log("(form submission ignored -- invalid hostname)");
   404             return;
   405         }
   407         // Somewhat gross hack - we don't want to show the "remember password"
   408         // notification on about:accounts for Firefox.
   409         let topWin = win.top;
   410         if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) {
   411             log("(form submission ignored -- about:accounts)");
   412             return;
   413         }
   415         var formSubmitURL = LoginUtils._getActionOrigin(form)
   416         if (!Services.logins.getLoginSavingEnabled(hostname)) {
   417             log("(form submission ignored -- saving is disabled for:", hostname, ")");
   418             return;
   419         }
   422         // Get the appropriate fields from the form.
   423         var [usernameField, newPasswordField, oldPasswordField] =
   424             this._getFormFields(form, true);
   426         // Need at least 1 valid password field to do anything.
   427         if (newPasswordField == null)
   428                 return;
   430         // Check for autocomplete=off attribute. We don't use it to prevent
   431         // autofilling (for existing logins), but won't save logins when it's
   432         // present and the storeWhenAutocompleteOff pref is false.
   433         // XXX spin out a bug that we don't update timeLastUsed in this case?
   434         if ((this._isAutocompleteDisabled(form) ||
   435              this._isAutocompleteDisabled(usernameField) ||
   436              this._isAutocompleteDisabled(newPasswordField) ||
   437              this._isAutocompleteDisabled(oldPasswordField)) &&
   438             !gStoreWhenAutocompleteOff) {
   439                 log("(form submission ignored -- autocomplete=off found)");
   440                 return;
   441         }
   444         var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
   445                         createInstance(Ci.nsILoginInfo);
   446         formLogin.init(hostname, formSubmitURL, null,
   447                     (usernameField ? usernameField.value : ""),
   448                     newPasswordField.value,
   449                     (usernameField ? usernameField.name  : ""),
   450                     newPasswordField.name);
   452         // If we didn't find a username field, but seem to be changing a
   453         // password, allow the user to select from a list of applicable
   454         // logins to update the password for.
   455         if (!usernameField && oldPasswordField) {
   457             var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null);
   459             if (logins.length == 0) {
   460                 // Could prompt to save this as a new password-only login.
   461                 // This seems uncommon, and might be wrong, so ignore.
   462                 log("(no logins for this host -- pwchange ignored)");
   463                 return;
   464             }
   466             var prompter = getPrompter(win);
   468             if (logins.length == 1) {
   469                 var oldLogin = logins[0];
   470                 formLogin.username      = oldLogin.username;
   471                 formLogin.usernameField = oldLogin.usernameField;
   473                 prompter.promptToChangePassword(oldLogin, formLogin);
   474             } else {
   475                 prompter.promptToChangePasswordWithUsernames(
   476                                     logins, logins.length, formLogin);
   477             }
   479             return;
   480         }
   483         // Look for an existing login that matches the form login.
   484         var existingLogin = null;
   485         var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null);
   487         for (var i = 0; i < logins.length; i++) {
   488             var same, login = logins[i];
   490             // If one login has a username but the other doesn't, ignore
   491             // the username when comparing and only match if they have the
   492             // same password. Otherwise, compare the logins and match even
   493             // if the passwords differ.
   494             if (!login.username && formLogin.username) {
   495                 var restoreMe = formLogin.username;
   496                 formLogin.username = "";
   497                 same = formLogin.matches(login, false);
   498                 formLogin.username = restoreMe;
   499             } else if (!formLogin.username && login.username) {
   500                 formLogin.username = login.username;
   501                 same = formLogin.matches(login, false);
   502                 formLogin.username = ""; // we know it's always blank.
   503             } else {
   504                 same = formLogin.matches(login, true);
   505             }
   507             if (same) {
   508                 existingLogin = login;
   509                 break;
   510             }
   511         }
   513         if (existingLogin) {
   514             log("Found an existing login matching this form submission");
   516             // Change password if needed.
   517             if (existingLogin.password != formLogin.password) {
   518                 log("...passwords differ, prompting to change.");
   519                 prompter = getPrompter(win);
   520                 prompter.promptToChangePassword(existingLogin, formLogin);
   521             } else {
   522                 // Update the lastUsed timestamp.
   523                 var propBag = Cc["@mozilla.org/hash-property-bag;1"].
   524                               createInstance(Ci.nsIWritablePropertyBag);
   525                 propBag.setProperty("timeLastUsed", Date.now());
   526                 propBag.setProperty("timesUsedIncrement", 1);
   527                 Services.logins.modifyLogin(existingLogin, propBag);
   528             }
   530             return;
   531         }
   534         // Prompt user to save login (via dialog or notification bar)
   535         prompter = getPrompter(win);
   536         prompter.promptToSavePassword(formLogin);
   537     },
   540     /*
   541      * _fillform
   542      *
   543      * Fill the form with login information if we can find it. This will find
   544      * an array of logins if not given any, otherwise it will use the logins
   545      * passed in. The logins are returned so they can be reused for
   546      * optimization. Success of action is also returned in format
   547      * [success, foundLogins]. autofillForm denotes if we should fill the form
   548      * in automatically, ignoreAutocomplete denotes if we should ignore
   549      * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo
   550      * for optimization
   551      */
   552     _fillForm : function (form, autofillForm, ignoreAutocomplete,
   553                           clobberPassword, foundLogins) {
   554         // Heuristically determine what the user/pass fields are
   555         // We do this before checking to see if logins are stored,
   556         // so that the user isn't prompted for a master password
   557         // without need.
   558         var [usernameField, passwordField, ignored] =
   559             this._getFormFields(form, false);
   561         // Need a valid password field to do anything.
   562         if (passwordField == null)
   563             return [false, foundLogins];
   565         // If the password field is disabled or read-only, there's nothing to do.
   566         if (passwordField.disabled || passwordField.readOnly) {
   567             log("not filling form, password field disabled or read-only");
   568             return [false, foundLogins];
   569         }
   571         // Need to get a list of logins if we weren't given them
   572         if (foundLogins == null) {
   573             var formOrigin =
   574                 LoginUtils._getPasswordOrigin(form.ownerDocument.documentURI);
   575             var actionOrigin = LoginUtils._getActionOrigin(form);
   576             foundLogins = Services.logins.findLogins({}, formOrigin, actionOrigin, null);
   577             log("found", foundLogins.length, "matching logins.");
   578         } else {
   579             log("reusing logins from last form.");
   580         }
   582         // Discard logins which have username/password values that don't
   583         // fit into the fields (as specified by the maxlength attribute).
   584         // The user couldn't enter these values anyway, and it helps
   585         // with sites that have an extra PIN to be entered (bug 391514)
   586         var maxUsernameLen = Number.MAX_VALUE;
   587         var maxPasswordLen = Number.MAX_VALUE;
   589         // If attribute wasn't set, default is -1.
   590         if (usernameField && usernameField.maxLength >= 0)
   591             maxUsernameLen = usernameField.maxLength;
   592         if (passwordField.maxLength >= 0)
   593             maxPasswordLen = passwordField.maxLength;
   595         var logins = foundLogins.filter(function (l) {
   596                 var fit = (l.username.length <= maxUsernameLen &&
   597                            l.password.length <= maxPasswordLen);
   598                 if (!fit)
   599                     log("Ignored", l.username, "login: won't fit");
   601                 return fit;
   602             }, this);
   605         // Nothing to do if we have no matching logins available.
   606         if (logins.length == 0)
   607             return [false, foundLogins];
   610         // The reason we didn't end up filling the form, if any.  We include
   611         // this in the formInfo object we send with the passwordmgr-found-logins
   612         // notification.  See the _notifyFoundLogins docs for possible values.
   613         var didntFillReason = null;
   615         // Attach autocomplete stuff to the username field, if we have
   616         // one. This is normally used to select from multiple accounts,
   617         // but even with one account we should refill if the user edits.
   618         if (usernameField)
   619             this._formFillService.markAsLoginManagerField(usernameField);
   621         // Don't clobber an existing password.
   622         if (passwordField.value && !clobberPassword) {
   623             didntFillReason = "existingPassword";
   624             this._notifyFoundLogins(didntFillReason, usernameField,
   625                                     passwordField, foundLogins, null);
   626             return [false, foundLogins];
   627         }
   629         // If the form has an autocomplete=off attribute in play, don't
   630         // fill in the login automatically. We check this after attaching
   631         // the autocomplete stuff to the username field, so the user can
   632         // still manually select a login to be filled in.
   633         var isFormDisabled = false;
   634         if (!ignoreAutocomplete &&
   635             (this._isAutocompleteDisabled(form) ||
   636              this._isAutocompleteDisabled(usernameField) ||
   637              this._isAutocompleteDisabled(passwordField))) {
   639             isFormDisabled = true;
   640             log("form not filled, has autocomplete=off");
   641         }
   643         // Variable such that we reduce code duplication and can be sure we
   644         // should be firing notifications if and only if we can fill the form.
   645         var selectedLogin = null;
   647         if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) {
   648             // If username was specified in the field, it's disabled or it's readOnly, only fill in the
   649             // password if we find a matching login.
   650             var username = usernameField.value.toLowerCase();
   652             let matchingLogins = logins.filter(function(l)
   653                                      l.username.toLowerCase() == username);
   654             if (matchingLogins.length) {
   655                 selectedLogin = matchingLogins[0];
   656             } else {
   657                 didntFillReason = "existingUsername";
   658                 log("Password not filled. None of the stored logins match the username already present.");
   659             }
   660         } else if (logins.length == 1) {
   661             selectedLogin = logins[0];
   662         } else {
   663             // We have multiple logins. Handle a special case here, for sites
   664             // which have a normal user+pass login *and* a password-only login
   665             // (eg, a PIN). Prefer the login that matches the type of the form
   666             // (user+pass or pass-only) when there's exactly one that matches.
   667             let matchingLogins;
   668             if (usernameField)
   669                 matchingLogins = logins.filter(function(l) l.username);
   670             else
   671                 matchingLogins = logins.filter(function(l) !l.username);
   672             if (matchingLogins.length == 1) {
   673                 selectedLogin = matchingLogins[0];
   674             } else {
   675                 didntFillReason = "multipleLogins";
   676                 log("Multiple logins for form, so not filling any.");
   677             }
   678         }
   680         var didFillForm = false;
   681         if (selectedLogin && autofillForm && !isFormDisabled) {
   682             // Fill the form
   683             // Don't modify the username field if it's disabled or readOnly so we preserve its case.
   684             if (usernameField && !(usernameField.disabled || usernameField.readOnly))
   685                 usernameField.value = selectedLogin.username;
   686             passwordField.value = selectedLogin.password;
   687             didFillForm = true;
   688         } else if (selectedLogin && !autofillForm) {
   689             // For when autofillForm is false, but we still have the information
   690             // to fill a form, we notify observers.
   691             didntFillReason = "noAutofillForms";
   692             Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
   693             log("autofillForms=false but form can be filled; notified observers");
   694         } else if (selectedLogin && isFormDisabled) {
   695             // For when autocomplete is off, but we still have the information
   696             // to fill a form, we notify observers.
   697             didntFillReason = "autocompleteOff";
   698             Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
   699             log("autocomplete=off but form can be filled; notified observers");
   700         }
   702         this._notifyFoundLogins(didntFillReason, usernameField, passwordField,
   703                                 foundLogins, selectedLogin);
   705         return [didFillForm, foundLogins];
   706     },
   709     /**
   710      * Notify observers about an attempt to fill a form that resulted in some
   711      * saved logins being found for the form.
   712      *
   713      * This does not get called if the login manager attempts to fill a form
   714      * but does not find any saved logins.  It does, however, get called when
   715      * the login manager does find saved logins whether or not it actually
   716      * fills the form with one of them.
   717      *
   718      * @param didntFillReason {String}
   719      *        the reason the login manager didn't fill the form, if any;
   720      *        if the value of this parameter is null, then the form was filled;
   721      *        otherwise, this parameter will be one of these values:
   722      *          existingUsername: the username field already contains a username
   723      *                            that doesn't match any stored usernames
   724      *          existingPassword: the password field already contains a password
   725      *          autocompleteOff:  autocomplete has been disabled for the form
   726      *                            or its username or password fields
   727      *          multipleLogins:   we have multiple logins for the form
   728      *          noAutofillForms:  the autofillForms pref is set to false
   729      *
   730      * @param usernameField   {HTMLInputElement}
   731      *        the username field detected by the login manager, if any;
   732      *        otherwise null
   733      *
   734      * @param passwordField   {HTMLInputElement}
   735      *        the password field detected by the login manager
   736      *
   737      * @param foundLogins     {Array}
   738      *        an array of nsILoginInfos that can be used to fill the form
   739      *
   740      * @param selectedLogin   {nsILoginInfo}
   741      *        the nsILoginInfo that was/would be used to fill the form, if any;
   742      *        otherwise null; whether or not it was actually used depends on
   743      *        the value of the didntFillReason parameter
   744      */
   745     _notifyFoundLogins : function (didntFillReason, usernameField,
   746                                    passwordField, foundLogins, selectedLogin) {
   747         // We need .setProperty(), which is a method on the original
   748         // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2
   749         // doesn't inherit from that, so the additional QI is needed.
   750         let formInfo = Cc["@mozilla.org/hash-property-bag;1"].
   751                        createInstance(Ci.nsIWritablePropertyBag2).
   752                        QueryInterface(Ci.nsIWritablePropertyBag);
   754         formInfo.setPropertyAsACString("didntFillReason", didntFillReason);
   755         formInfo.setPropertyAsInterface("usernameField", usernameField);
   756         formInfo.setPropertyAsInterface("passwordField", passwordField);
   757         formInfo.setProperty("foundLogins", foundLogins.concat());
   758         formInfo.setPropertyAsInterface("selectedLogin", selectedLogin);
   760         Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null);
   761     },
   763 };
   768 LoginUtils = {
   769     /*
   770      * _getPasswordOrigin
   771      *
   772      * Get the parts of the URL we want for identification.
   773      */
   774     _getPasswordOrigin : function (uriString, allowJS) {
   775         var realm = "";
   776         try {
   777             var uri = Services.io.newURI(uriString, null, null);
   779             if (allowJS && uri.scheme == "javascript")
   780                 return "javascript:"
   782             realm = uri.scheme + "://" + uri.host;
   784             // If the URI explicitly specified a port, only include it when
   785             // it's not the default. (We never want "http://foo.com:80")
   786             var port = uri.port;
   787             if (port != -1) {
   788                 var handler = Services.io.getProtocolHandler(uri.scheme);
   789                 if (port != handler.defaultPort)
   790                     realm += ":" + port;
   791             }
   793         } catch (e) {
   794             // bug 159484 - disallow url types that don't support a hostPort.
   795             // (although we handle "javascript:..." as a special case above.)
   796             log("Couldn't parse origin for", uriString);
   797             realm = null;
   798         }
   800         return realm;
   801     },
   803     _getActionOrigin : function (form) {
   804         var uriString = form.action;
   806         // A blank or missing action submits to where it came from.
   807         if (uriString == "")
   808             uriString = form.baseURI; // ala bug 297761
   810         return this._getPasswordOrigin(uriString, true);
   811     },
   813 };

mercurial