toolkit/components/satchel/nsFormHistory.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5
michael@0 6 const Cc = Components.classes;
michael@0 7 const Ci = Components.interfaces;
michael@0 8 const Cr = Components.results;
michael@0 9
michael@0 10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 11 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 12
michael@0 13 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
michael@0 14 "resource://gre/modules/Deprecated.jsm");
michael@0 15
michael@0 16 const DB_VERSION = 4;
michael@0 17 const DAY_IN_MS = 86400000; // 1 day in milliseconds
michael@0 18
michael@0 19 function FormHistory() {
michael@0 20 Deprecated.warning(
michael@0 21 "nsIFormHistory2 is deprecated and will be removed in a future version",
michael@0 22 "https://bugzilla.mozilla.org/show_bug.cgi?id=879118");
michael@0 23 this.init();
michael@0 24 }
michael@0 25
michael@0 26 FormHistory.prototype = {
michael@0 27 classID : Components.ID("{0c1bb408-71a2-403f-854a-3a0659829ded}"),
michael@0 28 QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormHistory2,
michael@0 29 Ci.nsIObserver,
michael@0 30 Ci.nsIMessageListener,
michael@0 31 Ci.nsISupportsWeakReference,
michael@0 32 ]),
michael@0 33
michael@0 34 debug : true,
michael@0 35 enabled : true,
michael@0 36
michael@0 37 // The current database schema.
michael@0 38 dbSchema : {
michael@0 39 tables : {
michael@0 40 moz_formhistory: {
michael@0 41 "id" : "INTEGER PRIMARY KEY",
michael@0 42 "fieldname" : "TEXT NOT NULL",
michael@0 43 "value" : "TEXT NOT NULL",
michael@0 44 "timesUsed" : "INTEGER",
michael@0 45 "firstUsed" : "INTEGER",
michael@0 46 "lastUsed" : "INTEGER",
michael@0 47 "guid" : "TEXT"
michael@0 48 },
michael@0 49 moz_deleted_formhistory: {
michael@0 50 "id" : "INTEGER PRIMARY KEY",
michael@0 51 "timeDeleted" : "INTEGER",
michael@0 52 "guid" : "TEXT"
michael@0 53 }
michael@0 54 },
michael@0 55 indices : {
michael@0 56 moz_formhistory_index : {
michael@0 57 table : "moz_formhistory",
michael@0 58 columns : ["fieldname"]
michael@0 59 },
michael@0 60 moz_formhistory_lastused_index : {
michael@0 61 table : "moz_formhistory",
michael@0 62 columns : ["lastUsed"]
michael@0 63 },
michael@0 64 moz_formhistory_guid_index : {
michael@0 65 table : "moz_formhistory",
michael@0 66 columns : ["guid"]
michael@0 67 },
michael@0 68 }
michael@0 69 },
michael@0 70 dbStmts : null, // Database statements for memoization
michael@0 71 dbFile : null,
michael@0 72
michael@0 73 _uuidService: null,
michael@0 74 get uuidService() {
michael@0 75 if (!this._uuidService)
michael@0 76 this._uuidService = Cc["@mozilla.org/uuid-generator;1"].
michael@0 77 getService(Ci.nsIUUIDGenerator);
michael@0 78 return this._uuidService;
michael@0 79 },
michael@0 80
michael@0 81 log : function log(message) {
michael@0 82 if (!this.debug)
michael@0 83 return;
michael@0 84 dump("FormHistory: " + message + "\n");
michael@0 85 Services.console.logStringMessage("FormHistory: " + message);
michael@0 86 },
michael@0 87
michael@0 88
michael@0 89 init : function init() {
michael@0 90 this.updatePrefs();
michael@0 91
michael@0 92 this.dbStmts = {};
michael@0 93
michael@0 94 // Add observer
michael@0 95 Services.obs.addObserver(this, "profile-before-change", true);
michael@0 96 },
michael@0 97
michael@0 98 /* ---- nsIFormHistory2 interfaces ---- */
michael@0 99
michael@0 100
michael@0 101 get hasEntries() {
michael@0 102 return (this.countAllEntries() > 0);
michael@0 103 },
michael@0 104
michael@0 105
michael@0 106 addEntry : function addEntry(name, value) {
michael@0 107 if (!this.enabled)
michael@0 108 return;
michael@0 109
michael@0 110 this.log("addEntry for " + name + "=" + value);
michael@0 111
michael@0 112 let now = Date.now() * 1000; // microseconds
michael@0 113
michael@0 114 let [id, guid] = this.getExistingEntryID(name, value);
michael@0 115 let stmt;
michael@0 116
michael@0 117 if (id != -1) {
michael@0 118 // Update existing entry.
michael@0 119 let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE id = :id";
michael@0 120 let params = {
michael@0 121 lastUsed : now,
michael@0 122 id : id
michael@0 123 };
michael@0 124
michael@0 125 try {
michael@0 126 stmt = this.dbCreateStatement(query, params);
michael@0 127 stmt.execute();
michael@0 128 this.sendStringNotification("modifyEntry", name, value, guid);
michael@0 129 } catch (e) {
michael@0 130 this.log("addEntry (modify) failed: " + e);
michael@0 131 throw e;
michael@0 132 } finally {
michael@0 133 if (stmt) {
michael@0 134 stmt.reset();
michael@0 135 }
michael@0 136 }
michael@0 137
michael@0 138 } else {
michael@0 139 // Add new entry.
michael@0 140 guid = this.generateGUID();
michael@0 141
michael@0 142 let query = "INSERT INTO moz_formhistory (fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
michael@0 143 "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
michael@0 144 let params = {
michael@0 145 fieldname : name,
michael@0 146 value : value,
michael@0 147 timesUsed : 1,
michael@0 148 firstUsed : now,
michael@0 149 lastUsed : now,
michael@0 150 guid : guid
michael@0 151 };
michael@0 152
michael@0 153 try {
michael@0 154 stmt = this.dbCreateStatement(query, params);
michael@0 155 stmt.execute();
michael@0 156 this.sendStringNotification("addEntry", name, value, guid);
michael@0 157 } catch (e) {
michael@0 158 this.log("addEntry (create) failed: " + e);
michael@0 159 throw e;
michael@0 160 } finally {
michael@0 161 if (stmt) {
michael@0 162 stmt.reset();
michael@0 163 }
michael@0 164 }
michael@0 165 }
michael@0 166 },
michael@0 167
michael@0 168
michael@0 169 removeEntry : function removeEntry(name, value) {
michael@0 170 this.log("removeEntry for " + name + "=" + value);
michael@0 171
michael@0 172 let [id, guid] = this.getExistingEntryID(name, value);
michael@0 173 this.sendStringNotification("before-removeEntry", name, value, guid);
michael@0 174
michael@0 175 let stmt;
michael@0 176 let query = "DELETE FROM moz_formhistory WHERE id = :id";
michael@0 177 let params = { id : id };
michael@0 178 let existingTransactionInProgress;
michael@0 179
michael@0 180 try {
michael@0 181 // Don't start a transaction if one is already in progress since we can't nest them.
michael@0 182 existingTransactionInProgress = this.dbConnection.transactionInProgress;
michael@0 183 if (!existingTransactionInProgress)
michael@0 184 this.dbConnection.beginTransaction();
michael@0 185 this.moveToDeletedTable("VALUES (:guid, :timeDeleted)", {
michael@0 186 guid: guid,
michael@0 187 timeDeleted: Date.now()
michael@0 188 });
michael@0 189
michael@0 190 // remove from the formhistory database
michael@0 191 stmt = this.dbCreateStatement(query, params);
michael@0 192 stmt.execute();
michael@0 193 this.sendStringNotification("removeEntry", name, value, guid);
michael@0 194 } catch (e) {
michael@0 195 if (!existingTransactionInProgress)
michael@0 196 this.dbConnection.rollbackTransaction();
michael@0 197 this.log("removeEntry failed: " + e);
michael@0 198 throw e;
michael@0 199 } finally {
michael@0 200 if (stmt) {
michael@0 201 stmt.reset();
michael@0 202 }
michael@0 203 }
michael@0 204 if (!existingTransactionInProgress)
michael@0 205 this.dbConnection.commitTransaction();
michael@0 206 },
michael@0 207
michael@0 208
michael@0 209 removeEntriesForName : function removeEntriesForName(name) {
michael@0 210 this.log("removeEntriesForName with name=" + name);
michael@0 211
michael@0 212 this.sendStringNotification("before-removeEntriesForName", name);
michael@0 213
michael@0 214 let stmt;
michael@0 215 let query = "DELETE FROM moz_formhistory WHERE fieldname = :fieldname";
michael@0 216 let params = { fieldname : name };
michael@0 217 let existingTransactionInProgress;
michael@0 218
michael@0 219 try {
michael@0 220 // Don't start a transaction if one is already in progress since we can't nest them.
michael@0 221 existingTransactionInProgress = this.dbConnection.transactionInProgress;
michael@0 222 if (!existingTransactionInProgress)
michael@0 223 this.dbConnection.beginTransaction();
michael@0 224 this.moveToDeletedTable(
michael@0 225 "SELECT guid, :timeDeleted FROM moz_formhistory " +
michael@0 226 "WHERE fieldname = :fieldname", {
michael@0 227 fieldname: name,
michael@0 228 timeDeleted: Date.now()
michael@0 229 });
michael@0 230
michael@0 231 stmt = this.dbCreateStatement(query, params);
michael@0 232 stmt.execute();
michael@0 233 this.sendStringNotification("removeEntriesForName", name);
michael@0 234 } catch (e) {
michael@0 235 if (!existingTransactionInProgress)
michael@0 236 this.dbConnection.rollbackTransaction();
michael@0 237 this.log("removeEntriesForName failed: " + e);
michael@0 238 throw e;
michael@0 239 } finally {
michael@0 240 if (stmt) {
michael@0 241 stmt.reset();
michael@0 242 }
michael@0 243 }
michael@0 244 if (!existingTransactionInProgress)
michael@0 245 this.dbConnection.commitTransaction();
michael@0 246 },
michael@0 247
michael@0 248
michael@0 249 removeAllEntries : function removeAllEntries() {
michael@0 250 this.log("removeAllEntries");
michael@0 251
michael@0 252 this.sendNotification("before-removeAllEntries", null);
michael@0 253
michael@0 254 let stmt;
michael@0 255 let query = "DELETE FROM moz_formhistory";
michael@0 256 let existingTransactionInProgress;
michael@0 257
michael@0 258 try {
michael@0 259 // Don't start a transaction if one is already in progress since we can't nest them.
michael@0 260 existingTransactionInProgress = this.dbConnection.transactionInProgress;
michael@0 261 if (!existingTransactionInProgress)
michael@0 262 this.dbConnection.beginTransaction();
michael@0 263 // TODO: Add these items to the deleted items table once we've sorted
michael@0 264 // out the issues from bug 756701
michael@0 265 stmt = this.dbCreateStatement(query);
michael@0 266 stmt.execute();
michael@0 267 this.sendNotification("removeAllEntries", null);
michael@0 268 } catch (e) {
michael@0 269 if (!existingTransactionInProgress)
michael@0 270 this.dbConnection.rollbackTransaction();
michael@0 271 this.log("removeAllEntries failed: " + e);
michael@0 272 throw e;
michael@0 273 } finally {
michael@0 274 if (stmt) {
michael@0 275 stmt.reset();
michael@0 276 }
michael@0 277 }
michael@0 278 if (!existingTransactionInProgress)
michael@0 279 this.dbConnection.commitTransaction();
michael@0 280 },
michael@0 281
michael@0 282
michael@0 283 nameExists : function nameExists(name) {
michael@0 284 this.log("nameExists for name=" + name);
michael@0 285 let stmt;
michael@0 286 let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory WHERE fieldname = :fieldname";
michael@0 287 let params = { fieldname : name };
michael@0 288 try {
michael@0 289 stmt = this.dbCreateStatement(query, params);
michael@0 290 stmt.executeStep();
michael@0 291 return (stmt.row.numEntries > 0);
michael@0 292 } catch (e) {
michael@0 293 this.log("nameExists failed: " + e);
michael@0 294 throw e;
michael@0 295 } finally {
michael@0 296 if (stmt) {
michael@0 297 stmt.reset();
michael@0 298 }
michael@0 299 }
michael@0 300 },
michael@0 301
michael@0 302 entryExists : function entryExists(name, value) {
michael@0 303 this.log("entryExists for " + name + "=" + value);
michael@0 304 let [id, guid] = this.getExistingEntryID(name, value);
michael@0 305 this.log("entryExists: id=" + id);
michael@0 306 return (id != -1);
michael@0 307 },
michael@0 308
michael@0 309 removeEntriesByTimeframe : function removeEntriesByTimeframe(beginTime, endTime) {
michael@0 310 this.log("removeEntriesByTimeframe for " + beginTime + " to " + endTime);
michael@0 311
michael@0 312 this.sendIntNotification("before-removeEntriesByTimeframe", beginTime, endTime);
michael@0 313
michael@0 314 let stmt;
michael@0 315 let query = "DELETE FROM moz_formhistory WHERE firstUsed >= :beginTime AND firstUsed <= :endTime";
michael@0 316 let params = {
michael@0 317 beginTime : beginTime,
michael@0 318 endTime : endTime
michael@0 319 };
michael@0 320 let existingTransactionInProgress;
michael@0 321
michael@0 322 try {
michael@0 323 // Don't start a transaction if one is already in progress since we can't nest them.
michael@0 324 existingTransactionInProgress = this.dbConnection.transactionInProgress;
michael@0 325 if (!existingTransactionInProgress)
michael@0 326 this.dbConnection.beginTransaction();
michael@0 327 this.moveToDeletedTable(
michael@0 328 "SELECT guid, :timeDeleted FROM moz_formhistory " +
michael@0 329 "WHERE firstUsed >= :beginTime AND firstUsed <= :endTime", {
michael@0 330 beginTime: beginTime,
michael@0 331 endTime: endTime
michael@0 332 });
michael@0 333
michael@0 334 stmt = this.dbCreateStatement(query, params);
michael@0 335 stmt.executeStep();
michael@0 336 this.sendIntNotification("removeEntriesByTimeframe", beginTime, endTime);
michael@0 337 } catch (e) {
michael@0 338 if (!existingTransactionInProgress)
michael@0 339 this.dbConnection.rollbackTransaction();
michael@0 340 this.log("removeEntriesByTimeframe failed: " + e);
michael@0 341 throw e;
michael@0 342 } finally {
michael@0 343 if (stmt) {
michael@0 344 stmt.reset();
michael@0 345 }
michael@0 346 }
michael@0 347 if (!existingTransactionInProgress)
michael@0 348 this.dbConnection.commitTransaction();
michael@0 349 },
michael@0 350
michael@0 351 moveToDeletedTable : function moveToDeletedTable(values, params) {
michael@0 352 #ifdef ANDROID
michael@0 353 this.log("Moving entries to deleted table.");
michael@0 354
michael@0 355 let stmt;
michael@0 356
michael@0 357 try {
michael@0 358 // Move the entries to the deleted items table.
michael@0 359 let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted) ";
michael@0 360 if (values) query += values;
michael@0 361 stmt = this.dbCreateStatement(query, params);
michael@0 362 stmt.execute();
michael@0 363 } catch (e) {
michael@0 364 this.log("Moving deleted entries failed: " + e);
michael@0 365 throw e;
michael@0 366 } finally {
michael@0 367 if (stmt) {
michael@0 368 stmt.reset();
michael@0 369 }
michael@0 370 }
michael@0 371 #endif
michael@0 372 },
michael@0 373
michael@0 374 get dbConnection() {
michael@0 375 // Make sure dbConnection can't be called from now to prevent infinite loops.
michael@0 376 delete FormHistory.prototype.dbConnection;
michael@0 377
michael@0 378 try {
michael@0 379 this.dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
michael@0 380 this.dbFile.append("formhistory.sqlite");
michael@0 381 this.log("Opening database at " + this.dbFile.path);
michael@0 382
michael@0 383 FormHistory.prototype.dbConnection = this.dbOpen();
michael@0 384 this.dbInit();
michael@0 385 } catch (e) {
michael@0 386 this.log("Initialization failed: " + e);
michael@0 387 // If dbInit fails...
michael@0 388 if (e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
michael@0 389 this.dbCleanup();
michael@0 390 FormHistory.prototype.dbConnection = this.dbOpen();
michael@0 391 this.dbInit();
michael@0 392 } else {
michael@0 393 throw "Initialization failed";
michael@0 394 }
michael@0 395 }
michael@0 396
michael@0 397 return FormHistory.prototype.dbConnection;
michael@0 398 },
michael@0 399
michael@0 400 get DBConnection() {
michael@0 401 return this.dbConnection;
michael@0 402 },
michael@0 403
michael@0 404
michael@0 405 /* ---- nsIObserver interface ---- */
michael@0 406
michael@0 407
michael@0 408 observe : function observe(subject, topic, data) {
michael@0 409 switch(topic) {
michael@0 410 case "nsPref:changed":
michael@0 411 this.updatePrefs();
michael@0 412 break;
michael@0 413 case "profile-before-change":
michael@0 414 this._dbClose(false);
michael@0 415 break;
michael@0 416 default:
michael@0 417 this.log("Oops! Unexpected notification: " + topic);
michael@0 418 break;
michael@0 419 }
michael@0 420 },
michael@0 421
michael@0 422
michael@0 423 /* ---- helpers ---- */
michael@0 424
michael@0 425
michael@0 426 generateGUID : function() {
michael@0 427 // string like: "{f60d9eac-9421-4abc-8491-8e8322b063d4}"
michael@0 428 let uuid = this.uuidService.generateUUID().toString();
michael@0 429 let raw = ""; // A string with the low bytes set to random values
michael@0 430 let bytes = 0;
michael@0 431 for (let i = 1; bytes < 12 ; i+= 2) {
michael@0 432 // Skip dashes
michael@0 433 if (uuid[i] == "-")
michael@0 434 i++;
michael@0 435 let hexVal = parseInt(uuid[i] + uuid[i + 1], 16);
michael@0 436 raw += String.fromCharCode(hexVal);
michael@0 437 bytes++;
michael@0 438 }
michael@0 439 return btoa(raw);
michael@0 440 },
michael@0 441
michael@0 442
michael@0 443 sendStringNotification : function (changeType, str1, str2, str3) {
michael@0 444 function wrapit(str) {
michael@0 445 let wrapper = Cc["@mozilla.org/supports-string;1"].
michael@0 446 createInstance(Ci.nsISupportsString);
michael@0 447 wrapper.data = str;
michael@0 448 return wrapper;
michael@0 449 }
michael@0 450
michael@0 451 let strData;
michael@0 452 if (arguments.length == 2) {
michael@0 453 // Just 1 string, no need to put it in an array
michael@0 454 strData = wrapit(str1);
michael@0 455 } else {
michael@0 456 // 3 strings, put them in an array.
michael@0 457 strData = Cc["@mozilla.org/array;1"].
michael@0 458 createInstance(Ci.nsIMutableArray);
michael@0 459 strData.appendElement(wrapit(str1), false);
michael@0 460 strData.appendElement(wrapit(str2), false);
michael@0 461 strData.appendElement(wrapit(str3), false);
michael@0 462 }
michael@0 463 this.sendNotification(changeType, strData);
michael@0 464 },
michael@0 465
michael@0 466
michael@0 467 sendIntNotification : function (changeType, int1, int2) {
michael@0 468 function wrapit(int) {
michael@0 469 let wrapper = Cc["@mozilla.org/supports-PRInt64;1"].
michael@0 470 createInstance(Ci.nsISupportsPRInt64);
michael@0 471 wrapper.data = int;
michael@0 472 return wrapper;
michael@0 473 }
michael@0 474
michael@0 475 let intData;
michael@0 476 if (arguments.length == 2) {
michael@0 477 // Just 1 int, no need for an array
michael@0 478 intData = wrapit(int1);
michael@0 479 } else {
michael@0 480 // 2 ints, put them in an array.
michael@0 481 intData = Cc["@mozilla.org/array;1"].
michael@0 482 createInstance(Ci.nsIMutableArray);
michael@0 483 intData.appendElement(wrapit(int1), false);
michael@0 484 intData.appendElement(wrapit(int2), false);
michael@0 485 }
michael@0 486 this.sendNotification(changeType, intData);
michael@0 487 },
michael@0 488
michael@0 489
michael@0 490 sendNotification : function (changeType, data) {
michael@0 491 Services.obs.notifyObservers(data, "satchel-storage-changed", changeType);
michael@0 492 },
michael@0 493
michael@0 494
michael@0 495 getExistingEntryID : function (name, value) {
michael@0 496 let id = -1, guid = null;
michael@0 497 let stmt;
michael@0 498 let query = "SELECT id, guid FROM moz_formhistory WHERE fieldname = :fieldname AND value = :value";
michael@0 499 let params = {
michael@0 500 fieldname : name,
michael@0 501 value : value
michael@0 502 };
michael@0 503 try {
michael@0 504 stmt = this.dbCreateStatement(query, params);
michael@0 505 if (stmt.executeStep()) {
michael@0 506 id = stmt.row.id;
michael@0 507 guid = stmt.row.guid;
michael@0 508 }
michael@0 509 } catch (e) {
michael@0 510 this.log("getExistingEntryID failed: " + e);
michael@0 511 throw e;
michael@0 512 } finally {
michael@0 513 if (stmt) {
michael@0 514 stmt.reset();
michael@0 515 }
michael@0 516 }
michael@0 517
michael@0 518 return [id, guid];
michael@0 519 },
michael@0 520
michael@0 521
michael@0 522 countAllEntries : function () {
michael@0 523 let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory";
michael@0 524
michael@0 525 let stmt, numEntries;
michael@0 526 try {
michael@0 527 stmt = this.dbCreateStatement(query, null);
michael@0 528 stmt.executeStep();
michael@0 529 numEntries = stmt.row.numEntries;
michael@0 530 } catch (e) {
michael@0 531 this.log("countAllEntries failed: " + e);
michael@0 532 throw e;
michael@0 533 } finally {
michael@0 534 if (stmt) {
michael@0 535 stmt.reset();
michael@0 536 }
michael@0 537 }
michael@0 538
michael@0 539 this.log("countAllEntries: counted entries: " + numEntries);
michael@0 540 return numEntries;
michael@0 541 },
michael@0 542
michael@0 543
michael@0 544 updatePrefs : function () {
michael@0 545 this.debug = Services.prefs.getBoolPref("browser.formfill.debug");
michael@0 546 this.enabled = Services.prefs.getBoolPref("browser.formfill.enable");
michael@0 547 },
michael@0 548
michael@0 549 //**************************************************************************//
michael@0 550 // Database Creation & Access
michael@0 551
michael@0 552 /*
michael@0 553 * dbCreateStatement
michael@0 554 *
michael@0 555 * Creates a statement, wraps it, and then does parameter replacement
michael@0 556 * Will use memoization so that statements can be reused.
michael@0 557 */
michael@0 558 dbCreateStatement : function (query, params) {
michael@0 559 let stmt = this.dbStmts[query];
michael@0 560 // Memoize the statements
michael@0 561 if (!stmt) {
michael@0 562 this.log("Creating new statement for query: " + query);
michael@0 563 stmt = this.dbConnection.createStatement(query);
michael@0 564 this.dbStmts[query] = stmt;
michael@0 565 }
michael@0 566 // Replace parameters, must be done 1 at a time
michael@0 567 if (params)
michael@0 568 for (let i in params)
michael@0 569 stmt.params[i] = params[i];
michael@0 570 return stmt;
michael@0 571 },
michael@0 572
michael@0 573 /*
michael@0 574 * dbOpen
michael@0 575 *
michael@0 576 * Open a connection with the database and returns it.
michael@0 577 *
michael@0 578 * @returns a db connection object.
michael@0 579 */
michael@0 580 dbOpen : function () {
michael@0 581 this.log("Open Database");
michael@0 582
michael@0 583 let storage = Cc["@mozilla.org/storage/service;1"].
michael@0 584 getService(Ci.mozIStorageService);
michael@0 585 return storage.openDatabase(this.dbFile);
michael@0 586 },
michael@0 587
michael@0 588 /*
michael@0 589 * dbInit
michael@0 590 *
michael@0 591 * Attempts to initialize the database. This creates the file if it doesn't
michael@0 592 * exist, performs any migrations, etc.
michael@0 593 */
michael@0 594 dbInit : function () {
michael@0 595 this.log("Initializing Database");
michael@0 596
michael@0 597 let version = this.dbConnection.schemaVersion;
michael@0 598
michael@0 599 // Note: Firefox 3 didn't set a schema value, so it started from 0.
michael@0 600 // So we can't depend on a simple version == 0 check
michael@0 601 if (version == 0 && !this.dbConnection.tableExists("moz_formhistory"))
michael@0 602 this.dbCreate();
michael@0 603 else if (version != DB_VERSION)
michael@0 604 this.dbMigrate(version);
michael@0 605 },
michael@0 606
michael@0 607
michael@0 608 dbCreate: function () {
michael@0 609 this.log("Creating DB -- tables");
michael@0 610 for (let name in this.dbSchema.tables) {
michael@0 611 let table = this.dbSchema.tables[name];
michael@0 612 this.dbCreateTable(name, table);
michael@0 613 }
michael@0 614
michael@0 615 this.log("Creating DB -- indices");
michael@0 616 for (let name in this.dbSchema.indices) {
michael@0 617 let index = this.dbSchema.indices[name];
michael@0 618 let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
michael@0 619 "(" + index.columns.join(", ") + ")";
michael@0 620 this.dbConnection.executeSimpleSQL(statement);
michael@0 621 }
michael@0 622
michael@0 623 this.dbConnection.schemaVersion = DB_VERSION;
michael@0 624 },
michael@0 625
michael@0 626 dbCreateTable: function(name, table) {
michael@0 627 let tSQL = [[col, table[col]].join(" ") for (col in table)].join(", ");
michael@0 628 this.log("Creating table " + name + " with " + tSQL);
michael@0 629 this.dbConnection.createTable(name, tSQL);
michael@0 630 },
michael@0 631
michael@0 632 dbMigrate : function (oldVersion) {
michael@0 633 this.log("Attempting to migrate from version " + oldVersion);
michael@0 634
michael@0 635 if (oldVersion > DB_VERSION) {
michael@0 636 this.log("Downgrading to version " + DB_VERSION);
michael@0 637 // User's DB is newer. Sanity check that our expected columns are
michael@0 638 // present, and if so mark the lower version and merrily continue
michael@0 639 // on. If the columns are borked, something is wrong so blow away
michael@0 640 // the DB and start from scratch. [Future incompatible upgrades
michael@0 641 // should swtich to a different table or file.]
michael@0 642
michael@0 643 if (!this.dbAreExpectedColumnsPresent())
michael@0 644 throw Components.Exception("DB is missing expected columns",
michael@0 645 Cr.NS_ERROR_FILE_CORRUPTED);
michael@0 646
michael@0 647 // Change the stored version to the current version. If the user
michael@0 648 // runs the newer code again, it will see the lower version number
michael@0 649 // and re-upgrade (to fixup any entries the old code added).
michael@0 650 this.dbConnection.schemaVersion = DB_VERSION;
michael@0 651 return;
michael@0 652 }
michael@0 653
michael@0 654 // Upgrade to newer version...
michael@0 655
michael@0 656 this.dbConnection.beginTransaction();
michael@0 657
michael@0 658 try {
michael@0 659 for (let v = oldVersion + 1; v <= DB_VERSION; v++) {
michael@0 660 this.log("Upgrading to version " + v + "...");
michael@0 661 let migrateFunction = "dbMigrateToVersion" + v;
michael@0 662 this[migrateFunction]();
michael@0 663 }
michael@0 664 } catch (e) {
michael@0 665 this.log("Migration failed: " + e);
michael@0 666 this.dbConnection.rollbackTransaction();
michael@0 667 throw e;
michael@0 668 }
michael@0 669
michael@0 670 this.dbConnection.schemaVersion = DB_VERSION;
michael@0 671 this.dbConnection.commitTransaction();
michael@0 672 this.log("DB migration completed.");
michael@0 673 },
michael@0 674
michael@0 675
michael@0 676 /*
michael@0 677 * dbMigrateToVersion1
michael@0 678 *
michael@0 679 * Updates the DB schema to v1 (bug 463154).
michael@0 680 * Adds firstUsed, lastUsed, timesUsed columns.
michael@0 681 */
michael@0 682 dbMigrateToVersion1 : function () {
michael@0 683 // Check to see if the new columns already exist (could be a v1 DB that
michael@0 684 // was downgraded to v0). If they exist, we don't need to add them.
michael@0 685 let query;
michael@0 686 ["timesUsed", "firstUsed", "lastUsed"].forEach(function(column) {
michael@0 687 if (!this.dbColumnExists(column)) {
michael@0 688 query = "ALTER TABLE moz_formhistory ADD COLUMN " + column + " INTEGER";
michael@0 689 this.dbConnection.executeSimpleSQL(query);
michael@0 690 }
michael@0 691 }, this);
michael@0 692
michael@0 693 // Set the default values for the new columns.
michael@0 694 //
michael@0 695 // Note that we set the timestamps to 24 hours in the past. We want a
michael@0 696 // timestamp that's recent (so that "keep form history for 90 days"
michael@0 697 // doesn't expire things surprisingly soon), but not so recent that
michael@0 698 // "forget the last hour of stuff" deletes all freshly migrated data.
michael@0 699 let stmt;
michael@0 700 query = "UPDATE moz_formhistory " +
michael@0 701 "SET timesUsed = 1, firstUsed = :time, lastUsed = :time " +
michael@0 702 "WHERE timesUsed isnull OR firstUsed isnull or lastUsed isnull";
michael@0 703 let params = { time: (Date.now() - DAY_IN_MS) * 1000 }
michael@0 704 try {
michael@0 705 stmt = this.dbCreateStatement(query, params);
michael@0 706 stmt.execute();
michael@0 707 } catch (e) {
michael@0 708 this.log("Failed setting timestamps: " + e);
michael@0 709 throw e;
michael@0 710 } finally {
michael@0 711 if (stmt) {
michael@0 712 stmt.reset();
michael@0 713 }
michael@0 714 }
michael@0 715 },
michael@0 716
michael@0 717
michael@0 718 /*
michael@0 719 * dbMigrateToVersion2
michael@0 720 *
michael@0 721 * Updates the DB schema to v2 (bug 243136).
michael@0 722 * Adds lastUsed index, removes moz_dummy_table
michael@0 723 */
michael@0 724 dbMigrateToVersion2 : function () {
michael@0 725 let query = "DROP TABLE IF EXISTS moz_dummy_table";
michael@0 726 this.dbConnection.executeSimpleSQL(query);
michael@0 727
michael@0 728 query = "CREATE INDEX IF NOT EXISTS moz_formhistory_lastused_index ON moz_formhistory (lastUsed)";
michael@0 729 this.dbConnection.executeSimpleSQL(query);
michael@0 730 },
michael@0 731
michael@0 732
michael@0 733 /*
michael@0 734 * dbMigrateToVersion3
michael@0 735 *
michael@0 736 * Updates the DB schema to v3 (bug 506402).
michael@0 737 * Adds guid column and index.
michael@0 738 */
michael@0 739 dbMigrateToVersion3 : function () {
michael@0 740 // Check to see if GUID column already exists, add if needed
michael@0 741 let query;
michael@0 742 if (!this.dbColumnExists("guid")) {
michael@0 743 query = "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT";
michael@0 744 this.dbConnection.executeSimpleSQL(query);
michael@0 745
michael@0 746 query = "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index ON moz_formhistory (guid)";
michael@0 747 this.dbConnection.executeSimpleSQL(query);
michael@0 748 }
michael@0 749
michael@0 750 // Get a list of IDs for existing logins
michael@0 751 let ids = [];
michael@0 752 query = "SELECT id FROM moz_formhistory WHERE guid isnull";
michael@0 753 let stmt;
michael@0 754 try {
michael@0 755 stmt = this.dbCreateStatement(query);
michael@0 756 while (stmt.executeStep())
michael@0 757 ids.push(stmt.row.id);
michael@0 758 } catch (e) {
michael@0 759 this.log("Failed getting IDs: " + e);
michael@0 760 throw e;
michael@0 761 } finally {
michael@0 762 if (stmt) {
michael@0 763 stmt.reset();
michael@0 764 }
michael@0 765 }
michael@0 766
michael@0 767 // Generate a GUID for each login and update the DB.
michael@0 768 query = "UPDATE moz_formhistory SET guid = :guid WHERE id = :id";
michael@0 769 for each (let id in ids) {
michael@0 770 let params = {
michael@0 771 id : id,
michael@0 772 guid : this.generateGUID()
michael@0 773 };
michael@0 774
michael@0 775 try {
michael@0 776 stmt = this.dbCreateStatement(query, params);
michael@0 777 stmt.execute();
michael@0 778 } catch (e) {
michael@0 779 this.log("Failed setting GUID: " + e);
michael@0 780 throw e;
michael@0 781 } finally {
michael@0 782 if (stmt) {
michael@0 783 stmt.reset();
michael@0 784 }
michael@0 785 }
michael@0 786 }
michael@0 787 },
michael@0 788
michael@0 789 dbMigrateToVersion4 : function () {
michael@0 790 if (!this.dbConnection.tableExists("moz_deleted_formhistory")) {
michael@0 791 this.dbCreateTable("moz_deleted_formhistory", this.dbSchema.tables.moz_deleted_formhistory);
michael@0 792 }
michael@0 793 },
michael@0 794
michael@0 795 /*
michael@0 796 * dbAreExpectedColumnsPresent
michael@0 797 *
michael@0 798 * Sanity check to ensure that the columns this version of the code expects
michael@0 799 * are present in the DB we're using.
michael@0 800 */
michael@0 801 dbAreExpectedColumnsPresent : function () {
michael@0 802 for (let name in this.dbSchema.tables) {
michael@0 803 let table = this.dbSchema.tables[name];
michael@0 804 let query = "SELECT " +
michael@0 805 [col for (col in table)].join(", ") +
michael@0 806 " FROM " + name;
michael@0 807 try {
michael@0 808 let stmt = this.dbConnection.createStatement(query);
michael@0 809 // (no need to execute statement, if it compiled we're good)
michael@0 810 stmt.finalize();
michael@0 811 } catch (e) {
michael@0 812 return false;
michael@0 813 }
michael@0 814 }
michael@0 815
michael@0 816 this.log("verified that expected columns are present in DB.");
michael@0 817 return true;
michael@0 818 },
michael@0 819
michael@0 820
michael@0 821 /*
michael@0 822 * dbColumnExists
michael@0 823 *
michael@0 824 * Checks to see if the named column already exists.
michael@0 825 */
michael@0 826 dbColumnExists : function (columnName) {
michael@0 827 let query = "SELECT " + columnName + " FROM moz_formhistory";
michael@0 828 try {
michael@0 829 let stmt = this.dbConnection.createStatement(query);
michael@0 830 // (no need to execute statement, if it compiled we're good)
michael@0 831 stmt.finalize();
michael@0 832 return true;
michael@0 833 } catch (e) {
michael@0 834 return false;
michael@0 835 }
michael@0 836 },
michael@0 837
michael@0 838 /**
michael@0 839 * _dbClose
michael@0 840 *
michael@0 841 * Finalize all statements and close the connection.
michael@0 842 *
michael@0 843 * @param aBlocking - Should we spin the loop waiting for the db to be
michael@0 844 * closed.
michael@0 845 */
michael@0 846 _dbClose : function FH__dbClose(aBlocking) {
michael@0 847 for each (let stmt in this.dbStmts) {
michael@0 848 stmt.finalize();
michael@0 849 }
michael@0 850 this.dbStmts = {};
michael@0 851
michael@0 852 let connectionDescriptor = Object.getOwnPropertyDescriptor(FormHistory.prototype, "dbConnection");
michael@0 853 // Return if the database hasn't been opened.
michael@0 854 if (!connectionDescriptor || connectionDescriptor.value === undefined)
michael@0 855 return;
michael@0 856
michael@0 857 let completed = false;
michael@0 858 try {
michael@0 859 this.dbConnection.asyncClose(function () { completed = true; });
michael@0 860 } catch (e) {
michael@0 861 completed = true;
michael@0 862 Components.utils.reportError(e);
michael@0 863 }
michael@0 864
michael@0 865 let thread = Services.tm.currentThread;
michael@0 866 while (aBlocking && !completed) {
michael@0 867 thread.processNextEvent(true);
michael@0 868 }
michael@0 869 },
michael@0 870
michael@0 871 /*
michael@0 872 * dbCleanup
michael@0 873 *
michael@0 874 * Called when database creation fails. Finalizes database statements,
michael@0 875 * closes the database connection, deletes the database file.
michael@0 876 */
michael@0 877 dbCleanup : function () {
michael@0 878 this.log("Cleaning up DB file - close & remove & backup")
michael@0 879
michael@0 880 // Create backup file
michael@0 881 let storage = Cc["@mozilla.org/storage/service;1"].
michael@0 882 getService(Ci.mozIStorageService);
michael@0 883 let backupFile = this.dbFile.leafName + ".corrupt";
michael@0 884 storage.backupDatabaseFile(this.dbFile, backupFile);
michael@0 885
michael@0 886 this._dbClose(true);
michael@0 887 this.dbFile.remove(false);
michael@0 888 }
michael@0 889 };
michael@0 890
michael@0 891 let component = [FormHistory];
michael@0 892 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial