PageRenderTime 88ms CodeModel.GetById 3ms app.highlight 74ms RepoModel.GetById 1ms app.codeStats 0ms

/toolkit/components/passwordmgr/nsLoginManager.js

http://github.com/zpao/v8monkey
JavaScript | 1433 lines | 764 code | 255 blank | 414 comment | 208 complexity | d6051b2d4886eb0f85bca66486fdf778 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/* ***** BEGIN LICENSE BLOCK *****
   2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
   3 *
   4 * The contents of this file are subject to the Mozilla Public License Version
   5 * 1.1 (the "License"); you may not use this file except in compliance with
   6 * the License. You may obtain a copy of the License at
   7 * http://www.mozilla.org/MPL/
   8 *
   9 * Software distributed under the License is distributed on an "AS IS" basis,
  10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11 * for the specific language governing rights and limitations under the
  12 * License.
  13 *
  14 * The Original Code is mozilla.org code.
  15 *
  16 * The Initial Developer of the Original Code is Mozilla Foundation.
  17 * Portions created by the Initial Developer are Copyright (C) 2007
  18 * the Initial Developer. All Rights Reserved.
  19 *
  20 * Contributor(s):
  21 *  Justin Dolske <dolske@mozilla.com> (original author)
  22 *  Ehsan Akhgari <ehsan.akhgari@gmail.com>
  23 *
  24 * Alternatively, the contents of this file may be used under the terms of
  25 * either the GNU General Public License Version 2 or later (the "GPL"), or
  26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27 * in which case the provisions of the GPL or the LGPL are applicable instead
  28 * of those above. If you wish to allow use of your version of this file only
  29 * under the terms of either the GPL or the LGPL, and not to allow others to
  30 * use your version of this file under the terms of the MPL, indicate your
  31 * decision by deleting the provisions above and replace them with the notice
  32 * and other provisions required by the GPL or the LGPL. If you do not delete
  33 * the provisions above, a recipient may use your version of this file under
  34 * the terms of any one of the MPL, the GPL or the LGPL.
  35 *
  36 * ***** END LICENSE BLOCK ***** */
  37
  38
  39const Cc = Components.classes;
  40const Ci = Components.interfaces;
  41
  42Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  43Components.utils.import("resource://gre/modules/Services.jsm");
  44
  45function LoginManager() {
  46    this.init();
  47}
  48
  49LoginManager.prototype = {
  50
  51    classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
  52    QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager,
  53                                            Ci.nsISupportsWeakReference,
  54                                            Ci.nsIInterfaceRequestor]),
  55    getInterface : function(aIID) {
  56      if (aIID.equals(Ci.mozIStorageConnection) && this._storage) {
  57        let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor);
  58        return ir.getInterface(aIID);
  59      }
  60
  61      throw Cr.NS_ERROR_NO_INTERFACE;
  62    },
  63
  64
  65    /* ---------- private memebers ---------- */
  66
  67
  68    __formFillService : null, // FormFillController, for username autocompleting
  69    get _formFillService() {
  70        if (!this.__formFillService)
  71            this.__formFillService =
  72                            Cc["@mozilla.org/satchel/form-fill-controller;1"].
  73                            getService(Ci.nsIFormFillController);
  74        return this.__formFillService;
  75    },
  76
  77
  78    __storage : null, // Storage component which contains the saved logins
  79    get _storage() {
  80        if (!this.__storage) {
  81
  82            var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
  83            try {
  84                var catMan = Cc["@mozilla.org/categorymanager;1"].
  85                             getService(Ci.nsICategoryManager);
  86                contractID = catMan.getCategoryEntry("login-manager-storage",
  87                                                     "nsILoginManagerStorage");
  88                this.log("Found alternate nsILoginManagerStorage with " +
  89                         "contract ID: " + contractID);
  90            } catch (e) {
  91                this.log("No alternate nsILoginManagerStorage registered");
  92            }
  93
  94            this.__storage = Cc[contractID].
  95                             createInstance(Ci.nsILoginManagerStorage);
  96            try {
  97                this.__storage.init();
  98            } catch (e) {
  99                this.log("Initialization of storage component failed: " + e);
 100                this.__storage = null;
 101            }
 102        }
 103
 104        return this.__storage;
 105    },
 106
 107
 108    // Private Browsing Service
 109    // If the service is not available, null will be returned.
 110    __privateBrowsingService : undefined,
 111    get _privateBrowsingService() {
 112        if (this.__privateBrowsingService == undefined) {
 113            if ("@mozilla.org/privatebrowsing;1" in Cc)
 114                this.__privateBrowsingService = Cc["@mozilla.org/privatebrowsing;1"].
 115                                                getService(Ci.nsIPrivateBrowsingService);
 116            else
 117                this.__privateBrowsingService = null;
 118        }
 119        return this.__privateBrowsingService;
 120    },
 121
 122
 123    // Whether we are in private browsing mode
 124    get _inPrivateBrowsing() {
 125        var pbSvc = this._privateBrowsingService;
 126        if (pbSvc)
 127            return pbSvc.privateBrowsingEnabled;
 128        else
 129            return false;
 130    },
 131
 132    _prefBranch  : null, // Preferences service
 133    _nsLoginInfo : null, // Constructor for nsILoginInfo implementation
 134
 135    _remember : true,  // mirrors signon.rememberSignons preference
 136    _debug    : false, // mirrors signon.debug
 137
 138
 139    /*
 140     * init
 141     *
 142     * Initialize the Login Manager. Automatically called when service
 143     * is created.
 144     *
 145     * Note: Service created in /browser/base/content/browser.js,
 146     *       delayedStartup()
 147     */
 148    init : function () {
 149
 150        // Cache references to current |this| in utility objects
 151        this._webProgressListener._domEventListener = this._domEventListener;
 152        this._webProgressListener._pwmgr = this;
 153        this._domEventListener._pwmgr    = this;
 154        this._observer._pwmgr            = this;
 155
 156        // Preferences. Add observer so we get notified of changes.
 157        this._prefBranch = Services.prefs.getBranch("signon.");
 158        this._prefBranch.addObserver("", this._observer, false);
 159
 160        // Get current preference values.
 161        this._debug = this._prefBranch.getBoolPref("debug");
 162
 163        this._remember = this._prefBranch.getBoolPref("rememberSignons");
 164
 165
 166        // Get constructor for nsILoginInfo
 167        this._nsLoginInfo = new Components.Constructor(
 168            "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
 169
 170
 171        // Form submit observer checks forms for new logins and pw changes.
 172        Services.obs.addObserver(this._observer, "earlyformsubmit", false);
 173        Services.obs.addObserver(this._observer, "xpcom-shutdown", false);
 174
 175        // WebProgressListener for getting notification of new doc loads.
 176        var progress = Cc["@mozilla.org/docloaderservice;1"].
 177                       getService(Ci.nsIWebProgress);
 178        progress.addProgressListener(this._webProgressListener,
 179                                     Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
 180    },
 181
 182
 183    /*
 184     * log
 185     *
 186     * Internal function for logging debug messages to the Error Console window
 187     */
 188    log : function (message) {
 189        if (!this._debug)
 190            return;
 191        dump("Login Manager: " + message + "\n");
 192        Services.console.logStringMessage("Login Manager: " + message);
 193    },
 194
 195
 196    /* ---------- Utility objects ---------- */
 197
 198
 199    /*
 200     * _observer object
 201     *
 202     * Internal utility object, implements the nsIObserver interface.
 203     * Used to receive notification for: form submission, preference changes.
 204     */
 205    _observer : {
 206        _pwmgr : null,
 207
 208        QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
 209                                                Ci.nsIFormSubmitObserver,
 210                                                Ci.nsISupportsWeakReference]),
 211
 212
 213        // nsFormSubmitObserver
 214        notify : function (formElement, aWindow, actionURI) {
 215            this._pwmgr.log("observer notified for form submission.");
 216
 217            // We're invoked before the content's |onsubmit| handlers, so we
 218            // can grab form data before it might be modified (see bug 257781).
 219
 220            try {
 221                this._pwmgr._onFormSubmit(formElement);
 222            } catch (e) {
 223                this._pwmgr.log("Caught error in onFormSubmit: " + e);
 224            }
 225
 226            return true; // Always return true, or form submit will be canceled.
 227        },
 228
 229        // nsObserver
 230        observe : function (subject, topic, data) {
 231
 232            if (topic == "nsPref:changed") {
 233                var prefName = data;
 234                this._pwmgr.log("got change to " + prefName + " preference");
 235
 236                if (prefName == "debug") {
 237                    this._pwmgr._debug =
 238                        this._pwmgr._prefBranch.getBoolPref("debug");
 239                } else if (prefName == "rememberSignons") {
 240                    this._pwmgr._remember =
 241                        this._pwmgr._prefBranch.getBoolPref("rememberSignons");
 242                } else {
 243                    this._pwmgr.log("Oops! Pref not handled, change ignored.");
 244                }
 245            } else if (topic == "xpcom-shutdown") {
 246                for (let i in this._pwmgr) {
 247                  try {
 248                    this._pwmgr[i] = null;
 249                  } catch(ex) {}
 250                }
 251                this._pwmgr = null;
 252            } else {
 253                this._pwmgr.log("Oops! Unexpected notification: " + topic);
 254            }
 255        }
 256    },
 257
 258
 259    /*
 260     * _webProgressListener object
 261     *
 262     * Internal utility object, implements nsIWebProgressListener interface.
 263     * This is attached to the document loader service, so we get
 264     * notifications about all page loads.
 265     */
 266    _webProgressListener : {
 267        _pwmgr : null,
 268        _domEventListener : null,
 269
 270        QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
 271                                                Ci.nsISupportsWeakReference]),
 272
 273
 274        onStateChange : function (aWebProgress, aRequest,
 275                                  aStateFlags,  aStatus) {
 276
 277            // STATE_START is too early, doc is still the old page.
 278            if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
 279                return;
 280
 281            if (!this._pwmgr._remember)
 282                return;
 283
 284            var domWin = aWebProgress.DOMWindow;
 285            var domDoc = domWin.document;
 286
 287            // Only process things which might have HTML forms.
 288            if (!(domDoc instanceof Ci.nsIDOMHTMLDocument))
 289                return;
 290            if (this._pwmgr._debug) {
 291                let requestName = "(null)";
 292                if (aRequest) {
 293                    try {
 294                        requestName = aRequest.name;
 295                    } catch (ex if ex.result == Components.results.NS_ERROR_NOT_IMPLEMENTED) {
 296                        // do nothing - leave requestName = "(null)"
 297                    }
 298                }
 299                this._pwmgr.log("onStateChange accepted: req = " + requestName +
 300                                ", flags = 0x" + aStateFlags.toString(16));
 301            }
 302
 303            // Fastback doesn't fire DOMContentLoaded, so process forms now.
 304            if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) {
 305                this._pwmgr.log("onStateChange: restoring document");
 306                return this._pwmgr._fillDocument(domDoc);
 307            }
 308
 309            // Add event listener to process page when DOM is complete.
 310            domDoc.addEventListener("DOMContentLoaded",
 311                                    this._domEventListener, false);
 312            return;
 313        },
 314
 315        // stubs for the nsIWebProgressListener interfaces which we don't use.
 316        onProgressChange : function() { throw "Unexpected onProgressChange"; },
 317        onLocationChange : function() { throw "Unexpected onLocationChange"; },
 318        onStatusChange   : function() { throw "Unexpected onStatusChange";   },
 319        onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
 320    },
 321
 322
 323    /*
 324     * _domEventListener object
 325     *
 326     * Internal utility object, implements nsIDOMEventListener
 327     * Used to catch certain DOM events needed to properly implement form fill.
 328     */
 329    _domEventListener : {
 330        _pwmgr : null,
 331
 332        QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
 333                                                Ci.nsISupportsWeakReference]),
 334
 335
 336        handleEvent : function (event) {
 337            if (!event.isTrusted)
 338                return;
 339
 340            this._pwmgr.log("domEventListener: got event " + event.type);
 341
 342            switch (event.type) {
 343                case "DOMContentLoaded":
 344                    this._pwmgr._fillDocument(event.target);
 345                    return;
 346
 347                case "DOMAutoComplete":
 348                case "blur":
 349                    var acInputField = event.target;
 350                    var acForm = acInputField.form;
 351
 352                    // If the username is blank, bail out now -- we don't want
 353                    // fillForm() to try filling in a login without a username
 354                    // to filter on (bug 471906).
 355                    if (!acInputField.value)
 356                        return;
 357
 358                    // Make sure the username field fillForm will use is the
 359                    // same field as the autocomplete was activated on. If
 360                    // not, the DOM has been altered and we'll just give up.
 361                    var [usernameField, passwordField, ignored] =
 362                        this._pwmgr._getFormFields(acForm, false);
 363                    if (usernameField == acInputField && passwordField) {
 364                        // This shouldn't trigger a master password prompt,
 365                        // because we don't attach to the input until after we
 366                        // successfully obtain logins for the form.
 367                        this._pwmgr._fillForm(acForm, true, true, true, null);
 368                    } else {
 369                        this._pwmgr.log("Oops, form changed before AC invoked");
 370                    }
 371                    return;
 372
 373                default:
 374                    this._pwmgr.log("Oops! This event unexpected.");
 375                    return;
 376            }
 377        }
 378    },
 379
 380
 381
 382
 383    /* ---------- Primary Public interfaces ---------- */
 384
 385
 386
 387
 388    /*
 389     * addLogin
 390     *
 391     * Add a new login to login storage.
 392     */
 393    addLogin : function (login) {
 394        // Sanity check the login
 395        if (login.hostname == null || login.hostname.length == 0)
 396            throw "Can't add a login with a null or empty hostname.";
 397
 398        // For logins w/o a username, set to "", not null.
 399        if (login.username == null)
 400            throw "Can't add a login with a null username.";
 401
 402        if (login.password == null || login.password.length == 0)
 403            throw "Can't add a login with a null or empty password.";
 404
 405        if (login.formSubmitURL || login.formSubmitURL == "") {
 406            // We have a form submit URL. Can't have a HTTP realm.
 407            if (login.httpRealm != null)
 408                throw "Can't add a login with both a httpRealm and formSubmitURL.";
 409        } else if (login.httpRealm) {
 410            // We have a HTTP realm. Can't have a form submit URL.
 411            if (login.formSubmitURL != null)
 412                throw "Can't add a login with both a httpRealm and formSubmitURL.";
 413        } else {
 414            // Need one or the other!
 415            throw "Can't add a login without a httpRealm or formSubmitURL.";
 416        }
 417
 418
 419        // Look for an existing entry.
 420        var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
 421                                     login.httpRealm);
 422
 423        if (logins.some(function(l) login.matches(l, true)))
 424            throw "This login already exists.";
 425
 426        this.log("Adding login: " + login);
 427        return this._storage.addLogin(login);
 428    },
 429
 430
 431    /*
 432     * removeLogin
 433     *
 434     * Remove the specified login from the stored logins.
 435     */
 436    removeLogin : function (login) {
 437        this.log("Removing login: " + login);
 438        return this._storage.removeLogin(login);
 439    },
 440
 441
 442    /*
 443     * modifyLogin
 444     *
 445     * Change the specified login to match the new login.
 446     */
 447    modifyLogin : function (oldLogin, newLogin) {
 448        this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin);
 449        return this._storage.modifyLogin(oldLogin, newLogin);
 450    },
 451
 452
 453    /*
 454     * getAllLogins
 455     *
 456     * Get a dump of all stored logins. Used by the login manager UI.
 457     *
 458     * |count| is only needed for XPCOM.
 459     *
 460     * Returns an array of logins. If there are no logins, the array is empty.
 461     */
 462    getAllLogins : function (count) {
 463        this.log("Getting a list of all logins");
 464        return this._storage.getAllLogins(count);
 465    },
 466
 467
 468    /*
 469     * removeAllLogins
 470     *
 471     * Remove all stored logins.
 472     */
 473    removeAllLogins : function () {
 474        this.log("Removing all logins");
 475        this._storage.removeAllLogins();
 476    },
 477
 478    /*
 479     * getAllDisabledHosts
 480     *
 481     * Get a list of all hosts for which logins are disabled.
 482     *
 483     * |count| is only needed for XPCOM.
 484     *
 485     * Returns an array of disabled logins. If there are no disabled logins,
 486     * the array is empty.
 487     */
 488    getAllDisabledHosts : function (count) {
 489        this.log("Getting a list of all disabled hosts");
 490        return this._storage.getAllDisabledHosts(count);
 491    },
 492
 493
 494    /*
 495     * findLogins
 496     *
 497     * Search for the known logins for entries matching the specified criteria.
 498     */
 499    findLogins : function (count, hostname, formSubmitURL, httpRealm) {
 500        this.log("Searching for logins matching host: " + hostname +
 501            ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
 502
 503        return this._storage.findLogins(count, hostname, formSubmitURL,
 504                                        httpRealm);
 505    },
 506
 507
 508    /*
 509     * searchLogins
 510     *
 511     * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
 512     * JavaScript object and decrypt the results.
 513     *
 514     * Returns an array of decrypted nsILoginInfo.
 515     */
 516    searchLogins : function(count, matchData) {
 517       this.log("Searching for logins");
 518
 519        return this._storage.searchLogins(count, matchData);
 520    },
 521
 522
 523    /*
 524     * countLogins
 525     *
 526     * Search for the known logins for entries matching the specified criteria,
 527     * returns only the count.
 528     */
 529    countLogins : function (hostname, formSubmitURL, httpRealm) {
 530        this.log("Counting logins matching host: " + hostname +
 531            ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
 532
 533        return this._storage.countLogins(hostname, formSubmitURL, httpRealm);
 534    },
 535
 536
 537    /*
 538     * uiBusy
 539     */
 540    get uiBusy() {
 541        return this._storage.uiBusy;
 542    },
 543
 544
 545    /*
 546     * getLoginSavingEnabled
 547     *
 548     * Check to see if user has disabled saving logins for the host.
 549     */
 550    getLoginSavingEnabled : function (host) {
 551        this.log("Checking if logins to " + host + " can be saved.");
 552        if (!this._remember)
 553            return false;
 554
 555        return this._storage.getLoginSavingEnabled(host);
 556    },
 557
 558
 559    /*
 560     * setLoginSavingEnabled
 561     *
 562     * Enable or disable storing logins for the specified host.
 563     */
 564    setLoginSavingEnabled : function (hostname, enabled) {
 565        // Nulls won't round-trip with getAllDisabledHosts().
 566        if (hostname.indexOf("\0") != -1)
 567            throw "Invalid hostname";
 568
 569        this.log("Saving logins for " + hostname + " enabled? " + enabled);
 570        return this._storage.setLoginSavingEnabled(hostname, enabled);
 571    },
 572
 573
 574    /*
 575     * autoCompleteSearch
 576     *
 577     * Yuck. This is called directly by satchel:
 578     * nsFormFillController::StartSearch()
 579     * [toolkit/components/satchel/src/nsFormFillController.cpp]
 580     *
 581     * We really ought to have a simple way for code to register an
 582     * auto-complete provider, and not have satchel calling pwmgr directly.
 583     */
 584    autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
 585        // aPreviousResult & aResult are nsIAutoCompleteResult,
 586        // aElement is nsIDOMHTMLInputElement
 587
 588        if (!this._remember)
 589            return null;
 590
 591        this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
 592
 593        var result = null;
 594
 595        if (aPreviousResult &&
 596                aSearchString.substr(0, aPreviousResult.searchString.length) == aPreviousResult.searchString) {
 597            this.log("Using previous autocomplete result");
 598            result = aPreviousResult;
 599            result.wrappedJSObject.searchString = aSearchString;
 600
 601            // We have a list of results for a shorter search string, so just
 602            // filter them further based on the new search string.
 603            // Count backwards, because result.matchCount is decremented
 604            // when we remove an entry.
 605            for (var i = result.matchCount - 1; i >= 0; i--) {
 606                var match = result.getValueAt(i);
 607
 608                // Remove results that are too short, or have different prefix.
 609                if (aSearchString.length > match.length ||
 610                    aSearchString.toLowerCase() !=
 611                        match.substr(0, aSearchString.length).toLowerCase())
 612                {
 613                    this.log("Removing autocomplete entry '" + match + "'");
 614                    result.removeValueAt(i, false);
 615                }
 616            }
 617        } else {
 618            this.log("Creating new autocomplete search result.");
 619
 620            var doc = aElement.ownerDocument;
 621            var origin = this._getPasswordOrigin(doc.documentURI);
 622            var actionOrigin = this._getActionOrigin(aElement.form);
 623
 624            // This shouldn't trigger a master password prompt, because we
 625            // don't attach to the input until after we successfully obtain
 626            // logins for the form.
 627            var logins = this.findLogins({}, origin, actionOrigin, null);
 628            var matchingLogins = [];
 629
 630            // Filter out logins that don't match the search prefix. Also
 631            // filter logins without a username, since that's confusing to see
 632            // in the dropdown and we can't autocomplete them anyway.
 633            for (i = 0; i < logins.length; i++) {
 634                var username = logins[i].username.toLowerCase();
 635                if (username &&
 636                    aSearchString.length <= username.length &&
 637                    aSearchString.toLowerCase() ==
 638                        username.substr(0, aSearchString.length))
 639                {
 640                    matchingLogins.push(logins[i]);
 641                }
 642            }
 643            this.log(matchingLogins.length + " autocomplete logins avail.");
 644            result = new UserAutoCompleteResult(aSearchString, matchingLogins);
 645        }
 646
 647        return result;
 648    },
 649
 650
 651
 652
 653    /* ------- Internal methods / callbacks for document integration ------- */
 654
 655
 656
 657
 658    /*
 659     * _getPasswordFields
 660     *
 661     * Returns an array of password field elements for the specified form.
 662     * If no pw fields are found, or if more than 3 are found, then null
 663     * is returned.
 664     *
 665     * skipEmptyFields can be set to ignore password fields with no value.
 666     */
 667    _getPasswordFields : function (form, skipEmptyFields) {
 668        // Locate the password fields in the form.
 669        var pwFields = [];
 670        for (var i = 0; i < form.elements.length; i++) {
 671            var element = form.elements[i];
 672            if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
 673                element.type != "password")
 674                continue;
 675
 676            if (skipEmptyFields && !element.value)
 677                continue;
 678
 679            pwFields[pwFields.length] = {
 680                                            index   : i,
 681                                            element : element
 682                                        };
 683        }
 684
 685        // If too few or too many fields, bail out.
 686        if (pwFields.length == 0) {
 687            this.log("(form ignored -- no password fields.)");
 688            return null;
 689        } else if (pwFields.length > 3) {
 690            this.log("(form ignored -- too many password fields. [got " +
 691                        pwFields.length + "])");
 692            return null;
 693        }
 694
 695        return pwFields;
 696    },
 697
 698
 699    /*
 700     * _getFormFields
 701     *
 702     * Returns the username and password fields found in the form.
 703     * Can handle complex forms by trying to figure out what the
 704     * relevant fields are.
 705     *
 706     * Returns: [usernameField, newPasswordField, oldPasswordField]
 707     *
 708     * usernameField may be null.
 709     * newPasswordField will always be non-null.
 710     * oldPasswordField may be null. If null, newPasswordField is just
 711     * "theLoginField". If not null, the form is apparently a
 712     * change-password field, with oldPasswordField containing the password
 713     * that is being changed.
 714     */
 715    _getFormFields : function (form, isSubmission) {
 716        var usernameField = null;
 717
 718        // Locate the password field(s) in the form. Up to 3 supported.
 719        // If there's no password field, there's nothing for us to do.
 720        var pwFields = this._getPasswordFields(form, isSubmission);
 721        if (!pwFields)
 722            return [null, null, null];
 723
 724
 725        // Locate the username field in the form by searching backwards
 726        // from the first passwordfield, assume the first text field is the
 727        // username. We might not find a username field if the user is
 728        // already logged in to the site.
 729        for (var i = pwFields[0].index - 1; i >= 0; i--) {
 730            var element = form.elements[i];
 731            var fieldType = (element.hasAttribute("type") ?
 732                             element.getAttribute("type").toLowerCase() :
 733                             element.type);
 734            if (fieldType == "text"  ||
 735                fieldType == "email" ||
 736                fieldType == "url"   ||
 737                fieldType == "tel"   ||
 738                fieldType == "number") {
 739                usernameField = element;
 740                break;
 741            }
 742        }
 743
 744        if (!usernameField)
 745            this.log("(form -- no username field found)");
 746
 747
 748        // If we're not submitting a form (it's a page load), there are no
 749        // password field values for us to use for identifying fields. So,
 750        // just assume the first password field is the one to be filled in.
 751        if (!isSubmission || pwFields.length == 1)
 752            return [usernameField, pwFields[0].element, null];
 753
 754
 755        // Try to figure out WTF is in the form based on the password values.
 756        var oldPasswordField, newPasswordField;
 757        var pw1 = pwFields[0].element.value;
 758        var pw2 = pwFields[1].element.value;
 759        var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
 760
 761        if (pwFields.length == 3) {
 762            // Look for two identical passwords, that's the new password
 763
 764            if (pw1 == pw2 && pw2 == pw3) {
 765                // All 3 passwords the same? Weird! Treat as if 1 pw field.
 766                newPasswordField = pwFields[0].element;
 767                oldPasswordField = null;
 768            } else if (pw1 == pw2) {
 769                newPasswordField = pwFields[0].element;
 770                oldPasswordField = pwFields[2].element;
 771            } else if (pw2 == pw3) {
 772                oldPasswordField = pwFields[0].element;
 773                newPasswordField = pwFields[2].element;
 774            } else  if (pw1 == pw3) {
 775                // A bit odd, but could make sense with the right page layout.
 776                newPasswordField = pwFields[0].element;
 777                oldPasswordField = pwFields[1].element;
 778            } else {
 779                // We can't tell which of the 3 passwords should be saved.
 780                this.log("(form ignored -- all 3 pw fields differ)");
 781                return [null, null, null];
 782            }
 783        } else { // pwFields.length == 2
 784            if (pw1 == pw2) {
 785                // Treat as if 1 pw field
 786                newPasswordField = pwFields[0].element;
 787                oldPasswordField = null;
 788            } else {
 789                // Just assume that the 2nd password is the new password
 790                oldPasswordField = pwFields[0].element;
 791                newPasswordField = pwFields[1].element;
 792            }
 793        }
 794
 795        return [usernameField, newPasswordField, oldPasswordField];
 796    },
 797
 798
 799    /*
 800     * _isAutoCompleteDisabled
 801     *
 802     * Returns true if the page requests autocomplete be disabled for the
 803     * specified form input.
 804     */
 805    _isAutocompleteDisabled :  function (element) {
 806        if (element && element.hasAttribute("autocomplete") &&
 807            element.getAttribute("autocomplete").toLowerCase() == "off")
 808            return true;
 809
 810        return false;
 811    },
 812
 813    /*
 814     * _onFormSubmit
 815     *
 816     * Called by the our observer when notified of a form submission.
 817     * [Note that this happens before any DOM onsubmit handlers are invoked.]
 818     * Looks for a password change in the submitted form, so we can update
 819     * our stored password.
 820     */
 821    _onFormSubmit : function (form) {
 822
 823        // local helper function
 824        function getPrompter(aWindow) {
 825            var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
 826                              createInstance(Ci.nsILoginManagerPrompter);
 827            prompterSvc.init(aWindow);
 828            return prompterSvc;
 829        }
 830
 831        if (this._inPrivateBrowsing) {
 832            // We won't do anything in private browsing mode anyway,
 833            // so there's no need to perform further checks.
 834            this.log("(form submission ignored in private browsing mode)");
 835            return;
 836        }
 837
 838        var doc = form.ownerDocument;
 839        var win = doc.defaultView;
 840
 841        // If password saving is disabled (globally or for host), bail out now.
 842        if (!this._remember)
 843            return;
 844
 845        var hostname = this._getPasswordOrigin(doc.documentURI);
 846        if (!hostname) {
 847            this.log("(form submission ignored -- invalid hostname)");
 848            return;
 849        }
 850
 851        var formSubmitURL = this._getActionOrigin(form)
 852        if (!this.getLoginSavingEnabled(hostname)) {
 853            this.log("(form submission ignored -- saving is " +
 854                     "disabled for: " + hostname + ")");
 855            return;
 856        }
 857
 858
 859        // Get the appropriate fields from the form.
 860        var [usernameField, newPasswordField, oldPasswordField] =
 861            this._getFormFields(form, true);
 862
 863        // Need at least 1 valid password field to do anything.
 864        if (newPasswordField == null)
 865                return;
 866
 867        // Check for autocomplete=off attribute. We don't use it to prevent
 868        // autofilling (for existing logins), but won't save logins when it's
 869        // present.
 870        // XXX spin out a bug that we don't update timeLastUsed in this case?
 871        if (this._isAutocompleteDisabled(form) ||
 872            this._isAutocompleteDisabled(usernameField) ||
 873            this._isAutocompleteDisabled(newPasswordField) ||
 874            this._isAutocompleteDisabled(oldPasswordField)) {
 875                this.log("(form submission ignored -- autocomplete=off found)");
 876                return;
 877        }
 878
 879
 880        var formLogin = new this._nsLoginInfo();
 881        formLogin.init(hostname, formSubmitURL, null,
 882                    (usernameField ? usernameField.value : ""),
 883                    newPasswordField.value,
 884                    (usernameField ? usernameField.name  : ""),
 885                    newPasswordField.name);
 886
 887        // If we didn't find a username field, but seem to be changing a
 888        // password, allow the user to select from a list of applicable
 889        // logins to update the password for.
 890        if (!usernameField && oldPasswordField) {
 891
 892            var logins = this.findLogins({}, hostname, formSubmitURL, null);
 893
 894            if (logins.length == 0) {
 895                // Could prompt to save this as a new password-only login.
 896                // This seems uncommon, and might be wrong, so ignore.
 897                this.log("(no logins for this host -- pwchange ignored)");
 898                return;
 899            }
 900
 901            var prompter = getPrompter(win);
 902
 903            if (logins.length == 1) {
 904                var oldLogin = logins[0];
 905                formLogin.username      = oldLogin.username;
 906                formLogin.usernameField = oldLogin.usernameField;
 907
 908                prompter.promptToChangePassword(oldLogin, formLogin);
 909            } else {
 910                prompter.promptToChangePasswordWithUsernames(
 911                                    logins, logins.length, formLogin);
 912            }
 913
 914            return;
 915        }
 916
 917
 918        // Look for an existing login that matches the form login.
 919        var existingLogin = null;
 920        var logins = this.findLogins({}, hostname, formSubmitURL, null);
 921
 922        for (var i = 0; i < logins.length; i++) {
 923            var same, login = logins[i];
 924
 925            // If one login has a username but the other doesn't, ignore
 926            // the username when comparing and only match if they have the
 927            // same password. Otherwise, compare the logins and match even
 928            // if the passwords differ.
 929            if (!login.username && formLogin.username) {
 930                var restoreMe = formLogin.username;
 931                formLogin.username = "";
 932                same = formLogin.matches(login, false);
 933                formLogin.username = restoreMe;
 934            } else if (!formLogin.username && login.username) {
 935                formLogin.username = login.username;
 936                same = formLogin.matches(login, false);
 937                formLogin.username = ""; // we know it's always blank.
 938            } else {
 939                same = formLogin.matches(login, true);
 940            }
 941
 942            if (same) {
 943                existingLogin = login;
 944                break;
 945            }
 946        }
 947
 948        if (existingLogin) {
 949            this.log("Found an existing login matching this form submission");
 950
 951            // Change password if needed.
 952            if (existingLogin.password != formLogin.password) {
 953                this.log("...passwords differ, prompting to change.");
 954                prompter = getPrompter(win);
 955                prompter.promptToChangePassword(existingLogin, formLogin);
 956            } else {
 957                // Update the lastUsed timestamp.
 958                var propBag = Cc["@mozilla.org/hash-property-bag;1"].
 959                              createInstance(Ci.nsIWritablePropertyBag);
 960                propBag.setProperty("timeLastUsed", Date.now());
 961                propBag.setProperty("timesUsedIncrement", 1);
 962                this.modifyLogin(existingLogin, propBag);
 963            }
 964
 965            return;
 966        }
 967
 968
 969        // Prompt user to save login (via dialog or notification bar)
 970        prompter = getPrompter(win);
 971        prompter.promptToSavePassword(formLogin);
 972    },
 973
 974
 975    /*
 976     * _getPasswordOrigin
 977     *
 978     * Get the parts of the URL we want for identification.
 979     */
 980    _getPasswordOrigin : function (uriString, allowJS) {
 981        var realm = "";
 982        try {
 983            var uri = Services.io.newURI(uriString, null, null);
 984
 985            if (allowJS && uri.scheme == "javascript")
 986                return "javascript:"
 987
 988            realm = uri.scheme + "://" + uri.host;
 989
 990            // If the URI explicitly specified a port, only include it when
 991            // it's not the default. (We never want "http://foo.com:80")
 992            var port = uri.port;
 993            if (port != -1) {
 994                var handler = Services.io.getProtocolHandler(uri.scheme);
 995                if (port != handler.defaultPort)
 996                    realm += ":" + port;
 997            }
 998
 999        } catch (e) {
1000            // bug 159484 - disallow url types that don't support a hostPort.
1001            // (although we handle "javascript:..." as a special case above.)
1002            this.log("Couldn't parse origin for " + uriString);
1003            realm = null;
1004        }
1005
1006        return realm;
1007    },
1008
1009    _getActionOrigin : function (form) {
1010        var uriString = form.action;
1011
1012        // A blank or missing action submits to where it came from.
1013        if (uriString == "")
1014            uriString = form.baseURI; // ala bug 297761
1015
1016        return this._getPasswordOrigin(uriString, true);
1017    },
1018
1019
1020    /*
1021     * _fillDocument
1022     *
1023     * Called when a page has loaded. For each form in the document,
1024     * we check to see if it can be filled with a stored login.
1025     */
1026    _fillDocument : function (doc) {
1027        var forms = doc.forms;
1028        if (!forms || forms.length == 0)
1029            return;
1030
1031        var formOrigin = this._getPasswordOrigin(doc.documentURI);
1032
1033        // If there are no logins for this site, bail out now.
1034        if (!this.countLogins(formOrigin, "", null))
1035            return;
1036
1037        // If we're currently displaying a master password prompt, defer
1038        // processing this document until the user handles the prompt.
1039        if (this.uiBusy) {
1040            this.log("deferring fillDoc for " + doc.documentURI);
1041            let self = this;
1042            let observer = {
1043                QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
1044
1045                observe: function (subject, topic, data) {
1046                    self.log("Got deferred fillDoc notification: " + topic);
1047                    // Only run observer once.
1048                    Services.obs.removeObserver(this, "passwordmgr-crypto-login");
1049                    Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
1050                    if (topic == "passwordmgr-crypto-loginCanceled")
1051                        return;
1052                    self._fillDocument(doc);
1053                },
1054                handleEvent : function (event) {
1055                    // Not expected to be called
1056                }
1057            };
1058            // Trickyness follows: We want an observer, but don't want it to
1059            // cause leaks. So add the observer with a weak reference, and use
1060            // a dummy event listener (a strong reference) to keep it alive
1061            // until the document is destroyed.
1062            Services.obs.addObserver(observer, "passwordmgr-crypto-login", true);
1063            Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true);
1064            doc.addEventListener("mozCleverClosureHack", observer, false);
1065            return;
1066        }
1067
1068        this.log("fillDocument processing " + forms.length +
1069                 " forms on " + doc.documentURI);
1070
1071        var autofillForm = !this._inPrivateBrowsing &&
1072                           this._prefBranch.getBoolPref("autofillForms");
1073        var previousActionOrigin = null;
1074        var foundLogins = null;
1075
1076        for (var i = 0; i < forms.length; i++) {
1077            var form = forms[i];
1078
1079            // Only the actionOrigin might be changing, so if it's the same
1080            // as the last form on the page we can reuse the same logins.
1081            var actionOrigin = this._getActionOrigin(form);
1082            if (actionOrigin != previousActionOrigin) {
1083                foundLogins = null;
1084                previousActionOrigin = actionOrigin;
1085            }
1086            this.log("_fillDocument processing form[" + i + "]");
1087            foundLogins = this._fillForm(form, autofillForm, false, false, foundLogins)[1];
1088        } // foreach form
1089    },
1090
1091
1092    /*
1093     * _fillform
1094     *
1095     * Fill the form with login information if we can find it. This will find
1096     * an array of logins if not given any, otherwise it will use the logins
1097     * passed in. The logins are returned so they can be reused for
1098     * optimization. Success of action is also returned in format
1099     * [success, foundLogins]. autofillForm denotes if we should fill the form
1100     * in automatically, ignoreAutocomplete denotes if we should ignore
1101     * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo
1102     * for optimization
1103     */
1104    _fillForm : function (form, autofillForm, ignoreAutocomplete,
1105                          clobberPassword, foundLogins) {
1106        // Heuristically determine what the user/pass fields are
1107        // We do this before checking to see if logins are stored,
1108        // so that the user isn't prompted for a master password
1109        // without need.
1110        var [usernameField, passwordField, ignored] =
1111            this._getFormFields(form, false);
1112
1113        // Need a valid password field to do anything.
1114        if (passwordField == null)
1115            return [false, foundLogins];
1116
1117        // If the fields are disabled or read-only, there's nothing to do.
1118        if (passwordField.disabled || passwordField.readOnly ||
1119            usernameField && (usernameField.disabled ||
1120                              usernameField.readOnly)) {
1121            this.log("not filling form, login fields disabled");
1122            return [false, foundLogins];
1123        }
1124
1125        // Need to get a list of logins if we weren't given them
1126        if (foundLogins == null) {
1127            var formOrigin =
1128                this._getPasswordOrigin(form.ownerDocument.documentURI);
1129            var actionOrigin = this._getActionOrigin(form);
1130            foundLogins = this.findLogins({}, formOrigin, actionOrigin, null);
1131            this.log("found " + foundLogins.length + " matching logins.");
1132        } else {
1133            this.log("reusing logins from last form.");
1134        }
1135
1136        // Discard logins which have username/password values that don't
1137        // fit into the fields (as specified by the maxlength attribute).
1138        // The user couldn't enter these values anyway, and it helps
1139        // with sites that have an extra PIN to be entered (bug 391514)
1140        var maxUsernameLen = Number.MAX_VALUE;
1141        var maxPasswordLen = Number.MAX_VALUE;
1142
1143        // If attribute wasn't set, default is -1.
1144        if (usernameField && usernameField.maxLength >= 0)
1145            maxUsernameLen = usernameField.maxLength;
1146        if (passwordField.maxLength >= 0)
1147            maxPasswordLen = passwordField.maxLength;
1148
1149        var logins = foundLogins.filter(function (l) {
1150                var fit = (l.username.length <= maxUsernameLen &&
1151                           l.password.length <= maxPasswordLen);
1152                if (!fit)
1153                    this.log("Ignored " + l.username + " login: won't fit");
1154
1155                return fit;
1156            }, this);
1157
1158
1159        // Nothing to do if we have no matching logins available.
1160        if (logins.length == 0)
1161            return [false, foundLogins];
1162
1163
1164        // The reason we didn't end up filling the form, if any.  We include
1165        // this in the formInfo object we send with the passwordmgr-found-logins
1166        // notification.  See the _notifyFoundLogins docs for possible values.
1167        var didntFillReason = null;
1168
1169        // Attach autocomplete stuff to the username field, if we have
1170        // one. This is normally used to select from multiple accounts,
1171        // but even with one account we should refill if the user edits.
1172        if (usernameField)
1173            this._attachToInput(usernameField);
1174
1175        // Don't clobber an existing password.
1176        if (passwordField.value && !clobberPassword) {
1177            didntFillReason = "existingPassword";
1178            this._notifyFoundLogins(didntFillReason, usernameField,
1179                                    passwordField, foundLogins, null);
1180            return [false, foundLogins];
1181        }
1182
1183        // If the form has an autocomplete=off attribute in play, don't
1184        // fill in the login automatically. We check this after attaching
1185        // the autocomplete stuff to the username field, so the user can
1186        // still manually select a login to be filled in.
1187        var isFormDisabled = false;
1188        if (!ignoreAutocomplete &&
1189            (this._isAutocompleteDisabled(form) ||
1190             this._isAutocompleteDisabled(usernameField) ||
1191             this._isAutocompleteDisabled(passwordField))) {
1192
1193            isFormDisabled = true;
1194            this.log("form not filled, has autocomplete=off");
1195        }
1196
1197        // Variable such that we reduce code duplication and can be sure we
1198        // should be firing notifications if and only if we can fill the form.
1199        var selectedLogin = null;
1200
1201        if (usernameField && usernameField.value) {
1202            // If username was specified in the form, only fill in the
1203            // password if we find a matching login.
1204            var username = usernameField.value.toLowerCase();
1205
1206            let matchingLogins = logins.filter(function(l)
1207                                     l.username.toLowerCase() == username);
1208            if (matchingLogins.length) {
1209                selectedLogin = matchingLogins[0];
1210            } else {
1211                didntFillReason = "existingUsername";
1212                this.log("Password not filled. None of the stored " +
1213                         "logins match the username already present.");
1214            }
1215        } else if (logins.length == 1) {
1216            selectedLogin = logins[0];
1217        } else {
1218            // We have multiple logins. Handle a special case here, for sites
1219            // which have a normal user+pass login *and* a password-only login
1220            // (eg, a PIN). Prefer the login that matches the type of the form
1221            // (user+pass or pass-only) when there's exactly one that matches.
1222            let matchingLogins;
1223            if (usernameField)
1224                matchingLogins = logins.filter(function(l) l.username);
1225            else
1226                matchingLogins = logins.filter(function(l) !l.username);
1227            if (matchingLogins.length == 1) {
1228                selectedLogin = matchingLogins[0];
1229            } else {
1230                didntFillReason = "multipleLogins";
1231                this.log("Multiple logins for form, so not filling any.");
1232            }
1233        }
1234
1235        var didFillForm = false;
1236        if (selectedLogin && autofillForm && !isFormDisabled) {
1237            // Fill the form
1238            if (usernameField)
1239                usernameField.value = selectedLogin.username;
1240            passwordField.value = selectedLogin.password;
1241            didFillForm = true;
1242        } else if (selectedLogin && !autofillForm) {
1243            // For when autofillForm is false, but we still have the information
1244            // to fill a form, we notify observers.
1245            didntFillReason = "noAutofillForms";
1246            Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
1247            this.log("autofillForms=false but form can be filled; notified observers");
1248        } else if (selectedLogin && isFormDisabled) {
1249            // For when autocomplete is off, but we still have the information
1250            // to fill a form, we notify observers.
1251            didntFillReason = "autocompleteOff";
1252            Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
1253            this.log("autocomplete=off but form can be filled; notified observers");
1254        }
1255
1256        this._notifyFoundLogins(didntFillReason, usernameField, passwordField,
1257                                foundLogins, selectedLogin);
1258
1259        return [didFillForm, foundLogins];
1260    },
1261
1262    /**
1263     * Notify observers about an attempt to fill a form that resulted in some
1264     * saved logins being found for the form.
1265     *
1266     * This does not get called if the login manager attempts to fill a form
1267     * but does not find any saved logins.  It does, however, get called when
1268     * the login manager does find saved logins whether or not it actually
1269     * fills the form with one of them.
1270     *
1271     * @param didntFillReason {String}
1272     *        the reason the login manager didn't fill the form, if any;
1273     *        if the value of this parameter is null, then the form was filled;
1274     *        otherwise, this parameter will be one of these values:
1275     *          existingUsername: the username field already contains a username
1276     *                            that doesn't match any stored usernames
1277     *          existingPassword: the password field already contains a password
1278     *          autocompleteOff:  autocomplete has been disabled for the form
1279     *                            or its username or password fields
1280     *          multipleLogins:   we have multiple logins for the form
1281     *          noAutofillForms:  the autofillForms pref is set to false
1282     *
1283     * @param usernameField   {HTMLInputElement}
1284     *        the username field detected by the login manager, if any;
1285     *        otherwise null
1286     *
1287     * @param passwordField   {HTMLInputElement}
1288     *        the password field detected by the login manager
1289     *
1290     * @param foundLogins     {Array}
1291     *        an array of nsILoginInfos that can be used to fill the form
1292     *
1293     * @param selectedLogin   {nsILoginInfo}
1294     *        the nsILoginInfo that was/would be used to fill the form, if any;
1295     *        otherwise null; whether or not it was actually used depends on
1296     *        the value of the didntFillReason parameter
1297     */
1298    _notifyFoundLogins : function (didntFillReason, usernameField,
1299                                   passwordField, foundLogins, selectedLogin) {
1300        // We need .setProperty(), which is a method on the original
1301        // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2
1302        // doesn't inherit from that, so the additional QI is needed.
1303        let formInfo = Cc["@mozilla.org/hash-property-bag;1"].
1304                       createInstance(Ci.nsIWritablePropertyBag2).
1305                       QueryInterface(Ci.nsIWritablePropertyBag);
1306
1307        formInfo.setPropertyAsACString("didntFillReason", didntFillReason);
1308        formInfo.setPropertyAs

Large files files are truncated, but you can click here to view the full file