Bug 1304322 - Refactor LoginStore.jsm to make it reusable for Form Autofill; r=MattN draft
authorLuke Chang <lchang@mozilla.com>
Wed, 21 Sep 2016 17:29:48 +0800
changeset 422060 c22f5e8d79e0727ebe562e746fa1f8232600f8aa
parent 415066 fc69febcbf6c0dcc4b3dfc7a346d8d348798a65f
child 533242 cd3fbb2a30b8251757954606ef511e29c0d3d939
push id31673
push userbmo:lchang@mozilla.com
push dateFri, 07 Oct 2016 09:27:45 +0000
reviewersMattN
bugs1304322
milestone51.0a1
Bug 1304322 - Refactor LoginStore.jsm to make it reusable for Form Autofill; r=MattN MozReview-Commit-ID: Kx8aALU7fu2
toolkit/components/passwordmgr/LoginStore.jsm
toolkit/modules/JSONFile.jsm
toolkit/modules/moz.build
--- a/toolkit/components/passwordmgr/LoginStore.jsm
+++ b/toolkit/components/passwordmgr/LoginStore.jsm
@@ -1,32 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
- * Handles serialization of login-related data and persistence into a file.
- *
- * This modules handles the raw data stored in JavaScript serializable objects,
- * and contains no special validation or query logic, that is handled entirely
- * by "storage.js" instead.
- *
- * The data can be manipulated only after it has been loaded from disk.  The
- * load process can happen asynchronously, through the "load" method, or
- * synchronously, through "ensureDataReady".  After any modification, the
- * "saveSoon" method must be called to flush the data to disk asynchronously.
- *
- * The raw data should be manipulated synchronously, without waiting for the
- * event loop or for promise resolution, so that the saved file is always
- * consistent.  This synchronous approach also simplifies the query and update
- * logic.  For example, it is possible to find an object and modify it
- * immediately without caring whether other code modifies it in the meantime.
- *
- * An asynchronous shutdown observer makes sure that data is always saved before
- * the browser is closed.  The data cannot be modified during shutdown.
+ * Handles serialization of the data and persistence into a file.
  *
  * The file is stored in JSON format, without indentation, using UTF-8 encoding.
  * With indentation applied, the file would look like this:
  *
  * {
  *   "logins": [
  *     {
  *       "id": 2,
@@ -65,44 +47,20 @@ this.EXPORTED_SYMBOLS = [
 ];
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
-                                  "resource://gre/modules/AsyncShutdown.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
-                                  "resource://gre/modules/DeferredTask.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-                                  "resource://gre/modules/FileUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
-                                  "resource://gre/modules/osfile.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
-  return new TextDecoder();
-});
-
-XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
-  return new TextEncoder();
-});
-
-const FileInputStream =
-      Components.Constructor("@mozilla.org/network/file-input-stream;1",
-                             "nsIFileInputStream", "init");
-
-/**
- * Delay between a change to the login data and the related save operation.
- */
-const kSaveDelayMs = 1500;
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+                                  "resource://gre/modules/JSONFile.jsm");
 
 /**
  * Current data version assigned by the code that last touched the data.
  *
  * This number should be updated only when it is important to understand whether
  * an old version of the code has touched the data, for example to execute an
  * update logic.  In most cases, this number should not be changed, in
  * particular when no special one-time update logic is needed.
@@ -114,217 +72,63 @@ const kDataVersion = 2;
 
 // The permission type we store in the permission manager.
 const PERMISSION_SAVE_LOGINS = "login-saving";
 
 ////////////////////////////////////////////////////////////////////////////////
 //// LoginStore
 
 /**
- * Handles serialization of login-related data and persistence into a file.
+ * Inherits from JSONFile and handles serialization of login-related data and
+ * persistence into a file.
  *
  * @param aPath
  *        String containing the file path where data should be saved.
  */
 function LoginStore(aPath) {
-  this.path = aPath;
-
-  this._saver = new DeferredTask(() => this.save(), kSaveDelayMs);
-  AsyncShutdown.profileBeforeChange.addBlocker("Login store: writing data",
-                                               () => this._saver.finalize());
+  JSONFile.call(this, {
+    path: aPath,
+    dataPostProcessor: this._dataPostProcessor.bind(this)
+  });
 }
 
