Files
tubestation/browser/extensions/formautofill/ProfileStorage.jsm
2016-12-16 01:23:48 -05:00

252 lines
6.1 KiB
JavaScript

/* 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/. */
/*
* Implements an interface of the storage of Form Autofill.
*
* The data is stored in JSON format, without indentation, using UTF-8 encoding.
* With indentation applied, the file would look like this:
*
* {
* version: 1,
* profiles: [
* {
* guid, // 12 character...
*
* // profile
* organization, // Company
* streetAddress, // (Multiline)
* addressLevel2, // City/Town
* addressLevel1, // Province (Standardized code if possible)
* postalCode,
* country, // ISO 3166
* tel,
* email,
*
* // metadata
* timeCreated, // in ms
* timeLastUsed, // in ms
* timeLastModified, // in ms
* timesUsed
* },
* {
* // ...
* }
* ]
* }
*/
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
"resource://gre/modules/JSONFile.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const SCHEMA_VERSION = 1;
// Name-related fields will be handled in follow-up bugs due to the complexity.
const VALID_FIELDS = [
"organization",
"streetAddress",
"addressLevel2",
"addressLevel1",
"postalCode",
"country",
"tel",
"email",
];
function ProfileStorage(path) {
this._path = path;
}
ProfileStorage.prototype = {
/**
* Loads the profile data from file to memory.
*
* @returns {Promise}
* @resolves When the operation finished successfully.
* @rejects JavaScript exception.
*/
initialize() {
this._store = new JSONFile({
path: this._path,
dataPostProcessor: this._dataPostProcessor.bind(this),
});
return this._store.load();
},
/**
* Adds a new profile.
*
* @param {Profile} profile
* The new profile for saving.
*/
add(profile) {
this._store.ensureDataReady();
let profileToSave = this._normalizeProfile(profile);
profileToSave.guid = gUUIDGenerator.generateUUID().toString()
.replace(/[{}-]/g, "").substring(0, 12);
// Metadata
let now = Date.now();
profileToSave.timeCreated = now;
profileToSave.timeLastModified = now;
profileToSave.timeLastUsed = 0;
profileToSave.timesUsed = 0;
this._store.data.profiles.push(profileToSave);
this._store.saveSoon();
},
/**
* Update the specified profile.
*
* @param {string} guid
* Indicates which profile to update.
* @param {Profile} profile
* The new profile used to overwrite the old one.
*/
update(guid, profile) {
this._store.ensureDataReady();
let profileFound = this._findByGUID(guid);
if (!profileFound) {
throw new Error("No matching profile.");
}
let profileToUpdate = this._normalizeProfile(profile);
for (let field of VALID_FIELDS) {
if (profileToUpdate[field] !== undefined) {
profileFound[field] = profileToUpdate[field];
} else {
delete profileFound[field];
}
}
profileFound.timeLastModified = Date.now();
this._store.saveSoon();
},
/**
* Notifies the stroage of the use of the specified profile, so we can update
* the metadata accordingly.
*
* @param {string} guid
* Indicates which profile to be notified.
*/
notifyUsed(guid) {
this._store.ensureDataReady();
let profileFound = this._findByGUID(guid);
if (!profileFound) {
throw new Error("No matching profile.");
}
profileFound.timesUsed++;
profileFound.timeLastUsed = Date.now();
this._store.saveSoon();
},
/**
* Removes the specified profile. No error occurs if the profile isn't found.
*
* @param {string} guid
* Indicates which profile to remove.
*/
remove(guid) {
this._store.ensureDataReady();
this._store.data.profiles =
this._store.data.profiles.filter(profile => profile.guid != guid);
this._store.saveSoon();
},
/**
* Returns the profile with the specified GUID.
*
* @param {string} guid
* Indicates which profile to retrieve.
* @returns {Profile}
* A clone of the profile.
*/
get(guid) {
this._store.ensureDataReady();
let profileFound = this._findByGUID(guid);
if (!profileFound) {
throw new Error("No matching profile.");
}
// Profile is cloned to avoid accidental modifications from outside.
return this._clone(profileFound);
},
/**
* Returns all profiles.
*
* @returns {Array.<Profile>}
* An array containing clones of all profiles.
*/
getAll() {
this._store.ensureDataReady();
// Profiles are cloned to avoid accidental modifications from outside.
return this._store.data.profiles.map(this._clone);
},
_clone(profile) {
return Object.assign({}, profile);
},
_findByGUID(guid) {
return this._store.data.profiles.find(profile => profile.guid == guid);
},
_normalizeProfile(profile) {
let result = {};
for (let key in profile) {
if (!VALID_FIELDS.includes(key)) {
throw new Error(`"${key}" is not a valid field.`);
}
if (typeof profile[key] !== "string" &&
typeof profile[key] !== "number") {
throw new Error(`"${key}" contains invalid data type.`);
}
result[key] = profile[key];
}
return result;
},
_dataPostProcessor(data) {
data.version = SCHEMA_VERSION;
if (!data.profiles) {
data.profiles = [];
}
return data;
},
// For test only.
_saveImmediately() {
return this._store._save();
},
};
this.EXPORTED_SYMBOLS = ["ProfileStorage"];