first take on storage refactor

This commit is contained in:
Paul Sawaya 2013-01-10 14:56:33 -08:00
Родитель 3ccf3962d1
Коммит bfa352d71b
8 изменённых файлов: 380 добавлений и 362 удалений

Просмотреть файл

@ -37,18 +37,14 @@ function formFillCurrentTab() {
function getPageDataForPopup(callback) {
getActiveTab(function(tab) {
var newURL = new Uri(tab.url);
getLoginsForSite(newURL.host(),function(logins) {
callback(logins);
});
callback(User.Logins.getForHostname(newURL.host()));
});
}
function formFillTab(tab) {
var newURL = new Uri(tab.url);
getLoginsForSite(newURL.host(),function(logins) {
chrome.tabs.sendMessage(tab.id,{
type: 'fill_form',
login: logins[0]
});
chrome.tabs.sendMessage(tab.id,{
type: 'fill_form',
login: User.Logins.getForHostname(newURL.host())[0]
});
}

Просмотреть файл

@ -18,17 +18,10 @@ function startFirstRunFlow() {
});
// Updates the browserAction popup, stores that we haven't
// yet completed first run in localStorage.
setIfDidFirstRun(false);
User.firstRun.setIfCompleted(false);
}
function closeFirstRunTab() {
chrome.tabs.remove(firstRunTab);
firstRunTab = -1;
}
function firstRunFinished() {
// Save the fact that the first run flow has been completed,
// so that the splash screen doesn't reopen the next time the
// add-on starts, and the regular interface appears in the browserAction.
setIfDidFirstRun(true);
}
}

Просмотреть файл

