Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
michael@0 | 2 | /* vim: set sw=4 ts=4 et lcs=trail\:.,tab\:>~ : */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | |
michael@0 | 8 | const Cc = Components.classes; |
michael@0 | 9 | const Ci = Components.interfaces; |
michael@0 | 10 | const Cr = Components.results; |
michael@0 | 11 | |
michael@0 | 12 | const DB_VERSION = 5; // The database schema version |
michael@0 | 13 | |
michael@0 | 14 | Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 15 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 16 | |
michael@0 | 17 | /** |
michael@0 | 18 | * Object that manages a database transaction properly so consumers don't have |
michael@0 | 19 | * to worry about it throwing. |
michael@0 | 20 | * |
michael@0 | 21 | * @param aDatabase |
michael@0 | 22 | * The mozIStorageConnection to start a transaction on. |
michael@0 | 23 | */ |
michael@0 | 24 | function Transaction(aDatabase) { |
michael@0 | 25 | this._db = aDatabase; |
michael@0 | 26 | |
michael@0 | 27 | this._hasTransaction = false; |
michael@0 | 28 | try { |
michael@0 | 29 | this._db.beginTransaction(); |
michael@0 | 30 | this._hasTransaction = true; |
michael@0 | 31 | } |
michael@0 | 32 | catch(e) { /* om nom nom exceptions */ } |
michael@0 | 33 | } |
michael@0 | 34 | |
michael@0 | 35 | Transaction.prototype = { |
michael@0 | 36 | commit : function() { |
michael@0 | 37 | if (this._hasTransaction) |
michael@0 | 38 | this._db.commitTransaction(); |
michael@0 | 39 | }, |
michael@0 | 40 | |
michael@0 | 41 | rollback : function() { |
michael@0 | 42 | if (this._hasTransaction) |
michael@0 | 43 | this._db.rollbackTransaction(); |
michael@0 | 44 | }, |
michael@0 | 45 | }; |
michael@0 | 46 | |
michael@0 | 47 | |
michael@0 | 48 | function LoginManagerStorage_mozStorage() { }; |
michael@0 | 49 | |
michael@0 | 50 | LoginManagerStorage_mozStorage.prototype = { |
michael@0 | 51 | |
michael@0 | 52 | classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"), |
michael@0 | 53 | QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage, |
michael@0 | 54 | Ci.nsIInterfaceRequestor]), |
michael@0 | 55 | getInterface : function(aIID) { |
michael@0 | 56 | if (aIID.equals(Ci.mozIStorageConnection)) { |
michael@0 | 57 | return this._dbConnection; |
michael@0 | 58 | } |
michael@0 | 59 | |
michael@0 | 60 | throw Cr.NS_ERROR_NO_INTERFACE; |
michael@0 | 61 | }, |
michael@0 | 62 | |
michael@0 | 63 | __crypto : null, // nsILoginManagerCrypto service |
michael@0 | 64 | get _crypto() { |
michael@0 | 65 | if (!this.__crypto) |
michael@0 | 66 | this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"]. |
michael@0 | 67 | getService(Ci.nsILoginManagerCrypto); |
michael@0 | 68 | return this.__crypto; |
michael@0 | 69 | }, |
michael@0 | 70 | |
michael@0 | 71 | __profileDir: null, // nsIFile for the user's profile dir |
michael@0 | 72 | get _profileDir() { |
michael@0 | 73 | if (!this.__profileDir) |
michael@0 | 74 | this.__profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); |
michael@0 | 75 | return this.__profileDir; |
michael@0 | 76 | }, |
michael@0 | 77 | |
michael@0 | 78 | __storageService: null, // Storage service for using mozStorage |
michael@0 | 79 | get _storageService() { |
michael@0 | 80 | if (!this.__storageService) |
michael@0 | 81 | this.__storageService = Cc["@mozilla.org/storage/service;1"]. |
michael@0 | 82 | getService(Ci.mozIStorageService); |
michael@0 | 83 | return this.__storageService; |
michael@0 | 84 | }, |
michael@0 | 85 | |
michael@0 | 86 | __uuidService: null, |
michael@0 | 87 | get _uuidService() { |
michael@0 | 88 | if (!this.__uuidService) |
michael@0 | 89 | this.__uuidService = Cc["@mozilla.org/uuid-generator;1"]. |
michael@0 | 90 | getService(Ci.nsIUUIDGenerator); |
michael@0 | 91 | return this.__uuidService; |
michael@0 | 92 | }, |
michael@0 | 93 | |
michael@0 | 94 | |
michael@0 | 95 | // The current database schema. |
michael@0 | 96 | _dbSchema: { |
michael@0 | 97 | tables: { |
michael@0 | 98 | moz_logins: "id INTEGER PRIMARY KEY," + |
michael@0 | 99 | "hostname TEXT NOT NULL," + |
michael@0 | 100 | "httpRealm TEXT," + |
michael@0 | 101 | "formSubmitURL TEXT," + |
michael@0 | 102 | "usernameField TEXT NOT NULL," + |
michael@0 | 103 | "passwordField TEXT NOT NULL," + |
michael@0 | 104 | "encryptedUsername TEXT NOT NULL," + |
michael@0 | 105 | "encryptedPassword TEXT NOT NULL," + |
michael@0 | 106 | "guid TEXT," + |
michael@0 | 107 | "encType INTEGER," + |
michael@0 | 108 | "timeCreated INTEGER," + |
michael@0 | 109 | "timeLastUsed INTEGER," + |
michael@0 | 110 | "timePasswordChanged INTEGER," + |
michael@0 | 111 | "timesUsed INTEGER", |
michael@0 | 112 | // Changes must be reflected in this._dbAreExpectedColumnsPresent(), |
michael@0 | 113 | // this._searchLogins(), and this.modifyLogin(). |
michael@0 | 114 | |
michael@0 | 115 | moz_disabledHosts: "id INTEGER PRIMARY KEY," + |
michael@0 | 116 | "hostname TEXT UNIQUE ON CONFLICT REPLACE", |
michael@0 | 117 | |
michael@0 | 118 | moz_deleted_logins: "id INTEGER PRIMARY KEY," + |
michael@0 | 119 | "guid TEXT," + |
michael@0 | 120 | "timeDeleted INTEGER", |
michael@0 | 121 | }, |
michael@0 | 122 | indices: { |
michael@0 | 123 | moz_logins_hostname_index: { |
michael@0 | 124 | table: "moz_logins", |
michael@0 | 125 | columns: ["hostname"] |
michael@0 | 126 | }, |
michael@0 | 127 | moz_logins_hostname_formSubmitURL_index: { |
michael@0 | 128 | table: "moz_logins", |
michael@0 | 129 | columns: ["hostname", "formSubmitURL"] |
michael@0 | 130 | }, |
michael@0 | 131 | moz_logins_hostname_httpRealm_index: { |
michael@0 | 132 | table: "moz_logins", |
michael@0 | 133 | columns: ["hostname", "httpRealm"] |
michael@0 | 134 | }, |
michael@0 | 135 | moz_logins_guid_index: { |
michael@0 | 136 | table: "moz_logins", |
michael@0 | 137 | columns: ["guid"] |
michael@0 | 138 | }, |
michael@0 | 139 | moz_logins_encType_index: { |
michael@0 | 140 | table: "moz_logins", |
michael@0 | 141 | columns: ["encType"] |
michael@0 | 142 | } |
michael@0 | 143 | } |
michael@0 | 144 | }, |
michael@0 | 145 | _dbConnection : null, // The database connection |
michael@0 | 146 | _dbStmts : null, // Database statements for memoization |
michael@0 | 147 | |
michael@0 | 148 | _prefBranch : null, // Preferences service |
michael@0 | 149 | _signonsFile : null, // nsIFile for "signons.sqlite" |
michael@0 | 150 | _debug : false, // mirrors signon.debug |
michael@0 | 151 | |
michael@0 | 152 | |
michael@0 | 153 | /* |
michael@0 | 154 | * log |
michael@0 | 155 | * |
michael@0 | 156 | * Internal function for logging debug messages to the Error Console. |
michael@0 | 157 | */ |
michael@0 | 158 | log : function (message) { |
michael@0 | 159 | if (!this._debug) |
michael@0 | 160 | return; |
michael@0 | 161 | dump("PwMgr mozStorage: " + message + "\n"); |
michael@0 | 162 | Services.console.logStringMessage("PwMgr mozStorage: " + message); |
michael@0 | 163 | }, |
michael@0 | 164 | |
michael@0 | 165 | |
michael@0 | 166 | /* |
michael@0 | 167 | * initWithFile |
michael@0 | 168 | * |
michael@0 | 169 | * Initialize the component, but override the default filename locations. |
michael@0 | 170 | * This is primarily used to the unit tests and profile migration. |
michael@0 | 171 | */ |
michael@0 | 172 | initWithFile : function(aDBFile) { |
michael@0 | 173 | if (aDBFile) |
michael@0 | 174 | this._signonsFile = aDBFile; |
michael@0 | 175 | |
michael@0 | 176 | this.init(); |
michael@0 | 177 | }, |
michael@0 | 178 | |
michael@0 | 179 | |
michael@0 | 180 | /* |
michael@0 | 181 | * init |
michael@0 | 182 | * |
michael@0 | 183 | */ |
michael@0 | 184 | init : function () { |
michael@0 | 185 | this._dbStmts = {}; |
michael@0 | 186 | |
michael@0 | 187 | // Connect to the correct preferences branch. |
michael@0 | 188 | this._prefBranch = Services.prefs.getBranch("signon."); |
michael@0 | 189 | this._debug = this._prefBranch.getBoolPref("debug"); |
michael@0 | 190 | |
michael@0 | 191 | let isFirstRun; |
michael@0 | 192 | try { |
michael@0 | 193 | // Force initialization of the crypto module. |
michael@0 | 194 | // See bug 717490 comment 17. |
michael@0 | 195 | this._crypto; |
michael@0 | 196 | |
michael@0 | 197 | // If initWithFile is calling us, _signonsFile may already be set. |
michael@0 | 198 | if (!this._signonsFile) { |
michael@0 | 199 | // Initialize signons.sqlite |
michael@0 | 200 | this._signonsFile = this._profileDir.clone(); |
michael@0 | 201 | this._signonsFile.append("signons.sqlite"); |
michael@0 | 202 | } |
michael@0 | 203 | this.log("Opening database at " + this._signonsFile.path); |
michael@0 | 204 | |
michael@0 | 205 | // Initialize the database (create, migrate as necessary) |
michael@0 | 206 | isFirstRun = this._dbInit(); |
michael@0 | 207 | |
michael@0 | 208 | this._initialized = true; |
michael@0 | 209 | } catch (e) { |
michael@0 | 210 | this.log("Initialization failed: " + e); |
michael@0 | 211 | // If the import fails on first run, we want to delete the db |
michael@0 | 212 | if (isFirstRun && e == "Import failed") |
michael@0 | 213 | this._dbCleanup(false); |
michael@0 | 214 | throw "Initialization failed"; |
michael@0 | 215 | } |
michael@0 | 216 | }, |
michael@0 | 217 | |
michael@0 | 218 | |
michael@0 | 219 | /* |
michael@0 | 220 | * addLogin |
michael@0 | 221 | * |
michael@0 | 222 | */ |
michael@0 | 223 | addLogin : function (login) { |
michael@0 | 224 | let encUsername, encPassword; |
michael@0 | 225 | |
michael@0 | 226 | // Throws if there are bogus values. |
michael@0 | 227 | this._checkLoginValues(login); |
michael@0 | 228 | |
michael@0 | 229 | [encUsername, encPassword, encType] = this._encryptLogin(login); |
michael@0 | 230 | |
michael@0 | 231 | // Clone the login, so we don't modify the caller's object. |
michael@0 | 232 | let loginClone = login.clone(); |
michael@0 | 233 | |
michael@0 | 234 | // Initialize the nsILoginMetaInfo fields, unless the caller gave us values |
michael@0 | 235 | loginClone.QueryInterface(Ci.nsILoginMetaInfo); |
michael@0 | 236 | if (loginClone.guid) { |
michael@0 | 237 | if (!this._isGuidUnique(loginClone.guid)) |
michael@0 | 238 | throw "specified GUID already exists"; |
michael@0 | 239 | } else { |
michael@0 | 240 | loginClone.guid = this._uuidService.generateUUID().toString(); |
michael@0 | 241 | } |
michael@0 | 242 | |
michael@0 | 243 | // Set timestamps |
michael@0 | 244 | let currentTime = Date.now(); |
michael@0 | 245 | if (!loginClone.timeCreated) |
michael@0 | 246 | loginClone.timeCreated = currentTime; |
michael@0 | 247 | if (!loginClone.timeLastUsed) |
michael@0 | 248 | loginClone.timeLastUsed = currentTime; |
michael@0 | 249 | if (!loginClone.timePasswordChanged) |
michael@0 | 250 | loginClone.timePasswordChanged = currentTime; |
michael@0 | 251 | if (!loginClone.timesUsed) |
michael@0 | 252 | loginClone.timesUsed = 1; |
michael@0 | 253 | |
michael@0 | 254 | let query = |
michael@0 | 255 | "INSERT INTO moz_logins " + |
michael@0 | 256 | "(hostname, httpRealm, formSubmitURL, usernameField, " + |
michael@0 | 257 | "passwordField, encryptedUsername, encryptedPassword, " + |
michael@0 | 258 | "guid, encType, timeCreated, timeLastUsed, timePasswordChanged, " + |
michael@0 | 259 | "timesUsed) " + |
michael@0 | 260 | "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " + |
michael@0 | 261 | ":passwordField, :encryptedUsername, :encryptedPassword, " + |
michael@0 | 262 | ":guid, :encType, :timeCreated, :timeLastUsed, " + |
michael@0 | 263 | ":timePasswordChanged, :timesUsed)"; |
michael@0 | 264 | |
michael@0 | 265 | let params = { |
michael@0 | 266 | hostname: loginClone.hostname, |
michael@0 | 267 | httpRealm: loginClone.httpRealm, |
michael@0 | 268 | formSubmitURL: loginClone.formSubmitURL, |
michael@0 | 269 | usernameField: loginClone.usernameField, |
michael@0 | 270 | passwordField: loginClone.passwordField, |
michael@0 | 271 | encryptedUsername: encUsername, |
michael@0 | 272 | encryptedPassword: encPassword, |
michael@0 | 273 | guid: loginClone.guid, |
michael@0 | 274 | encType: encType, |
michael@0 | 275 | timeCreated: loginClone.timeCreated, |
michael@0 | 276 | timeLastUsed: loginClone.timeLastUsed, |
michael@0 | 277 | timePasswordChanged: loginClone.timePasswordChanged, |
michael@0 | 278 | timesUsed: loginClone.timesUsed |
michael@0 | 279 | }; |
michael@0 | 280 | |
michael@0 | 281 | let stmt; |
michael@0 | 282 | try { |
michael@0 | 283 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 284 | stmt.execute(); |
michael@0 | 285 | } catch (e) { |
michael@0 | 286 | this.log("addLogin failed: " + e.name + " : " + e.message); |
michael@0 | 287 | throw "Couldn't write to database, login not added."; |
michael@0 | 288 | } finally { |
michael@0 | 289 | if (stmt) { |
michael@0 | 290 | stmt.reset(); |
michael@0 | 291 | } |
michael@0 | 292 | } |
michael@0 | 293 | |
michael@0 | 294 | // Send a notification that a login was added. |
michael@0 | 295 | this._sendNotification("addLogin", loginClone); |
michael@0 | 296 | }, |
michael@0 | 297 | |
michael@0 | 298 | |
michael@0 | 299 | /* |
michael@0 | 300 | * removeLogin |
michael@0 | 301 | * |
michael@0 | 302 | */ |
michael@0 | 303 | removeLogin : function (login) { |
michael@0 | 304 | let [idToDelete, storedLogin] = this._getIdForLogin(login); |
michael@0 | 305 | if (!idToDelete) |
michael@0 | 306 | throw "No matching logins"; |
michael@0 | 307 | |
michael@0 | 308 | // Execute the statement & remove from DB |
michael@0 | 309 | let query = "DELETE FROM moz_logins WHERE id = :id"; |
michael@0 | 310 | let params = { id: idToDelete }; |
michael@0 | 311 | let stmt; |
michael@0 | 312 | let transaction = new Transaction(this._dbConnection); |
michael@0 | 313 | try { |
michael@0 | 314 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 315 | stmt.execute(); |
michael@0 | 316 | this.storeDeletedLogin(storedLogin); |
michael@0 | 317 | transaction.commit(); |
michael@0 | 318 | } catch (e) { |
michael@0 | 319 | this.log("_removeLogin failed: " + e.name + " : " + e.message); |
michael@0 | 320 | throw "Couldn't write to database, login not removed."; |
michael@0 | 321 | transaction.rollback(); |
michael@0 | 322 | } finally { |
michael@0 | 323 | if (stmt) { |
michael@0 | 324 | stmt.reset(); |
michael@0 | 325 | } |
michael@0 | 326 | } |
michael@0 | 327 | this._sendNotification("removeLogin", storedLogin); |
michael@0 | 328 | }, |
michael@0 | 329 | |
michael@0 | 330 | |
michael@0 | 331 | /* |
michael@0 | 332 | * modifyLogin |
michael@0 | 333 | * |
michael@0 | 334 | */ |
michael@0 | 335 | modifyLogin : function (oldLogin, newLoginData) { |
michael@0 | 336 | let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin); |
michael@0 | 337 | if (!idToModify) |
michael@0 | 338 | throw "No matching logins"; |
michael@0 | 339 | oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo); |
michael@0 | 340 | |
michael@0 | 341 | let newLogin; |
michael@0 | 342 | if (newLoginData instanceof Ci.nsILoginInfo) { |
michael@0 | 343 | // Clone the existing login to get its nsILoginMetaInfo, then init it |
michael@0 | 344 | // with the replacement nsILoginInfo data from the new login. |
michael@0 | 345 | newLogin = oldStoredLogin.clone(); |
michael@0 | 346 | newLogin.init(newLoginData.hostname, |
michael@0 | 347 | newLoginData.formSubmitURL, newLoginData.httpRealm, |
michael@0 | 348 | newLoginData.username, newLoginData.password, |
michael@0 | 349 | newLoginData.usernameField, newLoginData.passwordField); |
michael@0 | 350 | newLogin.QueryInterface(Ci.nsILoginMetaInfo); |
michael@0 | 351 | |
michael@0 | 352 | // Automatically update metainfo when password is changed. |
michael@0 | 353 | if (newLogin.password != oldLogin.password) |
michael@0 | 354 | newLogin.timePasswordChanged = Date.now(); |
michael@0 | 355 | } else if (newLoginData instanceof Ci.nsIPropertyBag) { |
michael@0 | 356 | function _bagHasProperty(aPropName) { |
michael@0 | 357 | try { |
michael@0 | 358 | newLoginData.getProperty(aPropName); |
michael@0 | 359 | return true; |
michael@0 | 360 | } catch (e) { |
michael@0 | 361 | return false; |
michael@0 | 362 | } |
michael@0 | 363 | } |
michael@0 | 364 | |
michael@0 | 365 | // Clone the existing login, along with all its properties. |
michael@0 | 366 | newLogin = oldStoredLogin.clone(); |
michael@0 | 367 | newLogin.QueryInterface(Ci.nsILoginMetaInfo); |
michael@0 | 368 | |
michael@0 | 369 | // Automatically update metainfo when password is changed. |
michael@0 | 370 | // (Done before the main property updates, lest the caller be |
michael@0 | 371 | // explicitly updating both .password and .timePasswordChanged) |
michael@0 | 372 | if (_bagHasProperty("password")) { |
michael@0 | 373 | let newPassword = newLoginData.getProperty("password"); |
michael@0 | 374 | if (newPassword != oldLogin.password) |
michael@0 | 375 | newLogin.timePasswordChanged = Date.now(); |
michael@0 | 376 | } |
michael@0 | 377 | |
michael@0 | 378 | let propEnum = newLoginData.enumerator; |
michael@0 | 379 | while (propEnum.hasMoreElements()) { |
michael@0 | 380 | let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty); |
michael@0 | 381 | switch (prop.name) { |
michael@0 | 382 | // nsILoginInfo properties... |
michael@0 | 383 | case "hostname": |
michael@0 | 384 | case "httpRealm": |
michael@0 | 385 | case "formSubmitURL": |
michael@0 | 386 | case "username": |
michael@0 | 387 | case "password": |
michael@0 | 388 | case "usernameField": |
michael@0 | 389 | case "passwordField": |
michael@0 | 390 | // nsILoginMetaInfo properties... |
michael@0 | 391 | case "guid": |
michael@0 | 392 | case "timeCreated": |
michael@0 | 393 | case "timeLastUsed": |
michael@0 | 394 | case "timePasswordChanged": |
michael@0 | 395 | case "timesUsed": |
michael@0 | 396 | newLogin[prop.name] = prop.value; |
michael@0 | 397 | if (prop.name == "guid" && !this._isGuidUnique(newLogin.guid)) |
michael@0 | 398 | throw "specified GUID already exists"; |
michael@0 | 399 | break; |
michael@0 | 400 | |
michael@0 | 401 | // Fake property, allows easy incrementing. |
michael@0 | 402 | case "timesUsedIncrement": |
michael@0 | 403 | newLogin.timesUsed += prop.value; |
michael@0 | 404 | break; |
michael@0 | 405 | |
michael@0 | 406 | // Fail if caller requests setting an unknown property. |
michael@0 | 407 | default: |
michael@0 | 408 | throw "Unexpected propertybag item: " + prop.name; |
michael@0 | 409 | } |
michael@0 | 410 | } |
michael@0 | 411 | } else { |
michael@0 | 412 | throw "newLoginData needs an expected interface!"; |
michael@0 | 413 | } |
michael@0 | 414 | |
michael@0 | 415 | // Throws if there are bogus values. |
michael@0 | 416 | this._checkLoginValues(newLogin); |
michael@0 | 417 | |
michael@0 | 418 | // Get the encrypted value of the username and password. |
michael@0 | 419 | let [encUsername, encPassword, encType] = this._encryptLogin(newLogin); |
michael@0 | 420 | |
michael@0 | 421 | let query = |
michael@0 | 422 | "UPDATE moz_logins " + |
michael@0 | 423 | "SET hostname = :hostname, " + |
michael@0 | 424 | "httpRealm = :httpRealm, " + |
michael@0 | 425 | "formSubmitURL = :formSubmitURL, " + |
michael@0 | 426 | "usernameField = :usernameField, " + |
michael@0 | 427 | "passwordField = :passwordField, " + |
michael@0 | 428 | "encryptedUsername = :encryptedUsername, " + |
michael@0 | 429 | "encryptedPassword = :encryptedPassword, " + |
michael@0 | 430 | "guid = :guid, " + |
michael@0 | 431 | "encType = :encType, " + |
michael@0 | 432 | "timeCreated = :timeCreated, " + |
michael@0 | 433 | "timeLastUsed = :timeLastUsed, " + |
michael@0 | 434 | "timePasswordChanged = :timePasswordChanged, " + |
michael@0 | 435 | "timesUsed = :timesUsed " + |
michael@0 | 436 | "WHERE id = :id"; |
michael@0 | 437 | |
michael@0 | 438 | let params = { |
michael@0 | 439 | id: idToModify, |
michael@0 | 440 | hostname: newLogin.hostname, |
michael@0 | 441 | httpRealm: newLogin.httpRealm, |
michael@0 | 442 | formSubmitURL: newLogin.formSubmitURL, |
michael@0 | 443 | usernameField: newLogin.usernameField, |
michael@0 | 444 | passwordField: newLogin.passwordField, |
michael@0 | 445 | encryptedUsername: encUsername, |
michael@0 | 446 | encryptedPassword: encPassword, |
michael@0 | 447 | guid: newLogin.guid, |
michael@0 | 448 | encType: encType, |
michael@0 | 449 | timeCreated: newLogin.timeCreated, |
michael@0 | 450 | timeLastUsed: newLogin.timeLastUsed, |
michael@0 | 451 | timePasswordChanged: newLogin.timePasswordChanged, |
michael@0 | 452 | timesUsed: newLogin.timesUsed |
michael@0 | 453 | }; |
michael@0 | 454 | |
michael@0 | 455 | let stmt; |
michael@0 | 456 | try { |
michael@0 | 457 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 458 | stmt.execute(); |
michael@0 | 459 | } catch (e) { |
michael@0 | 460 | this.log("modifyLogin failed: " + e.name + " : " + e.message); |
michael@0 | 461 | throw "Couldn't write to database, login not modified."; |
michael@0 | 462 | } finally { |
michael@0 | 463 | if (stmt) { |
michael@0 | 464 | stmt.reset(); |
michael@0 | 465 | } |
michael@0 | 466 | } |
michael@0 | 467 | |
michael@0 | 468 | this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]); |
michael@0 | 469 | }, |
michael@0 | 470 | |
michael@0 | 471 | |
michael@0 | 472 | /* |
michael@0 | 473 | * getAllLogins |
michael@0 | 474 | * |
michael@0 | 475 | * Returns an array of nsILoginInfo. |
michael@0 | 476 | */ |
michael@0 | 477 | getAllLogins : function (count) { |
michael@0 | 478 | let [logins, ids] = this._searchLogins({}); |
michael@0 | 479 | |
michael@0 | 480 | // decrypt entries for caller. |
michael@0 | 481 | logins = this._decryptLogins(logins); |
michael@0 | 482 | |
michael@0 | 483 | this.log("_getAllLogins: returning " + logins.length + " logins."); |
michael@0 | 484 | if (count) |
michael@0 | 485 | count.value = logins.length; // needed for XPCOM |
michael@0 | 486 | return logins; |
michael@0 | 487 | }, |
michael@0 | 488 | |
michael@0 | 489 | |
michael@0 | 490 | /* |
michael@0 | 491 | * getAllEncryptedLogins |
michael@0 | 492 | * |
michael@0 | 493 | * Not implemented. This interface was added to extract logins from the |
michael@0 | 494 | * legacy storage module without decrypting them. Now that logins are in |
michael@0 | 495 | * mozStorage, if the encrypted data is really needed it can be easily |
michael@0 | 496 | * obtained with SQL and the mozStorage APIs. |
michael@0 | 497 | */ |
michael@0 | 498 | getAllEncryptedLogins : function (count) { |
michael@0 | 499 | throw Cr.NS_ERROR_NOT_IMPLEMENTED; |
michael@0 | 500 | }, |
michael@0 | 501 | |
michael@0 | 502 | |
michael@0 | 503 | /* |
michael@0 | 504 | * searchLogins |
michael@0 | 505 | * |
michael@0 | 506 | * Public wrapper around _searchLogins to convert the nsIPropertyBag to a |
michael@0 | 507 | * JavaScript object and decrypt the results. |
michael@0 | 508 | * |
michael@0 | 509 | * Returns an array of decrypted nsILoginInfo. |
michael@0 | 510 | */ |
michael@0 | 511 | searchLogins : function(count, matchData) { |
michael@0 | 512 | let realMatchData = {}; |
michael@0 | 513 | // Convert nsIPropertyBag to normal JS object |
michael@0 | 514 | let propEnum = matchData.enumerator; |
michael@0 | 515 | while (propEnum.hasMoreElements()) { |
michael@0 | 516 | let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty); |
michael@0 | 517 | realMatchData[prop.name] = prop.value; |
michael@0 | 518 | } |
michael@0 | 519 | |
michael@0 | 520 | let [logins, ids] = this._searchLogins(realMatchData); |
michael@0 | 521 | |
michael@0 | 522 | // Decrypt entries found for the caller. |
michael@0 | 523 | logins = this._decryptLogins(logins); |
michael@0 | 524 | |
michael@0 | 525 | count.value = logins.length; // needed for XPCOM |
michael@0 | 526 | return logins; |
michael@0 | 527 | }, |
michael@0 | 528 | |
michael@0 | 529 | |
michael@0 | 530 | /* |
michael@0 | 531 | * _searchLogins |
michael@0 | 532 | * |
michael@0 | 533 | * Private method to perform arbitrary searches on any field. Decryption is |
michael@0 | 534 | * left to the caller. |
michael@0 | 535 | * |
michael@0 | 536 | * Returns [logins, ids] for logins that match the arguments, where logins |
michael@0 | 537 | * is an array of encrypted nsLoginInfo and ids is an array of associated |
michael@0 | 538 | * ids in the database. |
michael@0 | 539 | */ |
michael@0 | 540 | _searchLogins : function (matchData) { |
michael@0 | 541 | let conditions = [], params = {}; |
michael@0 | 542 | |
michael@0 | 543 | for (let field in matchData) { |
michael@0 | 544 | let value = matchData[field]; |
michael@0 | 545 | switch (field) { |
michael@0 | 546 | // Historical compatibility requires this special case |
michael@0 | 547 | case "formSubmitURL": |
michael@0 | 548 | if (value != null) { |
michael@0 | 549 | conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''"); |
michael@0 | 550 | params["formSubmitURL"] = value; |
michael@0 | 551 | break; |
michael@0 | 552 | } |
michael@0 | 553 | // Normal cases. |
michael@0 | 554 | case "hostname": |
michael@0 | 555 | case "httpRealm": |
michael@0 | 556 | case "id": |
michael@0 | 557 | case "usernameField": |
michael@0 | 558 | case "passwordField": |
michael@0 | 559 | case "encryptedUsername": |
michael@0 | 560 | case "encryptedPassword": |
michael@0 | 561 | case "guid": |
michael@0 | 562 | case "encType": |
michael@0 | 563 | case "timeCreated": |
michael@0 | 564 | case "timeLastUsed": |
michael@0 | 565 | case "timePasswordChanged": |
michael@0 | 566 | case "timesUsed": |
michael@0 | 567 | if (value == null) { |
michael@0 | 568 | conditions.push(field + " isnull"); |
michael@0 | 569 | } else { |
michael@0 | 570 | conditions.push(field + " = :" + field); |
michael@0 | 571 | params[field] = value; |
michael@0 | 572 | } |
michael@0 | 573 | break; |
michael@0 | 574 | // Fail if caller requests an unknown property. |
michael@0 | 575 | default: |
michael@0 | 576 | throw "Unexpected field: " + field; |
michael@0 | 577 | } |
michael@0 | 578 | } |
michael@0 | 579 | |
michael@0 | 580 | // Build query |
michael@0 | 581 | let query = "SELECT * FROM moz_logins"; |
michael@0 | 582 | if (conditions.length) { |
michael@0 | 583 | conditions = conditions.map(function(c) "(" + c + ")"); |
michael@0 | 584 | query += " WHERE " + conditions.join(" AND "); |
michael@0 | 585 | } |
michael@0 | 586 | |
michael@0 | 587 | let stmt; |
michael@0 | 588 | let logins = [], ids = []; |
michael@0 | 589 | try { |
michael@0 | 590 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 591 | // We can't execute as usual here, since we're iterating over rows |
michael@0 | 592 | while (stmt.executeStep()) { |
michael@0 | 593 | // Create the new nsLoginInfo object, push to array |
michael@0 | 594 | let login = Cc["@mozilla.org/login-manager/loginInfo;1"]. |
michael@0 | 595 | createInstance(Ci.nsILoginInfo); |
michael@0 | 596 | login.init(stmt.row.hostname, stmt.row.formSubmitURL, |
michael@0 | 597 | stmt.row.httpRealm, stmt.row.encryptedUsername, |
michael@0 | 598 | stmt.row.encryptedPassword, stmt.row.usernameField, |
michael@0 | 599 | stmt.row.passwordField); |
michael@0 | 600 | // set nsILoginMetaInfo values |
michael@0 | 601 | login.QueryInterface(Ci.nsILoginMetaInfo); |
michael@0 | 602 | login.guid = stmt.row.guid; |
michael@0 | 603 | login.timeCreated = stmt.row.timeCreated; |
michael@0 | 604 | login.timeLastUsed = stmt.row.timeLastUsed; |
michael@0 | 605 | login.timePasswordChanged = stmt.row.timePasswordChanged; |
michael@0 | 606 | login.timesUsed = stmt.row.timesUsed; |
michael@0 | 607 | logins.push(login); |
michael@0 | 608 | ids.push(stmt.row.id); |
michael@0 | 609 | } |
michael@0 | 610 | } catch (e) { |
michael@0 | 611 | this.log("_searchLogins failed: " + e.name + " : " + e.message); |
michael@0 | 612 | } finally { |
michael@0 | 613 | if (stmt) { |
michael@0 | 614 | stmt.reset(); |
michael@0 | 615 | } |
michael@0 | 616 | } |
michael@0 | 617 | |
michael@0 | 618 | this.log("_searchLogins: returning " + logins.length + " logins"); |
michael@0 | 619 | return [logins, ids]; |
michael@0 | 620 | }, |
michael@0 | 621 | |
michael@0 | 622 | /* storeDeletedLogin |
michael@0 | 623 | * |
michael@0 | 624 | * Moves a login to the deleted logins table |
michael@0 | 625 | * |
michael@0 | 626 | */ |
michael@0 | 627 | storeDeletedLogin : function(aLogin) { |
michael@0 | 628 | #ifdef ANDROID |
michael@0 | 629 | let stmt = null; |
michael@0 | 630 | try { |
michael@0 | 631 | this.log("Storing " + aLogin.guid + " in deleted passwords\n"); |
michael@0 | 632 | let query = "INSERT INTO moz_deleted_logins (guid, timeDeleted) VALUES (:guid, :timeDeleted)"; |
michael@0 | 633 | let params = { guid: aLogin.guid, |
michael@0 | 634 | timeDeleted: Date.now() }; |
michael@0 | 635 | let stmt = this._dbCreateStatement(query, params); |
michael@0 | 636 | stmt.execute(); |
michael@0 | 637 | } catch(ex) { |
michael@0 | 638 | throw ex; |
michael@0 | 639 | } finally { |
michael@0 | 640 | if (stmt) |
michael@0 | 641 | stmt.reset(); |
michael@0 | 642 | } |
michael@0 | 643 | #endif |
michael@0 | 644 | }, |
michael@0 | 645 | |
michael@0 | 646 | |
michael@0 | 647 | /* |
michael@0 | 648 | * removeAllLogins |
michael@0 | 649 | * |
michael@0 | 650 | * Removes all logins from storage. |
michael@0 | 651 | */ |
michael@0 | 652 | removeAllLogins : function () { |
michael@0 | 653 | this.log("Removing all logins"); |
michael@0 | 654 | let query; |
michael@0 | 655 | let stmt; |
michael@0 | 656 | let transaction = new Transaction(this._dbConnection); |
michael@0 | 657 | |
michael@0 | 658 | // Disabled hosts kept, as one presumably doesn't want to erase those. |
michael@0 | 659 | // TODO: Add these items to the deleted items table once we've sorted |
michael@0 | 660 | // out the issues from bug 756701 |
michael@0 | 661 | query = "DELETE FROM moz_logins"; |
michael@0 | 662 | try { |
michael@0 | 663 | stmt = this._dbCreateStatement(query); |
michael@0 | 664 | stmt.execute(); |
michael@0 | 665 | transaction.commit(); |
michael@0 | 666 | } catch (e) { |
michael@0 | 667 | this.log("_removeAllLogins failed: " + e.name + " : " + e.message); |
michael@0 | 668 | transaction.rollback(); |
michael@0 | 669 | throw "Couldn't write to database"; |
michael@0 | 670 | } finally { |
michael@0 | 671 | if (stmt) { |
michael@0 | 672 | stmt.reset(); |
michael@0 | 673 | } |
michael@0 | 674 | } |
michael@0 | 675 | |
michael@0 | 676 | this._sendNotification("removeAllLogins", null); |
michael@0 | 677 | }, |
michael@0 | 678 | |
michael@0 | 679 | |
michael@0 | 680 | /* |
michael@0 | 681 | * getAllDisabledHosts |
michael@0 | 682 | * |
michael@0 | 683 | */ |
michael@0 | 684 | getAllDisabledHosts : function (count) { |
michael@0 | 685 | let disabledHosts = this._queryDisabledHosts(null); |
michael@0 | 686 | |
michael@0 | 687 | this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts."); |
michael@0 | 688 | if (count) |
michael@0 | 689 | count.value = disabledHosts.length; // needed for XPCOM |
michael@0 | 690 | return disabledHosts; |
michael@0 | 691 | }, |
michael@0 | 692 | |
michael@0 | 693 | |
michael@0 | 694 | /* |
michael@0 | 695 | * getLoginSavingEnabled |
michael@0 | 696 | * |
michael@0 | 697 | */ |
michael@0 | 698 | getLoginSavingEnabled : function (hostname) { |
michael@0 | 699 | this.log("Getting login saving is enabled for " + hostname); |
michael@0 | 700 | return this._queryDisabledHosts(hostname).length == 0 |
michael@0 | 701 | }, |
michael@0 | 702 | |
michael@0 | 703 | |
michael@0 | 704 | /* |
michael@0 | 705 | * setLoginSavingEnabled |
michael@0 | 706 | * |
michael@0 | 707 | */ |
michael@0 | 708 | setLoginSavingEnabled : function (hostname, enabled) { |
michael@0 | 709 | // Throws if there are bogus values. |
michael@0 | 710 | this._checkHostnameValue(hostname); |
michael@0 | 711 | |
michael@0 | 712 | this.log("Setting login saving enabled for " + hostname + " to " + enabled); |
michael@0 | 713 | let query; |
michael@0 | 714 | if (enabled) |
michael@0 | 715 | query = "DELETE FROM moz_disabledHosts " + |
michael@0 | 716 | "WHERE hostname = :hostname"; |
michael@0 | 717 | else |
michael@0 | 718 | query = "INSERT INTO moz_disabledHosts " + |
michael@0 | 719 | "(hostname) VALUES (:hostname)"; |
michael@0 | 720 | let params = { hostname: hostname }; |
michael@0 | 721 | |
michael@0 | 722 | let stmt |
michael@0 | 723 | try { |
michael@0 | 724 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 725 | stmt.execute(); |
michael@0 | 726 | } catch (e) { |
michael@0 | 727 | this.log("setLoginSavingEnabled failed: " + e.name + " : " + e.message); |
michael@0 | 728 | throw "Couldn't write to database" |
michael@0 | 729 | } finally { |
michael@0 | 730 | if (stmt) { |
michael@0 | 731 | stmt.reset(); |
michael@0 | 732 | } |
michael@0 | 733 | } |
michael@0 | 734 | |
michael@0 | 735 | this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname); |
michael@0 | 736 | }, |
michael@0 | 737 | |
michael@0 | 738 | |
michael@0 | 739 | /* |
michael@0 | 740 | * findLogins |
michael@0 | 741 | * |
michael@0 | 742 | */ |
michael@0 | 743 | findLogins : function (count, hostname, formSubmitURL, httpRealm) { |
michael@0 | 744 | let loginData = { |
michael@0 | 745 | hostname: hostname, |
michael@0 | 746 | formSubmitURL: formSubmitURL, |
michael@0 | 747 | httpRealm: httpRealm |
michael@0 | 748 | }; |
michael@0 | 749 | let matchData = { }; |
michael@0 | 750 | for each (let field in ["hostname", "formSubmitURL", "httpRealm"]) |
michael@0 | 751 | if (loginData[field] != '') |
michael@0 | 752 | matchData[field] = loginData[field]; |
michael@0 | 753 | let [logins, ids] = this._searchLogins(matchData); |
michael@0 | 754 | |
michael@0 | 755 | // Decrypt entries found for the caller. |
michael@0 | 756 | logins = this._decryptLogins(logins); |
michael@0 | 757 | |
michael@0 | 758 | this.log("_findLogins: returning " + logins.length + " logins"); |
michael@0 | 759 | count.value = logins.length; // needed for XPCOM |
michael@0 | 760 | return logins; |
michael@0 | 761 | }, |
michael@0 | 762 | |
michael@0 | 763 | |
michael@0 | 764 | /* |
michael@0 | 765 | * countLogins |
michael@0 | 766 | * |
michael@0 | 767 | */ |
michael@0 | 768 | countLogins : function (hostname, formSubmitURL, httpRealm) { |
michael@0 | 769 | // Do checks for null and empty strings, adjust conditions and params |
michael@0 | 770 | let [conditions, params] = |
michael@0 | 771 | this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm); |
michael@0 | 772 | |
michael@0 | 773 | let query = "SELECT COUNT(1) AS numLogins FROM moz_logins"; |
michael@0 | 774 | if (conditions.length) { |
michael@0 | 775 | conditions = conditions.map(function(c) "(" + c + ")"); |
michael@0 | 776 | query += " WHERE " + conditions.join(" AND "); |
michael@0 | 777 | } |
michael@0 | 778 | |
michael@0 | 779 | let stmt, numLogins; |
michael@0 | 780 | try { |
michael@0 | 781 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 782 | stmt.executeStep(); |
michael@0 | 783 | numLogins = stmt.row.numLogins; |
michael@0 | 784 | } catch (e) { |
michael@0 | 785 | this.log("_countLogins failed: " + e.name + " : " + e.message); |
michael@0 | 786 | } finally { |
michael@0 | 787 | if (stmt) { |
michael@0 | 788 | stmt.reset(); |
michael@0 | 789 | } |
michael@0 | 790 | } |
michael@0 | 791 | |
michael@0 | 792 | this.log("_countLogins: counted logins: " + numLogins); |
michael@0 | 793 | return numLogins; |
michael@0 | 794 | }, |
michael@0 | 795 | |
michael@0 | 796 | |
michael@0 | 797 | /* |
michael@0 | 798 | * uiBusy |
michael@0 | 799 | */ |
michael@0 | 800 | get uiBusy() { |
michael@0 | 801 | return this._crypto.uiBusy; |
michael@0 | 802 | }, |
michael@0 | 803 | |
michael@0 | 804 | |
michael@0 | 805 | /* |
michael@0 | 806 | * isLoggedIn |
michael@0 | 807 | */ |
michael@0 | 808 | get isLoggedIn() { |
michael@0 | 809 | return this._crypto.isLoggedIn; |
michael@0 | 810 | }, |
michael@0 | 811 | |
michael@0 | 812 | |
michael@0 | 813 | /* |
michael@0 | 814 | * _sendNotification |
michael@0 | 815 | * |
michael@0 | 816 | * Send a notification when stored data is changed. |
michael@0 | 817 | */ |
michael@0 | 818 | _sendNotification : function (changeType, data) { |
michael@0 | 819 | let dataObject = data; |
michael@0 | 820 | // Can't pass a raw JS string or array though notifyObservers(). :-( |
michael@0 | 821 | if (data instanceof Array) { |
michael@0 | 822 | dataObject = Cc["@mozilla.org/array;1"]. |
michael@0 | 823 | createInstance(Ci.nsIMutableArray); |
michael@0 | 824 | for (let i = 0; i < data.length; i++) |
michael@0 | 825 | dataObject.appendElement(data[i], false); |
michael@0 | 826 | } else if (typeof(data) == "string") { |
michael@0 | 827 | dataObject = Cc["@mozilla.org/supports-string;1"]. |
michael@0 | 828 | createInstance(Ci.nsISupportsString); |
michael@0 | 829 | dataObject.data = data; |
michael@0 | 830 | } |
michael@0 | 831 | Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType); |
michael@0 | 832 | }, |
michael@0 | 833 | |
michael@0 | 834 | |
michael@0 | 835 | /* |
michael@0 | 836 | * _getIdForLogin |
michael@0 | 837 | * |
michael@0 | 838 | * Returns an array with two items: [id, login]. If the login was not |
michael@0 | 839 | * found, both items will be null. The returned login contains the actual |
michael@0 | 840 | * stored login (useful for looking at the actual nsILoginMetaInfo values). |
michael@0 | 841 | */ |
michael@0 | 842 | _getIdForLogin : function (login) { |
michael@0 | 843 | let matchData = { }; |
michael@0 | 844 | for each (let field in ["hostname", "formSubmitURL", "httpRealm"]) |
michael@0 | 845 | if (login[field] != '') |
michael@0 | 846 | matchData[field] = login[field]; |
michael@0 | 847 | let [logins, ids] = this._searchLogins(matchData); |
michael@0 | 848 | |
michael@0 | 849 | let id = null; |
michael@0 | 850 | let foundLogin = null; |
michael@0 | 851 | |
michael@0 | 852 | // The specified login isn't encrypted, so we need to ensure |
michael@0 | 853 | // the logins we're comparing with are decrypted. We decrypt one entry |
michael@0 | 854 | // at a time, lest _decryptLogins return fewer entries and screw up |
michael@0 | 855 | // indices between the two. |
michael@0 | 856 | for (let i = 0; i < logins.length; i++) { |
michael@0 | 857 | let [decryptedLogin] = this._decryptLogins([logins[i]]); |
michael@0 | 858 | |
michael@0 | 859 | if (!decryptedLogin || !decryptedLogin.equals(login)) |
michael@0 | 860 | continue; |
michael@0 | 861 | |
michael@0 | 862 | // We've found a match, set id and break |
michael@0 | 863 | foundLogin = decryptedLogin; |
michael@0 | 864 | id = ids[i]; |
michael@0 | 865 | break; |
michael@0 | 866 | } |
michael@0 | 867 | |
michael@0 | 868 | return [id, foundLogin]; |
michael@0 | 869 | }, |
michael@0 | 870 | |
michael@0 | 871 | |
michael@0 | 872 | /* |
michael@0 | 873 | * _queryDisabledHosts |
michael@0 | 874 | * |
michael@0 | 875 | * Returns an array of hostnames from the database according to the |
michael@0 | 876 | * criteria given in the argument. If the argument hostname is null, the |
michael@0 | 877 | * result array contains all hostnames |
michael@0 | 878 | */ |
michael@0 | 879 | _queryDisabledHosts : function (hostname) { |
michael@0 | 880 | let disabledHosts = []; |
michael@0 | 881 | |
michael@0 | 882 | let query = "SELECT hostname FROM moz_disabledHosts"; |
michael@0 | 883 | let params = {}; |
michael@0 | 884 | if (hostname) { |
michael@0 | 885 | query += " WHERE hostname = :hostname"; |
michael@0 | 886 | params = { hostname: hostname }; |
michael@0 | 887 | } |
michael@0 | 888 | |
michael@0 | 889 | let stmt; |
michael@0 | 890 | try { |
michael@0 | 891 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 892 | while (stmt.executeStep()) |
michael@0 | 893 | disabledHosts.push(stmt.row.hostname); |
michael@0 | 894 | } catch (e) { |
michael@0 | 895 | this.log("_queryDisabledHosts failed: " + e.name + " : " + e.message); |
michael@0 | 896 | } finally { |
michael@0 | 897 | if (stmt) { |
michael@0 | 898 | stmt.reset(); |
michael@0 | 899 | } |
michael@0 | 900 | } |
michael@0 | 901 | |
michael@0 | 902 | return disabledHosts; |
michael@0 | 903 | }, |
michael@0 | 904 | |
michael@0 | 905 | |
michael@0 | 906 | /* |
michael@0 | 907 | * _buildConditionsAndParams |
michael@0 | 908 | * |
michael@0 | 909 | * Adjusts the WHERE conditions and parameters for statements prior to the |
michael@0 | 910 | * statement being created. This fixes the cases where nulls are involved |
michael@0 | 911 | * and the empty string is supposed to be a wildcard match |
michael@0 | 912 | */ |
michael@0 | 913 | _buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) { |
michael@0 | 914 | let conditions = [], params = {}; |
michael@0 | 915 | |
michael@0 | 916 | if (hostname == null) { |
michael@0 | 917 | conditions.push("hostname isnull"); |
michael@0 | 918 | } else if (hostname != '') { |
michael@0 | 919 | conditions.push("hostname = :hostname"); |
michael@0 | 920 | params["hostname"] = hostname; |
michael@0 | 921 | } |
michael@0 | 922 | |
michael@0 | 923 | if (formSubmitURL == null) { |
michael@0 | 924 | conditions.push("formSubmitURL isnull"); |
michael@0 | 925 | } else if (formSubmitURL != '') { |
michael@0 | 926 | conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''"); |
michael@0 | 927 | params["formSubmitURL"] = formSubmitURL; |
michael@0 | 928 | } |
michael@0 | 929 | |
michael@0 | 930 | if (httpRealm == null) { |
michael@0 | 931 | conditions.push("httpRealm isnull"); |
michael@0 | 932 | } else if (httpRealm != '') { |
michael@0 | 933 | conditions.push("httpRealm = :httpRealm"); |
michael@0 | 934 | params["httpRealm"] = httpRealm; |
michael@0 | 935 | } |
michael@0 | 936 | |
michael@0 | 937 | return [conditions, params]; |
michael@0 | 938 | }, |
michael@0 | 939 | |
michael@0 | 940 | |
michael@0 | 941 | /* |
michael@0 | 942 | * _checkLoginValues |
michael@0 | 943 | * |
michael@0 | 944 | * Due to the way the signons2.txt file is formatted, we need to make |
michael@0 | 945 | * sure certain field values or characters do not cause the file to |
michael@0 | 946 | * be parse incorrectly. Reject logins that we can't store correctly. |
michael@0 | 947 | */ |
michael@0 | 948 | _checkLoginValues : function (aLogin) { |
michael@0 | 949 | function badCharacterPresent(l, c) { |
michael@0 | 950 | return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) || |
michael@0 | 951 | (l.httpRealm && l.httpRealm.indexOf(c) != -1) || |
michael@0 | 952 | l.hostname.indexOf(c) != -1 || |
michael@0 | 953 | l.usernameField.indexOf(c) != -1 || |
michael@0 | 954 | l.passwordField.indexOf(c) != -1); |
michael@0 | 955 | } |
michael@0 | 956 | |
michael@0 | 957 | // Nulls are invalid, as they don't round-trip well. |
michael@0 | 958 | // Mostly not a formatting problem, although ".\0" can be quirky. |
michael@0 | 959 | if (badCharacterPresent(aLogin, "\0")) |
michael@0 | 960 | throw "login values can't contain nulls"; |
michael@0 | 961 | |
michael@0 | 962 | // In theory these nulls should just be rolled up into the encrypted |
michael@0 | 963 | // values, but nsISecretDecoderRing doesn't use nsStrings, so the |
michael@0 | 964 | // nulls cause truncation. Check for them here just to avoid |
michael@0 | 965 | // unexpected round-trip surprises. |
michael@0 | 966 | if (aLogin.username.indexOf("\0") != -1 || |
michael@0 | 967 | aLogin.password.indexOf("\0") != -1) |
michael@0 | 968 | throw "login values can't contain nulls"; |
michael@0 | 969 | |
michael@0 | 970 | // Newlines are invalid for any field stored as plaintext. |
michael@0 | 971 | if (badCharacterPresent(aLogin, "\r") || |
michael@0 | 972 | badCharacterPresent(aLogin, "\n")) |
michael@0 | 973 | throw "login values can't contain newlines"; |
michael@0 | 974 | |
michael@0 | 975 | // A line with just a "." can have special meaning. |
michael@0 | 976 | if (aLogin.usernameField == "." || |
michael@0 | 977 | aLogin.formSubmitURL == ".") |
michael@0 | 978 | throw "login values can't be periods"; |
michael@0 | 979 | |
michael@0 | 980 | // A hostname with "\ \(" won't roundtrip. |
michael@0 | 981 | // eg host="foo (", realm="bar" --> "foo ( (bar)" |
michael@0 | 982 | // vs host="foo", realm=" (bar" --> "foo ( (bar)" |
michael@0 | 983 | if (aLogin.hostname.indexOf(" (") != -1) |
michael@0 | 984 | throw "bad parens in hostname"; |
michael@0 | 985 | }, |
michael@0 | 986 | |
michael@0 | 987 | |
michael@0 | 988 | /* |
michael@0 | 989 | * _checkHostnameValue |
michael@0 | 990 | * |
michael@0 | 991 | * Legacy storage prohibited newlines and nulls in hostnames, so we'll keep |
michael@0 | 992 | * that standard here. Throws on illegal format. |
michael@0 | 993 | */ |
michael@0 | 994 | _checkHostnameValue : function (hostname) { |
michael@0 | 995 | // File format prohibits certain values. Also, nulls |
michael@0 | 996 | // won't round-trip with getAllDisabledHosts(). |
michael@0 | 997 | if (hostname == "." || |
michael@0 | 998 | hostname.indexOf("\r") != -1 || |
michael@0 | 999 | hostname.indexOf("\n") != -1 || |
michael@0 | 1000 | hostname.indexOf("\0") != -1) |
michael@0 | 1001 | throw "Invalid hostname"; |
michael@0 | 1002 | }, |
michael@0 | 1003 | |
michael@0 | 1004 | |
michael@0 | 1005 | /* |
michael@0 | 1006 | * _isGuidUnique |
michael@0 | 1007 | * |
michael@0 | 1008 | * Checks to see if the specified GUID already exists. |
michael@0 | 1009 | */ |
michael@0 | 1010 | _isGuidUnique : function (guid) { |
michael@0 | 1011 | let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid"; |
michael@0 | 1012 | let params = { guid: guid }; |
michael@0 | 1013 | |
michael@0 | 1014 | let stmt, numLogins; |
michael@0 | 1015 | try { |
michael@0 | 1016 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 1017 | stmt.executeStep(); |
michael@0 | 1018 | numLogins = stmt.row.numLogins; |
michael@0 | 1019 | } catch (e) { |
michael@0 | 1020 | this.log("_isGuidUnique failed: " + e.name + " : " + e.message); |
michael@0 | 1021 | } finally { |
michael@0 | 1022 | if (stmt) { |
michael@0 | 1023 | stmt.reset(); |
michael@0 | 1024 | } |
michael@0 | 1025 | } |
michael@0 | 1026 | |
michael@0 | 1027 | return (numLogins == 0); |
michael@0 | 1028 | }, |
michael@0 | 1029 | |
michael@0 | 1030 | |
michael@0 | 1031 | /* |
michael@0 | 1032 | * _encryptLogin |
michael@0 | 1033 | * |
michael@0 | 1034 | * Returns the encrypted username, password, and encrypton type for the specified |
michael@0 | 1035 | * login. Can throw if the user cancels a master password entry. |
michael@0 | 1036 | */ |
michael@0 | 1037 | _encryptLogin : function (login) { |
michael@0 | 1038 | let encUsername = this._crypto.encrypt(login.username); |
michael@0 | 1039 | let encPassword = this._crypto.encrypt(login.password); |
michael@0 | 1040 | let encType = this._crypto.defaultEncType; |
michael@0 | 1041 | |
michael@0 | 1042 | return [encUsername, encPassword, encType]; |
michael@0 | 1043 | }, |
michael@0 | 1044 | |
michael@0 | 1045 | |
michael@0 | 1046 | /* |
michael@0 | 1047 | * _decryptLogins |
michael@0 | 1048 | * |
michael@0 | 1049 | * Decrypts username and password fields in the provided array of |
michael@0 | 1050 | * logins. |
michael@0 | 1051 | * |
michael@0 | 1052 | * The entries specified by the array will be decrypted, if possible. |
michael@0 | 1053 | * An array of successfully decrypted logins will be returned. The return |
michael@0 | 1054 | * value should be given to external callers (since still-encrypted |
michael@0 | 1055 | * entries are useless), whereas internal callers generally don't want |
michael@0 | 1056 | * to lose unencrypted entries (eg, because the user clicked Cancel |
michael@0 | 1057 | * instead of entering their master password) |
michael@0 | 1058 | */ |
michael@0 | 1059 | _decryptLogins : function (logins) { |
michael@0 | 1060 | let result = []; |
michael@0 | 1061 | |
michael@0 | 1062 | for each (let login in logins) { |
michael@0 | 1063 | try { |
michael@0 | 1064 | login.username = this._crypto.decrypt(login.username); |
michael@0 | 1065 | login.password = this._crypto.decrypt(login.password); |
michael@0 | 1066 | } catch (e) { |
michael@0 | 1067 | // If decryption failed (corrupt entry?), just skip it. |
michael@0 | 1068 | // Rethrow other errors (like canceling entry of a master pw) |
michael@0 | 1069 | if (e.result == Cr.NS_ERROR_FAILURE) |
michael@0 | 1070 | continue; |
michael@0 | 1071 | throw e; |
michael@0 | 1072 | } |
michael@0 | 1073 | result.push(login); |
michael@0 | 1074 | } |
michael@0 | 1075 | |
michael@0 | 1076 | return result; |
michael@0 | 1077 | }, |
michael@0 | 1078 | |
michael@0 | 1079 | |
michael@0 | 1080 | //**************************************************************************// |
michael@0 | 1081 | // Database Creation & Access |
michael@0 | 1082 | |
michael@0 | 1083 | /* |
michael@0 | 1084 | * _dbCreateStatement |
michael@0 | 1085 | * |
michael@0 | 1086 | * Creates a statement, wraps it, and then does parameter replacement |
michael@0 | 1087 | * Returns the wrapped statement for execution. Will use memoization |
michael@0 | 1088 | * so that statements can be reused. |
michael@0 | 1089 | */ |
michael@0 | 1090 | _dbCreateStatement : function (query, params) { |
michael@0 | 1091 | let wrappedStmt = this._dbStmts[query]; |
michael@0 | 1092 | // Memoize the statements |
michael@0 | 1093 | if (!wrappedStmt) { |
michael@0 | 1094 | this.log("Creating new statement for query: " + query); |
michael@0 | 1095 | wrappedStmt = this._dbConnection.createStatement(query); |
michael@0 | 1096 | this._dbStmts[query] = wrappedStmt; |
michael@0 | 1097 | } |
michael@0 | 1098 | // Replace parameters, must be done 1 at a time |
michael@0 | 1099 | if (params) |
michael@0 | 1100 | for (let i in params) |
michael@0 | 1101 | wrappedStmt.params[i] = params[i]; |
michael@0 | 1102 | return wrappedStmt; |
michael@0 | 1103 | }, |
michael@0 | 1104 | |
michael@0 | 1105 | |
michael@0 | 1106 | /* |
michael@0 | 1107 | * _dbInit |
michael@0 | 1108 | * |
michael@0 | 1109 | * Attempts to initialize the database. This creates the file if it doesn't |
michael@0 | 1110 | * exist, performs any migrations, etc. Return if this is the first run. |
michael@0 | 1111 | */ |
michael@0 | 1112 | _dbInit : function () { |
michael@0 | 1113 | this.log("Initializing Database"); |
michael@0 | 1114 | let isFirstRun = false; |
michael@0 | 1115 | try { |
michael@0 | 1116 | this._dbConnection = this._storageService.openDatabase(this._signonsFile); |
michael@0 | 1117 | // Get the version of the schema in the file. It will be 0 if the |
michael@0 | 1118 | // database has not been created yet. |
michael@0 | 1119 | let version = this._dbConnection.schemaVersion; |
michael@0 | 1120 | if (version == 0) { |
michael@0 | 1121 | this._dbCreate(); |
michael@0 | 1122 | isFirstRun = true; |
michael@0 | 1123 | } else if (version != DB_VERSION) { |
michael@0 | 1124 | this._dbMigrate(version); |
michael@0 | 1125 | } |
michael@0 | 1126 | } catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) { |
michael@0 | 1127 | // Database is corrupted, so we backup the database, then throw |
michael@0 | 1128 | // causing initialization to fail and a new db to be created next use |
michael@0 | 1129 | this._dbCleanup(true); |
michael@0 | 1130 | throw e; |
michael@0 | 1131 | } |
michael@0 | 1132 | |
michael@0 | 1133 | Services.obs.addObserver(this, "profile-before-change", false); |
michael@0 | 1134 | return isFirstRun; |
michael@0 | 1135 | }, |
michael@0 | 1136 | |
michael@0 | 1137 | observe: function (subject, topic, data) { |
michael@0 | 1138 | switch (topic) { |
michael@0 | 1139 | case "profile-before-change": |
michael@0 | 1140 | Services.obs.removeObserver(this, "profile-before-change"); |
michael@0 | 1141 | this._dbClose(); |
michael@0 | 1142 | break; |
michael@0 | 1143 | } |
michael@0 | 1144 | }, |
michael@0 | 1145 | |
michael@0 | 1146 | _dbCreate: function () { |
michael@0 | 1147 | this.log("Creating Database"); |
michael@0 | 1148 | this._dbCreateSchema(); |
michael@0 | 1149 | this._dbConnection.schemaVersion = DB_VERSION; |
michael@0 | 1150 | }, |
michael@0 | 1151 | |
michael@0 | 1152 | |
michael@0 | 1153 | _dbCreateSchema : function () { |
michael@0 | 1154 | this._dbCreateTables(); |
michael@0 | 1155 | this._dbCreateIndices(); |
michael@0 | 1156 | }, |
michael@0 | 1157 | |
michael@0 | 1158 | |
michael@0 | 1159 | _dbCreateTables : function () { |
michael@0 | 1160 | this.log("Creating Tables"); |
michael@0 | 1161 | for (let name in this._dbSchema.tables) |
michael@0 | 1162 | this._dbConnection.createTable(name, this._dbSchema.tables[name]); |
michael@0 | 1163 | }, |
michael@0 | 1164 | |
michael@0 | 1165 | |
michael@0 | 1166 | _dbCreateIndices : function () { |
michael@0 | 1167 | this.log("Creating Indices"); |
michael@0 | 1168 | for (let name in this._dbSchema.indices) { |
michael@0 | 1169 | let index = this._dbSchema.indices[name]; |
michael@0 | 1170 | let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table + |
michael@0 | 1171 | "(" + index.columns.join(", ") + ")"; |
michael@0 | 1172 | this._dbConnection.executeSimpleSQL(statement); |
michael@0 | 1173 | } |
michael@0 | 1174 | }, |
michael@0 | 1175 | |
michael@0 | 1176 | |
michael@0 | 1177 | _dbMigrate : function (oldVersion) { |
michael@0 | 1178 | this.log("Attempting to migrate from version " + oldVersion); |
michael@0 | 1179 | |
michael@0 | 1180 | if (oldVersion > DB_VERSION) { |
michael@0 | 1181 | this.log("Downgrading to version " + DB_VERSION); |
michael@0 | 1182 | // User's DB is newer. Sanity check that our expected columns are |
michael@0 | 1183 | // present, and if so mark the lower version and merrily continue |
michael@0 | 1184 | // on. If the columns are borked, something is wrong so blow away |
michael@0 | 1185 | // the DB and start from scratch. [Future incompatible upgrades |
michael@0 | 1186 | // should swtich to a different table or file.] |
michael@0 | 1187 | |
michael@0 | 1188 | if (!this._dbAreExpectedColumnsPresent()) |
michael@0 | 1189 | throw Components.Exception("DB is missing expected columns", |
michael@0 | 1190 | Cr.NS_ERROR_FILE_CORRUPTED); |
michael@0 | 1191 | |
michael@0 | 1192 | // Change the stored version to the current version. If the user |
michael@0 | 1193 | // runs the newer code again, it will see the lower version number |
michael@0 | 1194 | // and re-upgrade (to fixup any entries the old code added). |
michael@0 | 1195 | this._dbConnection.schemaVersion = DB_VERSION; |
michael@0 | 1196 | return; |
michael@0 | 1197 | } |
michael@0 | 1198 | |
michael@0 | 1199 | // Upgrade to newer version... |
michael@0 | 1200 | |
michael@0 | 1201 | let transaction = new Transaction(this._dbConnection); |
michael@0 | 1202 | |
michael@0 | 1203 | try { |
michael@0 | 1204 | for (let v = oldVersion + 1; v <= DB_VERSION; v++) { |
michael@0 | 1205 | this.log("Upgrading to version " + v + "..."); |
michael@0 | 1206 | let migrateFunction = "_dbMigrateToVersion" + v; |
michael@0 | 1207 | this[migrateFunction](); |
michael@0 | 1208 | } |
michael@0 | 1209 | } catch (e) { |
michael@0 | 1210 | this.log("Migration failed: " + e); |
michael@0 | 1211 | transaction.rollback(); |
michael@0 | 1212 | throw e; |
michael@0 | 1213 | } |
michael@0 | 1214 | |
michael@0 | 1215 | this._dbConnection.schemaVersion = DB_VERSION; |
michael@0 | 1216 | transaction.commit(); |
michael@0 | 1217 | this.log("DB migration completed."); |
michael@0 | 1218 | }, |
michael@0 | 1219 | |
michael@0 | 1220 | |
michael@0 | 1221 | /* |
michael@0 | 1222 | * _dbMigrateToVersion2 |
michael@0 | 1223 | * |
michael@0 | 1224 | * Version 2 adds a GUID column. Existing logins are assigned a random GUID. |
michael@0 | 1225 | */ |
michael@0 | 1226 | _dbMigrateToVersion2 : function () { |
michael@0 | 1227 | // Check to see if GUID column already exists, add if needed |
michael@0 | 1228 | let query; |
michael@0 | 1229 | if (!this._dbColumnExists("guid")) { |
michael@0 | 1230 | query = "ALTER TABLE moz_logins ADD COLUMN guid TEXT"; |
michael@0 | 1231 | this._dbConnection.executeSimpleSQL(query); |
michael@0 | 1232 | |
michael@0 | 1233 | query = "CREATE INDEX IF NOT EXISTS moz_logins_guid_index ON moz_logins (guid)"; |
michael@0 | 1234 | this._dbConnection.executeSimpleSQL(query); |
michael@0 | 1235 | } |
michael@0 | 1236 | |
michael@0 | 1237 | // Get a list of IDs for existing logins |
michael@0 | 1238 | let ids = []; |
michael@0 | 1239 | let query = "SELECT id FROM moz_logins WHERE guid isnull"; |
michael@0 | 1240 | let stmt; |
michael@0 | 1241 | try { |
michael@0 | 1242 | stmt = this._dbCreateStatement(query); |
michael@0 | 1243 | while (stmt.executeStep()) |
michael@0 | 1244 | ids.push(stmt.row.id); |
michael@0 | 1245 | } catch (e) { |
michael@0 | 1246 | this.log("Failed getting IDs: " + e); |
michael@0 | 1247 | throw e; |
michael@0 | 1248 | } finally { |
michael@0 | 1249 | if (stmt) { |
michael@0 | 1250 | stmt.reset(); |
michael@0 | 1251 | } |
michael@0 | 1252 | } |
michael@0 | 1253 | |
michael@0 | 1254 | // Generate a GUID for each login and update the DB. |
michael@0 | 1255 | query = "UPDATE moz_logins SET guid = :guid WHERE id = :id"; |
michael@0 | 1256 | for each (let id in ids) { |
michael@0 | 1257 | let params = { |
michael@0 | 1258 | id: id, |
michael@0 | 1259 | guid: this._uuidService.generateUUID().toString() |
michael@0 | 1260 | }; |
michael@0 | 1261 | |
michael@0 | 1262 | try { |
michael@0 | 1263 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 1264 | stmt.execute(); |
michael@0 | 1265 | } catch (e) { |
michael@0 | 1266 | this.log("Failed setting GUID: " + e); |
michael@0 | 1267 | throw e; |
michael@0 | 1268 | } finally { |
michael@0 | 1269 | if (stmt) { |
michael@0 | 1270 | stmt.reset(); |
michael@0 | 1271 | } |
michael@0 | 1272 | } |
michael@0 | 1273 | } |
michael@0 | 1274 | }, |
michael@0 | 1275 | |
michael@0 | 1276 | |
michael@0 | 1277 | /* |
michael@0 | 1278 | * _dbMigrateToVersion3 |
michael@0 | 1279 | * |
michael@0 | 1280 | * Version 3 adds a encType column. |
michael@0 | 1281 | */ |
michael@0 | 1282 | _dbMigrateToVersion3 : function () { |
michael@0 | 1283 | // Check to see if encType column already exists, add if needed |
michael@0 | 1284 | let query; |
michael@0 | 1285 | if (!this._dbColumnExists("encType")) { |
michael@0 | 1286 | query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER"; |
michael@0 | 1287 | this._dbConnection.executeSimpleSQL(query); |
michael@0 | 1288 | |
michael@0 | 1289 | query = "CREATE INDEX IF NOT EXISTS " + |
michael@0 | 1290 | "moz_logins_encType_index ON moz_logins (encType)"; |
michael@0 | 1291 | this._dbConnection.executeSimpleSQL(query); |
michael@0 | 1292 | } |
michael@0 | 1293 | |
michael@0 | 1294 | // Get a list of existing logins |
michael@0 | 1295 | let logins = []; |
michael@0 | 1296 | let stmt; |
michael@0 | 1297 | query = "SELECT id, encryptedUsername, encryptedPassword " + |
michael@0 | 1298 | "FROM moz_logins WHERE encType isnull"; |
michael@0 | 1299 | try { |
michael@0 | 1300 | stmt = this._dbCreateStatement(query); |
michael@0 | 1301 | while (stmt.executeStep()) { |
michael@0 | 1302 | let params = { id: stmt.row.id }; |
michael@0 | 1303 | // We will tag base64 logins correctly, but no longer support their use. |
michael@0 | 1304 | if (stmt.row.encryptedUsername.charAt(0) == '~' || |
michael@0 | 1305 | stmt.row.encryptedPassword.charAt(0) == '~') |
michael@0 | 1306 | params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64; |
michael@0 | 1307 | else |
michael@0 | 1308 | params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_SDR; |
michael@0 | 1309 | logins.push(params); |
michael@0 | 1310 | } |
michael@0 | 1311 | } catch (e) { |
michael@0 | 1312 | this.log("Failed getting logins: " + e); |
michael@0 | 1313 | throw e; |
michael@0 | 1314 | } finally { |
michael@0 | 1315 | if (stmt) { |
michael@0 | 1316 | stmt.reset(); |
michael@0 | 1317 | } |
michael@0 | 1318 | } |
michael@0 | 1319 | |
michael@0 | 1320 | // Determine encryption type for each login and update the DB. |
michael@0 | 1321 | query = "UPDATE moz_logins SET encType = :encType WHERE id = :id"; |
michael@0 | 1322 | for each (let params in logins) { |
michael@0 | 1323 | try { |
michael@0 | 1324 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 1325 | stmt.execute(); |
michael@0 | 1326 | } catch (e) { |
michael@0 | 1327 | this.log("Failed setting encType: " + e); |
michael@0 | 1328 | throw e; |
michael@0 | 1329 | } finally { |
michael@0 | 1330 | if (stmt) { |
michael@0 | 1331 | stmt.reset(); |
michael@0 | 1332 | } |
michael@0 | 1333 | } |
michael@0 | 1334 | } |
michael@0 | 1335 | }, |
michael@0 | 1336 | |
michael@0 | 1337 | |
michael@0 | 1338 | /* |
michael@0 | 1339 | * _dbMigrateToVersion4 |
michael@0 | 1340 | * |
michael@0 | 1341 | * Version 4 adds timeCreated, timeLastUsed, timePasswordChanged, |
michael@0 | 1342 | * and timesUsed columns |
michael@0 | 1343 | */ |
michael@0 | 1344 | _dbMigrateToVersion4 : function () { |
michael@0 | 1345 | let query; |
michael@0 | 1346 | // Add the new columns, if needed. |
michael@0 | 1347 | for each (let column in ["timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) { |
michael@0 | 1348 | if (!this._dbColumnExists(column)) { |
michael@0 | 1349 | query = "ALTER TABLE moz_logins ADD COLUMN " + column + " INTEGER"; |
michael@0 | 1350 | this._dbConnection.executeSimpleSQL(query); |
michael@0 | 1351 | } |
michael@0 | 1352 | } |
michael@0 | 1353 | |
michael@0 | 1354 | // Get a list of IDs for existing logins. |
michael@0 | 1355 | let ids = []; |
michael@0 | 1356 | let stmt; |
michael@0 | 1357 | query = "SELECT id FROM moz_logins WHERE timeCreated isnull OR " + |
michael@0 | 1358 | "timeLastUsed isnull OR timePasswordChanged isnull OR timesUsed isnull"; |
michael@0 | 1359 | try { |
michael@0 | 1360 | stmt = this._dbCreateStatement(query); |
michael@0 | 1361 | while (stmt.executeStep()) |
michael@0 | 1362 | ids.push(stmt.row.id); |
michael@0 | 1363 | } catch (e) { |
michael@0 | 1364 | this.log("Failed getting IDs: " + e); |
michael@0 | 1365 | throw e; |
michael@0 | 1366 | } finally { |
michael@0 | 1367 | if (stmt) { |
michael@0 | 1368 | stmt.reset(); |
michael@0 | 1369 | } |
michael@0 | 1370 | } |
michael@0 | 1371 | |
michael@0 | 1372 | // Initialize logins with current time. |
michael@0 | 1373 | query = "UPDATE moz_logins SET timeCreated = :initTime, timeLastUsed = :initTime, " + |
michael@0 | 1374 | "timePasswordChanged = :initTime, timesUsed = 1 WHERE id = :id"; |
michael@0 | 1375 | let params = { |
michael@0 | 1376 | id: null, |
michael@0 | 1377 | initTime: Date.now() |
michael@0 | 1378 | }; |
michael@0 | 1379 | for each (let id in ids) { |
michael@0 | 1380 | params.id = id; |
michael@0 | 1381 | try { |
michael@0 | 1382 | stmt = this._dbCreateStatement(query, params); |
michael@0 | 1383 | stmt.execute(); |
michael@0 | 1384 | } catch (e) { |
michael@0 | 1385 | this.log("Failed setting timestamps: " + e); |
michael@0 | 1386 | throw e; |
michael@0 | 1387 | } finally { |
michael@0 | 1388 | if (stmt) { |
michael@0 | 1389 | stmt.reset(); |
michael@0 | 1390 | } |
michael@0 | 1391 | } |
michael@0 | 1392 | } |
michael@0 | 1393 | }, |
michael@0 | 1394 | |
michael@0 | 1395 | |
michael@0 | 1396 | /* |
michael@0 | 1397 | * _dbMigrateToVersion5 |
michael@0 | 1398 | * |
michael@0 | 1399 | * Version 5 adds the moz_deleted_logins table |
michael@0 | 1400 | */ |
michael@0 | 1401 | _dbMigrateToVersion5 : function () { |
michael@0 | 1402 | if (!this._dbConnection.tableExists("moz_deleted_logins")) { |
michael@0 | 1403 | this._dbConnection.createTable("moz_deleted_logins", this._dbSchema.tables.moz_deleted_logins); |
michael@0 | 1404 | } |
michael@0 | 1405 | }, |
michael@0 | 1406 | |
michael@0 | 1407 | /* |
michael@0 | 1408 | * _dbAreExpectedColumnsPresent |
michael@0 | 1409 | * |
michael@0 | 1410 | * Sanity check to ensure that the columns this version of the code expects |
michael@0 | 1411 | * are present in the DB we're using. |
michael@0 | 1412 | */ |
michael@0 | 1413 | _dbAreExpectedColumnsPresent : function () { |
michael@0 | 1414 | let query = "SELECT " + |
michael@0 | 1415 | "id, " + |
michael@0 | 1416 | "hostname, " + |
michael@0 | 1417 | "httpRealm, " + |
michael@0 | 1418 | "formSubmitURL, " + |
michael@0 | 1419 | "usernameField, " + |
michael@0 | 1420 | "passwordField, " + |
michael@0 | 1421 | "encryptedUsername, " + |
michael@0 | 1422 | "encryptedPassword, " + |
michael@0 | 1423 | "guid, " + |
michael@0 | 1424 | "encType, " + |
michael@0 | 1425 | "timeCreated, " + |
michael@0 | 1426 | "timeLastUsed, " + |
michael@0 | 1427 | "timePasswordChanged, " + |
michael@0 | 1428 | "timesUsed " + |
michael@0 | 1429 | "FROM moz_logins"; |
michael@0 | 1430 | try { |
michael@0 | 1431 | let stmt = this._dbConnection.createStatement(query); |
michael@0 | 1432 | // (no need to execute statement, if it compiled we're good) |
michael@0 | 1433 | stmt.finalize(); |
michael@0 | 1434 | } catch (e) { |
michael@0 | 1435 | return false; |
michael@0 | 1436 | } |
michael@0 | 1437 | |
michael@0 | 1438 | query = "SELECT " + |
michael@0 | 1439 | "id, " + |
michael@0 | 1440 | "hostname " + |
michael@0 | 1441 | "FROM moz_disabledHosts"; |
michael@0 | 1442 | try { |
michael@0 | 1443 | let stmt = this._dbConnection.createStatement(query); |
michael@0 | 1444 | // (no need to execute statement, if it compiled we're good) |
michael@0 | 1445 | stmt.finalize(); |
michael@0 | 1446 | } catch (e) { |
michael@0 | 1447 | return false; |
michael@0 | 1448 | } |
michael@0 | 1449 | |
michael@0 | 1450 | this.log("verified that expected columns are present in DB."); |
michael@0 | 1451 | return true; |
michael@0 | 1452 | }, |
michael@0 | 1453 | |
michael@0 | 1454 | |
michael@0 | 1455 | /* |
michael@0 | 1456 | * _dbColumnExists |
michael@0 | 1457 | * |
michael@0 | 1458 | * Checks to see if the named column already exists. |
michael@0 | 1459 | */ |
michael@0 | 1460 | _dbColumnExists : function (columnName) { |
michael@0 | 1461 | let query = "SELECT " + columnName + " FROM moz_logins"; |
michael@0 | 1462 | try { |
michael@0 | 1463 | let stmt = this._dbConnection.createStatement(query); |
michael@0 | 1464 | // (no need to execute statement, if it compiled we're good) |
michael@0 | 1465 | stmt.finalize(); |
michael@0 | 1466 | return true; |
michael@0 | 1467 | } catch (e) { |
michael@0 | 1468 | return false; |
michael@0 | 1469 | } |
michael@0 | 1470 | }, |
michael@0 | 1471 | |
michael@0 | 1472 | _dbClose : function () { |
michael@0 | 1473 | this.log("Closing the DB connection."); |
michael@0 | 1474 | // Finalize all statements to free memory, avoid errors later |
michael@0 | 1475 | for each (let stmt in this._dbStmts) { |
michael@0 | 1476 | stmt.finalize(); |
michael@0 | 1477 | } |
michael@0 | 1478 | this._dbStmts = {}; |
michael@0 | 1479 | |
michael@0 | 1480 | if (this._dbConnection !== null) { |
michael@0 | 1481 | try { |
michael@0 | 1482 | this._dbConnection.close(); |
michael@0 | 1483 | } catch (e) { |
michael@0 | 1484 | Components.utils.reportError(e); |
michael@0 | 1485 | } |
michael@0 | 1486 | } |
michael@0 | 1487 | this._dbConnection = null; |
michael@0 | 1488 | }, |
michael@0 | 1489 | |
michael@0 | 1490 | /* |
michael@0 | 1491 | * _dbCleanup |
michael@0 | 1492 | * |
michael@0 | 1493 | * Called when database creation fails. Finalizes database statements, |
michael@0 | 1494 | * closes the database connection, deletes the database file. |
michael@0 | 1495 | */ |
michael@0 | 1496 | _dbCleanup : function (backup) { |
michael@0 | 1497 | this.log("Cleaning up DB file - close & remove & backup=" + backup) |
michael@0 | 1498 | |
michael@0 | 1499 | // Create backup file |
michael@0 | 1500 | if (backup) { |
michael@0 | 1501 | let backupFile = this._signonsFile.leafName + ".corrupt"; |
michael@0 | 1502 | this._storageService.backupDatabaseFile(this._signonsFile, backupFile); |
michael@0 | 1503 | } |
michael@0 | 1504 | |
michael@0 | 1505 | this._dbClose(); |
michael@0 | 1506 | this._signonsFile.remove(false); |
michael@0 | 1507 | } |
michael@0 | 1508 | |
michael@0 | 1509 | }; // end of nsLoginManagerStorage_mozStorage implementation |
michael@0 | 1510 | |
michael@0 | 1511 | let component = [LoginManagerStorage_mozStorage]; |
michael@0 | 1512 | this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); |