-LoginStore.prototype = {
-  /**
-   * String containing the file path where data should be saved.
-   */
-  path: "",
-
-  /**
-   * Serializable object containing the login-related data.  This is populated
-   * directly with the data loaded from the file, and is saved without
-   * modifications.
-   *
-   * This contains one property for each list.
-   */
-  data: null,
+LoginStore.prototype = Object.create(JSONFile.prototype);
+LoginStore.prototype.constructor = LoginStore;
 
-  /**
-   * True when data has been loaded.
-   */
-  dataReady: false,
-
-  /**
-   * Loads persistent data from the file to memory.
-   *
-   * @return {Promise}
-   * @resolves When the operation finished successfully.
-   * @rejects JavaScript exception.
-   */
-  load() {
-    return Task.spawn(function* () {
-      try {
-        let bytes = yield OS.File.read(this.path);
-
-        // If synchronous loading happened in the meantime, exit now.
-        if (this.dataReady) {
-          return;
-        }
+/**
+ * Synchronously work on the data just loaded into memory.
+ */
+LoginStore.prototype._dataPostProcessor = function(data) {
+  // Create any arrays that are not present in the saved file.
+  if (!data.logins) {
+    data.logins = [];
+  }
 
-        this.data = JSON.parse(gTextDecoder.decode(bytes));
-      } catch (ex) {
-        // If an exception occurred because the file did not exist, we should
-        // just start with new data.  Other errors may indicate that the file is
-        // corrupt, thus we move it to a backup location before allowing it to
-        // be overwritten by an empty file.
-        if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
-          Cu.reportError(ex);
+  // Stub needed for login imports before data has been migrated.
+  if (!data.disabledHosts) {
+    data.disabledHosts = [];
+  }
 
-          // Move the original file to a backup location, ignoring errors.
-          try {
-            let openInfo = yield OS.File.openUnique(this.path + ".corrupt",
-                                                    { humanReadable: true });
-            yield openInfo.file.close();
-            yield OS.File.move(this.path, openInfo.path);
-          } catch (e2) {
-            Cu.reportError(e2);
-          }
-        }
+  if (data.version === 1) {
+    this._migrateDisabledHosts(data);
+  }
 
-        // In some rare cases it's possible for logins to have been added to
-        // our database between the call to OS.File.read and when we've been
-        // notified that there was a problem with it. In that case, leave the
-        // synchronously-added data alone. See bug 1029128, comment 4.
-        if (this.dataReady) {
-          return;
-        }
+  // Indicate that the current version of the code has touched the file.
+  data.version = kDataVersion;
+
+  return data;
+};
 
-        // In any case, initialize a new object to host the data.
-        this.data = {
-          nextId: 1,
-        };
-      }
-
-      this._processLoadedData();
-    }.bind(this));
-  },
-
-  /**
-   * Loads persistent data from the file to memory, synchronously.
-   */
-  ensureDataReady() {
-    if (this.dataReady) {
-      return;
-    }
-
+/**
+ * Migrates disabled hosts to the permission manager.
+ */
+LoginStore.prototype._migrateDisabledHosts = function (data) {
+  for (let host of this.data.disabledHosts) {
     try {
-      // This reads the file and automatically detects the UTF-8 encoding.
-      let inputStream = new FileInputStream(new FileUtils.File(this.path),
-                                            FileUtils.MODE_RDONLY,
-                                            FileUtils.PERMS_FILE, 0);
-      try {
-        let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
-        this.data = json.decodeFromStream(inputStream,
-                                          inputStream.available());
-      } finally {
-        inputStream.close();
-      }
-    } catch (ex) {
-      // If an exception occurred because the file did not exist, we should just
-      // start with new data.  Other errors may indicate that the file is
-      // corrupt, thus we move it to a backup location before allowing it to be
-      // overwritten by an empty file.
-      if (!(ex instanceof Components.Exception &&
-            ex.result == Cr.NS_ERROR_FILE_NOT_FOUND)) {
-        Cu.reportError(ex);
-        // Move the original file to a backup location, ignoring errors.
-        try {
-          let originalFile = new FileUtils.File(this.path);
-          let backupFile = originalFile.clone();
-          backupFile.leafName += ".corrupt";
-          backupFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE,
-                                  FileUtils.PERMS_FILE);
-          backupFile.remove(false);
-          originalFile.moveTo(backupFile.parent, backupFile.leafName);
-        } catch (e2) {
-          Cu.reportError(e2);
-        }
-      }
-
-      // In any case, initialize a new object to host the data.
-      this.data = {
-        nextId: 1,
-      };
+      let uri = Services.io.newURI(host, null, null);
+      Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+    } catch (e) {
+      Cu.reportError(e);
     }
-
-    this._processLoadedData();
-  },
-
-  /**
-   * Synchronously work on the data just loaded into memory.
-   */
-  _processLoadedData() {
-    // Create any arrays that are not present in the saved file.
-    if (!this.data.logins) {
-      this.data.logins = [];
-    }
-
-    // Stub needed for login imports before data has been migrated.
-    if (!this.data.disabledHosts) {
-      this.data.disabledHosts = [];
-    }
+  }
 
-    if (this.data.version === 1) {
-      this._migrateDisabledHosts();
-    }
-
-    // Indicate that the current version of the code has touched the file.
-    this.data.version = kDataVersion;
-
-    this.dataReady = true;
-  },
-
-  /**
-   * Migrates disabled hosts to the permission manager.
-   */
-  _migrateDisabledHosts: function () {
-    for (let host of this.data.disabledHosts) {
-      try {
-        let uri = Services.io.newURI(host, null, null);
-        Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
-      } catch (e) {
-        Cu.reportError(e);
-      }
-    }
-
-    delete this.data.disabledHosts;
-  },
-
-  /**
-   * Called when the data changed, this triggers asynchronous serialization.
-   */
-  saveSoon() {
-    return this._saver.arm();
-  },
-
-  /**
-   * DeferredTask that handles the save operation.
-   */
-  _saver: null,
-
-  /**
-   * Saves persistent data from memory to the file.
-   *
-   * If an error occurs, the previous file is not deleted.
-   *
-   * @return {Promise}
-   * @resolves When the operation finished successfully.
-   * @rejects JavaScript exception.
-   */
-  save() {
-    return Task.spawn(function* () {
-      // Create or overwrite the file.
-      let bytes = gTextEncoder.encode(JSON.stringify(this.data));
-      yield OS.File.writeAtomic(this.path, bytes,
-                                { tmpPath: this.path + ".tmp" });
-    }.bind(this));
-  },
+  delete this.data.disabledHosts;
 };
