michael@0: /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ michael@0: /* vim: set sw=4 ts=4 et lcs=trail\:.,tab\:>~ : */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: const DB_VERSION = 5; // The database schema version michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: /** michael@0: * Object that manages a database transaction properly so consumers don't have michael@0: * to worry about it throwing. michael@0: * michael@0: * @param aDatabase michael@0: * The mozIStorageConnection to start a transaction on. michael@0: */ michael@0: function Transaction(aDatabase) { michael@0: this._db = aDatabase; michael@0: michael@0: this._hasTransaction = false; michael@0: try { michael@0: this._db.beginTransaction(); michael@0: this._hasTransaction = true; michael@0: } michael@0: catch(e) { /* om nom nom exceptions */ } michael@0: } michael@0: michael@0: Transaction.prototype = { michael@0: commit : function() { michael@0: if (this._hasTransaction) michael@0: this._db.commitTransaction(); michael@0: }, michael@0: michael@0: rollback : function() { michael@0: if (this._hasTransaction) michael@0: this._db.rollbackTransaction(); michael@0: }, michael@0: }; michael@0: michael@0: michael@0: function LoginManagerStorage_mozStorage() { }; michael@0: michael@0: LoginManagerStorage_mozStorage.prototype = { michael@0: michael@0: classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"), michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage, michael@0: Ci.nsIInterfaceRequestor]), michael@0: getInterface : function(aIID) { michael@0: if (aIID.equals(Ci.mozIStorageConnection)) { michael@0: return this._dbConnection; michael@0: } michael@0: michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: __crypto : null, // nsILoginManagerCrypto service michael@0: get _crypto() { michael@0: if (!this.__crypto) michael@0: this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"]. michael@0: getService(Ci.nsILoginManagerCrypto); michael@0: return this.__crypto; michael@0: }, michael@0: michael@0: __profileDir: null, // nsIFile for the user's profile dir michael@0: get _profileDir() { michael@0: if (!this.__profileDir) michael@0: this.__profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); michael@0: return this.__profileDir; michael@0: }, michael@0: michael@0: __storageService: null, // Storage service for using mozStorage michael@0: get _storageService() { michael@0: if (!this.__storageService) michael@0: this.__storageService = Cc["@mozilla.org/storage/service;1"]. michael@0: getService(Ci.mozIStorageService); michael@0: return this.__storageService; michael@0: }, michael@0: michael@0: __uuidService: null, michael@0: get _uuidService() { michael@0: if (!this.__uuidService) michael@0: this.__uuidService = Cc["@mozilla.org/uuid-generator;1"]. michael@0: getService(Ci.nsIUUIDGenerator); michael@0: return this.__uuidService; michael@0: }, michael@0: michael@0: michael@0: // The current database schema. michael@0: _dbSchema: { michael@0: tables: { michael@0: moz_logins: "id INTEGER PRIMARY KEY," + michael@0: "hostname TEXT NOT NULL," + michael@0: "httpRealm TEXT," + michael@0: "formSubmitURL TEXT," + michael@0: "usernameField TEXT NOT NULL," + michael@0: "passwordField TEXT NOT NULL," + michael@0: "encryptedUsername TEXT NOT NULL," + michael@0: "encryptedPassword TEXT NOT NULL," + michael@0: "guid TEXT," + michael@0: "encType INTEGER," + michael@0: "timeCreated INTEGER," + michael@0: "timeLastUsed INTEGER," + michael@0: "timePasswordChanged INTEGER," + michael@0: "timesUsed INTEGER", michael@0: // Changes must be reflected in this._dbAreExpectedColumnsPresent(), michael@0: // this._searchLogins(), and this.modifyLogin(). michael@0: michael@0: moz_disabledHosts: "id INTEGER PRIMARY KEY," + michael@0: "hostname TEXT UNIQUE ON CONFLICT REPLACE", michael@0: michael@0: moz_deleted_logins: "id INTEGER PRIMARY KEY," + michael@0: "guid TEXT," + michael@0: "timeDeleted INTEGER", michael@0: }, michael@0: indices: { michael@0: moz_logins_hostname_index: { michael@0: table: "moz_logins", michael@0: columns: ["hostname"] michael@0: }, michael@0: moz_logins_hostname_formSubmitURL_index: { michael@0: table: "moz_logins", michael@0: columns: ["hostname", "formSubmitURL"] michael@0: }, michael@0: moz_logins_hostname_httpRealm_index: { michael@0: table: "moz_logins", michael@0: columns: ["hostname", "httpRealm"] michael@0: }, michael@0: moz_logins_guid_index: { michael@0: table: "moz_logins", michael@0: columns: ["guid"] michael@0: }, michael@0: moz_logins_encType_index: { michael@0: table: "moz_logins", michael@0: columns: ["encType"] michael@0: } michael@0: } michael@0: }, michael@0: _dbConnection : null, // The database connection michael@0: _dbStmts : null, // Database statements for memoization michael@0: michael@0: _prefBranch : null, // Preferences service michael@0: _signonsFile : null, // nsIFile for "signons.sqlite" michael@0: _debug : false, // mirrors signon.debug michael@0: michael@0: michael@0: /* michael@0: * log michael@0: * michael@0: * Internal function for logging debug messages to the Error Console. michael@0: */ michael@0: log : function (message) { michael@0: if (!this._debug) michael@0: return; michael@0: dump("PwMgr mozStorage: " + message + "\n"); michael@0: Services.console.logStringMessage("PwMgr mozStorage: " + message); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * initWithFile michael@0: * michael@0: * Initialize the component, but override the default filename locations. michael@0: * This is primarily used to the unit tests and profile migration. michael@0: */ michael@0: initWithFile : function(aDBFile) { michael@0: if (aDBFile) michael@0: this._signonsFile = aDBFile; michael@0: michael@0: this.init(); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * init michael@0: * michael@0: */ michael@0: init : function () { michael@0: this._dbStmts = {}; michael@0: michael@0: // Connect to the correct preferences branch. michael@0: this._prefBranch = Services.prefs.getBranch("signon."); michael@0: this._debug = this._prefBranch.getBoolPref("debug"); michael@0: michael@0: let isFirstRun; michael@0: try { michael@0: // Force initialization of the crypto module. michael@0: // See bug 717490 comment 17. michael@0: this._crypto; michael@0: michael@0: // If initWithFile is calling us, _signonsFile may already be set. michael@0: if (!this._signonsFile) { michael@0: // Initialize signons.sqlite michael@0: this._signonsFile = this._profileDir.clone(); michael@0: this._signonsFile.append("signons.sqlite"); michael@0: } michael@0: this.log("Opening database at " + this._signonsFile.path); michael@0: michael@0: // Initialize the database (create, migrate as necessary) michael@0: isFirstRun = this._dbInit(); michael@0: michael@0: this._initialized = true; michael@0: } catch (e) { michael@0: this.log("Initialization failed: " + e); michael@0: // If the import fails on first run, we want to delete the db michael@0: if (isFirstRun && e == "Import failed") michael@0: this._dbCleanup(false); michael@0: throw "Initialization failed"; michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * addLogin michael@0: * michael@0: */ michael@0: addLogin : function (login) { michael@0: let encUsername, encPassword; michael@0: michael@0: // Throws if there are bogus values. michael@0: this._checkLoginValues(login); michael@0: michael@0: [encUsername, encPassword, encType] = this._encryptLogin(login); michael@0: michael@0: // Clone the login, so we don't modify the caller's object. michael@0: let loginClone = login.clone(); michael@0: michael@0: // Initialize the nsILoginMetaInfo fields, unless the caller gave us values michael@0: loginClone.QueryInterface(Ci.nsILoginMetaInfo); michael@0: if (loginClone.guid) { michael@0: if (!this._isGuidUnique(loginClone.guid)) michael@0: throw "specified GUID already exists"; michael@0: } else { michael@0: loginClone.guid = this._uuidService.generateUUID().toString(); michael@0: } michael@0: michael@0: // Set timestamps michael@0: let currentTime = Date.now(); michael@0: if (!loginClone.timeCreated) michael@0: loginClone.timeCreated = currentTime; michael@0: if (!loginClone.timeLastUsed) michael@0: loginClone.timeLastUsed = currentTime; michael@0: if (!loginClone.timePasswordChanged) michael@0: loginClone.timePasswordChanged = currentTime; michael@0: if (!loginClone.timesUsed) michael@0: loginClone.timesUsed = 1; michael@0: michael@0: let query = michael@0: "INSERT INTO moz_logins " + michael@0: "(hostname, httpRealm, formSubmitURL, usernameField, " + michael@0: "passwordField, encryptedUsername, encryptedPassword, " + michael@0: "guid, encType, timeCreated, timeLastUsed, timePasswordChanged, " + michael@0: "timesUsed) " + michael@0: "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " + michael@0: ":passwordField, :encryptedUsername, :encryptedPassword, " + michael@0: ":guid, :encType, :timeCreated, :timeLastUsed, " + michael@0: ":timePasswordChanged, :timesUsed)"; michael@0: michael@0: let params = { michael@0: hostname: loginClone.hostname, michael@0: httpRealm: loginClone.httpRealm, michael@0: formSubmitURL: loginClone.formSubmitURL, michael@0: usernameField: loginClone.usernameField, michael@0: passwordField: loginClone.passwordField, michael@0: encryptedUsername: encUsername, michael@0: encryptedPassword: encPassword, michael@0: guid: loginClone.guid, michael@0: encType: encType, michael@0: timeCreated: loginClone.timeCreated, michael@0: timeLastUsed: loginClone.timeLastUsed, michael@0: timePasswordChanged: loginClone.timePasswordChanged, michael@0: timesUsed: loginClone.timesUsed michael@0: }; michael@0: michael@0: let stmt; michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("addLogin failed: " + e.name + " : " + e.message); michael@0: throw "Couldn't write to database, login not added."; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: // Send a notification that a login was added. michael@0: this._sendNotification("addLogin", loginClone); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * removeLogin michael@0: * michael@0: */ michael@0: removeLogin : function (login) { michael@0: let [idToDelete, storedLogin] = this._getIdForLogin(login); michael@0: if (!idToDelete) michael@0: throw "No matching logins"; michael@0: michael@0: // Execute the statement & remove from DB michael@0: let query = "DELETE FROM moz_logins WHERE id = :id"; michael@0: let params = { id: idToDelete }; michael@0: let stmt; michael@0: let transaction = new Transaction(this._dbConnection); michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: this.storeDeletedLogin(storedLogin); michael@0: transaction.commit(); michael@0: } catch (e) { michael@0: this.log("_removeLogin failed: " + e.name + " : " + e.message); michael@0: throw "Couldn't write to database, login not removed."; michael@0: transaction.rollback(); michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: this._sendNotification("removeLogin", storedLogin); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * modifyLogin michael@0: * michael@0: */ michael@0: modifyLogin : function (oldLogin, newLoginData) { michael@0: let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin); michael@0: if (!idToModify) michael@0: throw "No matching logins"; michael@0: oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo); michael@0: michael@0: let newLogin; michael@0: if (newLoginData instanceof Ci.nsILoginInfo) { michael@0: // Clone the existing login to get its nsILoginMetaInfo, then init it michael@0: // with the replacement nsILoginInfo data from the new login. michael@0: newLogin = oldStoredLogin.clone(); michael@0: newLogin.init(newLoginData.hostname, michael@0: newLoginData.formSubmitURL, newLoginData.httpRealm, michael@0: newLoginData.username, newLoginData.password, michael@0: newLoginData.usernameField, newLoginData.passwordField); michael@0: newLogin.QueryInterface(Ci.nsILoginMetaInfo); michael@0: michael@0: // Automatically update metainfo when password is changed. michael@0: if (newLogin.password != oldLogin.password) michael@0: newLogin.timePasswordChanged = Date.now(); michael@0: } else if (newLoginData instanceof Ci.nsIPropertyBag) { michael@0: function _bagHasProperty(aPropName) { michael@0: try { michael@0: newLoginData.getProperty(aPropName); michael@0: return true; michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: // Clone the existing login, along with all its properties. michael@0: newLogin = oldStoredLogin.clone(); michael@0: newLogin.QueryInterface(Ci.nsILoginMetaInfo); michael@0: michael@0: // Automatically update metainfo when password is changed. michael@0: // (Done before the main property updates, lest the caller be michael@0: // explicitly updating both .password and .timePasswordChanged) michael@0: if (_bagHasProperty("password")) { michael@0: let newPassword = newLoginData.getProperty("password"); michael@0: if (newPassword != oldLogin.password) michael@0: newLogin.timePasswordChanged = Date.now(); michael@0: } michael@0: michael@0: let propEnum = newLoginData.enumerator; michael@0: while (propEnum.hasMoreElements()) { michael@0: let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty); michael@0: switch (prop.name) { michael@0: // nsILoginInfo properties... michael@0: case "hostname": michael@0: case "httpRealm": michael@0: case "formSubmitURL": michael@0: case "username": michael@0: case "password": michael@0: case "usernameField": michael@0: case "passwordField": michael@0: // nsILoginMetaInfo properties... michael@0: case "guid": michael@0: case "timeCreated": michael@0: case "timeLastUsed": michael@0: case "timePasswordChanged": michael@0: case "timesUsed": michael@0: newLogin[prop.name] = prop.value; michael@0: if (prop.name == "guid" && !this._isGuidUnique(newLogin.guid)) michael@0: throw "specified GUID already exists"; michael@0: break; michael@0: michael@0: // Fake property, allows easy incrementing. michael@0: case "timesUsedIncrement": michael@0: newLogin.timesUsed += prop.value; michael@0: break; michael@0: michael@0: // Fail if caller requests setting an unknown property. michael@0: default: michael@0: throw "Unexpected propertybag item: " + prop.name; michael@0: } michael@0: } michael@0: } else { michael@0: throw "newLoginData needs an expected interface!"; michael@0: } michael@0: michael@0: // Throws if there are bogus values. michael@0: this._checkLoginValues(newLogin); michael@0: michael@0: // Get the encrypted value of the username and password. michael@0: let [encUsername, encPassword, encType] = this._encryptLogin(newLogin); michael@0: michael@0: let query = michael@0: "UPDATE moz_logins " + michael@0: "SET hostname = :hostname, " + michael@0: "httpRealm = :httpRealm, " + michael@0: "formSubmitURL = :formSubmitURL, " + michael@0: "usernameField = :usernameField, " + michael@0: "passwordField = :passwordField, " + michael@0: "encryptedUsername = :encryptedUsername, " + michael@0: "encryptedPassword = :encryptedPassword, " + michael@0: "guid = :guid, " + michael@0: "encType = :encType, " + michael@0: "timeCreated = :timeCreated, " + michael@0: "timeLastUsed = :timeLastUsed, " + michael@0: "timePasswordChanged = :timePasswordChanged, " + michael@0: "timesUsed = :timesUsed " + michael@0: "WHERE id = :id"; michael@0: michael@0: let params = { michael@0: id: idToModify, michael@0: hostname: newLogin.hostname, michael@0: httpRealm: newLogin.httpRealm, michael@0: formSubmitURL: newLogin.formSubmitURL, michael@0: usernameField: newLogin.usernameField, michael@0: passwordField: newLogin.passwordField, michael@0: encryptedUsername: encUsername, michael@0: encryptedPassword: encPassword, michael@0: guid: newLogin.guid, michael@0: encType: encType, michael@0: timeCreated: newLogin.timeCreated, michael@0: timeLastUsed: newLogin.timeLastUsed, michael@0: timePasswordChanged: newLogin.timePasswordChanged, michael@0: timesUsed: newLogin.timesUsed michael@0: }; michael@0: michael@0: let stmt; michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("modifyLogin failed: " + e.name + " : " + e.message); michael@0: throw "Couldn't write to database, login not modified."; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * getAllLogins michael@0: * michael@0: * Returns an array of nsILoginInfo. michael@0: */ michael@0: getAllLogins : function (count) { michael@0: let [logins, ids] = this._searchLogins({}); michael@0: michael@0: // decrypt entries for caller. michael@0: logins = this._decryptLogins(logins); michael@0: michael@0: this.log("_getAllLogins: returning " + logins.length + " logins."); michael@0: if (count) michael@0: count.value = logins.length; // needed for XPCOM michael@0: return logins; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * getAllEncryptedLogins michael@0: * michael@0: * Not implemented. This interface was added to extract logins from the michael@0: * legacy storage module without decrypting them. Now that logins are in michael@0: * mozStorage, if the encrypted data is really needed it can be easily michael@0: * obtained with SQL and the mozStorage APIs. michael@0: */ michael@0: getAllEncryptedLogins : function (count) { michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * searchLogins michael@0: * michael@0: * Public wrapper around _searchLogins to convert the nsIPropertyBag to a michael@0: * JavaScript object and decrypt the results. michael@0: * michael@0: * Returns an array of decrypted nsILoginInfo. michael@0: */ michael@0: searchLogins : function(count, matchData) { michael@0: let realMatchData = {}; michael@0: // Convert nsIPropertyBag to normal JS object michael@0: let propEnum = matchData.enumerator; michael@0: while (propEnum.hasMoreElements()) { michael@0: let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty); michael@0: realMatchData[prop.name] = prop.value; michael@0: } michael@0: michael@0: let [logins, ids] = this._searchLogins(realMatchData); michael@0: michael@0: // Decrypt entries found for the caller. michael@0: logins = this._decryptLogins(logins); michael@0: michael@0: count.value = logins.length; // needed for XPCOM michael@0: return logins; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _searchLogins michael@0: * michael@0: * Private method to perform arbitrary searches on any field. Decryption is michael@0: * left to the caller. michael@0: * michael@0: * Returns [logins, ids] for logins that match the arguments, where logins michael@0: * is an array of encrypted nsLoginInfo and ids is an array of associated michael@0: * ids in the database. michael@0: */ michael@0: _searchLogins : function (matchData) { michael@0: let conditions = [], params = {}; michael@0: michael@0: for (let field in matchData) { michael@0: let value = matchData[field]; michael@0: switch (field) { michael@0: // Historical compatibility requires this special case michael@0: case "formSubmitURL": michael@0: if (value != null) { michael@0: conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''"); michael@0: params["formSubmitURL"] = value; michael@0: break; michael@0: } michael@0: // Normal cases. michael@0: case "hostname": michael@0: case "httpRealm": michael@0: case "id": michael@0: case "usernameField": michael@0: case "passwordField": michael@0: case "encryptedUsername": michael@0: case "encryptedPassword": michael@0: case "guid": michael@0: case "encType": michael@0: case "timeCreated": michael@0: case "timeLastUsed": michael@0: case "timePasswordChanged": michael@0: case "timesUsed": michael@0: if (value == null) { michael@0: conditions.push(field + " isnull"); michael@0: } else { michael@0: conditions.push(field + " = :" + field); michael@0: params[field] = value; michael@0: } michael@0: break; michael@0: // Fail if caller requests an unknown property. michael@0: default: michael@0: throw "Unexpected field: " + field; michael@0: } michael@0: } michael@0: michael@0: // Build query michael@0: let query = "SELECT * FROM moz_logins"; michael@0: if (conditions.length) { michael@0: conditions = conditions.map(function(c) "(" + c + ")"); michael@0: query += " WHERE " + conditions.join(" AND "); michael@0: } michael@0: michael@0: let stmt; michael@0: let logins = [], ids = []; michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: // We can't execute as usual here, since we're iterating over rows michael@0: while (stmt.executeStep()) { michael@0: // Create the new nsLoginInfo object, push to array michael@0: let login = Cc["@mozilla.org/login-manager/loginInfo;1"]. michael@0: createInstance(Ci.nsILoginInfo); michael@0: login.init(stmt.row.hostname, stmt.row.formSubmitURL, michael@0: stmt.row.httpRealm, stmt.row.encryptedUsername, michael@0: stmt.row.encryptedPassword, stmt.row.usernameField, michael@0: stmt.row.passwordField); michael@0: // set nsILoginMetaInfo values michael@0: login.QueryInterface(Ci.nsILoginMetaInfo); michael@0: login.guid = stmt.row.guid; michael@0: login.timeCreated = stmt.row.timeCreated; michael@0: login.timeLastUsed = stmt.row.timeLastUsed; michael@0: login.timePasswordChanged = stmt.row.timePasswordChanged; michael@0: login.timesUsed = stmt.row.timesUsed; michael@0: logins.push(login); michael@0: ids.push(stmt.row.id); michael@0: } michael@0: } catch (e) { michael@0: this.log("_searchLogins failed: " + e.name + " : " + e.message); michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: this.log("_searchLogins: returning " + logins.length + " logins"); michael@0: return [logins, ids]; michael@0: }, michael@0: michael@0: /* storeDeletedLogin michael@0: * michael@0: * Moves a login to the deleted logins table michael@0: * michael@0: */ michael@0: storeDeletedLogin : function(aLogin) { michael@0: #ifdef ANDROID michael@0: let stmt = null; michael@0: try { michael@0: this.log("Storing " + aLogin.guid + " in deleted passwords\n"); michael@0: let query = "INSERT INTO moz_deleted_logins (guid, timeDeleted) VALUES (:guid, :timeDeleted)"; michael@0: let params = { guid: aLogin.guid, michael@0: timeDeleted: Date.now() }; michael@0: let stmt = this._dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch(ex) { michael@0: throw ex; michael@0: } finally { michael@0: if (stmt) michael@0: stmt.reset(); michael@0: } michael@0: #endif michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * removeAllLogins michael@0: * michael@0: * Removes all logins from storage. michael@0: */ michael@0: removeAllLogins : function () { michael@0: this.log("Removing all logins"); michael@0: let query; michael@0: let stmt; michael@0: let transaction = new Transaction(this._dbConnection); michael@0: michael@0: // Disabled hosts kept, as one presumably doesn't want to erase those. michael@0: // TODO: Add these items to the deleted items table once we've sorted michael@0: // out the issues from bug 756701 michael@0: query = "DELETE FROM moz_logins"; michael@0: try { michael@0: stmt = this._dbCreateStatement(query); michael@0: stmt.execute(); michael@0: transaction.commit(); michael@0: } catch (e) { michael@0: this.log("_removeAllLogins failed: " + e.name + " : " + e.message); michael@0: transaction.rollback(); michael@0: throw "Couldn't write to database"; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: this._sendNotification("removeAllLogins", null); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * getAllDisabledHosts michael@0: * michael@0: */ michael@0: getAllDisabledHosts : function (count) { michael@0: let disabledHosts = this._queryDisabledHosts(null); michael@0: michael@0: this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts."); michael@0: if (count) michael@0: count.value = disabledHosts.length; // needed for XPCOM michael@0: return disabledHosts; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * getLoginSavingEnabled michael@0: * michael@0: */ michael@0: getLoginSavingEnabled : function (hostname) { michael@0: this.log("Getting login saving is enabled for " + hostname); michael@0: return this._queryDisabledHosts(hostname).length == 0 michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * setLoginSavingEnabled michael@0: * michael@0: */ michael@0: setLoginSavingEnabled : function (hostname, enabled) { michael@0: // Throws if there are bogus values. michael@0: this._checkHostnameValue(hostname); michael@0: michael@0: this.log("Setting login saving enabled for " + hostname + " to " + enabled); michael@0: let query; michael@0: if (enabled) michael@0: query = "DELETE FROM moz_disabledHosts " + michael@0: "WHERE hostname = :hostname"; michael@0: else michael@0: query = "INSERT INTO moz_disabledHosts " + michael@0: "(hostname) VALUES (:hostname)"; michael@0: let params = { hostname: hostname }; michael@0: michael@0: let stmt michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("setLoginSavingEnabled failed: " + e.name + " : " + e.message); michael@0: throw "Couldn't write to database" michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * findLogins michael@0: * michael@0: */ michael@0: findLogins : function (count, hostname, formSubmitURL, httpRealm) { michael@0: let loginData = { michael@0: hostname: hostname, michael@0: formSubmitURL: formSubmitURL, michael@0: httpRealm: httpRealm michael@0: }; michael@0: let matchData = { }; michael@0: for each (let field in ["hostname", "formSubmitURL", "httpRealm"]) michael@0: if (loginData[field] != '') michael@0: matchData[field] = loginData[field]; michael@0: let [logins, ids] = this._searchLogins(matchData); michael@0: michael@0: // Decrypt entries found for the caller. michael@0: logins = this._decryptLogins(logins); michael@0: michael@0: this.log("_findLogins: returning " + logins.length + " logins"); michael@0: count.value = logins.length; // needed for XPCOM michael@0: return logins; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * countLogins michael@0: * michael@0: */ michael@0: countLogins : function (hostname, formSubmitURL, httpRealm) { michael@0: // Do checks for null and empty strings, adjust conditions and params michael@0: let [conditions, params] = michael@0: this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm); michael@0: michael@0: let query = "SELECT COUNT(1) AS numLogins FROM moz_logins"; michael@0: if (conditions.length) { michael@0: conditions = conditions.map(function(c) "(" + c + ")"); michael@0: query += " WHERE " + conditions.join(" AND "); michael@0: } michael@0: michael@0: let stmt, numLogins; michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.executeStep(); michael@0: numLogins = stmt.row.numLogins; michael@0: } catch (e) { michael@0: this.log("_countLogins failed: " + e.name + " : " + e.message); michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: this.log("_countLogins: counted logins: " + numLogins); michael@0: return numLogins; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * uiBusy michael@0: */ michael@0: get uiBusy() { michael@0: return this._crypto.uiBusy; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * isLoggedIn michael@0: */ michael@0: get isLoggedIn() { michael@0: return this._crypto.isLoggedIn; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _sendNotification michael@0: * michael@0: * Send a notification when stored data is changed. michael@0: */ michael@0: _sendNotification : function (changeType, data) { michael@0: let dataObject = data; michael@0: // Can't pass a raw JS string or array though notifyObservers(). :-( michael@0: if (data instanceof Array) { michael@0: dataObject = Cc["@mozilla.org/array;1"]. michael@0: createInstance(Ci.nsIMutableArray); michael@0: for (let i = 0; i < data.length; i++) michael@0: dataObject.appendElement(data[i], false); michael@0: } else if (typeof(data) == "string") { michael@0: dataObject = Cc["@mozilla.org/supports-string;1"]. michael@0: createInstance(Ci.nsISupportsString); michael@0: dataObject.data = data; michael@0: } michael@0: Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getIdForLogin michael@0: * michael@0: * Returns an array with two items: [id, login]. If the login was not michael@0: * found, both items will be null. The returned login contains the actual michael@0: * stored login (useful for looking at the actual nsILoginMetaInfo values). michael@0: */ michael@0: _getIdForLogin : function (login) { michael@0: let matchData = { }; michael@0: for each (let field in ["hostname", "formSubmitURL", "httpRealm"]) michael@0: if (login[field] != '') michael@0: matchData[field] = login[field]; michael@0: let [logins, ids] = this._searchLogins(matchData); michael@0: michael@0: let id = null; michael@0: let foundLogin = null; michael@0: michael@0: // The specified login isn't encrypted, so we need to ensure michael@0: // the logins we're comparing with are decrypted. We decrypt one entry michael@0: // at a time, lest _decryptLogins return fewer entries and screw up michael@0: // indices between the two. michael@0: for (let i = 0; i < logins.length; i++) { michael@0: let [decryptedLogin] = this._decryptLogins([logins[i]]); michael@0: michael@0: if (!decryptedLogin || !decryptedLogin.equals(login)) michael@0: continue; michael@0: michael@0: // We've found a match, set id and break michael@0: foundLogin = decryptedLogin; michael@0: id = ids[i]; michael@0: break; michael@0: } michael@0: michael@0: return [id, foundLogin]; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _queryDisabledHosts michael@0: * michael@0: * Returns an array of hostnames from the database according to the michael@0: * criteria given in the argument. If the argument hostname is null, the michael@0: * result array contains all hostnames michael@0: */ michael@0: _queryDisabledHosts : function (hostname) { michael@0: let disabledHosts = []; michael@0: michael@0: let query = "SELECT hostname FROM moz_disabledHosts"; michael@0: let params = {}; michael@0: if (hostname) { michael@0: query += " WHERE hostname = :hostname"; michael@0: params = { hostname: hostname }; michael@0: } michael@0: michael@0: let stmt; michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: while (stmt.executeStep()) michael@0: disabledHosts.push(stmt.row.hostname); michael@0: } catch (e) { michael@0: this.log("_queryDisabledHosts failed: " + e.name + " : " + e.message); michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: return disabledHosts; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _buildConditionsAndParams michael@0: * michael@0: * Adjusts the WHERE conditions and parameters for statements prior to the michael@0: * statement being created. This fixes the cases where nulls are involved michael@0: * and the empty string is supposed to be a wildcard match michael@0: */ michael@0: _buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) { michael@0: let conditions = [], params = {}; michael@0: michael@0: if (hostname == null) { michael@0: conditions.push("hostname isnull"); michael@0: } else if (hostname != '') { michael@0: conditions.push("hostname = :hostname"); michael@0: params["hostname"] = hostname; michael@0: } michael@0: michael@0: if (formSubmitURL == null) { michael@0: conditions.push("formSubmitURL isnull"); michael@0: } else if (formSubmitURL != '') { michael@0: conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''"); michael@0: params["formSubmitURL"] = formSubmitURL; michael@0: } michael@0: michael@0: if (httpRealm == null) { michael@0: conditions.push("httpRealm isnull"); michael@0: } else if (httpRealm != '') { michael@0: conditions.push("httpRealm = :httpRealm"); michael@0: params["httpRealm"] = httpRealm; michael@0: } michael@0: michael@0: return [conditions, params]; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _checkLoginValues michael@0: * michael@0: * Due to the way the signons2.txt file is formatted, we need to make michael@0: * sure certain field values or characters do not cause the file to michael@0: * be parse incorrectly. Reject logins that we can't store correctly. michael@0: */ michael@0: _checkLoginValues : function (aLogin) { michael@0: function badCharacterPresent(l, c) { michael@0: return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) || michael@0: (l.httpRealm && l.httpRealm.indexOf(c) != -1) || michael@0: l.hostname.indexOf(c) != -1 || michael@0: l.usernameField.indexOf(c) != -1 || michael@0: l.passwordField.indexOf(c) != -1); michael@0: } michael@0: michael@0: // Nulls are invalid, as they don't round-trip well. michael@0: // Mostly not a formatting problem, although ".\0" can be quirky. michael@0: if (badCharacterPresent(aLogin, "\0")) michael@0: throw "login values can't contain nulls"; michael@0: michael@0: // In theory these nulls should just be rolled up into the encrypted michael@0: // values, but nsISecretDecoderRing doesn't use nsStrings, so the michael@0: // nulls cause truncation. Check for them here just to avoid michael@0: // unexpected round-trip surprises. michael@0: if (aLogin.username.indexOf("\0") != -1 || michael@0: aLogin.password.indexOf("\0") != -1) michael@0: throw "login values can't contain nulls"; michael@0: michael@0: // Newlines are invalid for any field stored as plaintext. michael@0: if (badCharacterPresent(aLogin, "\r") || michael@0: badCharacterPresent(aLogin, "\n")) michael@0: throw "login values can't contain newlines"; michael@0: michael@0: // A line with just a "." can have special meaning. michael@0: if (aLogin.usernameField == "." || michael@0: aLogin.formSubmitURL == ".") michael@0: throw "login values can't be periods"; michael@0: michael@0: // A hostname with "\ \(" won't roundtrip. michael@0: // eg host="foo (", realm="bar" --> "foo ( (bar)" michael@0: // vs host="foo", realm=" (bar" --> "foo ( (bar)" michael@0: if (aLogin.hostname.indexOf(" (") != -1) michael@0: throw "bad parens in hostname"; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _checkHostnameValue michael@0: * michael@0: * Legacy storage prohibited newlines and nulls in hostnames, so we'll keep michael@0: * that standard here. Throws on illegal format. michael@0: */ michael@0: _checkHostnameValue : function (hostname) { michael@0: // File format prohibits certain values. Also, nulls michael@0: // won't round-trip with getAllDisabledHosts(). michael@0: if (hostname == "." || michael@0: hostname.indexOf("\r") != -1 || michael@0: hostname.indexOf("\n") != -1 || michael@0: hostname.indexOf("\0") != -1) michael@0: throw "Invalid hostname"; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _isGuidUnique michael@0: * michael@0: * Checks to see if the specified GUID already exists. michael@0: */ michael@0: _isGuidUnique : function (guid) { michael@0: let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid"; michael@0: let params = { guid: guid }; michael@0: michael@0: let stmt, numLogins; michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.executeStep(); michael@0: numLogins = stmt.row.numLogins; michael@0: } catch (e) { michael@0: this.log("_isGuidUnique failed: " + e.name + " : " + e.message); michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: return (numLogins == 0); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _encryptLogin michael@0: * michael@0: * Returns the encrypted username, password, and encrypton type for the specified michael@0: * login. Can throw if the user cancels a master password entry. michael@0: */ michael@0: _encryptLogin : function (login) { michael@0: let encUsername = this._crypto.encrypt(login.username); michael@0: let encPassword = this._crypto.encrypt(login.password); michael@0: let encType = this._crypto.defaultEncType; michael@0: michael@0: return [encUsername, encPassword, encType]; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _decryptLogins michael@0: * michael@0: * Decrypts username and password fields in the provided array of michael@0: * logins. michael@0: * michael@0: * The entries specified by the array will be decrypted, if possible. michael@0: * An array of successfully decrypted logins will be returned. The return michael@0: * value should be given to external callers (since still-encrypted michael@0: * entries are useless), whereas internal callers generally don't want michael@0: * to lose unencrypted entries (eg, because the user clicked Cancel michael@0: * instead of entering their master password) michael@0: */ michael@0: _decryptLogins : function (logins) { michael@0: let result = []; michael@0: michael@0: for each (let login in logins) { michael@0: try { michael@0: login.username = this._crypto.decrypt(login.username); michael@0: login.password = this._crypto.decrypt(login.password); michael@0: } catch (e) { michael@0: // If decryption failed (corrupt entry?), just skip it. michael@0: // Rethrow other errors (like canceling entry of a master pw) michael@0: if (e.result == Cr.NS_ERROR_FAILURE) michael@0: continue; michael@0: throw e; michael@0: } michael@0: result.push(login); michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: michael@0: //**************************************************************************// michael@0: // Database Creation & Access michael@0: michael@0: /* michael@0: * _dbCreateStatement michael@0: * michael@0: * Creates a statement, wraps it, and then does parameter replacement michael@0: * Returns the wrapped statement for execution. Will use memoization michael@0: * so that statements can be reused. michael@0: */ michael@0: _dbCreateStatement : function (query, params) { michael@0: let wrappedStmt = this._dbStmts[query]; michael@0: // Memoize the statements michael@0: if (!wrappedStmt) { michael@0: this.log("Creating new statement for query: " + query); michael@0: wrappedStmt = this._dbConnection.createStatement(query); michael@0: this._dbStmts[query] = wrappedStmt; michael@0: } michael@0: // Replace parameters, must be done 1 at a time michael@0: if (params) michael@0: for (let i in params) michael@0: wrappedStmt.params[i] = params[i]; michael@0: return wrappedStmt; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _dbInit michael@0: * michael@0: * Attempts to initialize the database. This creates the file if it doesn't michael@0: * exist, performs any migrations, etc. Return if this is the first run. michael@0: */ michael@0: _dbInit : function () { michael@0: this.log("Initializing Database"); michael@0: let isFirstRun = false; michael@0: try { michael@0: this._dbConnection = this._storageService.openDatabase(this._signonsFile); michael@0: // Get the version of the schema in the file. It will be 0 if the michael@0: // database has not been created yet. michael@0: let version = this._dbConnection.schemaVersion; michael@0: if (version == 0) { michael@0: this._dbCreate(); michael@0: isFirstRun = true; michael@0: } else if (version != DB_VERSION) { michael@0: this._dbMigrate(version); michael@0: } michael@0: } catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) { michael@0: // Database is corrupted, so we backup the database, then throw michael@0: // causing initialization to fail and a new db to be created next use michael@0: this._dbCleanup(true); michael@0: throw e; michael@0: } michael@0: michael@0: Services.obs.addObserver(this, "profile-before-change", false); michael@0: return isFirstRun; michael@0: }, michael@0: michael@0: observe: function (subject, topic, data) { michael@0: switch (topic) { michael@0: case "profile-before-change": michael@0: Services.obs.removeObserver(this, "profile-before-change"); michael@0: this._dbClose(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _dbCreate: function () { michael@0: this.log("Creating Database"); michael@0: this._dbCreateSchema(); michael@0: this._dbConnection.schemaVersion = DB_VERSION; michael@0: }, michael@0: michael@0: michael@0: _dbCreateSchema : function () { michael@0: this._dbCreateTables(); michael@0: this._dbCreateIndices(); michael@0: }, michael@0: michael@0: michael@0: _dbCreateTables : function () { michael@0: this.log("Creating Tables"); michael@0: for (let name in this._dbSchema.tables) michael@0: this._dbConnection.createTable(name, this._dbSchema.tables[name]); michael@0: }, michael@0: michael@0: michael@0: _dbCreateIndices : function () { michael@0: this.log("Creating Indices"); michael@0: for (let name in this._dbSchema.indices) { michael@0: let index = this._dbSchema.indices[name]; michael@0: let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table + michael@0: "(" + index.columns.join(", ") + ")"; michael@0: this._dbConnection.executeSimpleSQL(statement); michael@0: } michael@0: }, michael@0: michael@0: michael@0: _dbMigrate : function (oldVersion) { michael@0: this.log("Attempting to migrate from version " + oldVersion); michael@0: michael@0: if (oldVersion > DB_VERSION) { michael@0: this.log("Downgrading to version " + DB_VERSION); michael@0: // User's DB is newer. Sanity check that our expected columns are michael@0: // present, and if so mark the lower version and merrily continue michael@0: // on. If the columns are borked, something is wrong so blow away michael@0: // the DB and start from scratch. [Future incompatible upgrades michael@0: // should swtich to a different table or file.] michael@0: michael@0: if (!this._dbAreExpectedColumnsPresent()) michael@0: throw Components.Exception("DB is missing expected columns", michael@0: Cr.NS_ERROR_FILE_CORRUPTED); michael@0: michael@0: // Change the stored version to the current version. If the user michael@0: // runs the newer code again, it will see the lower version number michael@0: // and re-upgrade (to fixup any entries the old code added). michael@0: this._dbConnection.schemaVersion = DB_VERSION; michael@0: return; michael@0: } michael@0: michael@0: // Upgrade to newer version... michael@0: michael@0: let transaction = new Transaction(this._dbConnection); michael@0: michael@0: try { michael@0: for (let v = oldVersion + 1; v <= DB_VERSION; v++) { michael@0: this.log("Upgrading to version " + v + "..."); michael@0: let migrateFunction = "_dbMigrateToVersion" + v; michael@0: this[migrateFunction](); michael@0: } michael@0: } catch (e) { michael@0: this.log("Migration failed: " + e); michael@0: transaction.rollback(); michael@0: throw e; michael@0: } michael@0: michael@0: this._dbConnection.schemaVersion = DB_VERSION; michael@0: transaction.commit(); michael@0: this.log("DB migration completed."); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _dbMigrateToVersion2 michael@0: * michael@0: * Version 2 adds a GUID column. Existing logins are assigned a random GUID. michael@0: */ michael@0: _dbMigrateToVersion2 : function () { michael@0: // Check to see if GUID column already exists, add if needed michael@0: let query; michael@0: if (!this._dbColumnExists("guid")) { michael@0: query = "ALTER TABLE moz_logins ADD COLUMN guid TEXT"; michael@0: this._dbConnection.executeSimpleSQL(query); michael@0: michael@0: query = "CREATE INDEX IF NOT EXISTS moz_logins_guid_index ON moz_logins (guid)"; michael@0: this._dbConnection.executeSimpleSQL(query); michael@0: } michael@0: michael@0: // Get a list of IDs for existing logins michael@0: let ids = []; michael@0: let query = "SELECT id FROM moz_logins WHERE guid isnull"; michael@0: let stmt; michael@0: try { michael@0: stmt = this._dbCreateStatement(query); michael@0: while (stmt.executeStep()) michael@0: ids.push(stmt.row.id); michael@0: } catch (e) { michael@0: this.log("Failed getting IDs: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: // Generate a GUID for each login and update the DB. michael@0: query = "UPDATE moz_logins SET guid = :guid WHERE id = :id"; michael@0: for each (let id in ids) { michael@0: let params = { michael@0: id: id, michael@0: guid: this._uuidService.generateUUID().toString() michael@0: }; michael@0: michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("Failed setting GUID: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _dbMigrateToVersion3 michael@0: * michael@0: * Version 3 adds a encType column. michael@0: */ michael@0: _dbMigrateToVersion3 : function () { michael@0: // Check to see if encType column already exists, add if needed michael@0: let query; michael@0: if (!this._dbColumnExists("encType")) { michael@0: query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER"; michael@0: this._dbConnection.executeSimpleSQL(query); michael@0: michael@0: query = "CREATE INDEX IF NOT EXISTS " + michael@0: "moz_logins_encType_index ON moz_logins (encType)"; michael@0: this._dbConnection.executeSimpleSQL(query); michael@0: } michael@0: michael@0: // Get a list of existing logins michael@0: let logins = []; michael@0: let stmt; michael@0: query = "SELECT id, encryptedUsername, encryptedPassword " + michael@0: "FROM moz_logins WHERE encType isnull"; michael@0: try { michael@0: stmt = this._dbCreateStatement(query); michael@0: while (stmt.executeStep()) { michael@0: let params = { id: stmt.row.id }; michael@0: // We will tag base64 logins correctly, but no longer support their use. michael@0: if (stmt.row.encryptedUsername.charAt(0) == '~' || michael@0: stmt.row.encryptedPassword.charAt(0) == '~') michael@0: params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64; michael@0: else michael@0: params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_SDR; michael@0: logins.push(params); michael@0: } michael@0: } catch (e) { michael@0: this.log("Failed getting logins: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: // Determine encryption type for each login and update the DB. michael@0: query = "UPDATE moz_logins SET encType = :encType WHERE id = :id"; michael@0: for each (let params in logins) { michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("Failed setting encType: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _dbMigrateToVersion4 michael@0: * michael@0: * Version 4 adds timeCreated, timeLastUsed, timePasswordChanged, michael@0: * and timesUsed columns michael@0: */ michael@0: _dbMigrateToVersion4 : function () { michael@0: let query; michael@0: // Add the new columns, if needed. michael@0: for each (let column in ["timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) { michael@0: if (!this._dbColumnExists(column)) { michael@0: query = "ALTER TABLE moz_logins ADD COLUMN " + column + " INTEGER"; michael@0: this._dbConnection.executeSimpleSQL(query); michael@0: } michael@0: } michael@0: michael@0: // Get a list of IDs for existing logins. michael@0: let ids = []; michael@0: let stmt; michael@0: query = "SELECT id FROM moz_logins WHERE timeCreated isnull OR " + michael@0: "timeLastUsed isnull OR timePasswordChanged isnull OR timesUsed isnull"; michael@0: try { michael@0: stmt = this._dbCreateStatement(query); michael@0: while (stmt.executeStep()) michael@0: ids.push(stmt.row.id); michael@0: } catch (e) { michael@0: this.log("Failed getting IDs: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: // Initialize logins with current time. michael@0: query = "UPDATE moz_logins SET timeCreated = :initTime, timeLastUsed = :initTime, " + michael@0: "timePasswordChanged = :initTime, timesUsed = 1 WHERE id = :id"; michael@0: let params = { michael@0: id: null, michael@0: initTime: Date.now() michael@0: }; michael@0: for each (let id in ids) { michael@0: params.id = id; michael@0: try { michael@0: stmt = this._dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("Failed setting timestamps: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _dbMigrateToVersion5 michael@0: * michael@0: * Version 5 adds the moz_deleted_logins table michael@0: */ michael@0: _dbMigrateToVersion5 : function () { michael@0: if (!this._dbConnection.tableExists("moz_deleted_logins")) { michael@0: this._dbConnection.createTable("moz_deleted_logins", this._dbSchema.tables.moz_deleted_logins); michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * _dbAreExpectedColumnsPresent michael@0: * michael@0: * Sanity check to ensure that the columns this version of the code expects michael@0: * are present in the DB we're using. michael@0: */ michael@0: _dbAreExpectedColumnsPresent : function () { michael@0: let query = "SELECT " + michael@0: "id, " + michael@0: "hostname, " + michael@0: "httpRealm, " + michael@0: "formSubmitURL, " + michael@0: "usernameField, " + michael@0: "passwordField, " + michael@0: "encryptedUsername, " + michael@0: "encryptedPassword, " + michael@0: "guid, " + michael@0: "encType, " + michael@0: "timeCreated, " + michael@0: "timeLastUsed, " + michael@0: "timePasswordChanged, " + michael@0: "timesUsed " + michael@0: "FROM moz_logins"; michael@0: try { michael@0: let stmt = this._dbConnection.createStatement(query); michael@0: // (no need to execute statement, if it compiled we're good) michael@0: stmt.finalize(); michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: michael@0: query = "SELECT " + michael@0: "id, " + michael@0: "hostname " + michael@0: "FROM moz_disabledHosts"; michael@0: try { michael@0: let stmt = this._dbConnection.createStatement(query); michael@0: // (no need to execute statement, if it compiled we're good) michael@0: stmt.finalize(); michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: michael@0: this.log("verified that expected columns are present in DB."); michael@0: return true; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _dbColumnExists michael@0: * michael@0: * Checks to see if the named column already exists. michael@0: */ michael@0: _dbColumnExists : function (columnName) { michael@0: let query = "SELECT " + columnName + " FROM moz_logins"; michael@0: try { michael@0: let stmt = this._dbConnection.createStatement(query); michael@0: // (no need to execute statement, if it compiled we're good) michael@0: stmt.finalize(); michael@0: return true; michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: _dbClose : function () { michael@0: this.log("Closing the DB connection."); michael@0: // Finalize all statements to free memory, avoid errors later michael@0: for each (let stmt in this._dbStmts) { michael@0: stmt.finalize(); michael@0: } michael@0: this._dbStmts = {}; michael@0: michael@0: if (this._dbConnection !== null) { michael@0: try { michael@0: this._dbConnection.close(); michael@0: } catch (e) { michael@0: Components.utils.reportError(e); michael@0: } michael@0: } michael@0: this._dbConnection = null; michael@0: }, michael@0: michael@0: /* michael@0: * _dbCleanup michael@0: * michael@0: * Called when database creation fails. Finalizes database statements, michael@0: * closes the database connection, deletes the database file. michael@0: */ michael@0: _dbCleanup : function (backup) { michael@0: this.log("Cleaning up DB file - close & remove & backup=" + backup) michael@0: michael@0: // Create backup file michael@0: if (backup) { michael@0: let backupFile = this._signonsFile.leafName + ".corrupt"; michael@0: this._storageService.backupDatabaseFile(this._signonsFile, backupFile); michael@0: } michael@0: michael@0: this._dbClose(); michael@0: this._signonsFile.remove(false); michael@0: } michael@0: michael@0: }; // end of nsLoginManagerStorage_mozStorage implementation michael@0: michael@0: let component = [LoginManagerStorage_mozStorage]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);