toolkit/components/satchel/FormHistory.jsm

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

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

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

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /**
michael@0 6 * FormHistory
michael@0 7 *
michael@0 8 * Used to store values that have been entered into forms which may later
michael@0 9 * be used to automatically fill in the values when the form is visited again.
michael@0 10 *
michael@0 11 * search(terms, queryData, callback)
michael@0 12 * Look up values that have been previously stored.
michael@0 13 * terms - array of terms to return data for
michael@0 14 * queryData - object that contains the query terms
michael@0 15 * The query object contains properties for each search criteria to match, where the value
michael@0 16 * of the property specifies the value that term must have. For example,
michael@0 17 * { term1: value1, term2: value2 }
michael@0 18 * callback - callback that is called when results are available or an error occurs.
michael@0 19 * The callback is passed a result array containing each found entry. Each element in
michael@0 20 * the array is an object containing a property for each search term specified by 'terms'.
michael@0 21 * count(queryData, callback)
michael@0 22 * Find the number of stored entries that match the given criteria.
michael@0 23 * queryData - array of objects that indicate the query. See the search method for details.
michael@0 24 * callback - callback that is called when results are available or an error occurs.
michael@0 25 * The callback is passed the number of found entries.
michael@0 26 * update(changes, callback)
michael@0 27 * Write data to form history storage.
michael@0 28 * changes - an array of changes to be made. If only one change is to be made, it
michael@0 29 * may be passed as an object rather than a one-element array.
michael@0 30 * Each change object is of the form:
michael@0 31 * { op: operation, term1: value1, term2: value2, ... }
michael@0 32 * Valid operations are:
michael@0 33 * add - add a new entry
michael@0 34 * update - update an existing entry
michael@0 35 * remove - remove an entry
michael@0 36 * bump - update the last accessed time on an entry
michael@0 37 * The terms specified allow matching of one or more specific entries. If no terms
michael@0 38 * are specified then all entries are matched. This means that { op: "remove" } is
michael@0 39 * used to remove all entries and clear the form history.
michael@0 40 * callback - callback that is called when results have been stored.
michael@0 41 * getAutoCompeteResults(searchString, params, callback)
michael@0 42 * Retrieve an array of form history values suitable for display in an autocomplete list.
michael@0 43 * Returns an mozIStoragePendingStatement that can be used to cancel the operation if
michael@0 44 * needed.
michael@0 45 * searchString - the string to search for, typically the entered value of a textbox
michael@0 46 * params - zero or more filter arguments:
michael@0 47 * fieldname - form field name
michael@0 48 * agedWeight
michael@0 49 * bucketSize
michael@0 50 * expiryDate
michael@0 51 * maxTimeGroundings
michael@0 52 * timeGroupingSize
michael@0 53 * prefixWeight
michael@0 54 * boundaryWeight
michael@0 55 * callback - callback that is called with the array of results. Each result in the array
michael@0 56 * is an object with four arguments:
michael@0 57 * text, textLowerCase, frecency, totalScore
michael@0 58 * schemaVersion
michael@0 59 * This property holds the version of the database schema
michael@0 60 *
michael@0 61 * Terms:
michael@0 62 * guid - entry identifier. For 'add', a guid will be generated.
michael@0 63 * fieldname - form field name
michael@0 64 * value - form value
michael@0 65 * timesUsed - the number of times the entry has been accessed
michael@0 66 * firstUsed - the time the the entry was first created
michael@0 67 * lastUsed - the time the entry was last accessed
michael@0 68 * firstUsedStart - search for entries created after or at this time
michael@0 69 * firstUsedEnd - search for entries created before or at this time
michael@0 70 * lastUsedStart - search for entries last accessed after or at this time
michael@0 71 * lastUsedEnd - search for entries last accessed before or at this time
michael@0 72 * newGuid - a special case valid only for 'update' and allows the guid for
michael@0 73 * an existing record to be updated. The 'guid' term is the only
michael@0 74 * other term which can be used (ie, you can not also specify a
michael@0 75 * fieldname, value etc) and indicates the guid of the existing
michael@0 76 * record that should be updated.
michael@0 77 *
michael@0 78 * In all of the above methods, the callback argument should be an object with
michael@0 79 * handleResult(result), handleFailure(error) and handleCompletion(reason) functions.
michael@0 80 * For search and getAutoCompeteResults, result is an object containing the desired
michael@0 81 * properties. For count, result is the integer count. For, update, handleResult is
michael@0 82 * not called. For handleCompletion, reason is either 0 if successful or 1 if
michael@0 83 * an error occurred.
michael@0 84 */
michael@0 85
michael@0 86 this.EXPORTED_SYMBOLS = ["FormHistory"];
michael@0 87
michael@0 88 const Cc = Components.classes;
michael@0 89 const Ci = Components.interfaces;
michael@0 90 const Cr = Components.results;
michael@0 91
michael@0 92 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 93 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 94
michael@0 95 XPCOMUtils.defineLazyServiceGetter(this, "uuidService",
michael@0 96 "@mozilla.org/uuid-generator;1",
michael@0 97 "nsIUUIDGenerator");
michael@0 98
michael@0 99 const DB_SCHEMA_VERSION = 4;
michael@0 100 const DAY_IN_MS = 86400000; // 1 day in milliseconds
michael@0 101 const MAX_SEARCH_TOKENS = 10;
michael@0 102 const NOOP = function noop() {};
michael@0 103
michael@0 104 let supportsDeletedTable =
michael@0 105 #ifdef ANDROID
michael@0 106 true;
michael@0 107 #else
michael@0 108 false;
michael@0 109 #endif
michael@0 110
michael@0 111 let Prefs = {
michael@0 112 initialized: false,
michael@0 113
michael@0 114 get debug() { this.ensureInitialized(); return this._debug; },
michael@0 115 get enabled() { this.ensureInitialized(); return this._enabled; },
michael@0 116 get expireDays() { this.ensureInitialized(); return this._expireDays; },
michael@0 117
michael@0 118 ensureInitialized: function() {
michael@0 119 if (this.initialized)
michael@0 120 return;
michael@0 121
michael@0 122 this.initialized = true;
michael@0 123
michael@0 124 this._debug = Services.prefs.getBoolPref("browser.formfill.debug");
michael@0 125 this._enabled = Services.prefs.getBoolPref("browser.formfill.enable");
michael@0 126 this._expireDays = Services.prefs.getIntPref("browser.formfill.expire_days");
michael@0 127 }
michael@0 128 };
michael@0 129
michael@0 130 function log(aMessage) {
michael@0 131 if (Prefs.debug) {
michael@0 132 Services.console.logStringMessage("FormHistory: " + aMessage);
michael@0 133 }
michael@0 134 }
michael@0 135
michael@0 136 function sendNotification(aType, aData) {
michael@0 137 if (typeof aData == "string") {
michael@0 138 let strWrapper = Cc["@mozilla.org/supports-string;1"].
michael@0 139 createInstance(Ci.nsISupportsString);
michael@0 140 strWrapper.data = aData;
michael@0 141 aData = strWrapper;
michael@0 142 }
michael@0 143 else if (typeof aData == "number") {
michael@0 144 let intWrapper = Cc["@mozilla.org/supports-PRInt64;1"].
michael@0 145 createInstance(Ci.nsISupportsPRInt64);
michael@0 146 intWrapper.data = aData;
michael@0 147 aData = intWrapper;
michael@0 148 }
michael@0 149 else if (aData) {
michael@0 150 throw Components.Exception("Invalid type " + (typeof aType) + " passed to sendNotification",
michael@0 151 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 152 }
michael@0 153
michael@0 154 Services.obs.notifyObservers(aData, "satchel-storage-changed", aType);
michael@0 155 }
michael@0 156
michael@0 157 /**
michael@0 158 * Current database schema
michael@0 159 */
michael@0 160
michael@0 161 const dbSchema = {
michael@0 162 tables : {
michael@0 163 moz_formhistory : {
michael@0 164 "id" : "INTEGER PRIMARY KEY",
michael@0 165 "fieldname" : "TEXT NOT NULL",
michael@0 166 "value" : "TEXT NOT NULL",
michael@0 167 "timesUsed" : "INTEGER",
michael@0 168 "firstUsed" : "INTEGER",
michael@0 169 "lastUsed" : "INTEGER",
michael@0 170 "guid" : "TEXT",
michael@0 171 },
michael@0 172 moz_deleted_formhistory: {
michael@0 173 "id" : "INTEGER PRIMARY KEY",
michael@0 174 "timeDeleted" : "INTEGER",
michael@0 175 "guid" : "TEXT"
michael@0 176 }
michael@0 177 },
michael@0 178 indices : {
michael@0 179 moz_formhistory_index : {
michael@0 180 table : "moz_formhistory",
michael@0 181 columns : [ "fieldname" ]
michael@0 182 },
michael@0 183 moz_formhistory_lastused_index : {
michael@0 184 table : "moz_formhistory",
michael@0 185 columns : [ "lastUsed" ]
michael@0 186 },
michael@0 187 moz_formhistory_guid_index : {
michael@0 188 table : "moz_formhistory",
michael@0 189 columns : [ "guid" ]
michael@0 190 },
michael@0 191 }
michael@0 192 };
michael@0 193
michael@0 194 /**
michael@0 195 * Validating and processing API querying data
michael@0 196 */
michael@0 197
michael@0 198 const validFields = [
michael@0 199 "fieldname",
michael@0 200 "value",
michael@0 201 "timesUsed",
michael@0 202 "firstUsed",
michael@0 203 "lastUsed",
michael@0 204 "guid",
michael@0 205 ];
michael@0 206
michael@0 207 const searchFilters = [
michael@0 208 "firstUsedStart",
michael@0 209 "firstUsedEnd",
michael@0 210 "lastUsedStart",
michael@0 211 "lastUsedEnd",
michael@0 212 ];
michael@0 213
michael@0 214 function validateOpData(aData, aDataType) {
michael@0 215 let thisValidFields = validFields;
michael@0 216 // A special case to update the GUID - in this case there can be a 'newGuid'
michael@0 217 // field and of the normally valid fields, only 'guid' is accepted.
michael@0 218 if (aDataType == "Update" && "newGuid" in aData) {
michael@0 219 thisValidFields = ["guid", "newGuid"];
michael@0 220 }
michael@0 221 for (let field in aData) {
michael@0 222 if (field != "op" && thisValidFields.indexOf(field) == -1) {
michael@0 223 throw Components.Exception(
michael@0 224 aDataType + " query contains an unrecognized field: " + field,
michael@0 225 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 226 }
michael@0 227 }
michael@0 228 return aData;
michael@0 229 }
michael@0 230
michael@0 231 function validateSearchData(aData, aDataType) {
michael@0 232 for (let field in aData) {
michael@0 233 if (field != "op" && validFields.indexOf(field) == -1 && searchFilters.indexOf(field) == -1) {
michael@0 234 throw Components.Exception(
michael@0 235 aDataType + " query contains an unrecognized field: " + field,
michael@0 236 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 237 }
michael@0 238 }
michael@0 239 }
michael@0 240
michael@0 241 function makeQueryPredicates(aQueryData, delimiter = ' AND ') {
michael@0 242 return Object.keys(aQueryData).map(function(field) {
michael@0 243 if (field == "firstUsedStart") {
michael@0 244 return "firstUsed >= :" + field;
michael@0 245 } else if (field == "firstUsedEnd") {
michael@0 246 return "firstUsed <= :" + field;
michael@0 247 } else if (field == "lastUsedStart") {
michael@0 248 return "lastUsed >= :" + field;
michael@0 249 } else if (field == "lastUsedEnd") {
michael@0 250 return "lastUsed <= :" + field;
michael@0 251 }
michael@0 252 return field + " = :" + field;
michael@0 253 }).join(delimiter);
michael@0 254 }
michael@0 255
michael@0 256 /**
michael@0 257 * Storage statement creation and parameter binding
michael@0 258 */
michael@0 259
michael@0 260 function makeCountStatement(aSearchData) {
michael@0 261 let query = "SELECT COUNT(*) AS numEntries FROM moz_formhistory";
michael@0 262 let queryTerms = makeQueryPredicates(aSearchData);
michael@0 263 if (queryTerms) {
michael@0 264 query += " WHERE " + queryTerms;
michael@0 265 }
michael@0 266 return dbCreateAsyncStatement(query, aSearchData);
michael@0 267 }
michael@0 268
michael@0 269 function makeSearchStatement(aSearchData, aSelectTerms) {
michael@0 270 let query = "SELECT " + aSelectTerms.join(", ") + " FROM moz_formhistory";
michael@0 271 let queryTerms = makeQueryPredicates(aSearchData);
michael@0 272 if (queryTerms) {
michael@0 273 query += " WHERE " + queryTerms;
michael@0 274 }
michael@0 275
michael@0 276 return dbCreateAsyncStatement(query, aSearchData);
michael@0 277 }
michael@0 278
michael@0 279 function makeAddStatement(aNewData, aNow, aBindingArrays) {
michael@0 280 let query = "INSERT INTO moz_formhistory (fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
michael@0 281 "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
michael@0 282
michael@0 283 aNewData.timesUsed = aNewData.timesUsed || 1;
michael@0 284 aNewData.firstUsed = aNewData.firstUsed || aNow;
michael@0 285 aNewData.lastUsed = aNewData.lastUsed || aNow;
michael@0 286 return dbCreateAsyncStatement(query, aNewData, aBindingArrays);
michael@0 287 }
michael@0 288
michael@0 289 function makeBumpStatement(aGuid, aNow, aBindingArrays) {
michael@0 290 let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE guid = :guid";
michael@0 291 let queryParams = {
michael@0 292 lastUsed : aNow,
michael@0 293 guid : aGuid,
michael@0 294 };
michael@0 295
michael@0 296 return dbCreateAsyncStatement(query, queryParams, aBindingArrays);
michael@0 297 }
michael@0 298
michael@0 299 function makeRemoveStatement(aSearchData, aBindingArrays) {
michael@0 300 let query = "DELETE FROM moz_formhistory";
michael@0 301 let queryTerms = makeQueryPredicates(aSearchData);
michael@0 302
michael@0 303 if (queryTerms) {
michael@0 304 log("removeEntries");
michael@0 305 query += " WHERE " + queryTerms;
michael@0 306 } else {
michael@0 307 log("removeAllEntries");
michael@0 308 // Not specifying any fields means we should remove all entries. We
michael@0 309 // won't need to modify the query in this case.
michael@0 310 }
michael@0 311
michael@0 312 return dbCreateAsyncStatement(query, aSearchData, aBindingArrays);
michael@0 313 }
michael@0 314
michael@0 315 function makeUpdateStatement(aGuid, aNewData, aBindingArrays) {
michael@0 316 let query = "UPDATE moz_formhistory SET ";
michael@0 317 let queryTerms = makeQueryPredicates(aNewData, ', ');
michael@0 318
michael@0 319 if (!queryTerms) {
michael@0 320 throw Components.Exception("Update query must define fields to modify.",
michael@0 321 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 322 }
michael@0 323
michael@0 324 query += queryTerms + " WHERE guid = :existing_guid";
michael@0 325 aNewData["existing_guid"] = aGuid;
michael@0 326
michael@0 327 return dbCreateAsyncStatement(query, aNewData, aBindingArrays);
michael@0 328 }
michael@0 329
michael@0 330 function makeMoveToDeletedStatement(aGuid, aNow, aData, aBindingArrays) {
michael@0 331 if (supportsDeletedTable) {
michael@0 332 let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted)";
michael@0 333 let queryTerms = makeQueryPredicates(aData);
michael@0 334
michael@0 335 if (aGuid) {
michael@0 336 query += " VALUES (:guid, :timeDeleted)";
michael@0 337 } else {
michael@0 338 // TODO: Add these items to the deleted items table once we've sorted
michael@0 339 // out the issues from bug 756701
michael@0 340 if (!queryTerms)
michael@0 341 return;
michael@0 342
michael@0 343 query += " SELECT guid, :timeDeleted FROM moz_formhistory WHERE " + queryTerms;
michael@0 344 }
michael@0 345
michael@0 346 aData.timeDeleted = aNow;
michael@0 347
michael@0 348 return dbCreateAsyncStatement(query, aData, aBindingArrays);
michael@0 349 }
michael@0 350
michael@0 351 return null;
michael@0 352 }
michael@0 353
michael@0 354 function generateGUID() {
michael@0 355 // string like: "{f60d9eac-9421-4abc-8491-8e8322b063d4}"
michael@0 356 let uuid = uuidService.generateUUID().toString();
michael@0 357 let raw = ""; // A string with the low bytes set to random values
michael@0 358 let bytes = 0;
michael@0 359 for (let i = 1; bytes < 12 ; i+= 2) {
michael@0 360 // Skip dashes
michael@0 361 if (uuid[i] == "-")
michael@0 362 i++;
michael@0 363 let hexVal = parseInt(uuid[i] + uuid[i + 1], 16);
michael@0 364 raw += String.fromCharCode(hexVal);
michael@0 365 bytes++;
michael@0 366 }
michael@0 367 return btoa(raw);
michael@0 368 }
michael@0 369
michael@0 370 /**
michael@0 371 * Database creation and access
michael@0 372 */
michael@0 373
michael@0 374 let _dbConnection = null;
michael@0 375 XPCOMUtils.defineLazyGetter(this, "dbConnection", function() {
michael@0 376 let dbFile;
michael@0 377
michael@0 378 try {
michael@0 379 dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
michael@0 380 dbFile.append("formhistory.sqlite");
michael@0 381 log("Opening database at " + dbFile.path);
michael@0 382
michael@0 383 _dbConnection = Services.storage.openUnsharedDatabase(dbFile);
michael@0 384 dbInit();
michael@0 385 } catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
michael@0 386 dbCleanup(dbFile);
michael@0 387 _dbConnection = Services.storage.openUnsharedDatabase(dbFile);
michael@0 388 dbInit();
michael@0 389 }
michael@0 390
michael@0 391 return _dbConnection;
michael@0 392 });
michael@0 393
michael@0 394
michael@0 395 let dbStmts = new Map();
michael@0 396
michael@0 397 /*
michael@0 398 * dbCreateAsyncStatement
michael@0 399 *
michael@0 400 * Creates a statement, wraps it, and then does parameter replacement
michael@0 401 */
michael@0 402 function dbCreateAsyncStatement(aQuery, aParams, aBindingArrays) {
michael@0 403 if (!aQuery)
michael@0 404 return null;
michael@0 405
michael@0 406 let stmt = dbStmts.get(aQuery);
michael@0 407 if (!stmt) {
michael@0 408 log("Creating new statement for query: " + aQuery);
michael@0 409 stmt = dbConnection.createAsyncStatement(aQuery);
michael@0 410 dbStmts.set(aQuery, stmt);
michael@0 411 }
michael@0 412
michael@0 413 if (aBindingArrays) {
michael@0 414 let bindingArray = aBindingArrays.get(stmt);
michael@0 415 if (!bindingArray) {
michael@0 416 // first time using a particular statement in update
michael@0 417 bindingArray = stmt.newBindingParamsArray();
michael@0 418 aBindingArrays.set(stmt, bindingArray);
michael@0 419 }
michael@0 420
michael@0 421 if (aParams) {
michael@0 422 let bindingParams = bindingArray.newBindingParams();
michael@0 423 for (let field in aParams) {
michael@0 424 bindingParams.bindByName(field, aParams[field]);
michael@0 425 }
michael@0 426 bindingArray.addParams(bindingParams);
michael@0 427 }
michael@0 428 } else {
michael@0 429 if (aParams) {
michael@0 430 for (let field in aParams) {
michael@0 431 stmt.params[field] = aParams[field];
michael@0 432 }
michael@0 433 }
michael@0 434 }
michael@0 435
michael@0 436 return stmt;
michael@0 437 }
michael@0 438
michael@0 439 /**
michael@0 440 * dbInit
michael@0 441 *
michael@0 442 * Attempts to initialize the database. This creates the file if it doesn't
michael@0 443 * exist, performs any migrations, etc.
michael@0 444 */
michael@0 445 function dbInit() {
michael@0 446 log("Initializing Database");
michael@0 447
michael@0 448 if (!_dbConnection.tableExists("moz_formhistory")) {
michael@0 449 dbCreate();
michael@0 450 return;
michael@0 451 }
michael@0 452
michael@0 453 // When FormHistory is released, we will no longer support the various schema versions prior to
michael@0 454 // this release that nsIFormHistory2 once did.
michael@0 455 let version = _dbConnection.schemaVersion;
michael@0 456 if (version < 3) {
michael@0 457 throw Components.Exception("DB version is unsupported.",
michael@0 458 Cr.NS_ERROR_FILE_CORRUPTED);
michael@0 459 } else if (version != DB_SCHEMA_VERSION) {
michael@0 460 dbMigrate(version);
michael@0 461 }
michael@0 462 }
michael@0 463
michael@0 464 function dbCreate() {
michael@0 465 log("Creating DB -- tables");
michael@0 466 for (let name in dbSchema.tables) {
michael@0 467 let table = dbSchema.tables[name];
michael@0 468 let tSQL = [[col, table[col]].join(" ") for (col in table)].join(", ");
michael@0 469 log("Creating table " + name + " with " + tSQL);
michael@0 470 _dbConnection.createTable(name, tSQL);
michael@0 471 }
michael@0 472
michael@0 473 log("Creating DB -- indices");
michael@0 474 for (let name in dbSchema.indices) {
michael@0 475 let index = dbSchema.indices[name];
michael@0 476 let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
michael@0 477 "(" + index.columns.join(", ") + ")";
michael@0 478 _dbConnection.executeSimpleSQL(statement);
michael@0 479 }
michael@0 480
michael@0 481 _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
michael@0 482 }
michael@0 483
michael@0 484 function dbMigrate(oldVersion) {
michael@0 485 log("Attempting to migrate from version " + oldVersion);
michael@0 486
michael@0 487 if (oldVersion > DB_SCHEMA_VERSION) {
michael@0 488 log("Downgrading to version " + DB_SCHEMA_VERSION);
michael@0 489 // User's DB is newer. Sanity check that our expected columns are
michael@0 490 // present, and if so mark the lower version and merrily continue
michael@0 491 // on. If the columns are borked, something is wrong so blow away
michael@0 492 // the DB and start from scratch. [Future incompatible upgrades
michael@0 493 // should switch to a different table or file.]
michael@0 494
michael@0 495 if (!dbAreExpectedColumnsPresent()) {
michael@0 496 throw Components.Exception("DB is missing expected columns",
michael@0 497 Cr.NS_ERROR_FILE_CORRUPTED);
michael@0 498 }
michael@0 499
michael@0 500 // Change the stored version to the current version. If the user
michael@0 501 // runs the newer code again, it will see the lower version number
michael@0 502 // and re-upgrade (to fixup any entries the old code added).
michael@0 503 _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
michael@0 504 return;
michael@0 505 }
michael@0 506
michael@0 507 // Note that migration is currently performed synchronously.
michael@0 508 _dbConnection.beginTransaction();
michael@0 509
michael@0 510 try {
michael@0 511 for (let v = oldVersion + 1; v <= DB_SCHEMA_VERSION; v++) {
michael@0 512 this.log("Upgrading to version " + v + "...");
michael@0 513 Migrators["dbMigrateToVersion" + v]();
michael@0 514 }
michael@0 515 } catch (e) {
michael@0 516 this.log("Migration failed: " + e);
michael@0 517 this.dbConnection.rollbackTransaction();
michael@0 518 throw e;
michael@0 519 }
michael@0 520
michael@0 521 _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
michael@0 522 _dbConnection.commitTransaction();
michael@0 523
michael@0 524 log("DB migration completed.");
michael@0 525 }
michael@0 526
michael@0 527 var Migrators = {
michael@0 528 /*
michael@0 529 * Updates the DB schema to v3 (bug 506402).
michael@0 530 * Adds deleted form history table.
michael@0 531 */
michael@0 532 dbMigrateToVersion4: function dbMigrateToVersion4() {
michael@0 533 if (!_dbConnection.tableExists("moz_deleted_formhistory")) {
michael@0 534 let table = dbSchema.tables["moz_deleted_formhistory"];
michael@0 535 let tSQL = [[col, table[col]].join(" ") for (col in table)].join(", ");
michael@0 536 _dbConnection.createTable("moz_deleted_formhistory", tSQL);
michael@0 537 }
michael@0 538 }
michael@0 539 };
michael@0 540
michael@0 541 /**
michael@0 542 * dbAreExpectedColumnsPresent
michael@0 543 *
michael@0 544 * Sanity check to ensure that the columns this version of the code expects
michael@0 545 * are present in the DB we're using.
michael@0 546 */
michael@0 547 function dbAreExpectedColumnsPresent() {
michael@0 548 for (let name in dbSchema.tables) {
michael@0 549 let table = dbSchema.tables[name];
michael@0 550 let query = "SELECT " +
michael@0 551 [col for (col in table)].join(", ") +
michael@0 552 " FROM " + name;
michael@0 553 try {
michael@0 554 let stmt = _dbConnection.createStatement(query);
michael@0 555 // (no need to execute statement, if it compiled we're good)
michael@0 556 stmt.finalize();
michael@0 557 } catch (e) {
michael@0 558 return false;
michael@0 559 }
michael@0 560 }
michael@0 561
michael@0 562 log("verified that expected columns are present in DB.");
michael@0 563 return true;
michael@0 564 }
michael@0 565
michael@0 566 /**
michael@0 567 * dbCleanup
michael@0 568 *
michael@0 569 * Called when database creation fails. Finalizes database statements,
michael@0 570 * closes the database connection, deletes the database file.
michael@0 571 */
michael@0 572 function dbCleanup(dbFile) {
michael@0 573 log("Cleaning up DB file - close & remove & backup");
michael@0 574
michael@0 575 // Create backup file
michael@0 576 let backupFile = dbFile.leafName + ".corrupt";
michael@0 577 Services.storage.backupDatabaseFile(dbFile, backupFile);
michael@0 578
michael@0 579 dbClose(false);
michael@0 580 dbFile.remove(false);
michael@0 581 }
michael@0 582
michael@0 583 function dbClose(aShutdown) {
michael@0 584 log("dbClose(" + aShutdown + ")");
michael@0 585
michael@0 586 if (aShutdown) {
michael@0 587 sendNotification("formhistory-shutdown", null);
michael@0 588 }
michael@0 589
michael@0 590 // Connection may never have been created if say open failed but we still
michael@0 591 // end up calling dbClose as part of the rest of dbCleanup.
michael@0 592 if (!_dbConnection) {
michael@0 593 return;
michael@0 594 }
michael@0 595
michael@0 596 log("dbClose finalize statements");
michael@0 597 for (let stmt of dbStmts.values()) {
michael@0 598 stmt.finalize();
michael@0 599 }
michael@0 600
michael@0 601 dbStmts = new Map();
michael@0 602
michael@0 603 let closed = false;
michael@0 604 _dbConnection.asyncClose(function () closed = true);
michael@0 605
michael@0 606 if (!aShutdown) {
michael@0 607 let thread = Services.tm.currentThread;
michael@0 608 while (!closed) {
michael@0 609 thread.processNextEvent(true);
michael@0 610 }
michael@0 611 }
michael@0 612 }
michael@0 613
michael@0 614 /**
michael@0 615 * updateFormHistoryWrite
michael@0 616 *
michael@0 617 * Constructs and executes database statements from a pre-processed list of
michael@0 618 * inputted changes.
michael@0 619 */
michael@0 620 function updateFormHistoryWrite(aChanges, aCallbacks) {
michael@0 621 log("updateFormHistoryWrite " + aChanges.length);
michael@0 622
michael@0 623 // pass 'now' down so that every entry in the batch has the same timestamp
michael@0 624 let now = Date.now() * 1000;
michael@0 625
michael@0 626 // for each change, we either create and append a new storage statement to
michael@0 627 // stmts or bind a new set of parameters to an existing storage statement.
michael@0 628 // stmts and bindingArrays are updated when makeXXXStatement eventually
michael@0 629 // calls dbCreateAsyncStatement.
michael@0 630 let stmts = [];
michael@0 631 let notifications = [];
michael@0 632 let bindingArrays = new Map();
michael@0 633
michael@0 634 for each (let change in aChanges) {
michael@0 635 let operation = change.op;
michael@0 636 delete change.op;
michael@0 637 let stmt;
michael@0 638 switch (operation) {
michael@0 639 case "remove":
michael@0 640 log("Remove from form history " + change);
michael@0 641 let delStmt = makeMoveToDeletedStatement(change.guid, now, change, bindingArrays);
michael@0 642 if (delStmt && stmts.indexOf(delStmt) == -1)
michael@0 643 stmts.push(delStmt);
michael@0 644 if ("timeDeleted" in change)
michael@0 645 delete change.timeDeleted;
michael@0 646 stmt = makeRemoveStatement(change, bindingArrays);
michael@0 647 notifications.push([ "formhistory-remove", change.guid ]);
michael@0 648 break;
michael@0 649 case "update":
michael@0 650 log("Update form history " + change);
michael@0 651 let guid = change.guid;
michael@0 652 delete change.guid;
michael@0 653 // a special case for updating the GUID - the new value can be
michael@0 654 // specified in newGuid.
michael@0 655 if (change.newGuid) {
michael@0 656 change.guid = change.newGuid
michael@0 657 delete change.newGuid;
michael@0 658 }
michael@0 659 stmt = makeUpdateStatement(guid, change, bindingArrays);
michael@0 660 notifications.push([ "formhistory-update", guid ]);
michael@0 661 break;
michael@0 662 case "bump":
michael@0 663 log("Bump form history " + change);
michael@0 664 if (change.guid) {
michael@0 665 stmt = makeBumpStatement(change.guid, now, bindingArrays);
michael@0 666 notifications.push([ "formhistory-update", change.guid ]);
michael@0 667 } else {
michael@0 668 change.guid = generateGUID();
michael@0 669 stmt = makeAddStatement(change, now, bindingArrays);
michael@0 670 notifications.push([ "formhistory-add", change.guid ]);
michael@0 671 }
michael@0 672 break;
michael@0 673 case "add":
michael@0 674 log("Add to form history " + change);
michael@0 675 change.guid = generateGUID();
michael@0 676 stmt = makeAddStatement(change, now, bindingArrays);
michael@0 677 notifications.push([ "formhistory-add", change.guid ]);
michael@0 678 break;
michael@0 679 default:
michael@0 680 // We should've already guaranteed that change.op is one of the above
michael@0 681 throw Components.Exception("Invalid operation " + operation,
michael@0 682 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 683 }
michael@0 684
michael@0 685 // As identical statements are reused, only add statements if they aren't already present.
michael@0 686 if (stmt && stmts.indexOf(stmt) == -1) {
michael@0 687 stmts.push(stmt);
michael@0 688 }
michael@0 689 }
michael@0 690
michael@0 691 for (let stmt of stmts) {
michael@0 692 stmt.bindParameters(bindingArrays.get(stmt));
michael@0 693 }
michael@0 694
michael@0 695 let handlers = {
michael@0 696 handleCompletion : function(aReason) {
michael@0 697 if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
michael@0 698 for (let [notification, param] of notifications) {
michael@0 699 // We're either sending a GUID or nothing at all.
michael@0 700 sendNotification(notification, param);
michael@0 701 }
michael@0 702 }
michael@0 703
michael@0 704 if (aCallbacks && aCallbacks.handleCompletion) {
michael@0 705 aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
michael@0 706 }
michael@0 707 },
michael@0 708 handleError : function(aError) {
michael@0 709 if (aCallbacks && aCallbacks.handleError) {
michael@0 710 aCallbacks.handleError(aError);
michael@0 711 }
michael@0 712 },
michael@0 713 handleResult : NOOP
michael@0 714 };
michael@0 715
michael@0 716 dbConnection.executeAsync(stmts, stmts.length, handlers);
michael@0 717 }
michael@0 718
michael@0 719 /**
michael@0 720 * Functions that expire entries in form history and shrinks database
michael@0 721 * afterwards as necessary initiated by expireOldEntries.
michael@0 722 */
michael@0 723
michael@0 724 /**
michael@0 725 * expireOldEntriesDeletion
michael@0 726 *
michael@0 727 * Removes entries from database.
michael@0 728 */
michael@0 729 function expireOldEntriesDeletion(aExpireTime, aBeginningCount) {
michael@0 730 log("expireOldEntriesDeletion(" + aExpireTime + "," + aBeginningCount + ")");
michael@0 731
michael@0 732 FormHistory.update([
michael@0 733 {
michael@0 734 op: "remove",
michael@0 735 lastUsedEnd : aExpireTime,
michael@0 736 }], {
michael@0 737 handleCompletion: function() {
michael@0 738 expireOldEntriesVacuum(aExpireTime, aBeginningCount);
michael@0 739 },
michael@0 740 handleError: function(aError) {
michael@0 741 log("expireOldEntriesDeletionFailure");
michael@0 742 }
michael@0 743 });
michael@0 744 }
michael@0 745
michael@0 746 /**
michael@0 747 * expireOldEntriesVacuum
michael@0 748 *
michael@0 749 * Counts number of entries removed and shrinks database as necessary.
michael@0 750 */
michael@0 751 function expireOldEntriesVacuum(aExpireTime, aBeginningCount) {
michael@0 752 FormHistory.count({}, {
michael@0 753 handleResult: function(aEndingCount) {
michael@0 754 if (aBeginningCount - aEndingCount > 500) {
michael@0 755 log("expireOldEntriesVacuum");
michael@0 756
michael@0 757 let stmt = dbCreateAsyncStatement("VACUUM");
michael@0 758 stmt.executeAsync({
michael@0 759 handleResult : NOOP,
michael@0 760 handleError : function(aError) {
michael@0 761 log("expireVacuumError");
michael@0 762 },
michael@0 763 handleCompletion : NOOP
michael@0 764 });
michael@0 765 }
michael@0 766
michael@0 767 sendNotification("formhistory-expireoldentries", aExpireTime);
michael@0 768 },
michael@0 769 handleError: function(aError) {
michael@0 770 log("expireEndCountFailure");
michael@0 771 }
michael@0 772 });
michael@0 773 }
michael@0 774
michael@0 775 this.FormHistory = {
michael@0 776 get enabled() Prefs.enabled,
michael@0 777
michael@0 778 search : function formHistorySearch(aSelectTerms, aSearchData, aCallbacks) {
michael@0 779 // if no terms selected, select everything
michael@0 780 aSelectTerms = (aSelectTerms) ? aSelectTerms : validFields;
michael@0 781 validateSearchData(aSearchData, "Search");
michael@0 782
michael@0 783 let stmt = makeSearchStatement(aSearchData, aSelectTerms);
michael@0 784
michael@0 785 let handlers = {
michael@0 786 handleResult : function(aResultSet) {
michael@0 787 let formHistoryFields = dbSchema.tables.moz_formhistory;
michael@0 788 for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
michael@0 789 let result = {};
michael@0 790 for each (let field in aSelectTerms) {
michael@0 791 result[field] = row.getResultByName(field);
michael@0 792 }
michael@0 793
michael@0 794 if (aCallbacks && aCallbacks.handleResult) {
michael@0 795 aCallbacks.handleResult(result);
michael@0 796 }
michael@0 797 }
michael@0 798 },
michael@0 799
michael@0 800 handleError : function(aError) {
michael@0 801 if (aCallbacks && aCallbacks.handleError) {
michael@0 802 aCallbacks.handleError(aError);
michael@0 803 }
michael@0 804 },
michael@0 805
michael@0 806 handleCompletion : function searchCompletionHandler(aReason) {
michael@0 807 if (aCallbacks && aCallbacks.handleCompletion) {
michael@0 808 aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
michael@0 809 }
michael@0 810 }
michael@0 811 };
michael@0 812
michael@0 813 stmt.executeAsync(handlers);
michael@0 814 },
michael@0 815
michael@0 816 count : function formHistoryCount(aSearchData, aCallbacks) {
michael@0 817 validateSearchData(aSearchData, "Count");
michael@0 818 let stmt = makeCountStatement(aSearchData);
michael@0 819 let handlers = {
michael@0 820 handleResult : function countResultHandler(aResultSet) {
michael@0 821 let row = aResultSet.getNextRow();
michael@0 822 let count = row.getResultByName("numEntries");
michael@0 823 if (aCallbacks && aCallbacks.handleResult) {
michael@0 824 aCallbacks.handleResult(count);
michael@0 825 }
michael@0 826 },
michael@0 827
michael@0 828 handleError : function(aError) {
michael@0 829 if (aCallbacks && aCallbacks.handleError) {
michael@0 830 aCallbacks.handleError(aError);
michael@0 831 }
michael@0 832 },
michael@0 833
michael@0 834 handleCompletion : function searchCompletionHandler(aReason) {
michael@0 835 if (aCallbacks && aCallbacks.handleCompletion) {
michael@0 836 aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
michael@0 837 }
michael@0 838 }
michael@0 839 };
michael@0 840
michael@0 841 stmt.executeAsync(handlers);
michael@0 842 },
michael@0 843
michael@0 844 update : function formHistoryUpdate(aChanges, aCallbacks) {
michael@0 845 if (!Prefs.enabled) {
michael@0 846 return;
michael@0 847 }
michael@0 848
michael@0 849 // Used to keep track of how many searches have been started. When that number
michael@0 850 // are finished, updateFormHistoryWrite can be called.
michael@0 851 let numSearches = 0;
michael@0 852 let completedSearches = 0;
michael@0 853 let searchFailed = false;
michael@0 854
michael@0 855 function validIdentifier(change) {
michael@0 856 // The identifier is only valid if one of either the guid or the (fieldname/value) are set
michael@0 857 return Boolean(change.guid) != Boolean(change.fieldname && change.value);
michael@0 858 }
michael@0 859
michael@0 860 if (!("length" in aChanges))
michael@0 861 aChanges = [aChanges];
michael@0 862
michael@0 863 for each (let change in aChanges) {
michael@0 864 switch (change.op) {
michael@0 865 case "remove":
michael@0 866 validateSearchData(change, "Remove");
michael@0 867 continue;
michael@0 868 case "update":
michael@0 869 if (validIdentifier(change)) {
michael@0 870 validateOpData(change, "Update");
michael@0 871 if (change.guid) {
michael@0 872 continue;
michael@0 873 }
michael@0 874 } else {
michael@0 875 throw Components.Exception(
michael@0 876 "update op='update' does not correctly reference a entry.",
michael@0 877 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 878 }
michael@0 879 break;
michael@0 880 case "bump":
michael@0 881 if (validIdentifier(change)) {
michael@0 882 validateOpData(change, "Bump");
michael@0 883 if (change.guid) {
michael@0 884 continue;
michael@0 885 }
michael@0 886 } else {
michael@0 887 throw Components.Exception(
michael@0 888 "update op='bump' does not correctly reference a entry.",
michael@0 889 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 890 }
michael@0 891 break;
michael@0 892 case "add":
michael@0 893 if (change.guid) {
michael@0 894 throw Components.Exception(
michael@0 895 "op='add' cannot contain field 'guid'. Either use op='update' " +
michael@0 896 "explicitly or make 'guid' undefined.",
michael@0 897 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 898 } else if (change.fieldname && change.value) {
michael@0 899 validateOpData(change, "Add");
michael@0 900 }
michael@0 901 break;
michael@0 902 default:
michael@0 903 throw Components.Exception(
michael@0 904 "update does not recognize op='" + change.op + "'",
michael@0 905 Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 906 }
michael@0 907
michael@0 908 numSearches++;
michael@0 909 let changeToUpdate = change;
michael@0 910 FormHistory.search(
michael@0 911 [ "guid" ],
michael@0 912 {
michael@0 913 fieldname : change.fieldname,
michael@0 914 value : change.value
michael@0 915 }, {
michael@0 916 foundResult : false,
michael@0 917 handleResult : function(aResult) {
michael@0 918 if (this.foundResult) {
michael@0 919 log("Database contains multiple entries with the same fieldname/value pair.");
michael@0 920 if (aCallbacks && aCallbacks.handleError) {
michael@0 921 aCallbacks.handleError({
michael@0 922 message :
michael@0 923 "Database contains multiple entries with the same fieldname/value pair.",
michael@0 924 result : 19 // Constraint violation
michael@0 925 });
michael@0 926 }
michael@0 927
michael@0 928 searchFailed = true;
michael@0 929 return;
michael@0 930 }
michael@0 931
michael@0 932 this.foundResult = true;
michael@0 933 changeToUpdate.guid = aResult["guid"];
michael@0 934 },
michael@0 935
michael@0 936 handleError : function(aError) {
michael@0 937 if (aCallbacks && aCallbacks.handleError) {
michael@0 938 aCallbacks.handleError(aError);
michael@0 939 }
michael@0 940 },
michael@0 941
michael@0 942 handleCompletion : function(aReason) {
michael@0 943 completedSearches++;
michael@0 944 if (completedSearches == numSearches) {
michael@0 945 if (!aReason && !searchFailed) {
michael@0 946 updateFormHistoryWrite(aChanges, aCallbacks);
michael@0 947 }
michael@0 948 else if (aCallbacks && aCallbacks.handleCompletion) {
michael@0 949 aCallbacks.handleCompletion(1);
michael@0 950 }
michael@0 951 }
michael@0 952 }
michael@0 953 });
michael@0 954 }
michael@0 955
michael@0 956 if (numSearches == 0) {
michael@0 957 // We don't have to wait for any statements to return.
michael@0 958 updateFormHistoryWrite(aChanges, aCallbacks);
michael@0 959 }
michael@0 960 },
michael@0 961
michael@0 962 getAutoCompleteResults: function getAutoCompleteResults(searchString, params, aCallbacks) {
michael@0 963 // only do substring matching when the search string contains more than one character
michael@0 964 let searchTokens;
michael@0 965 let where = ""
michael@0 966 let boundaryCalc = "";
michael@0 967 if (searchString.length > 1) {
michael@0 968 searchTokens = searchString.split(/\s+/);
michael@0 969
michael@0 970 // build up the word boundary and prefix match bonus calculation
michael@0 971 boundaryCalc = "MAX(1, :prefixWeight * (value LIKE :valuePrefix ESCAPE '/') + (";
michael@0 972 // for each word, calculate word boundary weights for the SELECT clause and
michael@0 973 // add word to the WHERE clause of the query
michael@0 974 let tokenCalc = [];
michael@0 975 let searchTokenCount = Math.min(searchTokens.length, MAX_SEARCH_TOKENS);
michael@0 976 for (let i = 0; i < searchTokenCount; i++) {
michael@0 977 tokenCalc.push("(value LIKE :tokenBegin" + i + " ESCAPE '/') + " +
michael@0 978 "(value LIKE :tokenBoundary" + i + " ESCAPE '/')");
michael@0 979 where += "AND (value LIKE :tokenContains" + i + " ESCAPE '/') ";
michael@0 980 }
michael@0 981 // add more weight if we have a traditional prefix match and
michael@0 982 // multiply boundary bonuses by boundary weight
michael@0 983 boundaryCalc += tokenCalc.join(" + ") + ") * :boundaryWeight)";
michael@0 984 } else if (searchString.length == 1) {
michael@0 985 where = "AND (value LIKE :valuePrefix ESCAPE '/') ";
michael@0 986 boundaryCalc = "1";
michael@0 987 delete params.prefixWeight;
michael@0 988 delete params.boundaryWeight;
michael@0 989 } else {
michael@0 990 where = "";
michael@0 991 boundaryCalc = "1";
michael@0 992 delete params.prefixWeight;
michael@0 993 delete params.boundaryWeight;
michael@0 994 }
michael@0 995
michael@0 996 params.now = Date.now() * 1000; // convert from ms to microseconds
michael@0 997
michael@0 998 /* Three factors in the frecency calculation for an entry (in order of use in calculation):
michael@0 999 * 1) average number of times used - items used more are ranked higher
michael@0 1000 * 2) how recently it was last used - items used recently are ranked higher
michael@0 1001 * 3) additional weight for aged entries surviving expiry - these entries are relevant
michael@0 1002 * since they have been used multiple times over a large time span so rank them higher
michael@0 1003 * The score is then divided by the bucket size and we round the result so that entries
michael@0 1004 * with a very similar frecency are bucketed together with an alphabetical sort. This is
michael@0 1005 * to reduce the amount of moving around by entries while typing.
michael@0 1006 */
michael@0 1007
michael@0 1008 let query = "/* do not warn (bug 496471): can't use an index */ " +
michael@0 1009 "SELECT value, " +
michael@0 1010 "ROUND( " +
michael@0 1011 "timesUsed / MAX(1.0, (lastUsed - firstUsed) / :timeGroupingSize) * " +
michael@0 1012 "MAX(1.0, :maxTimeGroupings - (:now - lastUsed) / :timeGroupingSize) * "+
michael@0 1013 "MAX(1.0, :agedWeight * (firstUsed < :expiryDate)) / " +
michael@0 1014 ":bucketSize "+
michael@0 1015 ", 3) AS frecency, " +
michael@0 1016 boundaryCalc + " AS boundaryBonuses " +
michael@0 1017 "FROM moz_formhistory " +
michael@0 1018 "WHERE fieldname=:fieldname " + where +
michael@0 1019 "ORDER BY ROUND(frecency * boundaryBonuses) DESC, UPPER(value) ASC";
michael@0 1020
michael@0 1021 let stmt = dbCreateAsyncStatement(query, params);
michael@0 1022
michael@0 1023 // Chicken and egg problem: Need the statement to escape the params we
michael@0 1024 // pass to the function that gives us the statement. So, fix it up now.
michael@0 1025 if (searchString.length >= 1)
michael@0 1026 stmt.params.valuePrefix = stmt.escapeStringForLIKE(searchString, "/") + "%";
michael@0 1027 if (searchString.length > 1) {
michael@0 1028 let searchTokenCount = Math.min(searchTokens.length, MAX_SEARCH_TOKENS);
michael@0 1029 for (let i = 0; i < searchTokenCount; i++) {
michael@0 1030 let escapedToken = stmt.escapeStringForLIKE(searchTokens[i], "/");
michael@0 1031 stmt.params["tokenBegin" + i] = escapedToken + "%";
michael@0 1032 stmt.params["tokenBoundary" + i] = "% " + escapedToken + "%";
michael@0 1033 stmt.params["tokenContains" + i] = "%" + escapedToken + "%";
michael@0 1034 }
michael@0 1035 } else {
michael@0 1036 // no additional params need to be substituted into the query when the
michael@0 1037 // length is zero or one
michael@0 1038 }
michael@0 1039
michael@0 1040 let pending = stmt.executeAsync({
michael@0 1041 handleResult : function (aResultSet) {
michael@0 1042 for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
michael@0 1043 let value = row.getResultByName("value");
michael@0 1044 let frecency = row.getResultByName("frecency");
michael@0 1045 let entry = {
michael@0 1046 text : value,
michael@0 1047 textLowerCase : value.toLowerCase(),
michael@0 1048 frecency : frecency,
michael@0 1049 totalScore : Math.round(frecency * row.getResultByName("boundaryBonuses"))
michael@0 1050 };
michael@0 1051 if (aCallbacks && aCallbacks.handleResult) {
michael@0 1052 aCallbacks.handleResult(entry);
michael@0 1053 }
michael@0 1054 }
michael@0 1055 },
michael@0 1056
michael@0 1057 handleError : function (aError) {
michael@0 1058 if (aCallbacks && aCallbacks.handleError) {
michael@0 1059 aCallbacks.handleError(aError);
michael@0 1060 }
michael@0 1061 },
michael@0 1062
michael@0 1063 handleCompletion : function (aReason) {
michael@0 1064 if (aCallbacks && aCallbacks.handleCompletion) {
michael@0 1065 aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
michael@0 1066 }
michael@0 1067 }
michael@0 1068 });
michael@0 1069 return pending;
michael@0 1070 },
michael@0 1071
michael@0 1072 get schemaVersion() {
michael@0 1073 return dbConnection.schemaVersion;
michael@0 1074 },
michael@0 1075
michael@0 1076 // This is used only so that the test can verify deleted table support.
michael@0 1077 get _supportsDeletedTable() {
michael@0 1078 return supportsDeletedTable;
michael@0 1079 },
michael@0 1080 set _supportsDeletedTable(val) {
michael@0 1081 supportsDeletedTable = val;
michael@0 1082 },
michael@0 1083
michael@0 1084 // The remaining methods are called by FormHistoryStartup.js
michael@0 1085 updatePrefs: function updatePrefs() {
michael@0 1086 Prefs.initialized = false;
michael@0 1087 },
michael@0 1088
michael@0 1089 expireOldEntries: function expireOldEntries() {
michael@0 1090 log("expireOldEntries");
michael@0 1091
michael@0 1092 // Determine how many days of history we're supposed to keep.
michael@0 1093 // Calculate expireTime in microseconds
michael@0 1094 let expireTime = (Date.now() - Prefs.expireDays * DAY_IN_MS) * 1000;
michael@0 1095
michael@0 1096 sendNotification("formhistory-beforeexpireoldentries", expireTime);
michael@0 1097
michael@0 1098 FormHistory.count({}, {
michael@0 1099 handleResult: function(aBeginningCount) {
michael@0 1100 expireOldEntriesDeletion(expireTime, aBeginningCount);
michael@0 1101 },
michael@0 1102 handleError: function(aError) {
michael@0 1103 log("expireStartCountFailure");
michael@0 1104 }
michael@0 1105 });
michael@0 1106 },
michael@0 1107
michael@0 1108 shutdown: function shutdown() { dbClose(true); }
michael@0 1109 };
michael@0 1110
michael@0 1111 // Prevent add-ons from redefining this API
michael@0 1112 Object.freeze(FormHistory);

mercurial