copy from toolkit/components/passwordmgr/LoginStore.jsm
copy to toolkit/modules/JSONFile.jsm
--- a/toolkit/components/passwordmgr/LoginStore.jsm
+++ b/toolkit/modules/JSONFile.jsm
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
- * Handles serialization of login-related data and persistence into a file.
+ * Handles serialization of the data and persistence into a file.
  *
  * This modules handles the raw data stored in JavaScript serializable objects,
  * and contains no special validation or query logic, that is handled entirely
  * by "storage.js" instead.
  *
  * The data can be manipulated only after it has been loaded from disk.  The
  * load process can happen asynchronously, through the "load" method, or
  * synchronously, through "ensureDataReady".  After any modification, the
@@ -16,57 +16,25 @@
  *
  * The raw data should be manipulated synchronously, without waiting for the
  * event loop or for promise resolution, so that the saved file is always
  * consistent.  This synchronous approach also simplifies the query and update
  * logic.  For example, it is possible to find an object and modify it
  * immediately without caring whether other code modifies it in the meantime.
  *
  * An asynchronous shutdown observer makes sure that data is always saved before
- * the browser is closed.  The data cannot be modified during shutdown.
+ * the browser is closed. The data cannot be modified during shutdown.
  *
  * The file is stored in JSON format, without indentation, using UTF-8 encoding.
- * With indentation applied, the file would look like this:
- *
- * {
- *   "logins": [
- *     {
- *       "id": 2,
- *       "hostname": "http://www.example.com",
- *       "httpRealm": null,
- *       "formSubmitURL": "http://www.example.com/submit-url",
- *       "usernameField": "username_field",
- *       "passwordField": "password_field",
- *       "encryptedUsername": "...",
- *       "encryptedPassword": "...",
- *       "guid": "...",
- *       "encType": 1,
- *       "timeCreated": 1262304000000,
- *       "timeLastUsed": 1262304000000,
- *       "timePasswordChanged": 1262476800000,
- *       "timesUsed": 1
- *     },
- *     {
- *       "id": 4,
- *       (...)
- *     }
- *   ],
- *   "disabledHosts": [
- *     "http://www.example.org",
- *     "http://www.example.net"
- *   ],
- *   "nextId": 10,
- *   "version": 1
- * }
  */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
-  "LoginStore",
+  "JSONFile",
 ];
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -90,63 +58,61 @@ XPCOMUtils.defineLazyGetter(this, "gText
   return new TextEncoder();
 });
 
 const FileInputStream =
       Components.Constructor("@mozilla.org/network/file-input-stream;1",
                              "nsIFileInputStream", "init");
 
 /**
- * Delay between a change to the login data and the related save operation.
+ * Delay between a change to the data and the related save operation.
  */
 const kSaveDelayMs = 1500;
 