@ -10,16 +10,16 @@ var infobarHooks = {
'password_observed': function (notificationObj,infobarResponse) {
switch (infobarResponse.user_action) {
case 'save':
saveLoginToStorage(notificationObj.notification);
User.Logins.add(notificationObj.notification);
break;
case 'pin_lock':
notificationObj.notification.pinLocked = true;
saveLoginToStorage(notificationObj.notification);
User.Logins.add(notificationObj.notification);
break;
case 'never_for_this_site':
neverSaveOnSite(notificationObj.notification.hostname);
User.neverAsk.add(notificationObj.notification.hostname);
break;
default:
@ -41,6 +41,6 @@ var infobarHooks = {
}
},
'update_password': function(notificationObj,infobarResponse) {
saveLoginToStorage(notificationObj.notification);
User.Logins.add(notificationObj.notification);
}
}

Просмотреть файл

@ -9,16 +9,9 @@
initSkyCrane();
function initSkyCrane() {
// Load blacklisted sites from localStorage
loadNeverSaveOnSites();
// Load PIN lock state from localStorage
loadLoginsLock();
checkIfDidFirstRun(function(didFirstRun) {
console.log('didFirstRun: ', didFirstRun);
if (!didFirstRun) {
startFirstRunFlow();
}
});
if (!User.firstRun.wasCompleted()) {
startFirstRunFlow();
}
}
//
@ -26,119 +19,121 @@ function initSkyCrane() {
//
var messageHandlers = {
'add_login': function(message,tabID) {
var notificationObj = message;
// Check to see if the user disabled password saving on this site
if (neverSaveOnSites.indexOf(notificationObj.hostname) != -1) return;
notificationObj.type = 'password_observed';
notificationObj.hash = SHA1(notificationObj.password);
// Look for passwords in use on the current site
getLoginsForSite(notificationObj.hostname,function(logins) {
if (logins === undefined) logins = [];
var loginsForSameUsername = logins.filter(
function(l) { return l.username == notificationObj.username; }
);
if (loginsForSameUsername.length == 1) {
// User already has a login saved for this site.
if (loginsForSameUsername[0].password == notificationObj.password) {
// We're just logging into a site with an existing login. Bail.
return;
}
else {
// Prompt user to update password
notificationObj.type = 'update_password';
// If the existing login stored for this site was PIN locked,
// make sure this new one will be also.
notificationObj.pinLocked = logins[0].pinLocked;
}
}
// Has the user signed up for a Gombot account?
checkIfDidFirstRun(function(didFirstRun) {
if (didFirstRun) {
// Prompt the user to save the login
displayInfobar({
notify: true,
tabID: tabID,
notification: notificationObj
});
}
else {
// Browser not associated with Gombot account, offer
// to create one/log in.
displayInfobar({
notify: true,
tabID: tabID,
notification: {
type: 'signup_nag'
}
});
}
'add_login': function(message,tabID) {
var notificationObj = message;
// Check to see if the user disabled password saving on this site
User.NeverAsk.checkForHostname(notificationObj.hostname, function(shouldAsk) {
if (!shouldAsk) return;
notificationObj.type = 'password_observed';
notificationObj.hash = SHA1(notificationObj.password);
// Look for passwords in use on the current site
getLoginsForSite(notificationObj.hostname,function(logins) {
if (logins === undefined) logins = [];
var loginsForSameUsername = logins.filter(
function(l) { return l.username == notificationObj.username; }
);
if (loginsForSameUsername.length == 1) {
// User already has a login saved for this site.
if (loginsForSameUsername[0].password == notificationObj.password) {
// We're just logging into a site with an existing login. Bail.
return;
}
else {
// Prompt user to update password
notificationObj.type = 'update_password';
// If the existing login stored for this site was PIN locked,
// make sure this new one will be also.
notificationObj.pinLocked = logins[0].pinLocked;
}
}
// Has the user signed up for a Gombot account?
checkIfDidFirstRun(function(didFirstRun) {
if (didFirstRun) {
// Prompt the user to save the login
displayInfobar({
notify: true,
tabID: tabID,
notification: notificationObj
});
// TODO: Send new login to server
}
else {
// Browser not associated with Gombot account, offer
// to create one/log in.
displayInfobar({
notify: true,
tabID: tabID,
notification: {
type: 'signup_nag'
}
});
}
});
},
'observing_page': function(message,tabID) {
// See if there are login forms on this page. If not, there's nothing to do
// on the observing_page notification, so bail.
if (message.num_login_forms == 0) return;
// TODO: Send new login to server
});
})
},
'observing_page': function(message,tabID) {
// See if there are login forms on this page. If not, there's nothing to do
// on the observing_page notification, so bail.
if (message.num_login_forms == 0) return;
// Search for logins for this particular site
getLoginsForSite(message.hostname, function(logins) {
if (logins.length == 0) return;
if (logins.length == 1) {
// Is the login for this site PIN locked?
if (logins[0].pinLocked) {
// If it is, show the PIN entry infobar.
displayInfobar({
notify: true,
tabID: tabID,
notification: {
type: 'pin_entry',
// Include the tabID in the notification so the infobar handler
// can trigger autofill in the correct tab.
tabID: tabID
}
});
}
else {
// If it's not, form fill the page now.
chrome.tabs.sendMessage(tabID,{
type: 'fill_form',
login: logins[0]
});
}
// Search for logins for this particular site
getLoginsForSite(message.hostname, function(logins) {
if (logins.length == 0) return;
// If there's only a single login form on the page, we're fine. Otherwise,
// see if we were able to record an id or a name for the form when we observed
// the login.
// TODO: Check to see if the id/name is still valid
if (logins.length == 1) {
// Is the login for this site PIN locked?
if (logins[0].pinLocked) {
// If it is, show the PIN entry infobar.
displayInfobar({
notify: true,
tabID: tabID,
notification: {
type: 'pin_entry',
// Include the tabID in the notification so the infobar handler
// can trigger autofill in the correct tab.
tabID: tabID
}
});
}
else {
// If it's not, form fill the page now.
chrome.tabs.sendMessage(tabID,{
type: 'fill_form',
login: logins[0]
});
}
// If there's only a single login form on the page, we're fine. Otherwise,
// see if we were able to record an id or a name for the form when we observed
// the login.
// TODO: Check to see if the id/name is still valid
// If we remember specifics about a login form, check to see if it's there.
// If it is, offer to autologin.
if (logins[0].formEl && (logins[0].formEl.name || logins[0].formEl.id)) {
chrome.tabs.sendMessage(tabID,{
type: 'confirm_form_exists',
login: logins[0]
});
}
}
else {
// TODO: Prompt user for choice of logins
}
});
},
'validate_pin': function(message,tabID,sendResponse) {
sendResponse({
'is_valid': validatePIN(message.pin)
});
}
// If we remember specifics about a login form, check to see if it's there.
// If it is, offer to autologin.
if (logins[0].formEl && (logins[0].formEl.name || logins[0].formEl.id)) {
chrome.tabs.sendMessage(tabID,{
type: 'confirm_form_exists',
login: logins[0]
});
}
}
else {
// TODO: Prompt user for choice of logins
}
});
},
'validate_pin': function(message,tabID,sendResponse) {
sendResponse({
'is_valid': validatePIN(message.pin)
});
}
}
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
if (request.type && messageHandlers[request.type]) {
messageHandlers[request.type].call(messageHandlers,request.message,sender.tab.id,sendResponse);
}
if (request.type && messageHandlers[request.type]) {
messageHandlers[request.type].call(messageHandlers,request.message,sender.tab.id,sendResponse);
}
});
//
@ -146,58 +141,58 @@ chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
//
function displayInfobar(notificationObj) {
var infobarPaths = {
password_observed: "/infobars/remember_password_infobar.html",
update_password: "/infobars/update_password_infobar.html",
signup_nag: "/infobars/signup_nag_infobar.html",
pin_entry: "/infobars/pin_entry_infobar.html"
var infobarPaths = {
password_observed: "/infobars/remember_password_infobar.html",
update_password: "/infobars/update_password_infobar.html",
signup_nag: "/infobars/signup_nag_infobar.html",
pin_entry: "/infobars/pin_entry_infobar.html"
};
// Make sure we have a HTML infobar for this type of notification
if (!infobarPaths[notificationObj.notification.type]) return;
InfobarManager.run({
path: infobarPaths[notificationObj.notification.type],
tabId: notificationObj.tabID,
height: '32px'
}, genHandlerForNotification(notificationObj));
function genHandlerForNotification(notificationObj) {
return function(err,response) {
if (err) {
console.log(err);
return;
}
if (!response.type) return;
if (!infobarHooks[response.type]) {
console.log('Infobar returned unknown response type!');
return;
}
infobarHooks[response.type].call(infobarHooks,notificationObj,response);
};
// Make sure we have a HTML infobar for this type of notification
if (!infobarPaths[notificationObj.notification.type]) return;
InfobarManager.run({
path: infobarPaths[notificationObj.notification.type],
tabId: notificationObj.tabID,
height: '32px'
}, genHandlerForNotification(notificationObj));
function genHandlerForNotification(notificationObj) {
return function(err,response) {
if (err) {
console.log(err);
return;
}
if (!response.type) return;
if (!infobarHooks[response.type]) {
console.log('Infobar returned unknown response type!');
return;
}
infobarHooks[response.type].call(infobarHooks,notificationObj,response);
};
}
}
}
// Test function that spawns an example infobar on the current active tab.
function testInfobarNotification() {
getActiveTab(function(tab) {
console.log("tab url: ", tab.url,' ', tab);
displayInfobar({
notify: true,
tabID: tab.id,
notification: {
type: 'pin_entry',
formEl: {},
formSubmitURL: "",
hash: "bc74f4f071a5a33f00ab88a6d6385b5e6638b86c",
hostname: "t.nm.io",
httpRealm: null,
password: "green",
passwordField: {},
type: "password_observed",
username: "gombottest",
usernameField: {}
}
});
getActiveTab(function(tab) {
console.log("tab url: ", tab.url,' ', tab);
displayInfobar({
notify: true,
tabID: tab.id,
notification: {
type: 'pin_entry',
formEl: {},
formSubmitURL: "",
hash: "bc74f4f071a5a33f00ab88a6d6385b5e6638b86c",
hostname: "t.nm.io",
httpRealm: null,
password: "green",
passwordField: {},
type: "password_observed",
username: "gombottest",
usernameField: {}
}
});
});
}
//
@ -205,7 +200,6 @@ function testInfobarNotification() {
//
function validatePIN(_pin) {
// If there's no PIN set, accept.
if (!loginsLock || !loginsLock.pin) return true;
return _pin == loginsLock.pin;
// If there's no PIN set, accept. Otherwise, validate.
return (!Boolean(User.PIN.get())) || User.PIN.validate(_pin);
}

Просмотреть файл

@ -6,78 +6,93 @@
*
*/
//
// Getting and saving login data to/from localStorage
//
// List of hostnames the user has asked passwords not to be saved on.
// Kept in sync with local storage.
var neverSaveOnSites = [];
// This handles the low-level localStorage
var LLStorage = (function(){
return {
get: function(key, callback) {
chrome.storage.local.get(key, function(storageObj) {
callback(storageObj[key]);
});
},
save: function(key, data) {
var updatedObj = {};
updatedObj[key] = data;
chrome.storage.local.set(updatedObj);
}
}
})();
// The key for the document in localStorage which holds all of the user
// data synced with the server.
const LOCAL_STORAGE_KEY = 'gombot_user_data';
// Other type: 'pin' when PIN locking is enabled.
var loginsLock = {
type: 'none'
};
// Takes a callback, and passes it a boolean indicating whether
// or not the user has completed the first run flow.
// Note that this will return true if the user started the first
// run flow but did not complete it.
function checkIfDidFirstRun(callback) {
chrome.storage.local.get('did_first_run', function(storageObj) {
callback(Boolean(storageObj.did_first_run));
});
}
// Set (or unset) the flag indicating the user has finished the first
// run flow.
function setIfDidFirstRun(firstRunFinished) {
chrome.storage.local.set({
'did_first_run': Boolean(firstRunFinished)
var User = (function(Storage){
// In-memory storage for user data that gets synced.
var _logins = {};
var _neverAsk = {};
var _pin = null;
// In-memory storasge for user data that's persisted to localStorage, but not synced.
var _didFirstRun = false;
// The key for the document in localStorage which holds all of the user
// data synced with the server.
const SYNC_DOCUMENT_KEY = 'gombot_user_data';
const DATA_VERSION = 'identity.mozilla.com/gombot/v1/userData';
// NB: remove every call to fetchStoredData and updateStoredData before commit!
function fetchStoredData(callback) {
var keysToFetch = [
SYNC_DOCUMENT_KEY,
'did_first_run'
];
chrome.storage.local.get(SYNC_DOCUMENT_KEY, function(storageObj) {
var userData = storageObj;//[SYNC_DOCUMENT_KEY];
if (userData === undefined) {
userData = {
version: DATA_VERSION,
logins: {},
pin: loginsLock.pin || null,
neverAsk: {}
};
}
callback(userData);
});
}
// Updates the user's PIN in loginsLock and also updates localStorage.
function setAndSavePIN(pin) {
loginsLock = {
'type': 'pin',
'pin': pin
};
saveLoginsLock();
}
function loadLoginsLock() {
// Load PIN lock state from localStorage
}
// function updateStoredData(obj) {
// var updatedObj = {};
// updatedObj[SYNC_DOCUMENT_KEY] = obj;
// chrome.storage.local.set(updatedObj);
// }
function saveToLocalStorage() {
Storage.save(SYNC_DOCUMENT_KEY, {
version: DATA_VERSION,
logins: _logins || {},
pin: _pin || null,
neverAsk: _neverAsk || {}
});
Storage.save('did_first_run',_didFirstRun);
}
function loadFromLocalStorage() {
fetchStoredData(function(userData) {
if (userData['pin']) {
loginsLock = {
type: 'pin',
pin: userData.pin
};
}
else {
loginsLock = {
type: 'none'
};
}
_logins = userData[SYNC_DOCUMENT_KEY].logins;
_pin = userData[SYNC_DOCUMENT_KEY].logins;
_neverAsk = userData[SYNC_DOCUMENT_KEY].neverAsk;
_didFirstRun = userData['did_first_run'];
});
}
function saveLoginsLock() {
// Persist loginsLock to localStorage, to preserve PIN across sessions
fetchStoredData(function(userData) {
userData['pin'] = loginsLock.pin || null;
updateStoredData(userData);
});
}
function formatStoredLogin(login) {
return {
}
// Load from localStorage into memory when extension starts.
loadFromLocalStorage();
var userData = {};
var loginsObj = (function() {
function formatStoredLogin(login) {
return {
username: login.username,
password: login.password,
hostname: login.hostname,
@ -87,120 +102,140 @@ function formatStoredLogin(login) {
url: login['url'] || '',
pinLocked: login['pinLocked'] || false,
supplementalInformation: login['supplementalInformation'] || {}
};
}
function fetchStoredData(callback) {
chrome.storage.local.get(LOCAL_STORAGE_KEY, function(storageObj) {
var userData = storageObj[LOCAL_STORAGE_KEY];
if (userData === undefined) {
userData = {
version: "identity.mozilla.com/gombot/v1/userData",
logins: {},
pin: loginsLock.pin || null,
neverAsk: {}
};
}
callback(userData);
});
}
function updateStoredData(obj) {
var updatedObj = {};
updatedObj[LOCAL_STORAGE_KEY] = obj;
chrome.storage.local.set(updatedObj);
}
// Save a new login object to localStorage
function saveLoginToStorage(newLogin) {
var loginObj = formatStoredLogin(newLogin);
fetchStoredData(function(userData) {
};
}
return {
// Save a new login object to localStorage
add: function(newLogin) {
var loginObj = formatStoredLogin(newLogin);
// Filter for logins with the same username and hostname.
var existingLoginsForHost = userData.logins[newLogin.hostname] || [];
userData.logins[newLogin.hostname] =
existingLoginsForHost.filter(function(_login) {
return _login.username != loginObj.username;
});
userData.logins[newLogin.hostname].push(loginObj);
updateStoredData(userData);
});
}
function loadNeverSaveOnSites() {
getNeverSaveOnSites(function(siteNames) { neverSaveOnSites = siteNames; });
}
// Add a hostname to the list of sites for which we never
// prompt to save passwords
function neverSaveOnSite(siteHostname) {
fetchStoredData(function(userData) {
if (!(siteHostname in userData.neverAsk)) {
userData.neverAsk[siteHostname] = 'all';
updateStoredData(userData);
loadNeverSaveOnSites();
}
});
}
// Takes a callback, and passes it a list of domains the user
// has elected to never save logins on.
function getNeverSaveOnSites(callback) {
fetchStoredData(function(userData) {
callback(_.keys(userData.neverAsk));
});
}
// Takes a hostname and a callback, and passes it a list of login
// objects the user has saved for that domain.
function getLoginsForSite(hostname,callback) {
fetchStoredData(function(userData) {
callback(userData.logins[hostname] || []);
});
}
var existingLoginsForHost = _logins[newLogin.hostname] || [];
_logins[newLogin.hostname] =
existingLoginsForHost.filter(function(_login) {
return _login.username != loginObj.username;
});
_logins[newLogin.hostname].push(loginObj);
saveToLocalStorage();
},
// Takes a hostname and a callback, and passes it a list of login
// objects the user has saved for that domain.
getForHostname: function(hostname/*,callback*/) {
return (_logins[hostname] || []);
},
// Mainly for debugging purposes.
function deleteLoginsForSite(hostname) {
fetchStoredData(function(userData) {
delete userData[hostname];
updateStoredData(userData);
});
}
// Mainly for debugging purposes.
deleteForHostname: function(hostname) {
delete _logins[hostname];
saveToLocalStorage();
}
};
})();
var neverAskObj = (function() {
return {
// Add a hostname to the list of sites for which we never
// prompt to save passwords
add: function(siteHostname) {
if (!(siteHostname in _neverAsk)) {
_neverAsk[siteHostname] = 'all';
saveToLocalStorage();
}
},
// Takes a callback, and passes it a list of domains the user
// has elected to never save logins on.
get: function(/*callback*/) {
return _.keys(userData.neverAsk);
},
// Takes a hostname and a callback. Passes callback a boolean,
// indicating if the user *should* be asked about logins
// on that domain.
checkForHostname: function(hostname/*,callback*/) {
return !(hostname in this.get());
}
};
})();
var firstRunObj = (function(){
return {
wasCompleted: function() {
// Takes a callback, and passes it a boolean indicating whether
// or not the user has completed the first run flow.
// Note that this will return true if the user started the first
// run flow but did not complete it.
return _didFirstRun;
},
setIfCompleted: function(firstRunFinished) {
//setIfDidFirstRun
// Set (or unset) the flag indicating the user has finished the first
// run flow.
_didFirstRun = firstRunFinished;
saveToLocalStorage();
}
};
})();
var pinObj = (function() {
return {
validate: function(testPin) {
return testPin == _pin;
},
get: function() {
return _pin;
},
set: function(newPIN) {
_pin = newPIN || null;
saveToLocalStorage();
}
}
});
return {
Logins: loginsObj,
neverAsk: neverAskObj,
firstRun: firstRunObj,
PIN: pinObj
};
})(LLStorage);
//
// Getting and saving login data to/from localStorage
//
// Returns a string of a comma separated value file containing the hostname, username,
// and password for each login the user has saved.
function getLoginsCSV(callback) {
// Add header
var retVal = "hostname,username,password\n";
fetchStoredData(function(userData) {
for (var item in _.keys(userData.logins)) {
for (var login in userData.logins[item]) {
retVal += login.hostname + ',' + login.username
+ ',' + login.password + '\n';
}
}
callback(retVal);
});
// Add header
var retVal = "hostname,username,password\n";
fetchStoredData(function(userData) {
for (var item in _.keys(userData.logins)) {
for (var login in userData.logins[item]) {
retVal += login.hostname + ',' + login.username
+ ',' + login.password + '\n';
}
}
callback(retVal);
});
}
// Dump localStorage to CSV file, for debugging purposes.
function downloadExportDataFile() {
// Get entire content of localStorage
// NB: This contains all of the user's passwords in plaintext, as well as
// their PIN and not-so-useful flags like did_first_run.
getLoginsCSV(function(loginsCSV) {
// Turn storageObj into a blob
var blob = new window.Blob([loginsCSV], {type: 'text/csv'});
// Get entire content of localStorage
// NB: This contains all of the user's passwords in plaintext, as well as
// their PIN and not-so-useful flags like did_first_run.
getLoginsCSV(function(loginsCSV) {
// Turn storageObj into a blob
var blob = new window.Blob([loginsCSV], {type: 'text/csv'});
// Creates a link that opens the blob on the background page,
// and then clicks it. Cribbed from:
// http://stackoverflow.com/questions/4845215/making-a-chrome-extension-download-a-file
var a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = 'passwords_dump.csv';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
delete a;// we don't need this anymore
});
// Creates a link that opens the blob on the background page,
// and then clicks it. Cribbed from:
// http://stackoverflow.com/questions/4845215/making-a-chrome-extension-download-a-file
var a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = 'passwords_dump.csv';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
delete a;// we don't need this anymore
});
}

@ -1 +1 @@
Subproject commit 0e3bb2627c521bf79c69a8f60e4bfa9a8e9bc82e
Subproject commit fbe957e8631755100661d321a4eb640d797b6254

Просмотреть файл

@ -1,7 +1,7 @@
{
"name": "Gombot Alpha",
"manifest_version": 2,
"version": "0.1.5",
"version": "0.1.7",
"description": "Tired of remembering your usernames and passwords? Let Gombot do the work for you!",
"permissions": [
"tabs",

Просмотреть файл

@ -38,8 +38,8 @@ $(document).ready(function() {
var backgroundPage = chrome.extension.getBackgroundPage();
// Set user PIN
backgroundPage.setAndSavePIN($('[name="pin"]').get()[0].value);
backgroundPage.firstRunFinished();
backgroundPage.User.PIN.set($('[name="pin"]').get()[0].value);
backgroundPage.User.firstRun.setIfCompleted(true);
}
});
});