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