-/**
- * Current data version assigned by the code that last touched the data.
- *
- * This number should be updated only when it is important to understand whether
- * an old version of the code has touched the data, for example to execute an
- * update logic.  In most cases, this number should not be changed, in
- * particular when no special one-time update logic is needed.
- *
- * For example, this number should NOT be changed when a new optional field is
- * added to a login entry.
- */
-const kDataVersion = 2;
-
-// The permission type we store in the permission manager.
-const PERMISSION_SAVE_LOGINS = "login-saving";
-
 ////////////////////////////////////////////////////////////////////////////////
-//// LoginStore
+//// JSONFile
 
 /**
- * Handles serialization of login-related data and persistence into a file.
+ * Handles serialization of the data and persistence into a file.
  *
- * @param aPath
- *        String containing the file path where data should be saved.
+ * @param config An object containing following members:
+ *        - path: String containing the file path where data should be saved.
+ *        - dataPostProcessor: Function triggered when data is just loaded. The
+ *                             data object will be passed as the first argument
+ *                             and should be returned no matter it's modified or
+ *                             not.
+ *        - saveDelayMs: Number indicating the delay (in milliseconds) between a
+ *                       change to the data and the related save operation. The
+ *                       default value will be applied if omitted.
  */
-function LoginStore(aPath) {
-  this.path = aPath;
+function JSONFile(config) {
+  this.path = config.path;
+
+  if (typeof config.dataPostProcessor === "function") {
+    this._dataPostProcessor = config.dataPostProcessor;
+  }
 
-  this._saver = new DeferredTask(() => this.save(), kSaveDelayMs);
-  AsyncShutdown.profileBeforeChange.addBlocker("Login store: writing data",
+  if (config.saveDelayMs === undefined) {
+    config.saveDelayMs = kSaveDelayMs;
+  }
+  this._saver = new DeferredTask(() => this.save(), config.saveDelayMs);
+
+  AsyncShutdown.profileBeforeChange.addBlocker("JSON store: writing data",
                                                () => this._saver.finalize());
 }
 
-LoginStore.prototype = {
+JSONFile.prototype = {
   /**
    * String containing the file path where data should be saved.
    */
   path: "",
 
   /**
-   * Serializable object containing the login-related data.  This is populated
-   * directly with the data loaded from the file, and is saved without
-   * modifications.
+   * Serializable object containing the data.  This is populated directly with
+   * the data loaded from the file, and is saved without modifications.
    *
    * This contains one property for each list.
    */
   data: null,
 
   /**
    * True when data has been loaded.
    */
@@ -257,53 +223,23 @@ LoginStore.prototype = {
 
     this._processLoadedData();
   },
 
   /**
    * Synchronously work on the data just loaded into memory.
    */
   _processLoadedData() {
-    // Create any arrays that are not present in the saved file.
-    if (!this.data.logins) {
-      this.data.logins = [];
-    }
-
-    // Stub needed for login imports before data has been migrated.
-    if (!this.data.disabledHosts) {
-      this.data.disabledHosts = [];
-    }
-
-    if (this.data.version === 1) {
-      this._migrateDisabledHosts();
+    if (this._dataPostProcessor) {
+      this.data = this._dataPostProcessor(this.data);
     }
-
-    // Indicate that the current version of the code has touched the file.
-    this.data.version = kDataVersion;
-
     this.dataReady = true;
   },
 
   /**
-   * Migrates disabled hosts to the permission manager.
-   */
-  _migrateDisabledHosts: function () {
-    for (let host of this.data.disabledHosts) {
-      try {
-        let uri = Services.io.newURI(host, null, null);
-        Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
-      } catch (e) {
-        Cu.reportError(e);
-      }
-    }
-
-    delete this.data.disabledHosts;
-  },
-
-  /**
    * Called when the data changed, this triggers asynchronous serialization.
    */
   saveSoon() {
     return this._saver.arm();
   },
 
   /**
    * DeferredTask that handles the save operation.
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -44,16 +44,17 @@ EXTRA_JS_MODULES += [
     'FinderIterator.jsm',
     'Geometry.jsm',
     'GMPInstallManager.jsm',
     'GMPUtils.jsm',
     'Http.jsm',
     'InlineSpellChecker.jsm',
     'InlineSpellCheckerContent.jsm',
     'Integration.jsm',
+    'JSONFile.jsm',
     'LoadContextInfo.jsm',
     'Locale.jsm',
     'Log.jsm',
     'Memory.jsm',
     'NewTabUtils.jsm',
     'NLP.jsm',
     'ObjectUtils.jsm',
     'PageMenu.jsm',