michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", michael@0: "resource://gre/modules/Deprecated.jsm"); michael@0: michael@0: const DB_VERSION = 4; michael@0: const DAY_IN_MS = 86400000; // 1 day in milliseconds michael@0: michael@0: function FormHistory() { michael@0: Deprecated.warning( michael@0: "nsIFormHistory2 is deprecated and will be removed in a future version", michael@0: "https://bugzilla.mozilla.org/show_bug.cgi?id=879118"); michael@0: this.init(); michael@0: } michael@0: michael@0: FormHistory.prototype = { michael@0: classID : Components.ID("{0c1bb408-71a2-403f-854a-3a0659829ded}"), michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormHistory2, michael@0: Ci.nsIObserver, michael@0: Ci.nsIMessageListener, michael@0: Ci.nsISupportsWeakReference, michael@0: ]), michael@0: michael@0: debug : true, michael@0: enabled : true, michael@0: michael@0: // The current database schema. michael@0: dbSchema : { michael@0: tables : { michael@0: moz_formhistory: { michael@0: "id" : "INTEGER PRIMARY KEY", michael@0: "fieldname" : "TEXT NOT NULL", michael@0: "value" : "TEXT NOT NULL", michael@0: "timesUsed" : "INTEGER", michael@0: "firstUsed" : "INTEGER", michael@0: "lastUsed" : "INTEGER", michael@0: "guid" : "TEXT" michael@0: }, michael@0: moz_deleted_formhistory: { michael@0: "id" : "INTEGER PRIMARY KEY", michael@0: "timeDeleted" : "INTEGER", michael@0: "guid" : "TEXT" michael@0: } michael@0: }, michael@0: indices : { michael@0: moz_formhistory_index : { michael@0: table : "moz_formhistory", michael@0: columns : ["fieldname"] michael@0: }, michael@0: moz_formhistory_lastused_index : { michael@0: table : "moz_formhistory", michael@0: columns : ["lastUsed"] michael@0: }, michael@0: moz_formhistory_guid_index : { michael@0: table : "moz_formhistory", michael@0: columns : ["guid"] michael@0: }, michael@0: } michael@0: }, michael@0: dbStmts : null, // Database statements for memoization michael@0: dbFile : null, michael@0: michael@0: _uuidService: null, michael@0: get uuidService() { michael@0: if (!this._uuidService) michael@0: this._uuidService = Cc["@mozilla.org/uuid-generator;1"]. michael@0: getService(Ci.nsIUUIDGenerator); michael@0: return this._uuidService; michael@0: }, michael@0: michael@0: log : function log(message) { michael@0: if (!this.debug) michael@0: return; michael@0: dump("FormHistory: " + message + "\n"); michael@0: Services.console.logStringMessage("FormHistory: " + message); michael@0: }, michael@0: michael@0: michael@0: init : function init() { michael@0: this.updatePrefs(); michael@0: michael@0: this.dbStmts = {}; michael@0: michael@0: // Add observer michael@0: Services.obs.addObserver(this, "profile-before-change", true); michael@0: }, michael@0: michael@0: /* ---- nsIFormHistory2 interfaces ---- */ michael@0: michael@0: michael@0: get hasEntries() { michael@0: return (this.countAllEntries() > 0); michael@0: }, michael@0: michael@0: michael@0: addEntry : function addEntry(name, value) { michael@0: if (!this.enabled) michael@0: return; michael@0: michael@0: this.log("addEntry for " + name + "=" + value); michael@0: michael@0: let now = Date.now() * 1000; // microseconds michael@0: michael@0: let [id, guid] = this.getExistingEntryID(name, value); michael@0: let stmt; michael@0: michael@0: if (id != -1) { michael@0: // Update existing entry. michael@0: let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE id = :id"; michael@0: let params = { michael@0: lastUsed : now, michael@0: id : id michael@0: }; michael@0: michael@0: try { michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: this.sendStringNotification("modifyEntry", name, value, guid); michael@0: } catch (e) { michael@0: this.log("addEntry (modify) failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: } else { michael@0: // Add new entry. michael@0: guid = this.generateGUID(); michael@0: michael@0: let query = "INSERT INTO moz_formhistory (fieldname, value, timesUsed, firstUsed, lastUsed, guid) " + michael@0: "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)"; michael@0: let params = { michael@0: fieldname : name, michael@0: value : value, michael@0: timesUsed : 1, michael@0: firstUsed : now, michael@0: lastUsed : now, michael@0: guid : guid michael@0: }; michael@0: michael@0: try { michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: this.sendStringNotification("addEntry", name, value, guid); michael@0: } catch (e) { michael@0: this.log("addEntry (create) failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: removeEntry : function removeEntry(name, value) { michael@0: this.log("removeEntry for " + name + "=" + value); michael@0: michael@0: let [id, guid] = this.getExistingEntryID(name, value); michael@0: this.sendStringNotification("before-removeEntry", name, value, guid); michael@0: michael@0: let stmt; michael@0: let query = "DELETE FROM moz_formhistory WHERE id = :id"; michael@0: let params = { id : id }; michael@0: let existingTransactionInProgress; michael@0: michael@0: try { michael@0: // Don't start a transaction if one is already in progress since we can't nest them. michael@0: existingTransactionInProgress = this.dbConnection.transactionInProgress; michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.beginTransaction(); michael@0: this.moveToDeletedTable("VALUES (:guid, :timeDeleted)", { michael@0: guid: guid, michael@0: timeDeleted: Date.now() michael@0: }); michael@0: michael@0: // remove from the formhistory database michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: this.sendStringNotification("removeEntry", name, value, guid); michael@0: } catch (e) { michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.rollbackTransaction(); michael@0: this.log("removeEntry failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.commitTransaction(); michael@0: }, michael@0: michael@0: michael@0: removeEntriesForName : function removeEntriesForName(name) { michael@0: this.log("removeEntriesForName with name=" + name); michael@0: michael@0: this.sendStringNotification("before-removeEntriesForName", name); michael@0: michael@0: let stmt; michael@0: let query = "DELETE FROM moz_formhistory WHERE fieldname = :fieldname"; michael@0: let params = { fieldname : name }; michael@0: let existingTransactionInProgress; michael@0: michael@0: try { michael@0: // Don't start a transaction if one is already in progress since we can't nest them. michael@0: existingTransactionInProgress = this.dbConnection.transactionInProgress; michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.beginTransaction(); michael@0: this.moveToDeletedTable( michael@0: "SELECT guid, :timeDeleted FROM moz_formhistory " + michael@0: "WHERE fieldname = :fieldname", { michael@0: fieldname: name, michael@0: timeDeleted: Date.now() michael@0: }); michael@0: michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: this.sendStringNotification("removeEntriesForName", name); michael@0: } catch (e) { michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.rollbackTransaction(); michael@0: this.log("removeEntriesForName failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.commitTransaction(); michael@0: }, michael@0: michael@0: michael@0: removeAllEntries : function removeAllEntries() { michael@0: this.log("removeAllEntries"); michael@0: michael@0: this.sendNotification("before-removeAllEntries", null); michael@0: michael@0: let stmt; michael@0: let query = "DELETE FROM moz_formhistory"; michael@0: let existingTransactionInProgress; michael@0: michael@0: try { michael@0: // Don't start a transaction if one is already in progress since we can't nest them. michael@0: existingTransactionInProgress = this.dbConnection.transactionInProgress; michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.beginTransaction(); michael@0: // TODO: Add these items to the deleted items table once we've sorted michael@0: // out the issues from bug 756701 michael@0: stmt = this.dbCreateStatement(query); michael@0: stmt.execute(); michael@0: this.sendNotification("removeAllEntries", null); michael@0: } catch (e) { michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.rollbackTransaction(); michael@0: this.log("removeAllEntries failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.commitTransaction(); michael@0: }, michael@0: michael@0: michael@0: nameExists : function nameExists(name) { michael@0: this.log("nameExists for name=" + name); michael@0: let stmt; michael@0: let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory WHERE fieldname = :fieldname"; michael@0: let params = { fieldname : name }; michael@0: try { michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.executeStep(); michael@0: return (stmt.row.numEntries > 0); michael@0: } catch (e) { michael@0: this.log("nameExists failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: entryExists : function entryExists(name, value) { michael@0: this.log("entryExists for " + name + "=" + value); michael@0: let [id, guid] = this.getExistingEntryID(name, value); michael@0: this.log("entryExists: id=" + id); michael@0: return (id != -1); michael@0: }, michael@0: michael@0: removeEntriesByTimeframe : function removeEntriesByTimeframe(beginTime, endTime) { michael@0: this.log("removeEntriesByTimeframe for " + beginTime + " to " + endTime); michael@0: michael@0: this.sendIntNotification("before-removeEntriesByTimeframe", beginTime, endTime); michael@0: michael@0: let stmt; michael@0: let query = "DELETE FROM moz_formhistory WHERE firstUsed >= :beginTime AND firstUsed <= :endTime"; michael@0: let params = { michael@0: beginTime : beginTime, michael@0: endTime : endTime michael@0: }; michael@0: let existingTransactionInProgress; michael@0: michael@0: try { michael@0: // Don't start a transaction if one is already in progress since we can't nest them. michael@0: existingTransactionInProgress = this.dbConnection.transactionInProgress; michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.beginTransaction(); michael@0: this.moveToDeletedTable( michael@0: "SELECT guid, :timeDeleted FROM moz_formhistory " + michael@0: "WHERE firstUsed >= :beginTime AND firstUsed <= :endTime", { michael@0: beginTime: beginTime, michael@0: endTime: endTime michael@0: }); michael@0: michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.executeStep(); michael@0: this.sendIntNotification("removeEntriesByTimeframe", beginTime, endTime); michael@0: } catch (e) { michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.rollbackTransaction(); michael@0: this.log("removeEntriesByTimeframe failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: if (!existingTransactionInProgress) michael@0: this.dbConnection.commitTransaction(); michael@0: }, michael@0: michael@0: moveToDeletedTable : function moveToDeletedTable(values, params) { michael@0: #ifdef ANDROID michael@0: this.log("Moving entries to deleted table."); michael@0: michael@0: let stmt; michael@0: michael@0: try { michael@0: // Move the entries to the deleted items table. michael@0: let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted) "; michael@0: if (values) query += values; michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("Moving deleted entries failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: #endif michael@0: }, michael@0: michael@0: get dbConnection() { michael@0: // Make sure dbConnection can't be called from now to prevent infinite loops. michael@0: delete FormHistory.prototype.dbConnection; michael@0: michael@0: try { michael@0: this.dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone(); michael@0: this.dbFile.append("formhistory.sqlite"); michael@0: this.log("Opening database at " + this.dbFile.path); michael@0: michael@0: FormHistory.prototype.dbConnection = this.dbOpen(); michael@0: this.dbInit(); michael@0: } catch (e) { michael@0: this.log("Initialization failed: " + e); michael@0: // If dbInit fails... michael@0: if (e.result == Cr.NS_ERROR_FILE_CORRUPTED) { michael@0: this.dbCleanup(); michael@0: FormHistory.prototype.dbConnection = this.dbOpen(); michael@0: this.dbInit(); michael@0: } else { michael@0: throw "Initialization failed"; michael@0: } michael@0: } michael@0: michael@0: return FormHistory.prototype.dbConnection; michael@0: }, michael@0: michael@0: get DBConnection() { michael@0: return this.dbConnection; michael@0: }, michael@0: michael@0: michael@0: /* ---- nsIObserver interface ---- */ michael@0: michael@0: michael@0: observe : function observe(subject, topic, data) { michael@0: switch(topic) { michael@0: case "nsPref:changed": michael@0: this.updatePrefs(); michael@0: break; michael@0: case "profile-before-change": michael@0: this._dbClose(false); michael@0: break; michael@0: default: michael@0: this.log("Oops! Unexpected notification: " + topic); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* ---- helpers ---- */ michael@0: michael@0: michael@0: generateGUID : function() { michael@0: // string like: "{f60d9eac-9421-4abc-8491-8e8322b063d4}" michael@0: let uuid = this.uuidService.generateUUID().toString(); michael@0: let raw = ""; // A string with the low bytes set to random values michael@0: let bytes = 0; michael@0: for (let i = 1; bytes < 12 ; i+= 2) { michael@0: // Skip dashes michael@0: if (uuid[i] == "-") michael@0: i++; michael@0: let hexVal = parseInt(uuid[i] + uuid[i + 1], 16); michael@0: raw += String.fromCharCode(hexVal); michael@0: bytes++; michael@0: } michael@0: return btoa(raw); michael@0: }, michael@0: michael@0: michael@0: sendStringNotification : function (changeType, str1, str2, str3) { michael@0: function wrapit(str) { michael@0: let wrapper = Cc["@mozilla.org/supports-string;1"]. michael@0: createInstance(Ci.nsISupportsString); michael@0: wrapper.data = str; michael@0: return wrapper; michael@0: } michael@0: michael@0: let strData; michael@0: if (arguments.length == 2) { michael@0: // Just 1 string, no need to put it in an array michael@0: strData = wrapit(str1); michael@0: } else { michael@0: // 3 strings, put them in an array. michael@0: strData = Cc["@mozilla.org/array;1"]. michael@0: createInstance(Ci.nsIMutableArray); michael@0: strData.appendElement(wrapit(str1), false); michael@0: strData.appendElement(wrapit(str2), false); michael@0: strData.appendElement(wrapit(str3), false); michael@0: } michael@0: this.sendNotification(changeType, strData); michael@0: }, michael@0: michael@0: michael@0: sendIntNotification : function (changeType, int1, int2) { michael@0: function wrapit(int) { michael@0: let wrapper = Cc["@mozilla.org/supports-PRInt64;1"]. michael@0: createInstance(Ci.nsISupportsPRInt64); michael@0: wrapper.data = int; michael@0: return wrapper; michael@0: } michael@0: michael@0: let intData; michael@0: if (arguments.length == 2) { michael@0: // Just 1 int, no need for an array michael@0: intData = wrapit(int1); michael@0: } else { michael@0: // 2 ints, put them in an array. michael@0: intData = Cc["@mozilla.org/array;1"]. michael@0: createInstance(Ci.nsIMutableArray); michael@0: intData.appendElement(wrapit(int1), false); michael@0: intData.appendElement(wrapit(int2), false); michael@0: } michael@0: this.sendNotification(changeType, intData); michael@0: }, michael@0: michael@0: michael@0: sendNotification : function (changeType, data) { michael@0: Services.obs.notifyObservers(data, "satchel-storage-changed", changeType); michael@0: }, michael@0: michael@0: michael@0: getExistingEntryID : function (name, value) { michael@0: let id = -1, guid = null; michael@0: let stmt; michael@0: let query = "SELECT id, guid FROM moz_formhistory WHERE fieldname = :fieldname AND value = :value"; michael@0: let params = { michael@0: fieldname : name, michael@0: value : value michael@0: }; michael@0: try { michael@0: stmt = this.dbCreateStatement(query, params); michael@0: if (stmt.executeStep()) { michael@0: id = stmt.row.id; michael@0: guid = stmt.row.guid; michael@0: } michael@0: } catch (e) { michael@0: this.log("getExistingEntryID failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: return [id, guid]; michael@0: }, michael@0: michael@0: michael@0: countAllEntries : function () { michael@0: let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory"; michael@0: michael@0: let stmt, numEntries; michael@0: try { michael@0: stmt = this.dbCreateStatement(query, null); michael@0: stmt.executeStep(); michael@0: numEntries = stmt.row.numEntries; michael@0: } catch (e) { michael@0: this.log("countAllEntries failed: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: this.log("countAllEntries: counted entries: " + numEntries); michael@0: return numEntries; michael@0: }, michael@0: michael@0: michael@0: updatePrefs : function () { michael@0: this.debug = Services.prefs.getBoolPref("browser.formfill.debug"); michael@0: this.enabled = Services.prefs.getBoolPref("browser.formfill.enable"); michael@0: }, michael@0: michael@0: //**************************************************************************// michael@0: // Database Creation & Access michael@0: michael@0: /* michael@0: * dbCreateStatement michael@0: * michael@0: * Creates a statement, wraps it, and then does parameter replacement michael@0: * Will use memoization so that statements can be reused. michael@0: */ michael@0: dbCreateStatement : function (query, params) { michael@0: let stmt = this.dbStmts[query]; michael@0: // Memoize the statements michael@0: if (!stmt) { michael@0: this.log("Creating new statement for query: " + query); michael@0: stmt = this.dbConnection.createStatement(query); michael@0: this.dbStmts[query] = stmt; michael@0: } michael@0: // Replace parameters, must be done 1 at a time michael@0: if (params) michael@0: for (let i in params) michael@0: stmt.params[i] = params[i]; michael@0: return stmt; michael@0: }, michael@0: michael@0: /* michael@0: * dbOpen michael@0: * michael@0: * Open a connection with the database and returns it. michael@0: * michael@0: * @returns a db connection object. michael@0: */ michael@0: dbOpen : function () { michael@0: this.log("Open Database"); michael@0: michael@0: let storage = Cc["@mozilla.org/storage/service;1"]. michael@0: getService(Ci.mozIStorageService); michael@0: return storage.openDatabase(this.dbFile); michael@0: }, michael@0: michael@0: /* michael@0: * dbInit michael@0: * michael@0: * Attempts to initialize the database. This creates the file if it doesn't michael@0: * exist, performs any migrations, etc. michael@0: */ michael@0: dbInit : function () { michael@0: this.log("Initializing Database"); michael@0: michael@0: let version = this.dbConnection.schemaVersion; michael@0: michael@0: // Note: Firefox 3 didn't set a schema value, so it started from 0. michael@0: // So we can't depend on a simple version == 0 check michael@0: if (version == 0 && !this.dbConnection.tableExists("moz_formhistory")) michael@0: this.dbCreate(); michael@0: else if (version != DB_VERSION) michael@0: this.dbMigrate(version); michael@0: }, michael@0: michael@0: michael@0: dbCreate: function () { michael@0: this.log("Creating DB -- tables"); michael@0: for (let name in this.dbSchema.tables) { michael@0: let table = this.dbSchema.tables[name]; michael@0: this.dbCreateTable(name, table); michael@0: } michael@0: michael@0: this.log("Creating DB -- indices"); michael@0: for (let name in this.dbSchema.indices) { michael@0: let index = this.dbSchema.indices[name]; michael@0: let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table + michael@0: "(" + index.columns.join(", ") + ")"; michael@0: this.dbConnection.executeSimpleSQL(statement); michael@0: } michael@0: michael@0: this.dbConnection.schemaVersion = DB_VERSION; michael@0: }, michael@0: michael@0: dbCreateTable: function(name, table) { michael@0: let tSQL = [[col, table[col]].join(" ") for (col in table)].join(", "); michael@0: this.log("Creating table " + name + " with " + tSQL); michael@0: this.dbConnection.createTable(name, tSQL); michael@0: }, michael@0: michael@0: dbMigrate : function (oldVersion) { michael@0: this.log("Attempting to migrate from version " + oldVersion); michael@0: michael@0: if (oldVersion > DB_VERSION) { michael@0: this.log("Downgrading to version " + DB_VERSION); michael@0: // User's DB is newer. Sanity check that our expected columns are michael@0: // present, and if so mark the lower version and merrily continue michael@0: // on. If the columns are borked, something is wrong so blow away michael@0: // the DB and start from scratch. [Future incompatible upgrades michael@0: // should swtich to a different table or file.] michael@0: michael@0: if (!this.dbAreExpectedColumnsPresent()) michael@0: throw Components.Exception("DB is missing expected columns", michael@0: Cr.NS_ERROR_FILE_CORRUPTED); michael@0: michael@0: // Change the stored version to the current version. If the user michael@0: // runs the newer code again, it will see the lower version number michael@0: // and re-upgrade (to fixup any entries the old code added). michael@0: this.dbConnection.schemaVersion = DB_VERSION; michael@0: return; michael@0: } michael@0: michael@0: // Upgrade to newer version... michael@0: michael@0: this.dbConnection.beginTransaction(); michael@0: michael@0: try { michael@0: for (let v = oldVersion + 1; v <= DB_VERSION; v++) { michael@0: this.log("Upgrading to version " + v + "..."); michael@0: let migrateFunction = "dbMigrateToVersion" + v; michael@0: this[migrateFunction](); michael@0: } michael@0: } catch (e) { michael@0: this.log("Migration failed: " + e); michael@0: this.dbConnection.rollbackTransaction(); michael@0: throw e; michael@0: } michael@0: michael@0: this.dbConnection.schemaVersion = DB_VERSION; michael@0: this.dbConnection.commitTransaction(); michael@0: this.log("DB migration completed."); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * dbMigrateToVersion1 michael@0: * michael@0: * Updates the DB schema to v1 (bug 463154). michael@0: * Adds firstUsed, lastUsed, timesUsed columns. michael@0: */ michael@0: dbMigrateToVersion1 : function () { michael@0: // Check to see if the new columns already exist (could be a v1 DB that michael@0: // was downgraded to v0). If they exist, we don't need to add them. michael@0: let query; michael@0: ["timesUsed", "firstUsed", "lastUsed"].forEach(function(column) { michael@0: if (!this.dbColumnExists(column)) { michael@0: query = "ALTER TABLE moz_formhistory ADD COLUMN " + column + " INTEGER"; michael@0: this.dbConnection.executeSimpleSQL(query); michael@0: } michael@0: }, this); michael@0: michael@0: // Set the default values for the new columns. michael@0: // michael@0: // Note that we set the timestamps to 24 hours in the past. We want a michael@0: // timestamp that's recent (so that "keep form history for 90 days" michael@0: // doesn't expire things surprisingly soon), but not so recent that michael@0: // "forget the last hour of stuff" deletes all freshly migrated data. michael@0: let stmt; michael@0: query = "UPDATE moz_formhistory " + michael@0: "SET timesUsed = 1, firstUsed = :time, lastUsed = :time " + michael@0: "WHERE timesUsed isnull OR firstUsed isnull or lastUsed isnull"; michael@0: let params = { time: (Date.now() - DAY_IN_MS) * 1000 } michael@0: try { michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("Failed setting timestamps: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * dbMigrateToVersion2 michael@0: * michael@0: * Updates the DB schema to v2 (bug 243136). michael@0: * Adds lastUsed index, removes moz_dummy_table michael@0: */ michael@0: dbMigrateToVersion2 : function () { michael@0: let query = "DROP TABLE IF EXISTS moz_dummy_table"; michael@0: this.dbConnection.executeSimpleSQL(query); michael@0: michael@0: query = "CREATE INDEX IF NOT EXISTS moz_formhistory_lastused_index ON moz_formhistory (lastUsed)"; michael@0: this.dbConnection.executeSimpleSQL(query); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * dbMigrateToVersion3 michael@0: * michael@0: * Updates the DB schema to v3 (bug 506402). michael@0: * Adds guid column and index. michael@0: */ michael@0: dbMigrateToVersion3 : function () { michael@0: // Check to see if GUID column already exists, add if needed michael@0: let query; michael@0: if (!this.dbColumnExists("guid")) { michael@0: query = "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT"; michael@0: this.dbConnection.executeSimpleSQL(query); michael@0: michael@0: query = "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index ON moz_formhistory (guid)"; michael@0: this.dbConnection.executeSimpleSQL(query); michael@0: } michael@0: michael@0: // Get a list of IDs for existing logins michael@0: let ids = []; michael@0: query = "SELECT id FROM moz_formhistory WHERE guid isnull"; michael@0: let stmt; michael@0: try { michael@0: stmt = this.dbCreateStatement(query); michael@0: while (stmt.executeStep()) michael@0: ids.push(stmt.row.id); michael@0: } catch (e) { michael@0: this.log("Failed getting IDs: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: michael@0: // Generate a GUID for each login and update the DB. michael@0: query = "UPDATE moz_formhistory SET guid = :guid WHERE id = :id"; michael@0: for each (let id in ids) { michael@0: let params = { michael@0: id : id, michael@0: guid : this.generateGUID() michael@0: }; michael@0: michael@0: try { michael@0: stmt = this.dbCreateStatement(query, params); michael@0: stmt.execute(); michael@0: } catch (e) { michael@0: this.log("Failed setting GUID: " + e); michael@0: throw e; michael@0: } finally { michael@0: if (stmt) { michael@0: stmt.reset(); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: dbMigrateToVersion4 : function () { michael@0: if (!this.dbConnection.tableExists("moz_deleted_formhistory")) { michael@0: this.dbCreateTable("moz_deleted_formhistory", this.dbSchema.tables.moz_deleted_formhistory); michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * dbAreExpectedColumnsPresent michael@0: * michael@0: * Sanity check to ensure that the columns this version of the code expects michael@0: * are present in the DB we're using. michael@0: */ michael@0: dbAreExpectedColumnsPresent : function () { michael@0: for (let name in this.dbSchema.tables) { michael@0: let table = this.dbSchema.tables[name]; michael@0: let query = "SELECT " + michael@0: [col for (col in table)].join(", ") + michael@0: " FROM " + name; michael@0: try { michael@0: let stmt = this.dbConnection.createStatement(query); michael@0: // (no need to execute statement, if it compiled we're good) michael@0: stmt.finalize(); michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: this.log("verified that expected columns are present in DB."); michael@0: return true; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * dbColumnExists michael@0: * michael@0: * Checks to see if the named column already exists. michael@0: */ michael@0: dbColumnExists : function (columnName) { michael@0: let query = "SELECT " + columnName + " FROM moz_formhistory"; michael@0: try { michael@0: let stmt = this.dbConnection.createStatement(query); michael@0: // (no need to execute statement, if it compiled we're good) michael@0: stmt.finalize(); michael@0: return true; michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * _dbClose michael@0: * michael@0: * Finalize all statements and close the connection. michael@0: * michael@0: * @param aBlocking - Should we spin the loop waiting for the db to be michael@0: * closed. michael@0: */ michael@0: _dbClose : function FH__dbClose(aBlocking) { michael@0: for each (let stmt in this.dbStmts) { michael@0: stmt.finalize(); michael@0: } michael@0: this.dbStmts = {}; michael@0: michael@0: let connectionDescriptor = Object.getOwnPropertyDescriptor(FormHistory.prototype, "dbConnection"); michael@0: // Return if the database hasn't been opened. michael@0: if (!connectionDescriptor || connectionDescriptor.value === undefined) michael@0: return; michael@0: michael@0: let completed = false; michael@0: try { michael@0: this.dbConnection.asyncClose(function () { completed = true; }); michael@0: } catch (e) { michael@0: completed = true; michael@0: Components.utils.reportError(e); michael@0: } michael@0: michael@0: let thread = Services.tm.currentThread; michael@0: while (aBlocking && !completed) { michael@0: thread.processNextEvent(true); michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * dbCleanup michael@0: * michael@0: * Called when database creation fails. Finalizes database statements, michael@0: * closes the database connection, deletes the database file. michael@0: */ michael@0: dbCleanup : function () { michael@0: this.log("Cleaning up DB file - close & remove & backup") michael@0: michael@0: // Create backup file michael@0: let storage = Cc["@mozilla.org/storage/service;1"]. michael@0: getService(Ci.mozIStorageService); michael@0: let backupFile = this.dbFile.leafName + ".corrupt"; michael@0: storage.backupDatabaseFile(this.dbFile, backupFile); michael@0: michael@0: this._dbClose(true); michael@0: this.dbFile.remove(false); michael@0: } michael@0: }; michael@0: michael@0: let component = [FormHistory]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);