toolkit/components/satchel/nsFormHistory.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

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

mercurial