From 68ccc3549c43d203cab2637bd362fd15561aed76 Mon Sep 17 00:00:00 2001 From: Paul O'Shannessy Date: Fri, 17 Apr 2009 16:12:46 -0700 Subject: [PATCH] Bug 479894 - Add a property-bag based searchLogins API to login manager. r=dolske, sr=vlad --- .../passwordmgr/public/nsILoginManager.idl | 25 ++- .../public/nsILoginManagerStorage.idl | 26 ++- .../passwordmgr/src/nsLoginManager.js | 15 ++ .../passwordmgr/src/storage-Legacy.js | 12 ++ .../passwordmgr/src/storage-mozStorage.js | 194 ++++++++++++------ .../test/unit/head_storage_legacy_1.js | 49 ++--- .../test/unit/test_storage_mozStorage_7.js | 161 +++++++++++++++ 7 files changed, 397 insertions(+), 85 deletions(-) create mode 100644 toolkit/components/passwordmgr/test/unit/test_storage_mozStorage_7.js diff --git a/toolkit/components/passwordmgr/public/nsILoginManager.idl b/toolkit/components/passwordmgr/public/nsILoginManager.idl index a1b776c0649b..a15708e1063e 100644 --- a/toolkit/components/passwordmgr/public/nsILoginManager.idl +++ b/toolkit/components/passwordmgr/public/nsILoginManager.idl @@ -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++ diff --git a/toolkit/components/passwordmgr/public/nsILoginManagerStorage.idl b/toolkit/components/passwordmgr/public/nsILoginManagerStorage.idl index bdc1c1fe8fc7..e20b2fda4e95 100644 --- a/toolkit/components/passwordmgr/public/nsILoginManagerStorage.idl +++ b/toolkit/components/passwordmgr/public/nsILoginManagerStorage.idl @@ -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. * diff --git a/toolkit/components/passwordmgr/src/nsLoginManager.js b/toolkit/components/passwordmgr/src/nsLoginManager.js index c3ce6ead9386..7592294421bb 100644 --- a/toolkit/components/passwordmgr/src/nsLoginManager.js +++ b/toolkit/components/passwordmgr/src/nsLoginManager.js @@ -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 * diff --git a/toolkit/components/passwordmgr/src/storage-Legacy.js b/toolkit/components/passwordmgr/src/storage-Legacy.js index 253a0fe84274..0d70f991a893 100644 --- a/toolkit/components/passwordmgr/src/storage-Legacy.js +++ b/toolkit/components/passwordmgr/src/storage-Legacy.js @@ -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 * diff --git a/toolkit/components/passwordmgr/src/storage-mozStorage.js b/toolkit/components/passwordmgr/src/storage-mozStorage.js index 0719b6002513..1cab73d6cdfc 100644 --- a/toolkit/components/passwordmgr/src/storage-mozStorage.js +++ b/toolkit/components/passwordmgr/src/storage-mozStorage.js @@ -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; diff --git a/toolkit/components/passwordmgr/test/unit/head_storage_legacy_1.js b/toolkit/components/passwordmgr/test/unit/head_storage_legacy_1.js index 26acca7393b0..3e58f7e7b542 100644 --- a/toolkit/components/passwordmgr/test/unit/head_storage_legacy_1.js +++ b/toolkit/components/passwordmgr/test/unit/head_storage_legacy_1.js @@ -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); } - }, /* diff --git a/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage_7.js b/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage_7.js new file mode 100644 index 000000000000..5aeadcf82197 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage_7.js @@ -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); +} + +};