toolkit/components/passwordmgr/LoginManagerContent.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,813 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = ["LoginManagerContent"];
     1.9 +
    1.10 +const Ci = Components.interfaces;
    1.11 +const Cr = Components.results;
    1.12 +const Cc = Components.classes;
    1.13 +const Cu = Components.utils;
    1.14 +
    1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.16 +Cu.import("resource://gre/modules/Services.jsm");
    1.17 +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
    1.18 +
    1.19 +// These mirror signon.* prefs.
    1.20 +var gEnabled, gDebug, gAutofillForms, gStoreWhenAutocompleteOff;
    1.21 +
    1.22 +function log(...pieces) {
    1.23 +    function generateLogMessage(args) {
    1.24 +        let strings = ['Login Manager (content):'];
    1.25 +
    1.26 +        args.forEach(function(arg) {
    1.27 +            if (typeof arg === 'string') {
    1.28 +                strings.push(arg);
    1.29 +            } else if (typeof arg === 'undefined') {
    1.30 +                strings.push('undefined');
    1.31 +            } else if (arg === null) {
    1.32 +                strings.push('null');
    1.33 +            } else {
    1.34 +                try {
    1.35 +                  strings.push(JSON.stringify(arg, null, 2));
    1.36 +                } catch(err) {
    1.37 +                  strings.push("<<something>>");
    1.38 +                }
    1.39 +            }
    1.40 +        });
    1.41 +        return strings.join(' ');
    1.42 +    }
    1.43 +
    1.44 +    if (!gDebug)
    1.45 +        return;
    1.46 +
    1.47 +    let message = generateLogMessage(pieces);
    1.48 +    dump(message + "\n");
    1.49 +    Services.console.logStringMessage(message);
    1.50 +}
    1.51 +
    1.52 +
    1.53 +var observer = {
    1.54 +    QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
    1.55 +                                            Ci.nsIFormSubmitObserver,
    1.56 +                                            Ci.nsISupportsWeakReference]),
    1.57 +
    1.58 +    // nsIFormSubmitObserver
    1.59 +    notify : function (formElement, aWindow, actionURI) {
    1.60 +        log("observer notified for form submission.");
    1.61 +
    1.62 +        // We're invoked before the content's |onsubmit| handlers, so we
    1.63 +        // can grab form data before it might be modified (see bug 257781).
    1.64 +
    1.65 +        try {
    1.66 +            LoginManagerContent._onFormSubmit(formElement);
    1.67 +        } catch (e) {
    1.68 +            log("Caught error in onFormSubmit:", e);
    1.69 +        }
    1.70 +
    1.71 +        return true; // Always return true, or form submit will be canceled.
    1.72 +    },
    1.73 +
    1.74 +    onPrefChange : function() {
    1.75 +        gDebug = Services.prefs.getBoolPref("signon.debug");
    1.76 +        gEnabled = Services.prefs.getBoolPref("signon.rememberSignons");
    1.77 +        gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms");
    1.78 +        gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff");
    1.79 +    },
    1.80 +};
    1.81 +
    1.82 +Services.obs.addObserver(observer, "earlyformsubmit", false);
    1.83 +var prefBranch = Services.prefs.getBranch("signon.");
    1.84 +prefBranch.addObserver("", observer.onPrefChange, false);
    1.85 +
    1.86 +observer.onPrefChange(); // read initial values
    1.87 +
    1.88 +
    1.89 +var LoginManagerContent = {
    1.90 +
    1.91 +    __formFillService : null, // FormFillController, for username autocompleting
    1.92 +    get _formFillService() {
    1.93 +        if (!this.__formFillService)
    1.94 +            this.__formFillService =
    1.95 +                            Cc["@mozilla.org/satchel/form-fill-controller;1"].
    1.96 +                            getService(Ci.nsIFormFillController);
    1.97 +        return this.__formFillService;
    1.98 +    },
    1.99 +
   1.100 +
   1.101 +    onFormPassword: function (event) {
   1.102 +      if (!event.isTrusted)
   1.103 +          return;
   1.104 +
   1.105 +      if (!gEnabled)
   1.106 +          return;
   1.107 +
   1.108 +      let form = event.target;
   1.109 +      let doc = form.ownerDocument;
   1.110 +
   1.111 +      log("onFormPassword for", doc.documentURI);
   1.112 +
   1.113 +      // If there are no logins for this site, bail out now.
   1.114 +      let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
   1.115 +      if (!Services.logins.countLogins(formOrigin, "", null))
   1.116 +          return;
   1.117 +
   1.118 +      // If we're currently displaying a master password prompt, defer
   1.119 +      // processing this form until the user handles the prompt.
   1.120 +      if (Services.logins.uiBusy) {
   1.121 +        log("deferring onFormPassword for", doc.documentURI);
   1.122 +        let self = this;
   1.123 +        let observer = {
   1.124 +            QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
   1.125 +
   1.126 +            observe: function (subject, topic, data) {
   1.127 +                log("Got deferred onFormPassword notification:", topic);
   1.128 +                // Only run observer once.
   1.129 +                Services.obs.removeObserver(this, "passwordmgr-crypto-login");
   1.130 +                Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
   1.131 +                if (topic == "passwordmgr-crypto-loginCanceled")
   1.132 +                    return;
   1.133 +                self.onFormPassword(event);
   1.134 +            },
   1.135 +            handleEvent : function (event) {
   1.136 +                // Not expected to be called
   1.137 +            }
   1.138 +        };
   1.139 +        // Trickyness follows: We want an observer, but don't want it to
   1.140 +        // cause leaks. So add the observer with a weak reference, and use
   1.141 +        // a dummy event listener (a strong reference) to keep it alive
   1.142 +        // until the form is destroyed.
   1.143 +        Services.obs.addObserver(observer, "passwordmgr-crypto-login", true);
   1.144 +        Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true);
   1.145 +        form.addEventListener("mozCleverClosureHack", observer);
   1.146 +        return;
   1.147 +      }
   1.148 +
   1.149 +      let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isWindowPrivate(doc.defaultView);
   1.150 +
   1.151 +      this._fillForm(form, autofillForm, false, false, null);
   1.152 +    },
   1.153 +
   1.154 +
   1.155 +    /*
   1.156 +     * onUsernameInput
   1.157 +     *
   1.158 +     * Listens for DOMAutoComplete and blur events on an input field.
   1.159 +     */
   1.160 +    onUsernameInput : function(event) {
   1.161 +        if (!event.isTrusted)
   1.162 +            return;
   1.163 +
   1.164 +        if (!gEnabled)
   1.165 +            return;
   1.166 +
   1.167 +        var acInputField = event.target;
   1.168 +
   1.169 +        // This is probably a bit over-conservatative.
   1.170 +        if (!(acInputField.ownerDocument instanceof Ci.nsIDOMHTMLDocument))
   1.171 +            return;
   1.172 +
   1.173 +        if (!this._isUsernameFieldType(acInputField))
   1.174 +            return;
   1.175 +
   1.176 +        var acForm = acInputField.form;
   1.177 +        if (!acForm)
   1.178 +            return;
   1.179 +
   1.180 +        // If the username is blank, bail out now -- we don't want
   1.181 +        // fillForm() to try filling in a login without a username
   1.182 +        // to filter on (bug 471906).
   1.183 +        if (!acInputField.value)
   1.184 +            return;
   1.185 +
   1.186 +        log("onUsernameInput from", event.type);
   1.187 +
   1.188 +        // Make sure the username field fillForm will use is the
   1.189 +        // same field as the autocomplete was activated on.
   1.190 +        var [usernameField, passwordField, ignored] =
   1.191 +            this._getFormFields(acForm, false);
   1.192 +        if (usernameField == acInputField && passwordField) {
   1.193 +            // If the user has a master password but itsn't logged in, bail
   1.194 +            // out now to prevent annoying prompts.
   1.195 +            if (!Services.logins.isLoggedIn)
   1.196 +                return;
   1.197 +
   1.198 +            this._fillForm(acForm, true, true, true, null);
   1.199 +        } else {
   1.200 +            // Ignore the event, it's for some input we don't care about.
   1.201 +        }
   1.202 +    },
   1.203 +
   1.204 +
   1.205 +    /*
   1.206 +     * _getPasswordFields
   1.207 +     *
   1.208 +     * Returns an array of password field elements for the specified form.
   1.209 +     * If no pw fields are found, or if more than 3 are found, then null
   1.210 +     * is returned.
   1.211 +     *
   1.212 +     * skipEmptyFields can be set to ignore password fields with no value.
   1.213 +     */
   1.214 +    _getPasswordFields : function (form, skipEmptyFields) {
   1.215 +        // Locate the password fields in the form.
   1.216 +        var pwFields = [];
   1.217 +        for (var i = 0; i < form.elements.length; i++) {
   1.218 +            var element = form.elements[i];
   1.219 +            if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
   1.220 +                element.type != "password")
   1.221 +                continue;
   1.222 +
   1.223 +            if (skipEmptyFields && !element.value)
   1.224 +                continue;
   1.225 +
   1.226 +            pwFields[pwFields.length] = {
   1.227 +                                            index   : i,
   1.228 +                                            element : element
   1.229 +                                        };
   1.230 +        }
   1.231 +
   1.232 +        // If too few or too many fields, bail out.
   1.233 +        if (pwFields.length == 0) {
   1.234 +            log("(form ignored -- no password fields.)");
   1.235 +            return null;
   1.236 +        } else if (pwFields.length > 3) {
   1.237 +            log("(form ignored -- too many password fields. [ got ",
   1.238 +                        pwFields.length, "])");
   1.239 +            return null;
   1.240 +        }
   1.241 +
   1.242 +        return pwFields;
   1.243 +    },
   1.244 +
   1.245 +
   1.246 +    _isUsernameFieldType: function(element) {
   1.247 +        if (!(element instanceof Ci.nsIDOMHTMLInputElement))
   1.248 +            return false;
   1.249 +
   1.250 +        let fieldType = (element.hasAttribute("type") ?
   1.251 +                         element.getAttribute("type").toLowerCase() :
   1.252 +                         element.type);
   1.253 +        if (fieldType == "text"  ||
   1.254 +            fieldType == "email" ||
   1.255 +            fieldType == "url"   ||
   1.256 +            fieldType == "tel"   ||
   1.257 +            fieldType == "number") {
   1.258 +            return true;
   1.259 +        }
   1.260 +        return false;
   1.261 +    },
   1.262 +
   1.263 +
   1.264 +    /*
   1.265 +     * _getFormFields
   1.266 +     *
   1.267 +     * Returns the username and password fields found in the form.
   1.268 +     * Can handle complex forms by trying to figure out what the
   1.269 +     * relevant fields are.
   1.270 +     *
   1.271 +     * Returns: [usernameField, newPasswordField, oldPasswordField]
   1.272 +     *
   1.273 +     * usernameField may be null.
   1.274 +     * newPasswordField will always be non-null.
   1.275 +     * oldPasswordField may be null. If null, newPasswordField is just
   1.276 +     * "theLoginField". If not null, the form is apparently a
   1.277 +     * change-password field, with oldPasswordField containing the password
   1.278 +     * that is being changed.
   1.279 +     */
   1.280 +    _getFormFields : function (form, isSubmission) {
   1.281 +        var usernameField = null;
   1.282 +
   1.283 +        // Locate the password field(s) in the form. Up to 3 supported.
   1.284 +        // If there's no password field, there's nothing for us to do.
   1.285 +        var pwFields = this._getPasswordFields(form, isSubmission);
   1.286 +        if (!pwFields)
   1.287 +            return [null, null, null];
   1.288 +
   1.289 +
   1.290 +        // Locate the username field in the form by searching backwards
   1.291 +        // from the first passwordfield, assume the first text field is the
   1.292 +        // username. We might not find a username field if the user is
   1.293 +        // already logged in to the site.
   1.294 +        for (var i = pwFields[0].index - 1; i >= 0; i--) {
   1.295 +            var element = form.elements[i];
   1.296 +            if (this._isUsernameFieldType(element)) {
   1.297 +                usernameField = element;
   1.298 +                break;
   1.299 +            }
   1.300 +        }
   1.301 +
   1.302 +        if (!usernameField)
   1.303 +            log("(form -- no username field found)");
   1.304 +
   1.305 +
   1.306 +        // If we're not submitting a form (it's a page load), there are no
   1.307 +        // password field values for us to use for identifying fields. So,
   1.308 +        // just assume the first password field is the one to be filled in.
   1.309 +        if (!isSubmission || pwFields.length == 1)
   1.310 +            return [usernameField, pwFields[0].element, null];
   1.311 +
   1.312 +
   1.313 +        // Try to figure out WTF is in the form based on the password values.
   1.314 +        var oldPasswordField, newPasswordField;
   1.315 +        var pw1 = pwFields[0].element.value;
   1.316 +        var pw2 = pwFields[1].element.value;
   1.317 +        var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
   1.318 +
   1.319 +        if (pwFields.length == 3) {
   1.320 +            // Look for two identical passwords, that's the new password
   1.321 +
   1.322 +            if (pw1 == pw2 && pw2 == pw3) {
   1.323 +                // All 3 passwords the same? Weird! Treat as if 1 pw field.
   1.324 +                newPasswordField = pwFields[0].element;
   1.325 +                oldPasswordField = null;
   1.326 +            } else if (pw1 == pw2) {
   1.327 +                newPasswordField = pwFields[0].element;
   1.328 +                oldPasswordField = pwFields[2].element;
   1.329 +            } else if (pw2 == pw3) {
   1.330 +                oldPasswordField = pwFields[0].element;
   1.331 +                newPasswordField = pwFields[2].element;
   1.332 +            } else  if (pw1 == pw3) {
   1.333 +                // A bit odd, but could make sense with the right page layout.
   1.334 +                newPasswordField = pwFields[0].element;
   1.335 +                oldPasswordField = pwFields[1].element;
   1.336 +            } else {
   1.337 +                // We can't tell which of the 3 passwords should be saved.
   1.338 +                log("(form ignored -- all 3 pw fields differ)");
   1.339 +                return [null, null, null];
   1.340 +            }
   1.341 +        } else { // pwFields.length == 2
   1.342 +            if (pw1 == pw2) {
   1.343 +                // Treat as if 1 pw field
   1.344 +                newPasswordField = pwFields[0].element;
   1.345 +                oldPasswordField = null;
   1.346 +            } else {
   1.347 +                // Just assume that the 2nd password is the new password
   1.348 +                oldPasswordField = pwFields[0].element;
   1.349 +                newPasswordField = pwFields[1].element;
   1.350 +            }
   1.351 +        }
   1.352 +
   1.353 +        return [usernameField, newPasswordField, oldPasswordField];
   1.354 +    },
   1.355 +
   1.356 +
   1.357 +    /*
   1.358 +     * _isAutoCompleteDisabled
   1.359 +     *
   1.360 +     * Returns true if the page requests autocomplete be disabled for the
   1.361 +     * specified form input.
   1.362 +     */
   1.363 +    _isAutocompleteDisabled :  function (element) {
   1.364 +        if (element && element.hasAttribute("autocomplete") &&
   1.365 +            element.getAttribute("autocomplete").toLowerCase() == "off")
   1.366 +            return true;
   1.367 +        
   1.368 +        return false;
   1.369 +    },
   1.370 +
   1.371 +
   1.372 +    /*
   1.373 +     * _onFormSubmit
   1.374 +     *
   1.375 +     * Called by the our observer when notified of a form submission.
   1.376 +     * [Note that this happens before any DOM onsubmit handlers are invoked.]
   1.377 +     * Looks for a password change in the submitted form, so we can update
   1.378 +     * our stored password.
   1.379 +     */
   1.380 +    _onFormSubmit : function (form) {
   1.381 +
   1.382 +        // For E10S this will need to move.
   1.383 +        function getPrompter(aWindow) {
   1.384 +            var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
   1.385 +                              createInstance(Ci.nsILoginManagerPrompter);
   1.386 +            prompterSvc.init(aWindow);
   1.387 +            return prompterSvc;
   1.388 +        }
   1.389 +
   1.390 +        var doc = form.ownerDocument;
   1.391 +        var win = doc.defaultView;
   1.392 +
   1.393 +        if (PrivateBrowsingUtils.isWindowPrivate(win)) {
   1.394 +            // We won't do anything in private browsing mode anyway,
   1.395 +            // so there's no need to perform further checks.
   1.396 +            log("(form submission ignored in private browsing mode)");
   1.397 +            return;
   1.398 +        }
   1.399 +
   1.400 +        // If password saving is disabled (globally or for host), bail out now.
   1.401 +        if (!gEnabled)
   1.402 +            return;
   1.403 +
   1.404 +        var hostname = LoginUtils._getPasswordOrigin(doc.documentURI);
   1.405 +        if (!hostname) {
   1.406 +            log("(form submission ignored -- invalid hostname)");
   1.407 +            return;
   1.408 +        }
   1.409 +
   1.410 +        // Somewhat gross hack - we don't want to show the "remember password"
   1.411 +        // notification on about:accounts for Firefox.
   1.412 +        let topWin = win.top;
   1.413 +        if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) {
   1.414 +            log("(form submission ignored -- about:accounts)");
   1.415 +            return;
   1.416 +        }
   1.417 +
   1.418 +        var formSubmitURL = LoginUtils._getActionOrigin(form)
   1.419 +        if (!Services.logins.getLoginSavingEnabled(hostname)) {
   1.420 +            log("(form submission ignored -- saving is disabled for:", hostname, ")");
   1.421 +            return;
   1.422 +        }
   1.423 +
   1.424 +
   1.425 +        // Get the appropriate fields from the form.
   1.426 +        var [usernameField, newPasswordField, oldPasswordField] =
   1.427 +            this._getFormFields(form, true);
   1.428 +
   1.429 +        // Need at least 1 valid password field to do anything.
   1.430 +        if (newPasswordField == null)
   1.431 +                return;
   1.432 +
   1.433 +        // Check for autocomplete=off attribute. We don't use it to prevent
   1.434 +        // autofilling (for existing logins), but won't save logins when it's
   1.435 +        // present and the storeWhenAutocompleteOff pref is false.
   1.436 +        // XXX spin out a bug that we don't update timeLastUsed in this case?
   1.437 +        if ((this._isAutocompleteDisabled(form) ||
   1.438 +             this._isAutocompleteDisabled(usernameField) ||
   1.439 +             this._isAutocompleteDisabled(newPasswordField) ||
   1.440 +             this._isAutocompleteDisabled(oldPasswordField)) &&
   1.441 +            !gStoreWhenAutocompleteOff) {
   1.442 +                log("(form submission ignored -- autocomplete=off found)");
   1.443 +                return;
   1.444 +        }
   1.445 +
   1.446 +
   1.447 +        var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
   1.448 +                        createInstance(Ci.nsILoginInfo);
   1.449 +        formLogin.init(hostname, formSubmitURL, null,
   1.450 +                    (usernameField ? usernameField.value : ""),
   1.451 +                    newPasswordField.value,
   1.452 +                    (usernameField ? usernameField.name  : ""),
   1.453 +                    newPasswordField.name);
   1.454 +
   1.455 +        // If we didn't find a username field, but seem to be changing a
   1.456 +        // password, allow the user to select from a list of applicable
   1.457 +        // logins to update the password for.
   1.458 +        if (!usernameField && oldPasswordField) {
   1.459 +
   1.460 +            var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null);
   1.461 +
   1.462 +            if (logins.length == 0) {
   1.463 +                // Could prompt to save this as a new password-only login.
   1.464 +                // This seems uncommon, and might be wrong, so ignore.
   1.465 +                log("(no logins for this host -- pwchange ignored)");
   1.466 +                return;
   1.467 +            }
   1.468 +
   1.469 +            var prompter = getPrompter(win);
   1.470 +
   1.471 +            if (logins.length == 1) {
   1.472 +                var oldLogin = logins[0];
   1.473 +                formLogin.username      = oldLogin.username;
   1.474 +                formLogin.usernameField = oldLogin.usernameField;
   1.475 +
   1.476 +                prompter.promptToChangePassword(oldLogin, formLogin);
   1.477 +            } else {
   1.478 +                prompter.promptToChangePasswordWithUsernames(
   1.479 +                                    logins, logins.length, formLogin);
   1.480 +            }
   1.481 +
   1.482 +            return;
   1.483 +        }
   1.484 +
   1.485 +
   1.486 +        // Look for an existing login that matches the form login.
   1.487 +        var existingLogin = null;
   1.488 +        var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null);
   1.489 +
   1.490 +        for (var i = 0; i < logins.length; i++) {
   1.491 +            var same, login = logins[i];
   1.492 +
   1.493 +            // If one login has a username but the other doesn't, ignore
   1.494 +            // the username when comparing and only match if they have the
   1.495 +            // same password. Otherwise, compare the logins and match even
   1.496 +            // if the passwords differ.
   1.497 +            if (!login.username && formLogin.username) {
   1.498 +                var restoreMe = formLogin.username;
   1.499 +                formLogin.username = "";
   1.500 +                same = formLogin.matches(login, false);
   1.501 +                formLogin.username = restoreMe;
   1.502 +            } else if (!formLogin.username && login.username) {
   1.503 +                formLogin.username = login.username;
   1.504 +                same = formLogin.matches(login, false);
   1.505 +                formLogin.username = ""; // we know it's always blank.
   1.506 +            } else {
   1.507 +                same = formLogin.matches(login, true);
   1.508 +            }
   1.509 +
   1.510 +            if (same) {
   1.511 +                existingLogin = login;
   1.512 +                break;
   1.513 +            }
   1.514 +        }
   1.515 +
   1.516 +        if (existingLogin) {
   1.517 +            log("Found an existing login matching this form submission");
   1.518 +
   1.519 +            // Change password if needed.
   1.520 +            if (existingLogin.password != formLogin.password) {
   1.521 +                log("...passwords differ, prompting to change.");
   1.522 +                prompter = getPrompter(win);
   1.523 +                prompter.promptToChangePassword(existingLogin, formLogin);
   1.524 +            } else {
   1.525 +                // Update the lastUsed timestamp.
   1.526 +                var propBag = Cc["@mozilla.org/hash-property-bag;1"].
   1.527 +                              createInstance(Ci.nsIWritablePropertyBag);
   1.528 +                propBag.setProperty("timeLastUsed", Date.now());
   1.529 +                propBag.setProperty("timesUsedIncrement", 1);
   1.530 +                Services.logins.modifyLogin(existingLogin, propBag);
   1.531 +            }
   1.532 +
   1.533 +            return;
   1.534 +        }
   1.535 +
   1.536 +
   1.537 +        // Prompt user to save login (via dialog or notification bar)
   1.538 +        prompter = getPrompter(win);
   1.539 +        prompter.promptToSavePassword(formLogin);
   1.540 +    },
   1.541 +
   1.542 +
   1.543 +    /*
   1.544 +     * _fillform
   1.545 +     *
   1.546 +     * Fill the form with login information if we can find it. This will find
   1.547 +     * an array of logins if not given any, otherwise it will use the logins
   1.548 +     * passed in. The logins are returned so they can be reused for
   1.549 +     * optimization. Success of action is also returned in format
   1.550 +     * [success, foundLogins]. autofillForm denotes if we should fill the form
   1.551 +     * in automatically, ignoreAutocomplete denotes if we should ignore
   1.552 +     * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo
   1.553 +     * for optimization
   1.554 +     */
   1.555 +    _fillForm : function (form, autofillForm, ignoreAutocomplete,
   1.556 +                          clobberPassword, foundLogins) {
   1.557 +        // Heuristically determine what the user/pass fields are
   1.558 +        // We do this before checking to see if logins are stored,
   1.559 +        // so that the user isn't prompted for a master password
   1.560 +        // without need.
   1.561 +        var [usernameField, passwordField, ignored] =
   1.562 +            this._getFormFields(form, false);
   1.563 +
   1.564 +        // Need a valid password field to do anything.
   1.565 +        if (passwordField == null)
   1.566 +            return [false, foundLogins];
   1.567 +
   1.568 +        // If the password field is disabled or read-only, there's nothing to do.
   1.569 +        if (passwordField.disabled || passwordField.readOnly) {
   1.570 +            log("not filling form, password field disabled or read-only");
   1.571 +            return [false, foundLogins];
   1.572 +        }
   1.573 +
   1.574 +        // Need to get a list of logins if we weren't given them
   1.575 +        if (foundLogins == null) {
   1.576 +            var formOrigin =
   1.577 +                LoginUtils._getPasswordOrigin(form.ownerDocument.documentURI);
   1.578 +            var actionOrigin = LoginUtils._getActionOrigin(form);
   1.579 +            foundLogins = Services.logins.findLogins({}, formOrigin, actionOrigin, null);
   1.580 +            log("found", foundLogins.length, "matching logins.");
   1.581 +        } else {
   1.582 +            log("reusing logins from last form.");
   1.583 +        }
   1.584 +
   1.585 +        // Discard logins which have username/password values that don't
   1.586 +        // fit into the fields (as specified by the maxlength attribute).
   1.587 +        // The user couldn't enter these values anyway, and it helps
   1.588 +        // with sites that have an extra PIN to be entered (bug 391514)
   1.589 +        var maxUsernameLen = Number.MAX_VALUE;
   1.590 +        var maxPasswordLen = Number.MAX_VALUE;
   1.591 +
   1.592 +        // If attribute wasn't set, default is -1.
   1.593 +        if (usernameField && usernameField.maxLength >= 0)
   1.594 +            maxUsernameLen = usernameField.maxLength;
   1.595 +        if (passwordField.maxLength >= 0)
   1.596 +            maxPasswordLen = passwordField.maxLength;
   1.597 +
   1.598 +        var logins = foundLogins.filter(function (l) {
   1.599 +                var fit = (l.username.length <= maxUsernameLen &&
   1.600 +                           l.password.length <= maxPasswordLen);
   1.601 +                if (!fit)
   1.602 +                    log("Ignored", l.username, "login: won't fit");
   1.603 +
   1.604 +                return fit;
   1.605 +            }, this);
   1.606 +
   1.607 +
   1.608 +        // Nothing to do if we have no matching logins available.
   1.609 +        if (logins.length == 0)
   1.610 +            return [false, foundLogins];
   1.611 +
   1.612 +
   1.613 +        // The reason we didn't end up filling the form, if any.  We include
   1.614 +        // this in the formInfo object we send with the passwordmgr-found-logins
   1.615 +        // notification.  See the _notifyFoundLogins docs for possible values.
   1.616 +        var didntFillReason = null;
   1.617 +
   1.618 +        // Attach autocomplete stuff to the username field, if we have
   1.619 +        // one. This is normally used to select from multiple accounts,
   1.620 +        // but even with one account we should refill if the user edits.
   1.621 +        if (usernameField)
   1.622 +            this._formFillService.markAsLoginManagerField(usernameField);
   1.623 +
   1.624 +        // Don't clobber an existing password.
   1.625 +        if (passwordField.value && !clobberPassword) {
   1.626 +            didntFillReason = "existingPassword";
   1.627 +            this._notifyFoundLogins(didntFillReason, usernameField,
   1.628 +                                    passwordField, foundLogins, null);
   1.629 +            return [false, foundLogins];
   1.630 +        }
   1.631 +
   1.632 +        // If the form has an autocomplete=off attribute in play, don't
   1.633 +        // fill in the login automatically. We check this after attaching
   1.634 +        // the autocomplete stuff to the username field, so the user can
   1.635 +        // still manually select a login to be filled in.
   1.636 +        var isFormDisabled = false;
   1.637 +        if (!ignoreAutocomplete &&
   1.638 +            (this._isAutocompleteDisabled(form) ||
   1.639 +             this._isAutocompleteDisabled(usernameField) ||
   1.640 +             this._isAutocompleteDisabled(passwordField))) {
   1.641 +
   1.642 +            isFormDisabled = true;
   1.643 +            log("form not filled, has autocomplete=off");
   1.644 +        }
   1.645 +
   1.646 +        // Variable such that we reduce code duplication and can be sure we
   1.647 +        // should be firing notifications if and only if we can fill the form.
   1.648 +        var selectedLogin = null;
   1.649 +
   1.650 +        if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) {
   1.651 +            // If username was specified in the field, it's disabled or it's readOnly, only fill in the
   1.652 +            // password if we find a matching login.
   1.653 +            var username = usernameField.value.toLowerCase();
   1.654 +
   1.655 +            let matchingLogins = logins.filter(function(l)
   1.656 +                                     l.username.toLowerCase() == username);
   1.657 +            if (matchingLogins.length) {
   1.658 +                selectedLogin = matchingLogins[0];
   1.659 +            } else {
   1.660 +                didntFillReason = "existingUsername";
   1.661 +                log("Password not filled. None of the stored logins match the username already present.");
   1.662 +            }
   1.663 +        } else if (logins.length == 1) {
   1.664 +            selectedLogin = logins[0];
   1.665 +        } else {
   1.666 +            // We have multiple logins. Handle a special case here, for sites
   1.667 +            // which have a normal user+pass login *and* a password-only login
   1.668 +            // (eg, a PIN). Prefer the login that matches the type of the form
   1.669 +            // (user+pass or pass-only) when there's exactly one that matches.
   1.670 +            let matchingLogins;
   1.671 +            if (usernameField)
   1.672 +                matchingLogins = logins.filter(function(l) l.username);
   1.673 +            else
   1.674 +                matchingLogins = logins.filter(function(l) !l.username);
   1.675 +            if (matchingLogins.length == 1) {
   1.676 +                selectedLogin = matchingLogins[0];
   1.677 +            } else {
   1.678 +                didntFillReason = "multipleLogins";
   1.679 +                log("Multiple logins for form, so not filling any.");
   1.680 +            }
   1.681 +        }
   1.682 +
   1.683 +        var didFillForm = false;
   1.684 +        if (selectedLogin && autofillForm && !isFormDisabled) {
   1.685 +            // Fill the form
   1.686 +            // Don't modify the username field if it's disabled or readOnly so we preserve its case.
   1.687 +            if (usernameField && !(usernameField.disabled || usernameField.readOnly))
   1.688 +                usernameField.value = selectedLogin.username;
   1.689 +            passwordField.value = selectedLogin.password;
   1.690 +            didFillForm = true;
   1.691 +        } else if (selectedLogin && !autofillForm) {
   1.692 +            // For when autofillForm is false, but we still have the information
   1.693 +            // to fill a form, we notify observers.
   1.694 +            didntFillReason = "noAutofillForms";
   1.695 +            Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
   1.696 +            log("autofillForms=false but form can be filled; notified observers");
   1.697 +        } else if (selectedLogin && isFormDisabled) {
   1.698 +            // For when autocomplete is off, but we still have the information
   1.699 +            // to fill a form, we notify observers.
   1.700 +            didntFillReason = "autocompleteOff";
   1.701 +            Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
   1.702 +            log("autocomplete=off but form can be filled; notified observers");
   1.703 +        }
   1.704 +
   1.705 +        this._notifyFoundLogins(didntFillReason, usernameField, passwordField,
   1.706 +                                foundLogins, selectedLogin);
   1.707 +
   1.708 +        return [didFillForm, foundLogins];
   1.709 +    },
   1.710 +
   1.711 +
   1.712 +    /**
   1.713 +     * Notify observers about an attempt to fill a form that resulted in some
   1.714 +     * saved logins being found for the form.
   1.715 +     *
   1.716 +     * This does not get called if the login manager attempts to fill a form
   1.717 +     * but does not find any saved logins.  It does, however, get called when
   1.718 +     * the login manager does find saved logins whether or not it actually
   1.719 +     * fills the form with one of them.
   1.720 +     *
   1.721 +     * @param didntFillReason {String}
   1.722 +     *        the reason the login manager didn't fill the form, if any;
   1.723 +     *        if the value of this parameter is null, then the form was filled;
   1.724 +     *        otherwise, this parameter will be one of these values:
   1.725 +     *          existingUsername: the username field already contains a username
   1.726 +     *                            that doesn't match any stored usernames
   1.727 +     *          existingPassword: the password field already contains a password
   1.728 +     *          autocompleteOff:  autocomplete has been disabled for the form
   1.729 +     *                            or its username or password fields
   1.730 +     *          multipleLogins:   we have multiple logins for the form
   1.731 +     *          noAutofillForms:  the autofillForms pref is set to false
   1.732 +     *
   1.733 +     * @param usernameField   {HTMLInputElement}
   1.734 +     *        the username field detected by the login manager, if any;
   1.735 +     *        otherwise null
   1.736 +     *
   1.737 +     * @param passwordField   {HTMLInputElement}
   1.738 +     *        the password field detected by the login manager
   1.739 +     *
   1.740 +     * @param foundLogins     {Array}
   1.741 +     *        an array of nsILoginInfos that can be used to fill the form
   1.742 +     *
   1.743 +     * @param selectedLogin   {nsILoginInfo}
   1.744 +     *        the nsILoginInfo that was/would be used to fill the form, if any;
   1.745 +     *        otherwise null; whether or not it was actually used depends on
   1.746 +     *        the value of the didntFillReason parameter
   1.747 +     */
   1.748 +    _notifyFoundLogins : function (didntFillReason, usernameField,
   1.749 +                                   passwordField, foundLogins, selectedLogin) {
   1.750 +        // We need .setProperty(), which is a method on the original
   1.751 +        // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2
   1.752 +        // doesn't inherit from that, so the additional QI is needed.
   1.753 +        let formInfo = Cc["@mozilla.org/hash-property-bag;1"].
   1.754 +                       createInstance(Ci.nsIWritablePropertyBag2).
   1.755 +                       QueryInterface(Ci.nsIWritablePropertyBag);
   1.756 +
   1.757 +        formInfo.setPropertyAsACString("didntFillReason", didntFillReason);
   1.758 +        formInfo.setPropertyAsInterface("usernameField", usernameField);
   1.759 +        formInfo.setPropertyAsInterface("passwordField", passwordField);
   1.760 +        formInfo.setProperty("foundLogins", foundLogins.concat());
   1.761 +        formInfo.setPropertyAsInterface("selectedLogin", selectedLogin);
   1.762 +
   1.763 +        Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null);
   1.764 +    },
   1.765 +
   1.766 +};
   1.767 +
   1.768 +
   1.769 +
   1.770 +
   1.771 +LoginUtils = {
   1.772 +    /*
   1.773 +     * _getPasswordOrigin
   1.774 +     *
   1.775 +     * Get the parts of the URL we want for identification.
   1.776 +     */
   1.777 +    _getPasswordOrigin : function (uriString, allowJS) {
   1.778 +        var realm = "";
   1.779 +        try {
   1.780 +            var uri = Services.io.newURI(uriString, null, null);
   1.781 +
   1.782 +            if (allowJS && uri.scheme == "javascript")
   1.783 +                return "javascript:"
   1.784 +
   1.785 +            realm = uri.scheme + "://" + uri.host;
   1.786 +
   1.787 +            // If the URI explicitly specified a port, only include it when
   1.788 +            // it's not the default. (We never want "http://foo.com:80")
   1.789 +            var port = uri.port;
   1.790 +            if (port != -1) {
   1.791 +                var handler = Services.io.getProtocolHandler(uri.scheme);
   1.792 +                if (port != handler.defaultPort)
   1.793 +                    realm += ":" + port;
   1.794 +            }
   1.795 +
   1.796 +        } catch (e) {
   1.797 +            // bug 159484 - disallow url types that don't support a hostPort.
   1.798 +            // (although we handle "javascript:..." as a special case above.)
   1.799 +            log("Couldn't parse origin for", uriString);
   1.800 +            realm = null;
   1.801 +        }
   1.802 +
   1.803 +        return realm;
   1.804 +    },
   1.805 +
   1.806 +    _getActionOrigin : function (form) {
   1.807 +        var uriString = form.action;
   1.808 +
   1.809 +        // A blank or missing action submits to where it came from.
   1.810 +        if (uriString == "")
   1.811 +            uriString = form.baseURI; // ala bug 297761
   1.812 +
   1.813 +        return this._getPasswordOrigin(uriString, true);
   1.814 +    },
   1.815 +
   1.816 +};

mercurial