toolkit/components/passwordmgr/storage-mozStorage.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/passwordmgr/storage-mozStorage.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1512 @@
     1.4 +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
     1.5 +/* vim: set sw=4 ts=4 et lcs=trail\:.,tab\:>~ : */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +
    1.11 +const Cc = Components.classes;
    1.12 +const Ci = Components.interfaces;
    1.13 +const Cr = Components.results;
    1.14 +
    1.15 +const DB_VERSION = 5; // The database schema version
    1.16 +
    1.17 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    1.18 +Components.utils.import("resource://gre/modules/Services.jsm");
    1.19 +
    1.20 +/**
    1.21 + * Object that manages a database transaction properly so consumers don't have
    1.22 + * to worry about it throwing.
    1.23 + *
    1.24 + * @param aDatabase
    1.25 + *        The mozIStorageConnection to start a transaction on.
    1.26 + */
    1.27 +function Transaction(aDatabase) {
    1.28 +    this._db = aDatabase;
    1.29 +
    1.30 +    this._hasTransaction = false;
    1.31 +    try {
    1.32 +        this._db.beginTransaction();
    1.33 +        this._hasTransaction = true;
    1.34 +    }
    1.35 +    catch(e) { /* om nom nom exceptions */ }
    1.36 +}
    1.37 +
    1.38 +Transaction.prototype = {
    1.39 +    commit : function() {
    1.40 +        if (this._hasTransaction)
    1.41 +            this._db.commitTransaction();
    1.42 +    },
    1.43 +
    1.44 +    rollback : function() {
    1.45 +        if (this._hasTransaction)
    1.46 +            this._db.rollbackTransaction();
    1.47 +    },
    1.48 +};
    1.49 +
    1.50 +
    1.51 +function LoginManagerStorage_mozStorage() { };
    1.52 +
    1.53 +LoginManagerStorage_mozStorage.prototype = {
    1.54 +
    1.55 +    classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"),
    1.56 +    QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
    1.57 +                                            Ci.nsIInterfaceRequestor]),
    1.58 +    getInterface : function(aIID) {
    1.59 +        if (aIID.equals(Ci.mozIStorageConnection)) {
    1.60 +            return this._dbConnection;
    1.61 +        }
    1.62 +
    1.63 +        throw Cr.NS_ERROR_NO_INTERFACE;
    1.64 +    },
    1.65 +
    1.66 +    __crypto : null,  // nsILoginManagerCrypto service
    1.67 +    get _crypto() {
    1.68 +        if (!this.__crypto)
    1.69 +            this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
    1.70 +                            getService(Ci.nsILoginManagerCrypto);
    1.71 +        return this.__crypto;
    1.72 +    },
    1.73 +
    1.74 +    __profileDir: null,  // nsIFile for the user's profile dir
    1.75 +    get _profileDir() {
    1.76 +        if (!this.__profileDir)
    1.77 +            this.__profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
    1.78 +        return this.__profileDir;
    1.79 +    },
    1.80 +
    1.81 +    __storageService: null, // Storage service for using mozStorage
    1.82 +    get _storageService() {
    1.83 +        if (!this.__storageService)
    1.84 +            this.__storageService = Cc["@mozilla.org/storage/service;1"].
    1.85 +                                    getService(Ci.mozIStorageService);
    1.86 +        return this.__storageService;
    1.87 +    },
    1.88 +
    1.89 +    __uuidService: null,
    1.90 +    get _uuidService() {
    1.91 +        if (!this.__uuidService)
    1.92 +            this.__uuidService = Cc["@mozilla.org/uuid-generator;1"].
    1.93 +                                 getService(Ci.nsIUUIDGenerator);
    1.94 +        return this.__uuidService;
    1.95 +    },
    1.96 +
    1.97 +
    1.98 +    // The current database schema.
    1.99 +    _dbSchema: {
   1.100 +        tables: {
   1.101 +            moz_logins:         "id                  INTEGER PRIMARY KEY," +
   1.102 +                                "hostname            TEXT NOT NULL,"       +
   1.103 +                                "httpRealm           TEXT,"                +
   1.104 +                                "formSubmitURL       TEXT,"                +
   1.105 +                                "usernameField       TEXT NOT NULL,"       +
   1.106 +                                "passwordField       TEXT NOT NULL,"       +
   1.107 +                                "encryptedUsername   TEXT NOT NULL,"       +
   1.108 +                                "encryptedPassword   TEXT NOT NULL,"       +
   1.109 +                                "guid                TEXT,"                +
   1.110 +                                "encType             INTEGER,"             +
   1.111 +                                "timeCreated         INTEGER,"             +
   1.112 +                                "timeLastUsed        INTEGER,"             +
   1.113 +                                "timePasswordChanged INTEGER,"             +
   1.114 +                                "timesUsed           INTEGER",
   1.115 +            // Changes must be reflected in this._dbAreExpectedColumnsPresent(),
   1.116 +            // this._searchLogins(), and this.modifyLogin().
   1.117 +
   1.118 +            moz_disabledHosts:  "id                 INTEGER PRIMARY KEY," +
   1.119 +                                "hostname           TEXT UNIQUE ON CONFLICT REPLACE",
   1.120 +
   1.121 +            moz_deleted_logins: "id                  INTEGER PRIMARY KEY," +
   1.122 +                                "guid                TEXT,"                +
   1.123 +                                "timeDeleted         INTEGER",
   1.124 +        },
   1.125 +        indices: {
   1.126 +          moz_logins_hostname_index: {
   1.127 +            table: "moz_logins",
   1.128 +            columns: ["hostname"]
   1.129 +          },
   1.130 +          moz_logins_hostname_formSubmitURL_index: {
   1.131 +            table: "moz_logins",
   1.132 +            columns: ["hostname", "formSubmitURL"]
   1.133 +          },
   1.134 +          moz_logins_hostname_httpRealm_index: {
   1.135 +              table: "moz_logins",
   1.136 +              columns: ["hostname", "httpRealm"]
   1.137 +          },
   1.138 +          moz_logins_guid_index: {
   1.139 +              table: "moz_logins",
   1.140 +              columns: ["guid"]
   1.141 +          },
   1.142 +          moz_logins_encType_index: {
   1.143 +              table: "moz_logins",
   1.144 +              columns: ["encType"]
   1.145 +          }
   1.146 +        }
   1.147 +    },
   1.148 +    _dbConnection : null,  // The database connection
   1.149 +    _dbStmts      : null,  // Database statements for memoization
   1.150 +
   1.151 +    _prefBranch   : null,  // Preferences service
   1.152 +    _signonsFile  : null,  // nsIFile for "signons.sqlite"
   1.153 +    _debug        : false, // mirrors signon.debug
   1.154 +
   1.155 +
   1.156 +    /*
   1.157 +     * log
   1.158 +     *
   1.159 +     * Internal function for logging debug messages to the Error Console.
   1.160 +     */
   1.161 +    log : function (message) {
   1.162 +        if (!this._debug)
   1.163 +            return;
   1.164 +        dump("PwMgr mozStorage: " + message + "\n");
   1.165 +        Services.console.logStringMessage("PwMgr mozStorage: " + message);
   1.166 +    },
   1.167 +
   1.168 +
   1.169 +    /*
   1.170 +     * initWithFile
   1.171 +     *
   1.172 +     * Initialize the component, but override the default filename locations.
   1.173 +     * This is primarily used to the unit tests and profile migration.
   1.174 +     */
   1.175 +    initWithFile : function(aDBFile) {
   1.176 +        if (aDBFile)
   1.177 +            this._signonsFile = aDBFile;
   1.178 +
   1.179 +        this.init();
   1.180 +    },
   1.181 +
   1.182 +
   1.183 +    /*
   1.184 +     * init
   1.185 +     *
   1.186 +     */
   1.187 +    init : function () {
   1.188 +        this._dbStmts = {};
   1.189 +
   1.190 +        // Connect to the correct preferences branch.
   1.191 +        this._prefBranch = Services.prefs.getBranch("signon.");
   1.192 +        this._debug = this._prefBranch.getBoolPref("debug");
   1.193 +
   1.194 +        let isFirstRun;
   1.195 +        try {
   1.196 +            // Force initialization of the crypto module.
   1.197 +            // See bug 717490 comment 17.
   1.198 +            this._crypto;
   1.199 +
   1.200 +            // If initWithFile is calling us, _signonsFile may already be set.
   1.201 +            if (!this._signonsFile) {
   1.202 +                // Initialize signons.sqlite
   1.203 +                this._signonsFile = this._profileDir.clone();
   1.204 +                this._signonsFile.append("signons.sqlite");
   1.205 +            }
   1.206 +            this.log("Opening database at " + this._signonsFile.path);
   1.207 +
   1.208 +            // Initialize the database (create, migrate as necessary)
   1.209 +            isFirstRun = this._dbInit();
   1.210 +
   1.211 +            this._initialized = true;
   1.212 +        } catch (e) {
   1.213 +            this.log("Initialization failed: " + e);
   1.214 +            // If the import fails on first run, we want to delete the db
   1.215 +            if (isFirstRun && e == "Import failed")
   1.216 +                this._dbCleanup(false);
   1.217 +            throw "Initialization failed";
   1.218 +        }
   1.219 +    },
   1.220 +
   1.221 +
   1.222 +    /*
   1.223 +     * addLogin
   1.224 +     *
   1.225 +     */
   1.226 +    addLogin : function (login) {
   1.227 +        let encUsername, encPassword;
   1.228 +
   1.229 +        // Throws if there are bogus values.
   1.230 +        this._checkLoginValues(login);
   1.231 +
   1.232 +        [encUsername, encPassword, encType] = this._encryptLogin(login);
   1.233 +
   1.234 +        // Clone the login, so we don't modify the caller's object.
   1.235 +        let loginClone = login.clone();
   1.236 +
   1.237 +        // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
   1.238 +        loginClone.QueryInterface(Ci.nsILoginMetaInfo);
   1.239 +        if (loginClone.guid) {
   1.240 +            if (!this._isGuidUnique(loginClone.guid))
   1.241 +                throw "specified GUID already exists";
   1.242 +        } else {
   1.243 +            loginClone.guid = this._uuidService.generateUUID().toString();
   1.244 +        }
   1.245 +
   1.246 +        // Set timestamps
   1.247 +        let currentTime = Date.now();
   1.248 +        if (!loginClone.timeCreated)
   1.249 +            loginClone.timeCreated = currentTime;
   1.250 +        if (!loginClone.timeLastUsed)
   1.251 +            loginClone.timeLastUsed = currentTime;
   1.252 +        if (!loginClone.timePasswordChanged)
   1.253 +            loginClone.timePasswordChanged = currentTime;
   1.254 +        if (!loginClone.timesUsed)
   1.255 +            loginClone.timesUsed = 1;
   1.256 +
   1.257 +        let query =
   1.258 +            "INSERT INTO moz_logins " +
   1.259 +            "(hostname, httpRealm, formSubmitURL, usernameField, " +
   1.260 +             "passwordField, encryptedUsername, encryptedPassword, " +
   1.261 +             "guid, encType, timeCreated, timeLastUsed, timePasswordChanged, " +
   1.262 +             "timesUsed) " +
   1.263 +            "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " +
   1.264 +                    ":passwordField, :encryptedUsername, :encryptedPassword, " +
   1.265 +                    ":guid, :encType, :timeCreated, :timeLastUsed, " +
   1.266 +                    ":timePasswordChanged, :timesUsed)";
   1.267 +
   1.268 +        let params = {
   1.269 +            hostname:            loginClone.hostname,
   1.270 +            httpRealm:           loginClone.httpRealm,
   1.271 +            formSubmitURL:       loginClone.formSubmitURL,
   1.272 +            usernameField:       loginClone.usernameField,
   1.273 +            passwordField:       loginClone.passwordField,
   1.274 +            encryptedUsername:   encUsername,
   1.275 +            encryptedPassword:   encPassword,
   1.276 +            guid:                loginClone.guid,
   1.277 +            encType:             encType,
   1.278 +            timeCreated:         loginClone.timeCreated,
   1.279 +            timeLastUsed:        loginClone.timeLastUsed,
   1.280 +            timePasswordChanged: loginClone.timePasswordChanged,
   1.281 +            timesUsed:           loginClone.timesUsed
   1.282 +        };
   1.283 +
   1.284 +        let stmt;
   1.285 +        try {
   1.286 +            stmt = this._dbCreateStatement(query, params);
   1.287 +            stmt.execute();
   1.288 +        } catch (e) {
   1.289 +            this.log("addLogin failed: " + e.name + " : " + e.message);
   1.290 +            throw "Couldn't write to database, login not added.";
   1.291 +        } finally {
   1.292 +            if (stmt) {
   1.293 +                stmt.reset();
   1.294 +            }
   1.295 +        }
   1.296 +
   1.297 +        // Send a notification that a login was added.
   1.298 +        this._sendNotification("addLogin", loginClone);
   1.299 +    },
   1.300 +
   1.301 +
   1.302 +    /*
   1.303 +     * removeLogin
   1.304 +     *
   1.305 +     */
   1.306 +    removeLogin : function (login) {
   1.307 +        let [idToDelete, storedLogin] = this._getIdForLogin(login);
   1.308 +        if (!idToDelete)
   1.309 +            throw "No matching logins";
   1.310 +
   1.311 +        // Execute the statement & remove from DB
   1.312 +        let query  = "DELETE FROM moz_logins WHERE id = :id";
   1.313 +        let params = { id: idToDelete };
   1.314 +        let stmt;
   1.315 +        let transaction = new Transaction(this._dbConnection);
   1.316 +        try {
   1.317 +            stmt = this._dbCreateStatement(query, params);
   1.318 +            stmt.execute();
   1.319 +            this.storeDeletedLogin(storedLogin);
   1.320 +            transaction.commit();
   1.321 +        } catch (e) {
   1.322 +            this.log("_removeLogin failed: " + e.name + " : " + e.message);
   1.323 +            throw "Couldn't write to database, login not removed.";
   1.324 +            transaction.rollback();
   1.325 +        } finally {
   1.326 +            if (stmt) {
   1.327 +                stmt.reset();
   1.328 +            }
   1.329 +        }
   1.330 +        this._sendNotification("removeLogin", storedLogin);
   1.331 +    },
   1.332 +
   1.333 +
   1.334 +    /*
   1.335 +     * modifyLogin
   1.336 +     *
   1.337 +     */
   1.338 +    modifyLogin : function (oldLogin, newLoginData) {
   1.339 +        let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
   1.340 +        if (!idToModify)
   1.341 +            throw "No matching logins";
   1.342 +        oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
   1.343 +
   1.344 +        let newLogin;
   1.345 +        if (newLoginData instanceof Ci.nsILoginInfo) {
   1.346 +            // Clone the existing login to get its nsILoginMetaInfo, then init it
   1.347 +            // with the replacement nsILoginInfo data from the new login.
   1.348 +            newLogin = oldStoredLogin.clone();
   1.349 +            newLogin.init(newLoginData.hostname,
   1.350 +                          newLoginData.formSubmitURL, newLoginData.httpRealm,
   1.351 +                          newLoginData.username, newLoginData.password,
   1.352 +                          newLoginData.usernameField, newLoginData.passwordField);
   1.353 +            newLogin.QueryInterface(Ci.nsILoginMetaInfo);
   1.354 +
   1.355 +            // Automatically update metainfo when password is changed.
   1.356 +            if (newLogin.password != oldLogin.password)
   1.357 +                newLogin.timePasswordChanged = Date.now();
   1.358 +        } else if (newLoginData instanceof Ci.nsIPropertyBag) {
   1.359 +            function _bagHasProperty(aPropName) {
   1.360 +                try {
   1.361 +                    newLoginData.getProperty(aPropName);
   1.362 +                    return true;
   1.363 +                } catch (e) {
   1.364 +                    return false;
   1.365 +                }
   1.366 +            }
   1.367 +
   1.368 +            // Clone the existing login, along with all its properties.
   1.369 +            newLogin = oldStoredLogin.clone();
   1.370 +            newLogin.QueryInterface(Ci.nsILoginMetaInfo);
   1.371 +
   1.372 +            // Automatically update metainfo when password is changed.
   1.373 +            // (Done before the main property updates, lest the caller be
   1.374 +            // explicitly updating both .password and .timePasswordChanged)
   1.375 +            if (_bagHasProperty("password")) {
   1.376 +                let newPassword = newLoginData.getProperty("password");
   1.377 +                if (newPassword != oldLogin.password)
   1.378 +                    newLogin.timePasswordChanged = Date.now();
   1.379 +            }
   1.380 +
   1.381 +            let propEnum = newLoginData.enumerator;
   1.382 +            while (propEnum.hasMoreElements()) {
   1.383 +                let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
   1.384 +                switch (prop.name) {
   1.385 +                    // nsILoginInfo properties...
   1.386 +                    case "hostname":
   1.387 +                    case "httpRealm":
   1.388 +                    case "formSubmitURL":
   1.389 +                    case "username":
   1.390 +                    case "password":
   1.391 +                    case "usernameField":
   1.392 +                    case "passwordField":
   1.393 +                    // nsILoginMetaInfo properties...
   1.394 +                    case "guid":
   1.395 +                    case "timeCreated":
   1.396 +                    case "timeLastUsed":
   1.397 +                    case "timePasswordChanged":
   1.398 +                    case "timesUsed":
   1.399 +                        newLogin[prop.name] = prop.value;
   1.400 +                        if (prop.name == "guid" && !this._isGuidUnique(newLogin.guid))
   1.401 +                            throw "specified GUID already exists";
   1.402 +                        break;
   1.403 +
   1.404 +                    // Fake property, allows easy incrementing.
   1.405 +                    case "timesUsedIncrement":
   1.406 +                        newLogin.timesUsed += prop.value;
   1.407 +                        break;
   1.408 +
   1.409 +                    // Fail if caller requests setting an unknown property.
   1.410 +                    default:
   1.411 +                        throw "Unexpected propertybag item: " + prop.name;
   1.412 +                }
   1.413 +            }
   1.414 +        } else {
   1.415 +            throw "newLoginData needs an expected interface!";
   1.416 +        }
   1.417 +
   1.418 +        // Throws if there are bogus values.
   1.419 +        this._checkLoginValues(newLogin);
   1.420 +
   1.421 +        // Get the encrypted value of the username and password.
   1.422 +        let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
   1.423 +
   1.424 +        let query =
   1.425 +            "UPDATE moz_logins " +
   1.426 +            "SET hostname = :hostname, " +
   1.427 +                "httpRealm = :httpRealm, " +
   1.428 +                "formSubmitURL = :formSubmitURL, " +
   1.429 +                "usernameField = :usernameField, " +
   1.430 +                "passwordField = :passwordField, " +
   1.431 +                "encryptedUsername = :encryptedUsername, " +
   1.432 +                "encryptedPassword = :encryptedPassword, " +
   1.433 +                "guid = :guid, " +
   1.434 +                "encType = :encType, " +
   1.435 +                "timeCreated = :timeCreated, " +
   1.436 +                "timeLastUsed = :timeLastUsed, " +
   1.437 +                "timePasswordChanged = :timePasswordChanged, " +
   1.438 +                "timesUsed = :timesUsed " +
   1.439 +            "WHERE id = :id";
   1.440 +
   1.441 +        let params = {
   1.442 +            id:                  idToModify,
   1.443 +            hostname:            newLogin.hostname,
   1.444 +            httpRealm:           newLogin.httpRealm,
   1.445 +            formSubmitURL:       newLogin.formSubmitURL,
   1.446 +            usernameField:       newLogin.usernameField,
   1.447 +            passwordField:       newLogin.passwordField,
   1.448 +            encryptedUsername:   encUsername,
   1.449 +            encryptedPassword:   encPassword,
   1.450 +            guid:                newLogin.guid,
   1.451 +            encType:             encType,
   1.452 +            timeCreated:         newLogin.timeCreated,
   1.453 +            timeLastUsed:        newLogin.timeLastUsed,
   1.454 +            timePasswordChanged: newLogin.timePasswordChanged,
   1.455 +            timesUsed:           newLogin.timesUsed
   1.456 +        };
   1.457 +
   1.458 +        let stmt;
   1.459 +        try {
   1.460 +            stmt = this._dbCreateStatement(query, params);
   1.461 +            stmt.execute();
   1.462 +        } catch (e) {
   1.463 +            this.log("modifyLogin failed: " + e.name + " : " + e.message);
   1.464 +            throw "Couldn't write to database, login not modified.";
   1.465 +        } finally {
   1.466 +            if (stmt) {
   1.467 +                stmt.reset();
   1.468 +            }
   1.469 +        }
   1.470 +
   1.471 +        this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
   1.472 +    },
   1.473 +
   1.474 +
   1.475 +    /*
   1.476 +     * getAllLogins
   1.477 +     *
   1.478 +     * Returns an array of nsILoginInfo.
   1.479 +     */
   1.480 +    getAllLogins : function (count) {
   1.481 +        let [logins, ids] = this._searchLogins({});
   1.482 +
   1.483 +        // decrypt entries for caller.
   1.484 +        logins = this._decryptLogins(logins);
   1.485 +
   1.486 +        this.log("_getAllLogins: returning " + logins.length + " logins.");
   1.487 +        if (count)
   1.488 +            count.value = logins.length; // needed for XPCOM
   1.489 +        return logins;
   1.490 +    },
   1.491 +
   1.492 +
   1.493 +    /*
   1.494 +     * getAllEncryptedLogins
   1.495 +     *
   1.496 +     * Not implemented. This interface was added to extract logins from the
   1.497 +     * legacy storage module without decrypting them. Now that logins are in
   1.498 +     * mozStorage, if the encrypted data is really needed it can be easily
   1.499 +     * obtained with SQL and the mozStorage APIs.
   1.500 +     */
   1.501 +    getAllEncryptedLogins : function (count) {
   1.502 +        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   1.503 +    },
   1.504 +
   1.505 +
   1.506 +    /*
   1.507 +     * searchLogins
   1.508 +     *
   1.509 +     * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
   1.510 +     * JavaScript object and decrypt the results.
   1.511 +     *
   1.512 +     * Returns an array of decrypted nsILoginInfo.
   1.513 +     */
   1.514 +    searchLogins : function(count, matchData) {
   1.515 +        let realMatchData = {};
   1.516 +        // Convert nsIPropertyBag to normal JS object
   1.517 +        let propEnum = matchData.enumerator;
   1.518 +        while (propEnum.hasMoreElements()) {
   1.519 +            let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
   1.520 +            realMatchData[prop.name] = prop.value;
   1.521 +        }
   1.522 +
   1.523 +        let [logins, ids] = this._searchLogins(realMatchData);
   1.524 +
   1.525 +        // Decrypt entries found for the caller.
   1.526 +        logins = this._decryptLogins(logins);
   1.527 +
   1.528 +        count.value = logins.length; // needed for XPCOM
   1.529 +        return logins;
   1.530 +    },
   1.531 +
   1.532 +
   1.533 +    /*
   1.534 +     * _searchLogins
   1.535 +     *
   1.536 +     * Private method to perform arbitrary searches on any field. Decryption is
   1.537 +     * left to the caller.
   1.538 +     *
   1.539 +     * Returns [logins, ids] for logins that match the arguments, where logins
   1.540 +     * is an array of encrypted nsLoginInfo and ids is an array of associated
   1.541 +     * ids in the database.
   1.542 +     */
   1.543 +    _searchLogins : function (matchData) {
   1.544 +        let conditions = [], params = {};
   1.545 +
   1.546 +        for (let field in matchData) {
   1.547 +            let value = matchData[field];
   1.548 +            switch (field) {
   1.549 +                // Historical compatibility requires this special case
   1.550 +                case "formSubmitURL":
   1.551 +                    if (value != null) {
   1.552 +                        conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
   1.553 +                        params["formSubmitURL"] = value;
   1.554 +                        break;
   1.555 +                    }
   1.556 +                // Normal cases.
   1.557 +                case "hostname":
   1.558 +                case "httpRealm":
   1.559 +                case "id":
   1.560 +                case "usernameField":
   1.561 +                case "passwordField":
   1.562 +                case "encryptedUsername":
   1.563 +                case "encryptedPassword":
   1.564 +                case "guid":
   1.565 +                case "encType":
   1.566 +                case "timeCreated":
   1.567 +                case "timeLastUsed":
   1.568 +                case "timePasswordChanged":
   1.569 +                case "timesUsed":
   1.570 +                    if (value == null) {
   1.571 +                        conditions.push(field + " isnull");
   1.572 +                    } else {
   1.573 +                        conditions.push(field + " = :" + field);
   1.574 +                        params[field] = value;
   1.575 +                    }
   1.576 +                    break;
   1.577 +                // Fail if caller requests an unknown property.
   1.578 +                default:
   1.579 +                    throw "Unexpected field: " + field;
   1.580 +            }
   1.581 +        }
   1.582 +
   1.583 +        // Build query
   1.584 +        let query = "SELECT * FROM moz_logins";
   1.585 +        if (conditions.length) {
   1.586 +            conditions = conditions.map(function(c) "(" + c + ")");
   1.587 +            query += " WHERE " + conditions.join(" AND ");
   1.588 +        }
   1.589 +
   1.590 +        let stmt;
   1.591 +        let logins = [], ids = [];
   1.592 +        try {
   1.593 +            stmt = this._dbCreateStatement(query, params);
   1.594 +            // We can't execute as usual here, since we're iterating over rows
   1.595 +            while (stmt.executeStep()) {
   1.596 +                // Create the new nsLoginInfo object, push to array
   1.597 +                let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
   1.598 +                            createInstance(Ci.nsILoginInfo);
   1.599 +                login.init(stmt.row.hostname, stmt.row.formSubmitURL,
   1.600 +                           stmt.row.httpRealm, stmt.row.encryptedUsername,
   1.601 +                           stmt.row.encryptedPassword, stmt.row.usernameField,
   1.602 +                           stmt.row.passwordField);
   1.603 +                // set nsILoginMetaInfo values
   1.604 +                login.QueryInterface(Ci.nsILoginMetaInfo);
   1.605 +                login.guid = stmt.row.guid;
   1.606 +                login.timeCreated = stmt.row.timeCreated;
   1.607 +                login.timeLastUsed = stmt.row.timeLastUsed;
   1.608 +                login.timePasswordChanged = stmt.row.timePasswordChanged;
   1.609 +                login.timesUsed = stmt.row.timesUsed;
   1.610 +                logins.push(login);
   1.611 +                ids.push(stmt.row.id);
   1.612 +            }
   1.613 +        } catch (e) {
   1.614 +            this.log("_searchLogins failed: " + e.name + " : " + e.message);
   1.615 +        } finally {
   1.616 +            if (stmt) {
   1.617 +                stmt.reset();
   1.618 +            }
   1.619 +        }
   1.620 +
   1.621 +        this.log("_searchLogins: returning " + logins.length + " logins");
   1.622 +        return [logins, ids];
   1.623 +    },
   1.624 +
   1.625 +    /* storeDeletedLogin
   1.626 +     *
   1.627 +     * Moves a login to the deleted logins table
   1.628 +     *
   1.629 +     */
   1.630 +     storeDeletedLogin : function(aLogin) {
   1.631 +#ifdef ANDROID
   1.632 +          let stmt = null; 
   1.633 +          try {
   1.634 +              this.log("Storing " + aLogin.guid + " in deleted passwords\n");
   1.635 +              let query = "INSERT INTO moz_deleted_logins (guid, timeDeleted) VALUES (:guid, :timeDeleted)";
   1.636 +              let params = { guid: aLogin.guid,
   1.637 +                             timeDeleted: Date.now() };
   1.638 +              let stmt = this._dbCreateStatement(query, params);
   1.639 +              stmt.execute();
   1.640 +          } catch(ex) {
   1.641 +              throw ex;
   1.642 +          } finally {
   1.643 +              if (stmt)
   1.644 +                  stmt.reset();
   1.645 +          }		
   1.646 +#endif
   1.647 +     },
   1.648 +
   1.649 +
   1.650 +    /*
   1.651 +     * removeAllLogins
   1.652 +     *
   1.653 +     * Removes all logins from storage.
   1.654 +     */
   1.655 +    removeAllLogins : function () {
   1.656 +        this.log("Removing all logins");
   1.657 +        let query;
   1.658 +        let stmt;
   1.659 +        let transaction = new Transaction(this._dbConnection);
   1.660 + 
   1.661 +        // Disabled hosts kept, as one presumably doesn't want to erase those.
   1.662 +        // TODO: Add these items to the deleted items table once we've sorted
   1.663 +        //       out the issues from bug 756701
   1.664 +        query = "DELETE FROM moz_logins";
   1.665 +        try {
   1.666 +            stmt = this._dbCreateStatement(query);
   1.667 +            stmt.execute();
   1.668 +            transaction.commit();
   1.669 +        } catch (e) {
   1.670 +            this.log("_removeAllLogins failed: " + e.name + " : " + e.message);
   1.671 +            transaction.rollback();
   1.672 +            throw "Couldn't write to database";
   1.673 +        } finally {
   1.674 +            if (stmt) {
   1.675 +                stmt.reset();
   1.676 +            }
   1.677 +        }
   1.678 +
   1.679 +        this._sendNotification("removeAllLogins", null);
   1.680 +   },
   1.681 +
   1.682 +
   1.683 +    /*
   1.684 +     * getAllDisabledHosts
   1.685 +     *
   1.686 +     */
   1.687 +    getAllDisabledHosts : function (count) {
   1.688 +        let disabledHosts = this._queryDisabledHosts(null);
   1.689 +
   1.690 +        this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
   1.691 +        if (count)
   1.692 +            count.value = disabledHosts.length; // needed for XPCOM
   1.693 +        return disabledHosts;
   1.694 +    },
   1.695 +
   1.696 +
   1.697 +    /*
   1.698 +     * getLoginSavingEnabled
   1.699 +     *
   1.700 +     */
   1.701 +    getLoginSavingEnabled : function (hostname) {
   1.702 +        this.log("Getting login saving is enabled for " + hostname);
   1.703 +        return this._queryDisabledHosts(hostname).length == 0
   1.704 +    },
   1.705 +
   1.706 +
   1.707 +    /*
   1.708 +     * setLoginSavingEnabled
   1.709 +     *
   1.710 +     */
   1.711 +    setLoginSavingEnabled : function (hostname, enabled) {
   1.712 +        // Throws if there are bogus values.
   1.713 +        this._checkHostnameValue(hostname);
   1.714 +
   1.715 +        this.log("Setting login saving enabled for " + hostname + " to " + enabled);
   1.716 +        let query;
   1.717 +        if (enabled)
   1.718 +            query = "DELETE FROM moz_disabledHosts " +
   1.719 +                    "WHERE hostname = :hostname";
   1.720 +        else
   1.721 +            query = "INSERT INTO moz_disabledHosts " +
   1.722 +                    "(hostname) VALUES (:hostname)";
   1.723 +        let params = { hostname: hostname };
   1.724 +
   1.725 +        let stmt
   1.726 +        try {
   1.727 +            stmt = this._dbCreateStatement(query, params);
   1.728 +            stmt.execute();
   1.729 +        } catch (e) {
   1.730 +            this.log("setLoginSavingEnabled failed: " + e.name + " : " + e.message);
   1.731 +            throw "Couldn't write to database"
   1.732 +        } finally {
   1.733 +            if (stmt) {
   1.734 +                stmt.reset();
   1.735 +            }
   1.736 +        }
   1.737 +
   1.738 +        this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
   1.739 +    },
   1.740 +
   1.741 +
   1.742 +    /*
   1.743 +     * findLogins
   1.744 +     *
   1.745 +     */
   1.746 +    findLogins : function (count, hostname, formSubmitURL, httpRealm) {
   1.747 +        let loginData = {
   1.748 +            hostname: hostname,
   1.749 +            formSubmitURL: formSubmitURL,
   1.750 +            httpRealm: httpRealm
   1.751 +        };
   1.752 +        let matchData = { };
   1.753 +        for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
   1.754 +          if (loginData[field] != '')
   1.755 +              matchData[field] = loginData[field];
   1.756 +        let [logins, ids] = this._searchLogins(matchData);
   1.757 +
   1.758 +        // Decrypt entries found for the caller.
   1.759 +        logins = this._decryptLogins(logins);
   1.760 +
   1.761 +        this.log("_findLogins: returning " + logins.length + " logins");
   1.762 +        count.value = logins.length; // needed for XPCOM
   1.763 +        return logins;
   1.764 +    },
   1.765 +
   1.766 +
   1.767 +    /*
   1.768 +     * countLogins
   1.769 +     *
   1.770 +     */
   1.771 +    countLogins : function (hostname, formSubmitURL, httpRealm) {
   1.772 +        // Do checks for null and empty strings, adjust conditions and params
   1.773 +        let [conditions, params] =
   1.774 +            this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm);
   1.775 +
   1.776 +        let query = "SELECT COUNT(1) AS numLogins FROM moz_logins";
   1.777 +        if (conditions.length) {
   1.778 +            conditions = conditions.map(function(c) "(" + c + ")");
   1.779 +            query += " WHERE " + conditions.join(" AND ");
   1.780 +        }
   1.781 +
   1.782 +        let stmt, numLogins;
   1.783 +        try {
   1.784 +            stmt = this._dbCreateStatement(query, params);
   1.785 +            stmt.executeStep();
   1.786 +            numLogins = stmt.row.numLogins;
   1.787 +        } catch (e) {
   1.788 +            this.log("_countLogins failed: " + e.name + " : " + e.message);
   1.789 +        } finally {
   1.790 +            if (stmt) {
   1.791 +                stmt.reset();
   1.792 +            }
   1.793 +        }
   1.794 +
   1.795 +        this.log("_countLogins: counted logins: " + numLogins);
   1.796 +        return numLogins;
   1.797 +    },
   1.798 +
   1.799 +
   1.800 +    /*
   1.801 +     * uiBusy
   1.802 +     */
   1.803 +    get uiBusy() {
   1.804 +        return this._crypto.uiBusy;
   1.805 +    },
   1.806 +
   1.807 +
   1.808 +    /*
   1.809 +     * isLoggedIn
   1.810 +     */
   1.811 +    get isLoggedIn() {
   1.812 +        return this._crypto.isLoggedIn;
   1.813 +    },
   1.814 +
   1.815 +
   1.816 +    /*
   1.817 +     * _sendNotification
   1.818 +     *
   1.819 +     * Send a notification when stored data is changed.
   1.820 +     */
   1.821 +    _sendNotification : function (changeType, data) {
   1.822 +        let dataObject = data;
   1.823 +        // Can't pass a raw JS string or array though notifyObservers(). :-(
   1.824 +        if (data instanceof Array) {
   1.825 +            dataObject = Cc["@mozilla.org/array;1"].
   1.826 +                         createInstance(Ci.nsIMutableArray);
   1.827 +            for (let i = 0; i < data.length; i++)
   1.828 +                dataObject.appendElement(data[i], false);
   1.829 +        } else if (typeof(data) == "string") {
   1.830 +            dataObject = Cc["@mozilla.org/supports-string;1"].
   1.831 +                         createInstance(Ci.nsISupportsString);
   1.832 +            dataObject.data = data;
   1.833 +        }
   1.834 +        Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
   1.835 +    },
   1.836 +
   1.837 +
   1.838 +    /*
   1.839 +     * _getIdForLogin
   1.840 +     *
   1.841 +     * Returns an array with two items: [id, login]. If the login was not
   1.842 +     * found, both items will be null. The returned login contains the actual
   1.843 +     * stored login (useful for looking at the actual nsILoginMetaInfo values).
   1.844 +     */
   1.845 +    _getIdForLogin : function (login) {
   1.846 +        let matchData = { };
   1.847 +        for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
   1.848 +            if (login[field] != '')
   1.849 +                matchData[field] = login[field];
   1.850 +        let [logins, ids] = this._searchLogins(matchData);
   1.851 +
   1.852 +        let id = null;
   1.853 +        let foundLogin = null;
   1.854 +
   1.855 +        // The specified login isn't encrypted, so we need to ensure
   1.856 +        // the logins we're comparing with are decrypted. We decrypt one entry
   1.857 +        // at a time, lest _decryptLogins return fewer entries and screw up
   1.858 +        // indices between the two.
   1.859 +        for (let i = 0; i < logins.length; i++) {
   1.860 +            let [decryptedLogin] = this._decryptLogins([logins[i]]);
   1.861 +
   1.862 +            if (!decryptedLogin || !decryptedLogin.equals(login))
   1.863 +                continue;
   1.864 +
   1.865 +            // We've found a match, set id and break
   1.866 +            foundLogin = decryptedLogin;
   1.867 +            id = ids[i];
   1.868 +            break;
   1.869 +        }
   1.870 +
   1.871 +        return [id, foundLogin];
   1.872 +    },
   1.873 +
   1.874 +
   1.875 +    /*
   1.876 +     * _queryDisabledHosts
   1.877 +     *
   1.878 +     * Returns an array of hostnames from the database according to the
   1.879 +     * criteria given in the argument. If the argument hostname is null, the
   1.880 +     * result array contains all hostnames
   1.881 +     */
   1.882 +    _queryDisabledHosts : function (hostname) {
   1.883 +        let disabledHosts = [];
   1.884 +
   1.885 +        let query = "SELECT hostname FROM moz_disabledHosts";
   1.886 +        let params = {};
   1.887 +        if (hostname) {
   1.888 +            query += " WHERE hostname = :hostname";
   1.889 +            params = { hostname: hostname };
   1.890 +        }
   1.891 +
   1.892 +        let stmt;
   1.893 +        try {
   1.894 +            stmt = this._dbCreateStatement(query, params);
   1.895 +            while (stmt.executeStep())
   1.896 +                disabledHosts.push(stmt.row.hostname);
   1.897 +        } catch (e) {
   1.898 +            this.log("_queryDisabledHosts failed: " + e.name + " : " + e.message);
   1.899 +        } finally {
   1.900 +            if (stmt) {
   1.901 +                stmt.reset();
   1.902 +            }
   1.903 +        }
   1.904 +
   1.905 +        return disabledHosts;
   1.906 +    },
   1.907 +
   1.908 +
   1.909 +    /*
   1.910 +     * _buildConditionsAndParams
   1.911 +     *
   1.912 +     * Adjusts the WHERE conditions and parameters for statements prior to the
   1.913 +     * statement being created. This fixes the cases where nulls are involved
   1.914 +     * and the empty string is supposed to be a wildcard match
   1.915 +     */
   1.916 +    _buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) {
   1.917 +        let conditions = [], params = {};
   1.918 +
   1.919 +        if (hostname == null) {
   1.920 +            conditions.push("hostname isnull");
   1.921 +        } else if (hostname != '') {
   1.922 +            conditions.push("hostname = :hostname");
   1.923 +            params["hostname"] = hostname;
   1.924 +        }
   1.925 +
   1.926 +        if (formSubmitURL == null) {
   1.927 +            conditions.push("formSubmitURL isnull");
   1.928 +        } else if (formSubmitURL != '') {
   1.929 +            conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
   1.930 +            params["formSubmitURL"] = formSubmitURL;
   1.931 +        }
   1.932 +
   1.933 +        if (httpRealm == null) {
   1.934 +            conditions.push("httpRealm isnull");
   1.935 +        } else if (httpRealm != '') {
   1.936 +            conditions.push("httpRealm = :httpRealm");
   1.937 +            params["httpRealm"] = httpRealm;
   1.938 +        }
   1.939 +
   1.940 +        return [conditions, params];
   1.941 +    },
   1.942 +
   1.943 +
   1.944 +    /*
   1.945 +     * _checkLoginValues
   1.946 +     *
   1.947 +     * Due to the way the signons2.txt file is formatted, we need to make
   1.948 +     * sure certain field values or characters do not cause the file to
   1.949 +     * be parse incorrectly. Reject logins that we can't store correctly.
   1.950 +     */
   1.951 +    _checkLoginValues : function (aLogin) {
   1.952 +        function badCharacterPresent(l, c) {
   1.953 +            return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
   1.954 +                    (l.httpRealm     && l.httpRealm.indexOf(c)     != -1) ||
   1.955 +                                        l.hostname.indexOf(c)      != -1  ||
   1.956 +                                        l.usernameField.indexOf(c) != -1  ||
   1.957 +                                        l.passwordField.indexOf(c) != -1);
   1.958 +        }
   1.959 +
   1.960 +        // Nulls are invalid, as they don't round-trip well.
   1.961 +        // Mostly not a formatting problem, although ".\0" can be quirky.
   1.962 +        if (badCharacterPresent(aLogin, "\0"))
   1.963 +            throw "login values can't contain nulls";
   1.964 +
   1.965 +        // In theory these nulls should just be rolled up into the encrypted
   1.966 +        // values, but nsISecretDecoderRing doesn't use nsStrings, so the
   1.967 +        // nulls cause truncation. Check for them here just to avoid
   1.968 +        // unexpected round-trip surprises.
   1.969 +        if (aLogin.username.indexOf("\0") != -1 ||
   1.970 +            aLogin.password.indexOf("\0") != -1)
   1.971 +            throw "login values can't contain nulls";
   1.972 +
   1.973 +        // Newlines are invalid for any field stored as plaintext.
   1.974 +        if (badCharacterPresent(aLogin, "\r") ||
   1.975 +            badCharacterPresent(aLogin, "\n"))
   1.976 +            throw "login values can't contain newlines";
   1.977 +
   1.978 +        // A line with just a "." can have special meaning.
   1.979 +        if (aLogin.usernameField == "." ||
   1.980 +            aLogin.formSubmitURL == ".")
   1.981 +            throw "login values can't be periods";
   1.982 +
   1.983 +        // A hostname with "\ \(" won't roundtrip.
   1.984 +        // eg host="foo (", realm="bar" --> "foo ( (bar)"
   1.985 +        // vs host="foo", realm=" (bar" --> "foo ( (bar)"
   1.986 +        if (aLogin.hostname.indexOf(" (") != -1)
   1.987 +            throw "bad parens in hostname";
   1.988 +    },
   1.989 +
   1.990 +
   1.991 +    /*
   1.992 +     * _checkHostnameValue
   1.993 +     *
   1.994 +     * Legacy storage prohibited newlines and nulls in hostnames, so we'll keep
   1.995 +     * that standard here. Throws on illegal format.
   1.996 +     */
   1.997 +    _checkHostnameValue : function (hostname) {
   1.998 +        // File format prohibits certain values. Also, nulls
   1.999 +        // won't round-trip with getAllDisabledHosts().
  1.1000 +        if (hostname == "." ||
  1.1001 +            hostname.indexOf("\r") != -1 ||
  1.1002 +            hostname.indexOf("\n") != -1 ||
  1.1003 +            hostname.indexOf("\0") != -1)
  1.1004 +            throw "Invalid hostname";
  1.1005 +    },
  1.1006 +
  1.1007 +
  1.1008 +    /*
  1.1009 +     * _isGuidUnique
  1.1010 +     *
  1.1011 +     * Checks to see if the specified GUID already exists.
  1.1012 +     */
  1.1013 +    _isGuidUnique : function (guid) {
  1.1014 +        let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid";
  1.1015 +        let params = { guid: guid };
  1.1016 +
  1.1017 +        let stmt, numLogins;
  1.1018 +        try {
  1.1019 +            stmt = this._dbCreateStatement(query, params);
  1.1020 +            stmt.executeStep();
  1.1021 +            numLogins = stmt.row.numLogins;
  1.1022 +        } catch (e) {
  1.1023 +            this.log("_isGuidUnique failed: " + e.name + " : " + e.message);
  1.1024 +        } finally {
  1.1025 +            if (stmt) {
  1.1026 +                stmt.reset();
  1.1027 +            }
  1.1028 +        }
  1.1029 +
  1.1030 +        return (numLogins == 0);
  1.1031 +    },
  1.1032 +
  1.1033 +
  1.1034 +    /*
  1.1035 +     * _encryptLogin
  1.1036 +     *
  1.1037 +     * Returns the encrypted username, password, and encrypton type for the specified
  1.1038 +     * login. Can throw if the user cancels a master password entry.
  1.1039 +     */
  1.1040 +    _encryptLogin : function (login) {
  1.1041 +        let encUsername = this._crypto.encrypt(login.username);
  1.1042 +        let encPassword = this._crypto.encrypt(login.password);
  1.1043 +        let encType     = this._crypto.defaultEncType;
  1.1044 +
  1.1045 +        return [encUsername, encPassword, encType];
  1.1046 +    },
  1.1047 +
  1.1048 +
  1.1049 +    /*
  1.1050 +     * _decryptLogins
  1.1051 +     *
  1.1052 +     * Decrypts username and password fields in the provided array of
  1.1053 +     * logins.
  1.1054 +     *
  1.1055 +     * The entries specified by the array will be decrypted, if possible.
  1.1056 +     * An array of successfully decrypted logins will be returned. The return
  1.1057 +     * value should be given to external callers (since still-encrypted
  1.1058 +     * entries are useless), whereas internal callers generally don't want
  1.1059 +     * to lose unencrypted entries (eg, because the user clicked Cancel
  1.1060 +     * instead of entering their master password)
  1.1061 +     */
  1.1062 +    _decryptLogins : function (logins) {
  1.1063 +        let result = [];
  1.1064 +
  1.1065 +        for each (let login in logins) {
  1.1066 +            try {
  1.1067 +                login.username = this._crypto.decrypt(login.username);
  1.1068 +                login.password = this._crypto.decrypt(login.password);
  1.1069 +            } catch (e) {
  1.1070 +                // If decryption failed (corrupt entry?), just skip it.
  1.1071 +                // Rethrow other errors (like canceling entry of a master pw)
  1.1072 +                if (e.result == Cr.NS_ERROR_FAILURE)
  1.1073 +                    continue;
  1.1074 +                throw e;
  1.1075 +            }
  1.1076 +            result.push(login);
  1.1077 +        }
  1.1078 +
  1.1079 +        return result;
  1.1080 +    },
  1.1081 +
  1.1082 +
  1.1083 +    //**************************************************************************//
  1.1084 +    // Database Creation & Access
  1.1085 +
  1.1086 +    /*
  1.1087 +     * _dbCreateStatement
  1.1088 +     *
  1.1089 +     * Creates a statement, wraps it, and then does parameter replacement
  1.1090 +     * Returns the wrapped statement for execution.  Will use memoization
  1.1091 +     * so that statements can be reused.
  1.1092 +     */
  1.1093 +    _dbCreateStatement : function (query, params) {
  1.1094 +        let wrappedStmt = this._dbStmts[query];
  1.1095 +        // Memoize the statements
  1.1096 +        if (!wrappedStmt) {
  1.1097 +            this.log("Creating new statement for query: " + query);
  1.1098 +            wrappedStmt = this._dbConnection.createStatement(query);
  1.1099 +            this._dbStmts[query] = wrappedStmt;
  1.1100 +        }
  1.1101 +        // Replace parameters, must be done 1 at a time
  1.1102 +        if (params)
  1.1103 +            for (let i in params)
  1.1104 +                wrappedStmt.params[i] = params[i];
  1.1105 +        return wrappedStmt;
  1.1106 +    },
  1.1107 +
  1.1108 +
  1.1109 +    /*
  1.1110 +     * _dbInit
  1.1111 +     *
  1.1112 +     * Attempts to initialize the database. This creates the file if it doesn't
  1.1113 +     * exist, performs any migrations, etc. Return if this is the first run.
  1.1114 +     */
  1.1115 +    _dbInit : function () {
  1.1116 +        this.log("Initializing Database");
  1.1117 +        let isFirstRun = false;
  1.1118 +        try {
  1.1119 +            this._dbConnection = this._storageService.openDatabase(this._signonsFile);
  1.1120 +            // Get the version of the schema in the file. It will be 0 if the
  1.1121 +            // database has not been created yet.
  1.1122 +            let version = this._dbConnection.schemaVersion;
  1.1123 +            if (version == 0) {
  1.1124 +                this._dbCreate();
  1.1125 +                isFirstRun = true;
  1.1126 +            } else if (version != DB_VERSION) {
  1.1127 +                this._dbMigrate(version);
  1.1128 +            }
  1.1129 +        } catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
  1.1130 +            // Database is corrupted, so we backup the database, then throw
  1.1131 +            // causing initialization to fail and a new db to be created next use
  1.1132 +            this._dbCleanup(true);
  1.1133 +            throw e;
  1.1134 +        }
  1.1135 +
  1.1136 +        Services.obs.addObserver(this, "profile-before-change", false);
  1.1137 +        return isFirstRun;
  1.1138 +    },
  1.1139 +
  1.1140 +    observe: function (subject, topic, data) {
  1.1141 +        switch (topic) {
  1.1142 +            case "profile-before-change":
  1.1143 +                Services.obs.removeObserver(this, "profile-before-change");
  1.1144 +                this._dbClose();
  1.1145 +            break;
  1.1146 +        }
  1.1147 +    },
  1.1148 +
  1.1149 +    _dbCreate: function () {
  1.1150 +        this.log("Creating Database");
  1.1151 +        this._dbCreateSchema();
  1.1152 +        this._dbConnection.schemaVersion = DB_VERSION;
  1.1153 +    },
  1.1154 +
  1.1155 +
  1.1156 +    _dbCreateSchema : function () {
  1.1157 +        this._dbCreateTables();
  1.1158 +        this._dbCreateIndices();
  1.1159 +    },
  1.1160 +
  1.1161 +
  1.1162 +    _dbCreateTables : function () {
  1.1163 +        this.log("Creating Tables");
  1.1164 +        for (let name in this._dbSchema.tables)
  1.1165 +            this._dbConnection.createTable(name, this._dbSchema.tables[name]);
  1.1166 +    },
  1.1167 +
  1.1168 +
  1.1169 +    _dbCreateIndices : function () {
  1.1170 +        this.log("Creating Indices");
  1.1171 +        for (let name in this._dbSchema.indices) {
  1.1172 +            let index = this._dbSchema.indices[name];
  1.1173 +            let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
  1.1174 +                            "(" + index.columns.join(", ") + ")";
  1.1175 +            this._dbConnection.executeSimpleSQL(statement);
  1.1176 +        }
  1.1177 +    },
  1.1178 +
  1.1179 +
  1.1180 +    _dbMigrate : function (oldVersion) {
  1.1181 +        this.log("Attempting to migrate from version " + oldVersion);
  1.1182 +
  1.1183 +        if (oldVersion > DB_VERSION) {
  1.1184 +            this.log("Downgrading to version " + DB_VERSION);
  1.1185 +            // User's DB is newer. Sanity check that our expected columns are
  1.1186 +            // present, and if so mark the lower version and merrily continue
  1.1187 +            // on. If the columns are borked, something is wrong so blow away
  1.1188 +            // the DB and start from scratch. [Future incompatible upgrades
  1.1189 +            // should swtich to a different table or file.]
  1.1190 +
  1.1191 +            if (!this._dbAreExpectedColumnsPresent())
  1.1192 +                throw Components.Exception("DB is missing expected columns",
  1.1193 +                                           Cr.NS_ERROR_FILE_CORRUPTED);
  1.1194 +
  1.1195 +            // Change the stored version to the current version. If the user
  1.1196 +            // runs the newer code again, it will see the lower version number
  1.1197 +            // and re-upgrade (to fixup any entries the old code added).
  1.1198 +            this._dbConnection.schemaVersion = DB_VERSION;
  1.1199 +            return;
  1.1200 +        }
  1.1201 +
  1.1202 +        // Upgrade to newer version...
  1.1203 +
  1.1204 +        let transaction = new Transaction(this._dbConnection);
  1.1205 +
  1.1206 +        try {
  1.1207 +            for (let v = oldVersion + 1; v <= DB_VERSION; v++) {
  1.1208 +                this.log("Upgrading to version " + v + "...");
  1.1209 +                let migrateFunction = "_dbMigrateToVersion" + v;
  1.1210 +                this[migrateFunction]();
  1.1211 +            }
  1.1212 +        } catch (e) {
  1.1213 +            this.log("Migration failed: "  + e);
  1.1214 +            transaction.rollback();
  1.1215 +            throw e;
  1.1216 +        }
  1.1217 +
  1.1218 +        this._dbConnection.schemaVersion = DB_VERSION;
  1.1219 +        transaction.commit();
  1.1220 +        this.log("DB migration completed.");
  1.1221 +    },
  1.1222 +
  1.1223 +
  1.1224 +    /*
  1.1225 +     * _dbMigrateToVersion2
  1.1226 +     *
  1.1227 +     * Version 2 adds a GUID column. Existing logins are assigned a random GUID.
  1.1228 +     */
  1.1229 +    _dbMigrateToVersion2 : function () {
  1.1230 +        // Check to see if GUID column already exists, add if needed
  1.1231 +        let query;
  1.1232 +        if (!this._dbColumnExists("guid")) {
  1.1233 +            query = "ALTER TABLE moz_logins ADD COLUMN guid TEXT";
  1.1234 +            this._dbConnection.executeSimpleSQL(query);
  1.1235 +
  1.1236 +            query = "CREATE INDEX IF NOT EXISTS moz_logins_guid_index ON moz_logins (guid)";
  1.1237 +            this._dbConnection.executeSimpleSQL(query);
  1.1238 +        }
  1.1239 +
  1.1240 +        // Get a list of IDs for existing logins
  1.1241 +        let ids = [];
  1.1242 +        let query = "SELECT id FROM moz_logins WHERE guid isnull";
  1.1243 +        let stmt;
  1.1244 +        try {
  1.1245 +            stmt = this._dbCreateStatement(query);
  1.1246 +            while (stmt.executeStep())
  1.1247 +                ids.push(stmt.row.id);
  1.1248 +        } catch (e) {
  1.1249 +            this.log("Failed getting IDs: " + e);
  1.1250 +            throw e;
  1.1251 +        } finally {
  1.1252 +            if (stmt) {
  1.1253 +                stmt.reset();
  1.1254 +            }
  1.1255 +        }
  1.1256 +
  1.1257 +        // Generate a GUID for each login and update the DB.
  1.1258 +        query = "UPDATE moz_logins SET guid = :guid WHERE id = :id";
  1.1259 +        for each (let id in ids) {
  1.1260 +            let params = {
  1.1261 +                id:   id,
  1.1262 +                guid: this._uuidService.generateUUID().toString()
  1.1263 +            };
  1.1264 +
  1.1265 +            try {
  1.1266 +                stmt = this._dbCreateStatement(query, params);
  1.1267 +                stmt.execute();
  1.1268 +            } catch (e) {
  1.1269 +                this.log("Failed setting GUID: " + e);
  1.1270 +                throw e;
  1.1271 +            } finally {
  1.1272 +                if (stmt) {
  1.1273 +                    stmt.reset();
  1.1274 +                }
  1.1275 +            }
  1.1276 +        }
  1.1277 +    },
  1.1278 +
  1.1279 +
  1.1280 +    /*
  1.1281 +     * _dbMigrateToVersion3
  1.1282 +     *
  1.1283 +     * Version 3 adds a encType column.
  1.1284 +     */
  1.1285 +    _dbMigrateToVersion3 : function () {
  1.1286 +        // Check to see if encType column already exists, add if needed
  1.1287 +        let query;
  1.1288 +        if (!this._dbColumnExists("encType")) {
  1.1289 +            query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER";
  1.1290 +            this._dbConnection.executeSimpleSQL(query);
  1.1291 +
  1.1292 +            query = "CREATE INDEX IF NOT EXISTS " +
  1.1293 +                        "moz_logins_encType_index ON moz_logins (encType)";
  1.1294 +            this._dbConnection.executeSimpleSQL(query);
  1.1295 +        }
  1.1296 +
  1.1297 +        // Get a list of existing logins
  1.1298 +        let logins = [];
  1.1299 +        let stmt;
  1.1300 +        query = "SELECT id, encryptedUsername, encryptedPassword " +
  1.1301 +                    "FROM moz_logins WHERE encType isnull";
  1.1302 +        try {
  1.1303 +            stmt = this._dbCreateStatement(query);
  1.1304 +            while (stmt.executeStep()) {
  1.1305 +                let params = { id: stmt.row.id };
  1.1306 +                // We will tag base64 logins correctly, but no longer support their use.
  1.1307 +                if (stmt.row.encryptedUsername.charAt(0) == '~' ||
  1.1308 +                    stmt.row.encryptedPassword.charAt(0) == '~')
  1.1309 +                    params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64;
  1.1310 +                else
  1.1311 +                    params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
  1.1312 +                logins.push(params);
  1.1313 +            }
  1.1314 +        } catch (e) {
  1.1315 +            this.log("Failed getting logins: " + e);
  1.1316 +            throw e;
  1.1317 +        } finally {
  1.1318 +            if (stmt) {
  1.1319 +                stmt.reset();
  1.1320 +            }
  1.1321 +        }
  1.1322 +
  1.1323 +        // Determine encryption type for each login and update the DB.
  1.1324 +        query = "UPDATE moz_logins SET encType = :encType WHERE id = :id";
  1.1325 +        for each (let params in logins) {
  1.1326 +            try {
  1.1327 +                stmt = this._dbCreateStatement(query, params);
  1.1328 +                stmt.execute();
  1.1329 +            } catch (e) {
  1.1330 +                this.log("Failed setting encType: " + e);
  1.1331 +                throw e;
  1.1332 +            } finally {
  1.1333 +                if (stmt) {
  1.1334 +                    stmt.reset();
  1.1335 +                }
  1.1336 +            }
  1.1337 +        }
  1.1338 +    },
  1.1339 +
  1.1340 +
  1.1341 +    /*
  1.1342 +     * _dbMigrateToVersion4
  1.1343 +     *
  1.1344 +     * Version 4 adds timeCreated, timeLastUsed, timePasswordChanged,
  1.1345 +     * and timesUsed columns
  1.1346 +     */
  1.1347 +    _dbMigrateToVersion4 : function () {
  1.1348 +        let query;
  1.1349 +        // Add the new columns, if needed.
  1.1350 +        for each (let column in ["timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
  1.1351 +            if (!this._dbColumnExists(column)) {
  1.1352 +                query = "ALTER TABLE moz_logins ADD COLUMN " + column + " INTEGER";
  1.1353 +                this._dbConnection.executeSimpleSQL(query);
  1.1354 +            }
  1.1355 +        }
  1.1356 +
  1.1357 +        // Get a list of IDs for existing logins.
  1.1358 +        let ids = [];
  1.1359 +        let stmt;
  1.1360 +        query = "SELECT id FROM moz_logins WHERE timeCreated isnull OR " +
  1.1361 +                "timeLastUsed isnull OR timePasswordChanged isnull OR timesUsed isnull";
  1.1362 +        try {
  1.1363 +            stmt = this._dbCreateStatement(query);
  1.1364 +            while (stmt.executeStep())
  1.1365 +                ids.push(stmt.row.id);
  1.1366 +        } catch (e) {
  1.1367 +            this.log("Failed getting IDs: " + e);
  1.1368 +            throw e;
  1.1369 +        } finally {
  1.1370 +            if (stmt) {
  1.1371 +                stmt.reset();
  1.1372 +            }
  1.1373 +        }
  1.1374 +
  1.1375 +        // Initialize logins with current time.
  1.1376 +        query = "UPDATE moz_logins SET timeCreated = :initTime, timeLastUsed = :initTime, " +
  1.1377 +                "timePasswordChanged = :initTime, timesUsed = 1 WHERE id = :id";
  1.1378 +        let params = {
  1.1379 +            id:       null,
  1.1380 +            initTime: Date.now()
  1.1381 +        };
  1.1382 +        for each (let id in ids) {
  1.1383 +            params.id = id;
  1.1384 +            try {
  1.1385 +                stmt = this._dbCreateStatement(query, params);
  1.1386 +                stmt.execute();
  1.1387 +            } catch (e) {
  1.1388 +                this.log("Failed setting timestamps: " + e);
  1.1389 +                throw e;
  1.1390 +            } finally {
  1.1391 +                if (stmt) {
  1.1392 +                    stmt.reset();
  1.1393 +                }
  1.1394 +            }
  1.1395 +        }
  1.1396 +    },
  1.1397 +
  1.1398 +
  1.1399 +    /*
  1.1400 +     * _dbMigrateToVersion5
  1.1401 +     *
  1.1402 +     * Version 5 adds the moz_deleted_logins table
  1.1403 +     */
  1.1404 +    _dbMigrateToVersion5 : function () {
  1.1405 +        if (!this._dbConnection.tableExists("moz_deleted_logins")) {
  1.1406 +          this._dbConnection.createTable("moz_deleted_logins", this._dbSchema.tables.moz_deleted_logins);
  1.1407 +        }
  1.1408 +    },
  1.1409 +
  1.1410 +    /*
  1.1411 +     * _dbAreExpectedColumnsPresent
  1.1412 +     *
  1.1413 +     * Sanity check to ensure that the columns this version of the code expects
  1.1414 +     * are present in the DB we're using.
  1.1415 +     */
  1.1416 +    _dbAreExpectedColumnsPresent : function () {
  1.1417 +        let query = "SELECT " +
  1.1418 +                       "id, " +
  1.1419 +                       "hostname, " +
  1.1420 +                       "httpRealm, " +
  1.1421 +                       "formSubmitURL, " +
  1.1422 +                       "usernameField, " +
  1.1423 +                       "passwordField, " +
  1.1424 +                       "encryptedUsername, " +
  1.1425 +                       "encryptedPassword, " +
  1.1426 +                       "guid, " +
  1.1427 +                       "encType, " +
  1.1428 +                       "timeCreated, " +
  1.1429 +                       "timeLastUsed, " +
  1.1430 +                       "timePasswordChanged, " +
  1.1431 +                       "timesUsed " +
  1.1432 +                    "FROM moz_logins";
  1.1433 +        try {
  1.1434 +            let stmt = this._dbConnection.createStatement(query);
  1.1435 +            // (no need to execute statement, if it compiled we're good)
  1.1436 +            stmt.finalize();
  1.1437 +        } catch (e) {
  1.1438 +            return false;
  1.1439 +        }
  1.1440 +
  1.1441 +        query = "SELECT " +
  1.1442 +                   "id, " +
  1.1443 +                   "hostname " +
  1.1444 +                "FROM moz_disabledHosts";
  1.1445 +        try {
  1.1446 +            let stmt = this._dbConnection.createStatement(query);
  1.1447 +            // (no need to execute statement, if it compiled we're good)
  1.1448 +            stmt.finalize();
  1.1449 +        } catch (e) {
  1.1450 +            return false;
  1.1451 +        }
  1.1452 +
  1.1453 +        this.log("verified that expected columns are present in DB.");
  1.1454 +        return true;
  1.1455 +    },
  1.1456 +
  1.1457 +
  1.1458 +    /*
  1.1459 +     * _dbColumnExists
  1.1460 +     *
  1.1461 +     * Checks to see if the named column already exists.
  1.1462 +     */
  1.1463 +    _dbColumnExists : function (columnName) {
  1.1464 +        let query = "SELECT " + columnName + " FROM moz_logins";
  1.1465 +        try {
  1.1466 +            let stmt = this._dbConnection.createStatement(query);
  1.1467 +            // (no need to execute statement, if it compiled we're good)
  1.1468 +            stmt.finalize();
  1.1469 +            return true;
  1.1470 +        } catch (e) {
  1.1471 +            return false;
  1.1472 +        }
  1.1473 +    },
  1.1474 +
  1.1475 +    _dbClose : function () {
  1.1476 +        this.log("Closing the DB connection.");
  1.1477 +        // Finalize all statements to free memory, avoid errors later
  1.1478 +        for each (let stmt in this._dbStmts) {
  1.1479 +            stmt.finalize();
  1.1480 +        }
  1.1481 +        this._dbStmts = {};
  1.1482 +
  1.1483 +        if (this._dbConnection !== null) {
  1.1484 +            try {
  1.1485 +                this._dbConnection.close();
  1.1486 +            } catch (e) {
  1.1487 +                Components.utils.reportError(e);
  1.1488 +            }
  1.1489 +        }
  1.1490 +        this._dbConnection = null;
  1.1491 +    },
  1.1492 +
  1.1493 +    /*
  1.1494 +     * _dbCleanup
  1.1495 +     *
  1.1496 +     * Called when database creation fails. Finalizes database statements,
  1.1497 +     * closes the database connection, deletes the database file.
  1.1498 +     */
  1.1499 +    _dbCleanup : function (backup) {
  1.1500 +        this.log("Cleaning up DB file - close & remove & backup=" + backup)
  1.1501 +
  1.1502 +        // Create backup file
  1.1503 +        if (backup) {
  1.1504 +            let backupFile = this._signonsFile.leafName + ".corrupt";
  1.1505 +            this._storageService.backupDatabaseFile(this._signonsFile, backupFile);
  1.1506 +        }
  1.1507 +
  1.1508 +        this._dbClose();
  1.1509 +        this._signonsFile.remove(false);
  1.1510 +    }
  1.1511 +
  1.1512 +}; // end of nsLoginManagerStorage_mozStorage implementation
  1.1513 +
  1.1514 +let component = [LoginManagerStorage_mozStorage];
  1.1515 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial