1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/satchel/nsFormHistory.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,892 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 + 1.9 +const Cc = Components.classes; 1.10 +const Ci = Components.interfaces; 1.11 +const Cr = Components.results; 1.12 + 1.13 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.14 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.15 + 1.16 +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", 1.17 + "resource://gre/modules/Deprecated.jsm"); 1.18 + 1.19 +const DB_VERSION = 4; 1.20 +const DAY_IN_MS = 86400000; // 1 day in milliseconds 1.21 + 1.22 +function FormHistory() { 1.23 + Deprecated.warning( 1.24 + "nsIFormHistory2 is deprecated and will be removed in a future version", 1.25 + "https://bugzilla.mozilla.org/show_bug.cgi?id=879118"); 1.26 + this.init(); 1.27 +} 1.28 + 1.29 +FormHistory.prototype = { 1.30 + classID : Components.ID("{0c1bb408-71a2-403f-854a-3a0659829ded}"), 1.31 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormHistory2, 1.32 + Ci.nsIObserver, 1.33 + Ci.nsIMessageListener, 1.34 + Ci.nsISupportsWeakReference, 1.35 + ]), 1.36 + 1.37 + debug : true, 1.38 + enabled : true, 1.39 + 1.40 + // The current database schema. 1.41 + dbSchema : { 1.42 + tables : { 1.43 + moz_formhistory: { 1.44 + "id" : "INTEGER PRIMARY KEY", 1.45 + "fieldname" : "TEXT NOT NULL", 1.46 + "value" : "TEXT NOT NULL", 1.47 + "timesUsed" : "INTEGER", 1.48 + "firstUsed" : "INTEGER", 1.49 + "lastUsed" : "INTEGER", 1.50 + "guid" : "TEXT" 1.51 + }, 1.52 + moz_deleted_formhistory: { 1.53 + "id" : "INTEGER PRIMARY KEY", 1.54 + "timeDeleted" : "INTEGER", 1.55 + "guid" : "TEXT" 1.56 + } 1.57 + }, 1.58 + indices : { 1.59 + moz_formhistory_index : { 1.60 + table : "moz_formhistory", 1.61 + columns : ["fieldname"] 1.62 + }, 1.63 + moz_formhistory_lastused_index : { 1.64 + table : "moz_formhistory", 1.65 + columns : ["lastUsed"] 1.66 + }, 1.67 + moz_formhistory_guid_index : { 1.68 + table : "moz_formhistory", 1.69 + columns : ["guid"] 1.70 + }, 1.71 + } 1.72 + }, 1.73 + dbStmts : null, // Database statements for memoization 1.74 + dbFile : null, 1.75 + 1.76 + _uuidService: null, 1.77 + get uuidService() { 1.78 + if (!this._uuidService) 1.79 + this._uuidService = Cc["@mozilla.org/uuid-generator;1"]. 1.80 + getService(Ci.nsIUUIDGenerator); 1.81 + return this._uuidService; 1.82 + }, 1.83 + 1.84 + log : function log(message) { 1.85 + if (!this.debug) 1.86 + return; 1.87 + dump("FormHistory: " + message + "\n"); 1.88 + Services.console.logStringMessage("FormHistory: " + message); 1.89 + }, 1.90 + 1.91 + 1.92 + init : function init() { 1.93 + this.updatePrefs(); 1.94 + 1.95 + this.dbStmts = {}; 1.96 + 1.97 + // Add observer 1.98 + Services.obs.addObserver(this, "profile-before-change", true); 1.99 + }, 1.100 + 1.101 + /* ---- nsIFormHistory2 interfaces ---- */ 1.102 + 1.103 + 1.104 + get hasEntries() { 1.105 + return (this.countAllEntries() > 0); 1.106 + }, 1.107 + 1.108 + 1.109 + addEntry : function addEntry(name, value) { 1.110 + if (!this.enabled) 1.111 + return; 1.112 + 1.113 + this.log("addEntry for " + name + "=" + value); 1.114 + 1.115 + let now = Date.now() * 1000; // microseconds 1.116 + 1.117 + let [id, guid] = this.getExistingEntryID(name, value); 1.118 + let stmt; 1.119 + 1.120 + if (id != -1) { 1.121 + // Update existing entry. 1.122 + let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE id = :id"; 1.123 + let params = { 1.124 + lastUsed : now, 1.125 + id : id 1.126 + }; 1.127 + 1.128 + try { 1.129 + stmt = this.dbCreateStatement(query, params); 1.130 + stmt.execute(); 1.131 + this.sendStringNotification("modifyEntry", name, value, guid); 1.132 + } catch (e) { 1.133 + this.log("addEntry (modify) failed: " + e); 1.134 + throw e; 1.135 + } finally { 1.136 + if (stmt) { 1.137 + stmt.reset(); 1.138 + } 1.139 + } 1.140 + 1.141 + } else { 1.142 + // Add new entry. 1.143 + guid = this.generateGUID(); 1.144 + 1.145 + let query = "INSERT INTO moz_formhistory (fieldname, value, timesUsed, firstUsed, lastUsed, guid) " + 1.146 + "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)"; 1.147 + let params = { 1.148 + fieldname : name, 1.149 + value : value, 1.150 + timesUsed : 1, 1.151 + firstUsed : now, 1.152 + lastUsed : now, 1.153 + guid : guid 1.154 + }; 1.155 + 1.156 + try { 1.157 + stmt = this.dbCreateStatement(query, params); 1.158 + stmt.execute(); 1.159 + this.sendStringNotification("addEntry", name, value, guid); 1.160 + } catch (e) { 1.161 + this.log("addEntry (create) failed: " + e); 1.162 + throw e; 1.163 + } finally { 1.164 + if (stmt) { 1.165 + stmt.reset(); 1.166 + } 1.167 + } 1.168 + } 1.169 + }, 1.170 + 1.171 + 1.172 + removeEntry : function removeEntry(name, value) { 1.173 + this.log("removeEntry for " + name + "=" + value); 1.174 + 1.175 + let [id, guid] = this.getExistingEntryID(name, value); 1.176 + this.sendStringNotification("before-removeEntry", name, value, guid); 1.177 + 1.178 + let stmt; 1.179 + let query = "DELETE FROM moz_formhistory WHERE id = :id"; 1.180 + let params = { id : id }; 1.181 + let existingTransactionInProgress; 1.182 + 1.183 + try { 1.184 + // Don't start a transaction if one is already in progress since we can't nest them. 1.185 + existingTransactionInProgress = this.dbConnection.transactionInProgress; 1.186 + if (!existingTransactionInProgress) 1.187 + this.dbConnection.beginTransaction(); 1.188 + this.moveToDeletedTable("VALUES (:guid, :timeDeleted)", { 1.189 + guid: guid, 1.190 + timeDeleted: Date.now() 1.191 + }); 1.192 + 1.193 + // remove from the formhistory database 1.194 + stmt = this.dbCreateStatement(query, params); 1.195 + stmt.execute(); 1.196 + this.sendStringNotification("removeEntry", name, value, guid); 1.197 + } catch (e) { 1.198 + if (!existingTransactionInProgress) 1.199 + this.dbConnection.rollbackTransaction(); 1.200 + this.log("removeEntry failed: " + e); 1.201 + throw e; 1.202 + } finally { 1.203 + if (stmt) { 1.204 + stmt.reset(); 1.205 + } 1.206 + } 1.207 + if (!existingTransactionInProgress) 1.208 + this.dbConnection.commitTransaction(); 1.209 + }, 1.210 + 1.211 + 1.212 + removeEntriesForName : function removeEntriesForName(name) { 1.213 + this.log("removeEntriesForName with name=" + name); 1.214 + 1.215 + this.sendStringNotification("before-removeEntriesForName", name); 1.216 + 1.217 + let stmt; 1.218 + let query = "DELETE FROM moz_formhistory WHERE fieldname = :fieldname"; 1.219 + let params = { fieldname : name }; 1.220 + let existingTransactionInProgress; 1.221 + 1.222 + try { 1.223 + // Don't start a transaction if one is already in progress since we can't nest them. 1.224 + existingTransactionInProgress = this.dbConnection.transactionInProgress; 1.225 + if (!existingTransactionInProgress) 1.226 + this.dbConnection.beginTransaction(); 1.227 + this.moveToDeletedTable( 1.228 + "SELECT guid, :timeDeleted FROM moz_formhistory " + 1.229 + "WHERE fieldname = :fieldname", { 1.230 + fieldname: name, 1.231 + timeDeleted: Date.now() 1.232 + }); 1.233 + 1.234 + stmt = this.dbCreateStatement(query, params); 1.235 + stmt.execute(); 1.236 + this.sendStringNotification("removeEntriesForName", name); 1.237 + } catch (e) { 1.238 + if (!existingTransactionInProgress) 1.239 + this.dbConnection.rollbackTransaction(); 1.240 + this.log("removeEntriesForName failed: " + e); 1.241 + throw e; 1.242 + } finally { 1.243 + if (stmt) { 1.244 + stmt.reset(); 1.245 + } 1.246 + } 1.247 + if (!existingTransactionInProgress) 1.248 + this.dbConnection.commitTransaction(); 1.249 + }, 1.250 + 1.251 + 1.252 + removeAllEntries : function removeAllEntries() { 1.253 + this.log("removeAllEntries"); 1.254 + 1.255 + this.sendNotification("before-removeAllEntries", null); 1.256 + 1.257 + let stmt; 1.258 + let query = "DELETE FROM moz_formhistory"; 1.259 + let existingTransactionInProgress; 1.260 + 1.261 + try { 1.262 + // Don't start a transaction if one is already in progress since we can't nest them. 1.263 + existingTransactionInProgress = this.dbConnection.transactionInProgress; 1.264 + if (!existingTransactionInProgress) 1.265 + this.dbConnection.beginTransaction(); 1.266 + // TODO: Add these items to the deleted items table once we've sorted 1.267 + // out the issues from bug 756701 1.268 + stmt = this.dbCreateStatement(query); 1.269 + stmt.execute(); 1.270 + this.sendNotification("removeAllEntries", null); 1.271 + } catch (e) { 1.272 + if (!existingTransactionInProgress) 1.273 + this.dbConnection.rollbackTransaction(); 1.274 + this.log("removeAllEntries failed: " + e); 1.275 + throw e; 1.276 + } finally { 1.277 + if (stmt) { 1.278 + stmt.reset(); 1.279 + } 1.280 + } 1.281 + if (!existingTransactionInProgress) 1.282 + this.dbConnection.commitTransaction(); 1.283 + }, 1.284 + 1.285 + 1.286 + nameExists : function nameExists(name) { 1.287 + this.log("nameExists for name=" + name); 1.288 + let stmt; 1.289 + let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory WHERE fieldname = :fieldname"; 1.290 + let params = { fieldname : name }; 1.291 + try { 1.292 + stmt = this.dbCreateStatement(query, params); 1.293 + stmt.executeStep(); 1.294 + return (stmt.row.numEntries > 0); 1.295 + } catch (e) { 1.296 + this.log("nameExists failed: " + e); 1.297 + throw e; 1.298 + } finally { 1.299 + if (stmt) { 1.300 + stmt.reset(); 1.301 + } 1.302 + } 1.303 + }, 1.304 + 1.305 + entryExists : function entryExists(name, value) { 1.306 + this.log("entryExists for " + name + "=" + value); 1.307 + let [id, guid] = this.getExistingEntryID(name, value); 1.308 + this.log("entryExists: id=" + id); 1.309 + return (id != -1); 1.310 + }, 1.311 + 1.312 + removeEntriesByTimeframe : function removeEntriesByTimeframe(beginTime, endTime) { 1.313 + this.log("removeEntriesByTimeframe for " + beginTime + " to " + endTime); 1.314 + 1.315 + this.sendIntNotification("before-removeEntriesByTimeframe", beginTime, endTime); 1.316 + 1.317 + let stmt; 1.318 + let query = "DELETE FROM moz_formhistory WHERE firstUsed >= :beginTime AND firstUsed <= :endTime"; 1.319 + let params = { 1.320 + beginTime : beginTime, 1.321 + endTime : endTime 1.322 + }; 1.323 + let existingTransactionInProgress; 1.324 + 1.325 + try { 1.326 + // Don't start a transaction if one is already in progress since we can't nest them. 1.327 + existingTransactionInProgress = this.dbConnection.transactionInProgress; 1.328 + if (!existingTransactionInProgress) 1.329 + this.dbConnection.beginTransaction(); 1.330 + this.moveToDeletedTable( 1.331 + "SELECT guid, :timeDeleted FROM moz_formhistory " + 1.332 + "WHERE firstUsed >= :beginTime AND firstUsed <= :endTime", { 1.333 + beginTime: beginTime, 1.334 + endTime: endTime 1.335 + }); 1.336 + 1.337 + stmt = this.dbCreateStatement(query, params); 1.338 + stmt.executeStep(); 1.339 + this.sendIntNotification("removeEntriesByTimeframe", beginTime, endTime); 1.340 + } catch (e) { 1.341 + if (!existingTransactionInProgress) 1.342 + this.dbConnection.rollbackTransaction(); 1.343 + this.log("removeEntriesByTimeframe failed: " + e); 1.344 + throw e; 1.345 + } finally { 1.346 + if (stmt) { 1.347 + stmt.reset(); 1.348 + } 1.349 + } 1.350 + if (!existingTransactionInProgress) 1.351 + this.dbConnection.commitTransaction(); 1.352 + }, 1.353 + 1.354 + moveToDeletedTable : function moveToDeletedTable(values, params) { 1.355 +#ifdef ANDROID 1.356 + this.log("Moving entries to deleted table."); 1.357 + 1.358 + let stmt; 1.359 + 1.360 + try { 1.361 + // Move the entries to the deleted items table. 1.362 + let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted) "; 1.363 + if (values) query += values; 1.364 + stmt = this.dbCreateStatement(query, params); 1.365 + stmt.execute(); 1.366 + } catch (e) { 1.367 + this.log("Moving deleted entries failed: " + e); 1.368 + throw e; 1.369 + } finally { 1.370 + if (stmt) { 1.371 + stmt.reset(); 1.372 + } 1.373 + } 1.374 +#endif 1.375 + }, 1.376 + 1.377 + get dbConnection() { 1.378 + // Make sure dbConnection can't be called from now to prevent infinite loops. 1.379 + delete FormHistory.prototype.dbConnection; 1.380 + 1.381 + try { 1.382 + this.dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone(); 1.383 + this.dbFile.append("formhistory.sqlite"); 1.384 + this.log("Opening database at " + this.dbFile.path); 1.385 + 1.386 + FormHistory.prototype.dbConnection = this.dbOpen(); 1.387 + this.dbInit(); 1.388 + } catch (e) { 1.389 + this.log("Initialization failed: " + e); 1.390 + // If dbInit fails... 1.391 + if (e.result == Cr.NS_ERROR_FILE_CORRUPTED) { 1.392 + this.dbCleanup(); 1.393 + FormHistory.prototype.dbConnection = this.dbOpen(); 1.394 + this.dbInit(); 1.395 + } else { 1.396 + throw "Initialization failed"; 1.397 + } 1.398 + } 1.399 + 1.400 + return FormHistory.prototype.dbConnection; 1.401 + }, 1.402 + 1.403 + get DBConnection() { 1.404 + return this.dbConnection; 1.405 + }, 1.406 + 1.407 + 1.408 + /* ---- nsIObserver interface ---- */ 1.409 + 1.410 + 1.411 + observe : function observe(subject, topic, data) { 1.412 + switch(topic) { 1.413 + case "nsPref:changed": 1.414 + this.updatePrefs(); 1.415 + break; 1.416 + case "profile-before-change": 1.417 + this._dbClose(false); 1.418 + break; 1.419 + default: 1.420 + this.log("Oops! Unexpected notification: " + topic); 1.421 + break; 1.422 + } 1.423 + }, 1.424 + 1.425 + 1.426 + /* ---- helpers ---- */ 1.427 + 1.428 + 1.429 + generateGUID : function() { 1.430 + // string like: "{f60d9eac-9421-4abc-8491-8e8322b063d4}" 1.431 + let uuid = this.uuidService.generateUUID().toString(); 1.432 + let raw = ""; // A string with the low bytes set to random values 1.433 + let bytes = 0; 1.434 + for (let i = 1; bytes < 12 ; i+= 2) { 1.435 + // Skip dashes 1.436 + if (uuid[i] == "-") 1.437 + i++; 1.438 + let hexVal = parseInt(uuid[i] + uuid[i + 1], 16); 1.439 + raw += String.fromCharCode(hexVal); 1.440 + bytes++; 1.441 + } 1.442 + return btoa(raw); 1.443 + }, 1.444 + 1.445 + 1.446 + sendStringNotification : function (changeType, str1, str2, str3) { 1.447 + function wrapit(str) { 1.448 + let wrapper = Cc["@mozilla.org/supports-string;1"]. 1.449 + createInstance(Ci.nsISupportsString); 1.450 + wrapper.data = str; 1.451 + return wrapper; 1.452 + } 1.453 + 1.454 + let strData; 1.455 + if (arguments.length == 2) { 1.456 + // Just 1 string, no need to put it in an array 1.457 + strData = wrapit(str1); 1.458 + } else { 1.459 + // 3 strings, put them in an array. 1.460 + strData = Cc["@mozilla.org/array;1"]. 1.461 + createInstance(Ci.nsIMutableArray); 1.462 + strData.appendElement(wrapit(str1), false); 1.463 + strData.appendElement(wrapit(str2), false); 1.464 + strData.appendElement(wrapit(str3), false); 1.465 + } 1.466 + this.sendNotification(changeType, strData); 1.467 + }, 1.468 + 1.469 + 1.470 + sendIntNotification : function (changeType, int1, int2) { 1.471 + function wrapit(int) { 1.472 + let wrapper = Cc["@mozilla.org/supports-PRInt64;1"]. 1.473 + createInstance(Ci.nsISupportsPRInt64); 1.474 + wrapper.data = int; 1.475 + return wrapper; 1.476 + } 1.477 + 1.478 + let intData; 1.479 + if (arguments.length == 2) { 1.480 + // Just 1 int, no need for an array 1.481 + intData = wrapit(int1); 1.482 + } else { 1.483 + // 2 ints, put them in an array. 1.484 + intData = Cc["@mozilla.org/array;1"]. 1.485 + createInstance(Ci.nsIMutableArray); 1.486 + intData.appendElement(wrapit(int1), false); 1.487 + intData.appendElement(wrapit(int2), false); 1.488 + } 1.489 + this.sendNotification(changeType, intData); 1.490 + }, 1.491 + 1.492 + 1.493 + sendNotification : function (changeType, data) { 1.494 + Services.obs.notifyObservers(data, "satchel-storage-changed", changeType); 1.495 + }, 1.496 + 1.497 + 1.498 + getExistingEntryID : function (name, value) { 1.499 + let id = -1, guid = null; 1.500 + let stmt; 1.501 + let query = "SELECT id, guid FROM moz_formhistory WHERE fieldname = :fieldname AND value = :value"; 1.502 + let params = { 1.503 + fieldname : name, 1.504 + value : value 1.505 + }; 1.506 + try { 1.507 + stmt = this.dbCreateStatement(query, params); 1.508 + if (stmt.executeStep()) { 1.509 + id = stmt.row.id; 1.510 + guid = stmt.row.guid; 1.511 + } 1.512 + } catch (e) { 1.513 + this.log("getExistingEntryID failed: " + e); 1.514 + throw e; 1.515 + } finally { 1.516 + if (stmt) { 1.517 + stmt.reset(); 1.518 + } 1.519 + } 1.520 + 1.521 + return [id, guid]; 1.522 + }, 1.523 + 1.524 + 1.525 + countAllEntries : function () { 1.526 + let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory"; 1.527 + 1.528 + let stmt, numEntries; 1.529 + try { 1.530 + stmt = this.dbCreateStatement(query, null); 1.531 + stmt.executeStep(); 1.532 + numEntries = stmt.row.numEntries; 1.533 + } catch (e) { 1.534 + this.log("countAllEntries failed: " + e); 1.535 + throw e; 1.536 + } finally { 1.537 + if (stmt) { 1.538 + stmt.reset(); 1.539 + } 1.540 + } 1.541 + 1.542 + this.log("countAllEntries: counted entries: " + numEntries); 1.543 + return numEntries; 1.544 + }, 1.545 + 1.546 + 1.547 + updatePrefs : function () { 1.548 + this.debug = Services.prefs.getBoolPref("browser.formfill.debug"); 1.549 + this.enabled = Services.prefs.getBoolPref("browser.formfill.enable"); 1.550 + }, 1.551 + 1.552 +//**************************************************************************// 1.553 + // Database Creation & Access 1.554 + 1.555 + /* 1.556 + * dbCreateStatement 1.557 + * 1.558 + * Creates a statement, wraps it, and then does parameter replacement 1.559 + * Will use memoization so that statements can be reused. 1.560 + */ 1.561 + dbCreateStatement : function (query, params) { 1.562 + let stmt = this.dbStmts[query]; 1.563 + // Memoize the statements 1.564 + if (!stmt) { 1.565 + this.log("Creating new statement for query: " + query); 1.566 + stmt = this.dbConnection.createStatement(query); 1.567 + this.dbStmts[query] = stmt; 1.568 + } 1.569 + // Replace parameters, must be done 1 at a time 1.570 + if (params) 1.571 + for (let i in params) 1.572 + stmt.params[i] = params[i]; 1.573 + return stmt; 1.574 + }, 1.575 + 1.576 + /* 1.577 + * dbOpen 1.578 + * 1.579 + * Open a connection with the database and returns it. 1.580 + * 1.581 + * @returns a db connection object. 1.582 + */ 1.583 + dbOpen : function () { 1.584 + this.log("Open Database"); 1.585 + 1.586 + let storage = Cc["@mozilla.org/storage/service;1"]. 1.587 + getService(Ci.mozIStorageService); 1.588 + return storage.openDatabase(this.dbFile); 1.589 + }, 1.590 + 1.591 + /* 1.592 + * dbInit 1.593 + * 1.594 + * Attempts to initialize the database. This creates the file if it doesn't 1.595 + * exist, performs any migrations, etc. 1.596 + */ 1.597 + dbInit : function () { 1.598 + this.log("Initializing Database"); 1.599 + 1.600 + let version = this.dbConnection.schemaVersion; 1.601 + 1.602 + // Note: Firefox 3 didn't set a schema value, so it started from 0. 1.603 + // So we can't depend on a simple version == 0 check 1.604 + if (version == 0 && !this.dbConnection.tableExists("moz_formhistory")) 1.605 + this.dbCreate(); 1.606 + else if (version != DB_VERSION) 1.607 + this.dbMigrate(version); 1.608 + }, 1.609 + 1.610 + 1.611 + dbCreate: function () { 1.612 + this.log("Creating DB -- tables"); 1.613 + for (let name in this.dbSchema.tables) { 1.614 + let table = this.dbSchema.tables[name]; 1.615 + this.dbCreateTable(name, table); 1.616 + } 1.617 + 1.618 + this.log("Creating DB -- indices"); 1.619 + for (let name in this.dbSchema.indices) { 1.620 + let index = this.dbSchema.indices[name]; 1.621 + let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table + 1.622 + "(" + index.columns.join(", ") + ")"; 1.623 + this.dbConnection.executeSimpleSQL(statement); 1.624 + } 1.625 + 1.626 + this.dbConnection.schemaVersion = DB_VERSION; 1.627 + }, 1.628 + 1.629 + dbCreateTable: function(name, table) { 1.630 + let tSQL = [[col, table[col]].join(" ") for (col in table)].join(", "); 1.631 + this.log("Creating table " + name + " with " + tSQL); 1.632 + this.dbConnection.createTable(name, tSQL); 1.633 + }, 1.634 + 1.635 + dbMigrate : function (oldVersion) { 1.636 + this.log("Attempting to migrate from version " + oldVersion); 1.637 + 1.638 + if (oldVersion > DB_VERSION) { 1.639 + this.log("Downgrading to version " + DB_VERSION); 1.640 + // User's DB is newer. Sanity check that our expected columns are 1.641 + // present, and if so mark the lower version and merrily continue 1.642 + // on. If the columns are borked, something is wrong so blow away 1.643 + // the DB and start from scratch. [Future incompatible upgrades 1.644 + // should swtich to a different table or file.] 1.645 + 1.646 + if (!this.dbAreExpectedColumnsPresent()) 1.647 + throw Components.Exception("DB is missing expected columns", 1.648 + Cr.NS_ERROR_FILE_CORRUPTED); 1.649 + 1.650 + // Change the stored version to the current version. If the user 1.651 + // runs the newer code again, it will see the lower version number 1.652 + // and re-upgrade (to fixup any entries the old code added). 1.653 + this.dbConnection.schemaVersion = DB_VERSION; 1.654 + return; 1.655 + } 1.656 + 1.657 + // Upgrade to newer version... 1.658 + 1.659 + this.dbConnection.beginTransaction(); 1.660 + 1.661 + try { 1.662 + for (let v = oldVersion + 1; v <= DB_VERSION; v++) { 1.663 + this.log("Upgrading to version " + v + "..."); 1.664 + let migrateFunction = "dbMigrateToVersion" + v; 1.665 + this[migrateFunction](); 1.666 + } 1.667 + } catch (e) { 1.668 + this.log("Migration failed: " + e); 1.669 + this.dbConnection.rollbackTransaction(); 1.670 + throw e; 1.671 + } 1.672 + 1.673 + this.dbConnection.schemaVersion = DB_VERSION; 1.674 + this.dbConnection.commitTransaction(); 1.675 + this.log("DB migration completed."); 1.676 + }, 1.677 + 1.678 + 1.679 + /* 1.680 + * dbMigrateToVersion1 1.681 + * 1.682 + * Updates the DB schema to v1 (bug 463154). 1.683 + * Adds firstUsed, lastUsed, timesUsed columns. 1.684 + */ 1.685 + dbMigrateToVersion1 : function () { 1.686 + // Check to see if the new columns already exist (could be a v1 DB that 1.687 + // was downgraded to v0). If they exist, we don't need to add them. 1.688 + let query; 1.689 + ["timesUsed", "firstUsed", "lastUsed"].forEach(function(column) { 1.690 + if (!this.dbColumnExists(column)) { 1.691 + query = "ALTER TABLE moz_formhistory ADD COLUMN " + column + " INTEGER"; 1.692 + this.dbConnection.executeSimpleSQL(query); 1.693 + } 1.694 + }, this); 1.695 + 1.696 + // Set the default values for the new columns. 1.697 + // 1.698 + // Note that we set the timestamps to 24 hours in the past. We want a 1.699 + // timestamp that's recent (so that "keep form history for 90 days" 1.700 + // doesn't expire things surprisingly soon), but not so recent that 1.701 + // "forget the last hour of stuff" deletes all freshly migrated data. 1.702 + let stmt; 1.703 + query = "UPDATE moz_formhistory " + 1.704 + "SET timesUsed = 1, firstUsed = :time, lastUsed = :time " + 1.705 + "WHERE timesUsed isnull OR firstUsed isnull or lastUsed isnull"; 1.706 + let params = { time: (Date.now() - DAY_IN_MS) * 1000 } 1.707 + try { 1.708 + stmt = this.dbCreateStatement(query, params); 1.709 + stmt.execute(); 1.710 + } catch (e) { 1.711 + this.log("Failed setting timestamps: " + e); 1.712 + throw e; 1.713 + } finally { 1.714 + if (stmt) { 1.715 + stmt.reset(); 1.716 + } 1.717 + } 1.718 + }, 1.719 + 1.720 + 1.721 + /* 1.722 + * dbMigrateToVersion2 1.723 + * 1.724 + * Updates the DB schema to v2 (bug 243136). 1.725 + * Adds lastUsed index, removes moz_dummy_table 1.726 + */ 1.727 + dbMigrateToVersion2 : function () { 1.728 + let query = "DROP TABLE IF EXISTS moz_dummy_table"; 1.729 + this.dbConnection.executeSimpleSQL(query); 1.730 + 1.731 + query = "CREATE INDEX IF NOT EXISTS moz_formhistory_lastused_index ON moz_formhistory (lastUsed)"; 1.732 + this.dbConnection.executeSimpleSQL(query); 1.733 + }, 1.734 + 1.735 + 1.736 + /* 1.737 + * dbMigrateToVersion3 1.738 + * 1.739 + * Updates the DB schema to v3 (bug 506402). 1.740 + * Adds guid column and index. 1.741 + */ 1.742 + dbMigrateToVersion3 : function () { 1.743 + // Check to see if GUID column already exists, add if needed 1.744 + let query; 1.745 + if (!this.dbColumnExists("guid")) { 1.746 + query = "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT"; 1.747 + this.dbConnection.executeSimpleSQL(query); 1.748 + 1.749 + query = "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index ON moz_formhistory (guid)"; 1.750 + this.dbConnection.executeSimpleSQL(query); 1.751 + } 1.752 + 1.753 + // Get a list of IDs for existing logins 1.754 + let ids = []; 1.755 + query = "SELECT id FROM moz_formhistory WHERE guid isnull"; 1.756 + let stmt; 1.757 + try { 1.758 + stmt = this.dbCreateStatement(query); 1.759 + while (stmt.executeStep()) 1.760 + ids.push(stmt.row.id); 1.761 + } catch (e) { 1.762 + this.log("Failed getting IDs: " + e); 1.763 + throw e; 1.764 + } finally { 1.765 + if (stmt) { 1.766 + stmt.reset(); 1.767 + } 1.768 + } 1.769 + 1.770 + // Generate a GUID for each login and update the DB. 1.771 + query = "UPDATE moz_formhistory SET guid = :guid WHERE id = :id"; 1.772 + for each (let id in ids) { 1.773 + let params = { 1.774 + id : id, 1.775 + guid : this.generateGUID() 1.776 + }; 1.777 + 1.778 + try { 1.779 + stmt = this.dbCreateStatement(query, params); 1.780 + stmt.execute(); 1.781 + } catch (e) { 1.782 + this.log("Failed setting GUID: " + e); 1.783 + throw e; 1.784 + } finally { 1.785 + if (stmt) { 1.786 + stmt.reset(); 1.787 + } 1.788 + } 1.789 + } 1.790 + }, 1.791 + 1.792 + dbMigrateToVersion4 : function () { 1.793 + if (!this.dbConnection.tableExists("moz_deleted_formhistory")) { 1.794 + this.dbCreateTable("moz_deleted_formhistory", this.dbSchema.tables.moz_deleted_formhistory); 1.795 + } 1.796 + }, 1.797 + 1.798 + /* 1.799 + * dbAreExpectedColumnsPresent 1.800 + * 1.801 + * Sanity check to ensure that the columns this version of the code expects 1.802 + * are present in the DB we're using. 1.803 + */ 1.804 + dbAreExpectedColumnsPresent : function () { 1.805 + for (let name in this.dbSchema.tables) { 1.806 + let table = this.dbSchema.tables[name]; 1.807 + let query = "SELECT " + 1.808 + [col for (col in table)].join(", ") + 1.809 + " FROM " + name; 1.810 + try { 1.811 + let stmt = this.dbConnection.createStatement(query); 1.812 + // (no need to execute statement, if it compiled we're good) 1.813 + stmt.finalize(); 1.814 + } catch (e) { 1.815 + return false; 1.816 + } 1.817 + } 1.818 + 1.819 + this.log("verified that expected columns are present in DB."); 1.820 + return true; 1.821 + }, 1.822 + 1.823 + 1.824 + /* 1.825 + * dbColumnExists 1.826 + * 1.827 + * Checks to see if the named column already exists. 1.828 + */ 1.829 + dbColumnExists : function (columnName) { 1.830 + let query = "SELECT " + columnName + " FROM moz_formhistory"; 1.831 + try { 1.832 + let stmt = this.dbConnection.createStatement(query); 1.833 + // (no need to execute statement, if it compiled we're good) 1.834 + stmt.finalize(); 1.835 + return true; 1.836 + } catch (e) { 1.837 + return false; 1.838 + } 1.839 + }, 1.840 + 1.841 + /** 1.842 + * _dbClose 1.843 + * 1.844 + * Finalize all statements and close the connection. 1.845 + * 1.846 + * @param aBlocking - Should we spin the loop waiting for the db to be 1.847 + * closed. 1.848 + */ 1.849 + _dbClose : function FH__dbClose(aBlocking) { 1.850 + for each (let stmt in this.dbStmts) { 1.851 + stmt.finalize(); 1.852 + } 1.853 + this.dbStmts = {}; 1.854 + 1.855 + let connectionDescriptor = Object.getOwnPropertyDescriptor(FormHistory.prototype, "dbConnection"); 1.856 + // Return if the database hasn't been opened. 1.857 + if (!connectionDescriptor || connectionDescriptor.value === undefined) 1.858 + return; 1.859 + 1.860 + let completed = false; 1.861 + try { 1.862 + this.dbConnection.asyncClose(function () { completed = true; }); 1.863 + } catch (e) { 1.864 + completed = true; 1.865 + Components.utils.reportError(e); 1.866 + } 1.867 + 1.868 + let thread = Services.tm.currentThread; 1.869 + while (aBlocking && !completed) { 1.870 + thread.processNextEvent(true); 1.871 + } 1.872 + }, 1.873 + 1.874 + /* 1.875 + * dbCleanup 1.876 + * 1.877 + * Called when database creation fails. Finalizes database statements, 1.878 + * closes the database connection, deletes the database file. 1.879 + */ 1.880 + dbCleanup : function () { 1.881 + this.log("Cleaning up DB file - close & remove & backup") 1.882 + 1.883 + // Create backup file 1.884 + let storage = Cc["@mozilla.org/storage/service;1"]. 1.885 + getService(Ci.mozIStorageService); 1.886 + let backupFile = this.dbFile.leafName + ".corrupt"; 1.887 + storage.backupDatabaseFile(this.dbFile, backupFile); 1.888 + 1.889 + this._dbClose(true); 1.890 + this.dbFile.remove(false); 1.891 + } 1.892 +}; 1.893 + 1.894 +let component = [FormHistory]; 1.895 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);