Bug 479894 - Add a property-bag based searchLogins API to login manager. r=dolske, sr=vlad

This commit is contained in:
Paul O'Shannessy 2009-04-17 16:12:46 -07:00
parent 506999dce8
commit 68ccc3549c
7 changed files with 397 additions and 85 deletions

View File

@ -42,8 +42,9 @@ interface nsILoginInfo;
interface nsIAutoCompleteResult;
interface nsIDOMHTMLInputElement;
interface nsIDOMHTMLFormElement;
interface nsIPropertyBag;
[scriptable, uuid(9c78bfc1-422b-4f4f-ba09-f7eb3c4e72b2)]
[scriptable, uuid(30534ff7-fb95-45c5-8336-5448638f2aa1)]
interface nsILoginManager : nsISupports {
@ -239,6 +240,28 @@ interface nsILoginManager : nsISupports {
* @return Success of attempt fill form
*/
boolean fillForm(in nsIDOMHTMLFormElement aForm);
/**
* Search for logins in the login manager. An array is always returned;
* if there are no logins the array is empty.
*
* @param count
* The number of elements in the array. JS callers can simply use
* the array's .length property, and supply an dummy object for
* this out param. For example: |searchLogins({}, matchData)|
* @param matchData
* The data used to search. This does not follow the same
* requirements as findLogins for those fields. Wildcard matches are
* simply not specified.
* @param logins
* An array of nsILoginInfo objects.
*
* NOTE: This can be called from JS as:
* var logins = pwmgr.searchLogins({}, matchData);
* (|logins| is an array).
*/
void searchLogins(out unsigned long count, in nsIPropertyBag matchData,
[retval, array, size_is(count)] out nsILoginInfo logins);
};
%{C++

View File

@ -39,8 +39,9 @@
interface nsIFile;
interface nsILoginInfo;
interface nsIPropertyBag;
[scriptable, uuid(199ebbff-4656-4a18-8da9-9401c64619f9)]
[scriptable, uuid(e66c97cd-3bcf-4eee-9937-38f650372d77)]
/*
* NOTE: This interface is intended to be implemented by modules
@ -164,6 +165,29 @@ interface nsILoginManagerStorage : nsISupports {
[retval, array, size_is(count)] out nsILoginInfo logins);
/**
* Search for logins in the login manager. An array is always returned;
* if there are no logins the array is empty.
*
* @param count
* The number of elements in the array. JS callers can simply use
* the array's .length property, and supply an dummy object for
* this out param. For example: |searchLogins({}, matchData)|
* @param matchData
* The data used to search. This does not follow the same
* requirements as findLogins for those fields. Wildcard matches are
* simply not specified.
* @param logins
* An array of nsILoginInfo objects.
*
* NOTE: This can be called from JS as:
* var logins = pwmgr.searchLogins({}, matchData);
* (|logins| is an array).
*/
void searchLogins(out unsigned long count, in nsIPropertyBag matchData,
[retval, array, size_is(count)] out nsILoginInfo logins);
/**
* Obtain a list of all hosts for which password saving is disabled.
*

View File

@ -509,6 +509,21 @@ LoginManager.prototype = {
},
/*
* searchLogins
*
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
* JavaScript object and decrypt the results.
*
* Returns an array of decrypted nsILoginInfo.
*/
searchLogins : function(count, matchData) {
this.log("Searching for logins");
return this._storage.searchLogins(count, matchData);
},
/*
* countLogins
*

View File

@ -373,6 +373,18 @@ LoginManagerStorage_legacy.prototype = {
},
/*
* searchLogins
*
* Not implemented. This interface was added to perform arbitrary searches.
* Since the legacy storage module is no longer used, there is no need to
* implement it here.
*/
searchLogins : function (count, matchData) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/*
* removeAllLogins
*

View File

@ -135,6 +135,7 @@ LoginManagerStorage_mozStorage.prototype = {
"guid TEXT," +
"encType INTEGER",
// Changes must be reflected in this._dbAreExpectedColumnsPresent
// and this._searchLogins
moz_disabledHosts: "id INTEGER PRIMARY KEY," +
"hostname TEXT UNIQUE ON CONFLICT REPLACE",
},
@ -480,11 +481,11 @@ LoginManagerStorage_mozStorage.prototype = {
/*
* getAllLogins
*
* Returns an array of nsAccountInfo.
* Returns an array of nsILoginInfo.
*/
getAllLogins : function (count) {
let userCanceled;
let [logins, ids] = this._queryLogins("", "", "");
let [logins, ids] = this._searchLogins({});
// decrypt entries for caller.
[logins, userCanceled] = this._decryptLogins(logins);
@ -511,6 +512,120 @@ LoginManagerStorage_mozStorage.prototype = {
},
/*
* searchLogins
*
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
* JavaScript object and decrypt the results.
*
* Returns an array of decrypted nsILoginInfo.
*/
searchLogins : function(count, matchData) {
let realMatchData = {};
// Convert nsIPropertyBag to normal JS object
let propEnum = matchData.enumerator;
while (propEnum.hasMoreElements()) {
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
realMatchData[prop.name] = prop.value;
}
let [logins, ids] = this._searchLogins(realMatchData);
let userCanceled;
// Decrypt entries found for the caller.
[logins, userCanceled] = this._decryptLogins(logins);
if (userCanceled)
throw "User canceled Master Password entry";
count.value = logins.length; // needed for XPCOM
return logins;
},
/*
* _searchLogins
*
* Private method to perform arbitrary searches on any field. Decryption is
* left to the caller.
*
* Returns [logins, ids] for logins that match the arguments, where logins
* is an array of encrypted nsLoginInfo and ids is an array of associated
* ids in the database.
*/
_searchLogins : function (matchData) {
let conditions = [], params = {};
for (field in matchData) {
let value = matchData[field];
switch (field) {
// Historical compatibility requires this special case
case "formSubmitURL":
if (value != null) {
conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
params["formSubmitURL"] = value;
break;
}
// Normal cases.
case "hostname":
case "httpRealm":
case "id":
case "usernameField":
case "passwordField":
case "encryptedUsername":
case "encryptedPassword":
case "guid":
case "encType":
if (value == null) {
conditions.push(field + " isnull");
} else {
conditions.push(field + " = :" + field);
params[field] = value;
}
break;
// Fail if caller requests an unknown property.
default:
throw "Unexpected field: " + field;
}
}
// Build query
let query = "SELECT * FROM moz_logins";
if (conditions.length) {
conditions = conditions.map(function(c) "(" + c + ")");
query += " WHERE " + conditions.join(" AND ");
}
let stmt;
let logins = [], ids = [];
try {
stmt = this._dbCreateStatement(query, params);
// We can't execute as usual here, since we're iterating over rows
while (stmt.step()) {
// Create the new nsLoginInfo object, push to array
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
login.init(stmt.row.hostname, stmt.row.formSubmitURL,
stmt.row.httpRealm, stmt.row.encryptedUsername,
stmt.row.encryptedPassword, stmt.row.usernameField,
stmt.row.passwordField);
// set nsILoginMetaInfo values
login.QueryInterface(Ci.nsILoginMetaInfo);
login.guid = stmt.row.guid;
logins.push(login);
ids.push(stmt.row.id);
}
} catch (e) {
this.log("_searchLogins failed: " + e.name + " : " + e.message);
} finally {
stmt.reset();
}
this.log("_searchLogins: returning " + logins.length + " logins");
return [logins, ids];
},
/*
* removeAllLogins
*
@ -600,8 +715,16 @@ LoginManagerStorage_mozStorage.prototype = {
*/
findLogins : function (count, hostname, formSubmitURL, httpRealm) {
let userCanceled;
let [logins, ids] =
this._queryLogins(hostname, formSubmitURL, httpRealm);
let loginData = {
hostname: hostname,
formSubmitURL: formSubmitURL,
httpRealm: httpRealm
};
let matchData = { };
for each (field in ["hostname", "formSubmitURL", "httpRealm"])
if (loginData[field] != '')
matchData[field] = loginData[field];
let [logins, ids] = this._searchLogins(matchData);
// Decrypt entries found for the caller.
[logins, userCanceled] = this._decryptLogins(logins);
@ -679,8 +802,12 @@ LoginManagerStorage_mozStorage.prototype = {
* stored login (useful for looking at the actual nsILoginMetaInfo values).
*/
_getIdForLogin : function (login) {
let [logins, ids] =
this._queryLogins(login.hostname, login.formSubmitURL, login.httpRealm);
let matchData = { };
for each (field in ["hostname", "formSubmitURL", "httpRealm"])
if (login[field] != '')
matchData[field] = login[field];
let [logins, ids] = this._searchLogins(matchData);
let id = null;
let foundLogin = null;
@ -708,58 +835,6 @@ LoginManagerStorage_mozStorage.prototype = {
},
/*
* _queryLogins
*
* Returns [logins, ids] for logins that match the arguments, where logins
* is an array of encrypted nsLoginInfo and ids is an array of associated
* ids in the database.
*/
_queryLogins : function (hostname, formSubmitURL, httpRealm, encType) {
let logins = [], ids = [];
let query = "SELECT * FROM moz_logins";
let [conditions, params] =
this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm);
if (typeof encType != "undefined") {
conditions.push("encType = :encType");
params.encType = encType;
}
if (conditions.length) {
conditions = conditions.map(function(c) "(" + c + ")");
query += " WHERE " + conditions.join(" AND ");
}
let stmt;
try {
stmt = this._dbCreateStatement(query, params);
// We can't execute as usual here, since we're iterating over rows
while (stmt.step()) {
// Create the new nsLoginInfo object, push to array
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
login.init(stmt.row.hostname, stmt.row.formSubmitURL,
stmt.row.httpRealm, stmt.row.encryptedUsername,
stmt.row.encryptedPassword, stmt.row.usernameField,
stmt.row.passwordField);
// set nsILoginMetaInfo values
login.QueryInterface(Ci.nsILoginMetaInfo);
login.guid = stmt.row.guid;
logins.push(login);
ids.push(stmt.row.id);
}
} catch (e) {
this.log("_queryLogins failed: " + e.name + " : " + e.message);
} finally {
stmt.reset();
}
return [logins, ids];
},
/*
* _queryDisabledHosts
*
@ -1067,8 +1142,7 @@ LoginManagerStorage_mozStorage.prototype = {
// Ignore failures, will try again next session...
try {
let [logins, ids] =
this._queryLogins("", "", "", 0);
let [logins, ids] = this._searchLogins({ encType: 0 });
if (!logins.length)
return;

View File

@ -150,37 +150,40 @@ const LoginTest = {
* Compare info from component to what we expected.
*/
checkStorageData : function (storage, ref_disabledHosts, ref_logins) {
this.checkLogins(ref_logins, storage.getAllLogins({}));
this.checkDisabledHosts(ref_disabledHosts, storage.getAllDisabledHosts({}));
},
var stor_disabledHosts = storage.getAllDisabledHosts({});
do_check_eq(ref_disabledHosts.length, stor_disabledHosts.length);
var stor_logins = storage.getAllLogins({});
do_check_eq(ref_logins.length, stor_logins.length);
/*
* Check values of the disabled list.
*/
var i, j, found;
for (i = 0; i < ref_disabledHosts.length; i++) {
found = false;
for (j = 0; !found && j < stor_disabledHosts.length; j++) {
found = (ref_disabledHosts[i] == stor_disabledHosts[j]);
/*
* checkLogins
*
* Check values of the logins list.
*/
checkLogins : function (expectedLogins, actualLogins) {
do_check_eq(expectedLogins.length, actualLogins.length);
for (let i = 0; i < expectedLogins.length; i++) {
let found = false;
for (let j = 0; !found && j < actualLogins.length; j++) {
found = expectedLogins[i].equals(actualLogins[j]);
}
do_check_true(found);
}
},
/*
* Check values of the logins list.
*/
var ref, stor;
for (i = 0; i < ref_logins.length; i++) {
found = false;
for (j = 0; !found && j < stor_logins.length; j++) {
found = ref_logins[i].equals(stor_logins[j]);
/*
* checkDisabledHosts
*
* Check values of the disabled list.
*/
checkDisabledHosts : function (expectedHosts, actualHosts) {
do_check_eq(expectedHosts.length, actualHosts.length);
for (let i = 0; i < expectedHosts.length; i++) {
let found = false;
for (let j = 0; !found && j < actualHosts.length; j++) {
found = (expectedHosts[i] == actualHosts[j]);
}
do_check_true(found);
}
},
/*

View File

@ -0,0 +1,161 @@
/*
* Test suite for storage-mozStorage.js -- Testing searchLogins.
*
* This test interfaces directly with the mozStorage login storage module,
* bypassing the normal login manager usage.
*
*/
const STORAGE_TYPE = "mozStorage";
function run_test() {
try {
var storage, testnum = 0;
/* ========== 1 ========== */
testnum++;
var testdesc = "Create nsILoginInfo instances for testing with"
var dummyuser1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
var dummyuser2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
var dummyuser3 = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
var dummyuser4 = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
dummyuser1.init("http://dummyhost.mozilla.org", "", null,
"testuser1", "testpass1", "put_user_here", "put_pw_here");
dummyuser2.init("http://dummyhost2.mozilla.org", "", null,
"testuser2", "testpass2", "put_user2_here", "put_pw2_here");
dummyuser3.init("http://dummyhost2.mozilla.org", "http://dummyhost2.mozilla.org", null,
"testuser3", "testpass3", "put_user3_here", "put_pw3_here");
dummyuser4.init("http://dummyhost3.mozilla.org", null, null,
"testuser4", "testpass4", "put_user4_here", "put_pw4_here");
/* ========== 2 ========== */
testnum++;
testdesc = "checking that searchLogins works with values passed"
storage = LoginTest.initStorage(INDIR, null,
OUTDIR, "output-searchLogins-1.sqlite");
storage.addLogin(dummyuser1);
storage.addLogin(dummyuser2);
storage.addLogin(dummyuser3);
LoginTest.checkStorageData(storage, [], [dummyuser1, dummyuser2, dummyuser3]);
let matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("id", "1");
let logins = storage.searchLogins({}, matchData);
do_check_eq(1, logins.length, "expecting single login with id");
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
logins = storage.searchLogins({}, matchData);
do_check_eq(3, logins.length, "should match all logins");
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("hostname", "http://dummyhost2.mozilla.org");
logins = storage.searchLogins({}, matchData);
do_check_eq(2, logins.length, "should match some logins");
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("hostname", "http://dummyhost2.mozilla.org");
matchData.setPropertyAsAString("httpRealm", null);
logins = storage.searchLogins({}, matchData);
do_check_eq(2, logins.length, "should match some logins");
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("formSubmitURL", "");
logins = storage.searchLogins({}, matchData);
do_check_eq(2, logins.length, "should match some logins");
/* ========== 3 ========== */
testnum++;
// uses storage from test #2
testdesc = "checking that searchLogins throws when it's supposed"
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("id", "100");
logins = storage.searchLogins({}, matchData);
do_check_eq(0, logins.length, "bogus value should return 0 results");
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("error", "value");
try {
logins = storage.searchLogins({}, matchData);
} catch (e) {
error = e;
}
LoginTest.checkExpectedError(/Unexpected field/, error, "nonexistant field should throw");
LoginTest.deleteFile(OUTDIR, "output-searchLogins-1.sqlite");
/* ========== 4 ========== */
testnum++;
testdesc = "checking that searchLogins works as findLogins"
storage = LoginTest.initStorage(INDIR, null,
OUTDIR, "output-searchLogins-3.sqlite");
storage.addLogin(dummyuser1);
storage.addLogin(dummyuser2);
storage.addLogin(dummyuser3);
storage.addLogin(dummyuser4);
LoginTest.checkStorageData(storage, [], [dummyuser1, dummyuser2, dummyuser3, dummyuser4]);
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
logins = storage.searchLogins({}, matchData);
loginsF = storage.findLogins({}, "", "", "");
LoginTest.checkLogins(loginsF, logins);
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("hostname", "http://dummyhost2.mozilla.org");
logins = storage.searchLogins({}, matchData);
loginsF = storage.findLogins({}, "http://dummyhost2.mozilla.org", "", "");
LoginTest.checkLogins(loginsF, logins);
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("hostname", "http://dummyhost2.mozilla.org");
matchData.setPropertyAsAString("formSubmitURL", "http://dummyhost2.mozilla.org");
logins = storage.searchLogins({}, matchData);
loginsF = storage.findLogins({}, "http://dummyhost2.mozilla.org", "http://dummyhost2.mozilla.org", "");
LoginTest.checkLogins(loginsF, logins);
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("formSubmitURL", "http://dummyhost2.mozilla.org");
matchData.setPropertyAsAString("httpRealm", null);
logins = storage.searchLogins({}, matchData);
loginsF = storage.findLogins({}, "", "http://dummyhost2.mozilla.org", null);
LoginTest.checkLogins(loginsF, logins);
matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
matchData.setPropertyAsAString("formSubmitURL", null);
matchData.setPropertyAsAString("httpRealm", null);
logins = storage.searchLogins({}, matchData);
loginsF = storage.findLogins({}, "", null, null);
LoginTest.checkLogins(loginsF, logins);
LoginTest.deleteFile(OUTDIR, "output-searchLogins-3.sqlite");
/* ========== end ========== */
} catch (e) {
throw ("FAILED in test #" + testnum + " -- " + testdesc + ": " + e);
}
};