--- a/mobile/android/b2gdroid/installer/package-manifest.in
+++ b/mobile/android/b2gdroid/installer/package-manifest.in
@@ -333,17 +333,17 @@
@BINPATH@/components/nsSetDefaultBrowser.js
@BINPATH@/components/toolkitsearch.manifest
@BINPATH@/components/nsSearchService.js
@BINPATH@/components/nsSearchSuggestions.js
@BINPATH@/components/passwordmgr.manifest
@BINPATH@/components/nsLoginInfo.js
@BINPATH@/components/nsLoginManager.js
@BINPATH@/components/nsLoginManagerPrompter.js
-@BINPATH@/components/storage-mozStorage.js
+@BINPATH@/components/storage-fennecStorage.js
@BINPATH@/components/crypto-SDR.js
@BINPATH@/components/jsconsole-clhandler.manifest
@BINPATH@/components/jsconsole-clhandler.js
@BINPATH@/components/nsHelperAppDlg.manifest
@BINPATH@/components/nsHelperAppDlg.js
@BINPATH@/components/NetworkGeolocationProvider.manifest
@BINPATH@/components/NetworkGeolocationProvider.js
@BINPATH@/components/nsSidebar.manifest
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
@@ -32,16 +32,18 @@ import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import org.mozilla.gecko.annotation.JNITarget;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.LoginsAccessor;
+import org.mozilla.gecko.db.StubBrowserDB;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.gfx.LayerView;
import org.mozilla.gecko.gfx.PanZoomController;
import org.mozilla.gecko.mozglue.ContextUtils;
import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.prompts.PromptService;
@@ -198,16 +200,23 @@ public class GeckoAppShell
}
};
public static CrashHandler ensureCrashHandling() {
// Crash handling is automatically enabled when GeckoAppShell is loaded.
return CRASH_HANDLER;
}
+ @JNITarget
+ public static LoginsAccessor getLoginsAccessor() {
+ final Context context = getApplicationContext();
+ final BrowserDB db = GeckoProfile.get(context).getDB();
+ return db.getLoginsAccessor();
+ }
+
private static final Map<String, String> ALERT_COOKIES = new ConcurrentHashMap<String, String>();
private static volatile boolean locationHighAccuracyEnabled;
// Accessed by NotificationHelper. This should be encapsulated.
/* package */ static NotificationClient notificationClient;
// See also HardwareUtils.LOW_MEMORY_THRESHOLD_MB.
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -38,16 +38,17 @@ public interface BrowserDB {
public static enum FilterFlags {
EXCLUDE_PINNED_SITES
}
public abstract Searches getSearches();
public abstract TabsAccessor getTabsAccessor();
public abstract URLMetadata getURLMetadata();
public abstract ReadingListAccessor getReadingListAccessor();
+ public abstract LoginsAccessor getLoginsAccessor();
/**
* Add default bookmarks to the database.
* Takes an offset; returns a new offset.
*/
public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset);
/**
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -96,16 +96,17 @@ public class LocalBrowserDB implements B
private final Uri mFaviconsUriWithProfile;
private final Uri mThumbnailsUriWithProfile;
private final Uri mSearchHistoryUri;
private LocalSearches searches;
private LocalTabsAccessor tabsAccessor;
private LocalURLMetadata urlMetadata;
private LocalReadingListAccessor readingListAccessor;
+ private LoginsAccessor loginsAccessor;
private static final String[] DEFAULT_BOOKMARK_COLUMNS =
new String[] { Bookmarks._ID,
Bookmarks.GUID,
Bookmarks.URL,
Bookmarks.TITLE,
Bookmarks.TYPE,
Bookmarks.PARENT };
@@ -129,16 +130,17 @@ public class LocalBrowserDB implements B
.appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
.appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
.build();
searches = new LocalSearches(mProfile);
tabsAccessor = new LocalTabsAccessor(mProfile);
urlMetadata = new LocalURLMetadata(mProfile);
readingListAccessor = new LocalReadingListAccessor(mProfile);
+ loginsAccessor = new LocalLoginsAccessor(mProfile);
}
@Override
public Searches getSearches() {
return searches;
}
@Override
@@ -151,16 +153,21 @@ public class LocalBrowserDB implements B
return urlMetadata;
}
@Override
public ReadingListAccessor getReadingListAccessor() {
return readingListAccessor;
}
+ @Override
+ public LoginsAccessor getLoginsAccessor() {
+ return loginsAccessor;
+ }
+
/**
* Not thread safe. A helper to allocate new IDs for arbitrary strings.
*/
private static class NameCounter {
private final HashMap<String, Integer> names = new HashMap<String, Integer>();
private int counter;
private final int increment;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalLoginsAccessor.java
@@ -0,0 +1,102 @@
+/* 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/. */
+
+package org.mozilla.gecko.db;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.GeckoAppShell;
+
+import static org.mozilla.gecko.db.BrowserContract.DeletedLogins;
+import static org.mozilla.gecko.db.BrowserContract.DisabledHosts;
+import static org.mozilla.gecko.db.BrowserContract.Logins;
+
+public class LocalLoginsAccessor implements LoginsAccessor {
+ private static final String LOGTAG = "GeckoLoginsAccessor";
+
+ private final Uri loginsUriWithProfile;
+ private final Uri deletedLoginsUriWithProfile;
+ private final Uri disabledHostsUriWithProfile;
+
+ public LocalLoginsAccessor(String profileName) {
+ loginsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, Logins.CONTENT_URI);
+ deletedLoginsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, DeletedLogins.CONTENT_URI);
+ disabledHostsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, DisabledHosts.CONTENT_URI);
+ }
+
+ private Context getContext() {
+ return GeckoAppShell.getGeckoInterface().getActivity();
+ }
+
+ @Override
+ public void addLogin(ContentValues values) {
+ getContext().getContentResolver().insert(loginsUriWithProfile, values);
+ }
+
+ @Override
+ public void removeLogin(long id) {
+ getContext().getContentResolver().delete(loginsUriWithProfile.buildUpon().appendPath(String.valueOf(id)).build(), null, null);
+ }
+
+ @Override
+ public void modifyLogin(long id, ContentValues values) {
+ getContext().getContentResolver().update(loginsUriWithProfile.buildUpon().appendPath(String.valueOf(id)).build(), values, null, null);
+ }
+
+ @Override
+ public void removeAllLogins() {
+ getContext().getContentResolver().delete(loginsUriWithProfile, null, null);
+ getContext().getContentResolver().delete(deletedLoginsUriWithProfile, null, null);
+ }
+
+ @Override
+ public int countLogins() {
+ Cursor cursor = getContext().getContentResolver().query(loginsUriWithProfile, null, null, null, null);
+ try {
+ return cursor == null ? 0 : cursor.getCount();
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Override
+ public Cursor searchLogins(String selection, String[] selectionArgs) {
+ final Cursor cursor = getContext().getContentResolver().query(loginsUriWithProfile, null, selection, selectionArgs, null);
+ return cursor;
+ }
+
+ @Override
+ public Cursor getAllDisabledHosts() {
+ final Cursor cursor = getContext().getContentResolver().query(disabledHostsUriWithProfile, null, null, null, null);
+ return cursor;
+ }
+
+ @Override
+ public Cursor getLoginsSavedEnabled(String hostname) {
+ final Uri uri = disabledHostsUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT, "1").build();
+ boolean isHostnameEmpty = TextUtils.isEmpty(hostname);
+ return getContext()
+ .getContentResolver()
+ .query(uri,
+ null,
+ isHostnameEmpty ? null : DisabledHosts.HOSTNAME + "= ?",
+ isHostnameEmpty ? null : new String[] {hostname},
+ null);
+ }
+
+ @Override
+ public void setLoginSavingEnabled(String hostname, boolean isEnabled) {
+ if (isEnabled) {
+ getContext().getContentResolver().delete(disabledHostsUriWithProfile, DisabledHosts.HOSTNAME + "=?", new String[]{hostname});
+ } else {
+ final ContentValues values = new ContentValues();
+ values.put(DisabledHosts.HOSTNAME, hostname);
+ getContext().getContentResolver().insert(disabledHostsUriWithProfile, values);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LoginsAccessor.java
@@ -0,0 +1,87 @@
+/* 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/. */
+
+package org.mozilla.gecko.db;
+
+import org.mozilla.gecko.annotation.JNITarget;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+/**
+ * Interface for interactions with {@link LoginsProvider} that is accessed from
+ * {@link nsILoginManagerStorage.idl} implementation through JNI.
+ *
+ * Nota bene: The concrete implementor of this interface does not validate the parameters and the parameters are passed
+ * to {@link android.content.ContentProvider} as it is. It is the responsibility of the caller to validate
+ * the arguments.
+ */
+
+@JNITarget
+public interface LoginsAccessor {
+ /**
+ * Store a new login entry from the content values after deleting the entry from deletedLogins table if needed.
+ *
+ * @param values The ContentValues to use.
+ */
+ void addLogin(ContentValues values);
+
+ /**
+ * Remove the login from logins table and store it as a deletedLogins.
+ *
+ * @param id login id to delete.
+ */
+ void removeLogin(long id);
+
+ /**
+ * Update the login from content values.
+ *
+ * @param id login id to update.
+ * @param values The ContentValues to use.
+ */
+ void modifyLogin(long id, ContentValues values);
+
+ // Wipe all stored logins and deletedLogins.
+ void removeAllLogins();
+
+ // Count the number of all stored logins.
+ int countLogins();
+
+ /**
+ * Search for logins based on selection condition.
+ * The resultant cursor has all columns for {@link org.mozilla.gecko.db.BrowserContract.Logins} selected.
+ *
+ * @param selection The SQL selection where clause.
+ * @param selectionArgs The conditional values to substitute in place of format parameters.
+ * @return A cursor representing the contents of the logins table filtered according to the arguments.
+ * Can return <code>null</code>.
+ */
+ Cursor searchLogins(String selection, String[] selectionArgs);
+
+ /**
+ * Query for hostname of all disabled hosts.
+ *
+ * @return A cursor representing the contents of the disabledHosts table.
+ * Can return <code>null</code>.
+ */
+ Cursor getAllDisabledHosts();
+
+ /**
+ * Check if hostname has entry in disabledHosts table.
+ *
+ * @param hostname The hostname for querying. <code>null</code> will return all the rows.
+ * @return A cursor representing the contents of the disabledHosts table.
+ * Can return <code>null</code>.
+ */
+ Cursor getLoginsSavedEnabled(String hostname);
+
+ /**
+ * Store/remove hostname from disabledHosts table based on boolean parameter. The login saving is
+ * dependent on hostname not present in disabledHosts table.
+ *
+ * @param hostname The hostname to store/remove from disabledHosts table
+ * @param isEnabled The hostname is removed from disabledHosts table if true and stored if false.
+ */
+ void setLoginSavingEnabled(String hostname, boolean isEnabled);
+}
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -145,25 +145,71 @@ class StubTabsAccessor implements TabsAc
}
public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
listener.onQueryTabsComplete(new ArrayList<RemoteClient>());
}
public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) { }
}
+class StubLoginsAccessor implements LoginsAccessor {
+ public StubLoginsAccessor() {
+ }
+
+ @Override
+ public void addLogin(ContentValues values) {
+ }
+
+ @Override
+ public void removeLogin(long id) {
+ }
+
+ @Override
+ public void modifyLogin(long id, ContentValues values) {
+ }
+
+ @Override
+ public void removeAllLogins() {
+ }
+
+ @Override
+ public int countLogins() {
+ return 0;
+ }
+
+ @Override
+ public Cursor searchLogins(String selection, String[] selectionArgs) {
+ return null;
+ }
+
+ @Override
+ public Cursor getAllDisabledHosts() {
+ return null;
+ }
+
+ @Override
+ public Cursor getLoginsSavedEnabled(String hostname) {
+ return null;
+ }
+
+ @Override
+ public void setLoginSavingEnabled(String hostname, boolean isEnabled) {
+ }
+}
+
/*
* This base implementation just stubs all methods. For the
* real implementations, see LocalBrowserDB.java.
*/
public class StubBrowserDB implements BrowserDB {
private final StubSearches searches = new StubSearches();
private final StubTabsAccessor tabsAccessor = new StubTabsAccessor();
private final StubURLMetadata urlMetadata = new StubURLMetadata();
private final StubReadingListAccessor readingListAccessor = new StubReadingListAccessor();
+ private final StubLoginsAccessor loginsAccessor = new StubLoginsAccessor();
@Override
public Searches getSearches() {
return searches;
}
@Override
public TabsAccessor getTabsAccessor() {
@@ -175,16 +221,21 @@ public class StubBrowserDB implements Br
return urlMetadata;
}
@Override
public ReadingListAccessor getReadingListAccessor() {
return readingListAccessor;
}
+ @Override
+ public LoginsAccessor getLoginsAccessor() {
+ return loginsAccessor;
+ }
+
protected static final Integer FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
public StubBrowserDB(String profile) {
}
public void invalidate() { }
public int addDefaultBookmarks(Context context, ContentResolver cr, final int offset) {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -216,20 +216,22 @@ gbjar.sources += ['java/org/mozilla/geck
'db/BaseTable.java',
'db/BrowserDatabaseHelper.java',
'db/BrowserDB.java',
'db/BrowserProvider.java',
'db/DBUtils.java',
'db/FormHistoryProvider.java',
'db/HomeProvider.java',
'db/LocalBrowserDB.java',
+ 'db/LocalLoginsAccessor.java',
'db/LocalReadingListAccessor.java',
'db/LocalSearches.java',
'db/LocalTabsAccessor.java',
'db/LocalURLMetadata.java',
+ 'db/LoginsAccessor.java',
'db/LoginsProvider.java',
'db/PasswordsProvider.java',
'db/PerProfileDatabaseProvider.java',
'db/PerProfileDatabases.java',
'db/ReadingListAccessor.java',
'db/ReadingListProvider.java',
'db/RemoteClient.java',
'db/RemoteTab.java',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -294,17 +294,17 @@
@BINPATH@/components/nsDNSServiceDiscovery.js
@BINPATH@/components/toolkitsearch.manifest
@BINPATH@/components/nsSearchService.js
@BINPATH@/components/nsSidebar.js
@BINPATH@/components/passwordmgr.manifest
@BINPATH@/components/nsLoginInfo.js
@BINPATH@/components/nsLoginManager.js
@BINPATH@/components/nsLoginManagerPrompter.js
-@BINPATH@/components/storage-mozStorage.js
+@BINPATH@/components/storage-fennecStorage.js
@BINPATH@/components/crypto-SDR.js
@BINPATH@/components/NetworkGeolocationProvider.manifest
@BINPATH@/components/NetworkGeolocationProvider.js
@BINPATH@/components/extensions.manifest
@BINPATH@/components/addonManager.js
@BINPATH@/components/amContentHandler.js
@BINPATH@/components/amInstallTrigger.js
@BINPATH@/components/amWebInstallListener.js
--- a/mobile/android/tests/browser/chrome/chrome.ini
+++ b/mobile/android/tests/browser/chrome/chrome.ini
@@ -13,16 +13,17 @@ support-files =
[test_about_logins.html]
[test_accounts.html]
[test_android_log.html]
[test_app_constants.html]
[test_debugger_server.html]
[test_desktop_useragent.html]
[test_device_search_engine.html]
+[test_fennecStorage.html]
[test_get_last_visited.html]
[test_home_provider.html]
[test_java_addons.html]
[test_jni.html]
[test_migrate_ui.html]
[test_network_manager.html]
[test_offline_page.html]
[test_reader_view.html]
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_fennecStorage.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1136477
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1136477</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <script type="application/javascript;version=1.8">
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ const LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+ const storage = Cc["@mozilla.org/login-manager/storage/fennecStorage;1"]
+ .getService(Ci.nsILoginManagerStorage);
+
+ var testLogin1 = new LoginInfo("http://test.com", null, "http://test.com",
+ "testLogin1", "testpass1", "u1", "p1");
+
+ var testLogin2 = new LoginInfo("http://test.org", "https://test.org", null,
+ "testLogin2", "testpass2", "u2", "p2");
+
+ /**
+ * Checks that the two provided arrays of nsILoginInfo have the same length,
+ * and every login in "expected" is also found in "actual". The comparison
+ * uses the "equals" method of nsILoginInfo, that does not include
+ * nsILoginMetaInfo properties in the test.
+ */
+ function assertLoginListsEqual(actual, expected) {
+ is(expected.length, actual.length, "loginlist length equals");
+ ok(expected.every(e => actual.some(a => a.equals(e))), "loginlist elements equals");
+ }
+
+ /**
+ * Checks that every login in "expected" matches one in "actual".
+ * The comparison uses the "matches" method of nsILoginInfo.
+ */
+ function assertLoginListsMatches(actual, expected, ignorePassword) {
+ is(expected.length, actual.length, "loginlist length equals");
+ ok(expected.every(e => actual.some(a => a.matches(e, ignorePassword))), "loginlist elements matches");
+ }
+
+ /**
+ * Checks that the two provided arrays of strings contain the same values,
+ * maybe in a different order, case-sensitively.
+ */
+ function assertDisabledHostsEqual(actual, expected) {
+ is(expected.length, actual.length, "disabledHosts length equals");
+ ok(expected.every(e => actual.some(a => (a === e))), "disabledHosts elements equals");
+ }
+
+ function checkStorageData(storage, ref_disabledHosts, ref_logins)
+ {
+ assertLoginListsEqual(storage.getAllLogins(), ref_logins);
+ assertDisabledHostsEqual(storage.getAllDisabledHosts(), ref_disabledHosts);
+ }
+
+ function retrieveAndVerifyLoginByProps(propName, propValue, actualLogin) {
+ var prop = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
+ prop.setPropertyAsAUTF8String(propName, propValue);
+ let logins = storage.searchLogins({}, prop);
+ assertLoginListsEqual(logins, [actualLogin]);
+ }
+
+ function test_disabledHosts_Test() {
+ checkStorageData(storage, [], []);
+ // Disable saving logins to localhost and verify.
+ storage.setLoginSavingEnabled("localhost", false);
+ ok(!storage.getLoginSavingEnabled("localhost"), "localhost is a disabled hostname");
+ checkStorageData(storage, ["localhost"], []);
+ // Enable saving logins to localhost and verify
+ storage.setLoginSavingEnabled("localhost", true);
+ ok(storage.getLoginSavingEnabled("localhost"), "localhost is not a disabled hostname");
+ checkStorageData(storage, [], []);
+ }
+
+ function test_login_CRUD() {
+ // Create a new login and verify storage data.
+ storage.addLogin(testLogin1);
+ checkStorageData(storage, [], [testLogin1]);
+
+ // Add another login and verify all logins are retrieved.
+ storage.addLogin(testLogin2);
+ checkStorageData(storage, [], [testLogin1, testLogin2]);
+
+ // Verify login counts.
+ var count = storage.countLogins(testLogin2.hostname, testLogin2.formSubmitURL, testLogin2.httpRealm);
+ is(1, count, "countLogins is correct");
+ // Verify formSubmitUrl schema adjustment.
+ count = storage.countLogins(testLogin2.hostname, "http://test.org", testLogin2.httpRealm);
+ is(1, count, "countLogins is correct");
+ //count = storage.countLogins(testLogin2.hostname, "javascript://test.org", testLogin2.httpRealm);
+ //is(0, count, "countLogins is correct");
+ count = storage.countLogins(null, null, null);
+ is(0, count, "countLogins is correct");
+
+ // Search logins by properties test.
+ var logins = storage.findLogins({}, testLogin1.hostname, testLogin1.formSubmitURL, testLogin1.httpRealm);
+ assertLoginListsEqual(logins, [testLogin1]);
+
+ logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ retrieveAndVerifyLoginByProps("guid", logins[0].guid, testLogin1);
+ retrieveAndVerifyLoginByProps("encryptedUsername", testLogin1.username, testLogin1);
+ retrieveAndVerifyLoginByProps("encryptedPassword", testLogin1.password, testLogin1);
+ retrieveAndVerifyLoginByProps("usernameField", testLogin2.usernameField, testLogin2);
+ retrieveAndVerifyLoginByProps("passwordField", testLogin2.passwordField, testLogin2);
+
+ // Update logins and verify stored data.
+ testLogin1.password = "p3";
+ storage.modifyLogin(logins[0], testLogin1);
+ retrieveAndVerifyLoginByProps("encryptedPassword", "p3", testLogin1);
+
+ // Delete logins test.
+ storage.removeLogin(testLogin2);
+ checkStorageData(storage, [], [testLogin1]);
+ storage.removeAllLogins();
+ checkStorageData(storage, [], []);
+ };
+
+ test_disabledHosts_Test();
+ test_login_CRUD();
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=946857">Mozilla Bug 946857</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -44,17 +44,17 @@ EXTRA_JS_MODULES += [
'LoginManagerContent.jsm',
'LoginManagerParent.jsm',
'LoginRecipes.jsm',
'OSCrypto.jsm',
]
if CONFIG['OS_TARGET'] == 'Android':
EXTRA_COMPONENTS += [
- 'storage-mozStorage.js',
+ 'storage-fennecStorage.js',
]
else:
EXTRA_COMPONENTS += [
'storage-json.js',
]
EXTRA_JS_MODULES += [
'LoginImport.jsm',
'LoginStore.jsm',
--- a/toolkit/components/passwordmgr/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -103,17 +103,17 @@ LoginManager.prototype = {
}
Services.obs.addObserver(this._observer, "gather-telemetry", false);
},
_initStorage : function () {
#ifdef ANDROID
- var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
+ var contractID = "@mozilla.org/login-manager/storage/fennecStorage;1";
#else
var contractID = "@mozilla.org/login-manager/storage/json;1";
#endif
try {
var catMan = Cc["@mozilla.org/categorymanager;1"].
getService(Ci.nsICategoryManager);
contractID = catMan.getCategoryEntry("login-manager-storage",
"nsILoginManagerStorage");
--- a/toolkit/components/passwordmgr/passwordmgr.manifest
+++ b/toolkit/components/passwordmgr/passwordmgr.manifest
@@ -2,16 +2,16 @@ component {cb9e0de8-3598-4ed7-857b-827f0
contract @mozilla.org/login-manager;1 {cb9e0de8-3598-4ed7-857b-827f011ad5d8}
component {749e62f4-60ae-4569-a8a2-de78b649660e} nsLoginManagerPrompter.js
contract @mozilla.org/passwordmanager/authpromptfactory;1 {749e62f4-60ae-4569-a8a2-de78b649660e}
component {8aa66d77-1bbb-45a6-991e-b8f47751c291} nsLoginManagerPrompter.js
contract @mozilla.org/login-manager/prompter;1 {8aa66d77-1bbb-45a6-991e-b8f47751c291}
component {0f2f347c-1e4f-40cc-8efd-792dea70a85e} nsLoginInfo.js
contract @mozilla.org/login-manager/loginInfo;1 {0f2f347c-1e4f-40cc-8efd-792dea70a85e}
#ifdef ANDROID
-component {8c2023b9-175c-477e-9761-44ae7b549756} storage-mozStorage.js
-contract @mozilla.org/login-manager/storage/mozStorage;1 {8c2023b9-175c-477e-9761-44ae7b549756}
+component {4859e221-74ca-48d2-9ad2-05248f3ec745} storage-fennecStorage.js
+contract @mozilla.org/login-manager/storage/fennecStorage;1 {4859e221-74ca-48d2-9ad2-05248f3ec745}
#else
component {c00c432d-a0c9-46d7-bef6-9c45b4d07341} storage-json.js
contract @mozilla.org/login-manager/storage/json;1 {c00c432d-a0c9-46d7-bef6-9c45b4d07341}
#endif
component {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} crypto-SDR.js
contract @mozilla.org/login-manager/crypto/SDR;1 {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-fennecStorage.js
@@ -0,0 +1,824 @@
+/* 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const DB_VERSION = 5; // The database schema version
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/ctypes.jsm")
+Components.utils.import("resource://gre/modules/JNI.jsm");
+Components.utils.import("resource://services-common/async.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+
+function LoginManagerStorage_fennecStorage() { };
+
+LoginManagerStorage_fennecStorage.prototype = {
+
+ classID : Components.ID("{4859e221-74ca-48d2-9ad2-05248f3ec745}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
+ Ci.nsIInterfaceRequestor]),
+
+
+ __uuidService: null,
+ get _uuidService() {
+ if (!this.__uuidService) {
+ this.__uuidService = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ }
+ return this.__uuidService;
+ },
+
+
+ // Common Java Class signatures.
+ _SIG: {
+ ContentValues: 'Landroid/content/ContentValues;',
+ Cursor: 'Landroid/database/Cursor;',
+ GeckoAppShell: 'Lorg/mozilla/gecko/GeckoAppShell;',
+ LoginsAccessor: 'Lorg/mozilla/gecko/db/LoginsAccessor;',
+ String: 'Ljava/lang/String;',
+ NULL: new ctypes.voidptr_t(null)
+ },
+
+
+ /*
+ * initialize
+ *
+ * Database initialization are done in Android, so return immediately.
+ */
+ initialize : function () {
+ return Promise.resolve();
+ },
+
+
+ /*
+ * terminate
+ *
+ * Internal method used by regression tests only. It is called before
+ * replacing this storage module with a new instance.
+ */
+ terminate : function () {
+ return Promise.resolve();
+ },
+
+
+ /*
+ * addLogin
+ *
+ */
+ addLogin : function (login) {
+ // Throws if there are bogus values.
+ LoginHelper.checkLoginValues(login);
+
+ // Clone the login, so we don't modify the caller's object.
+ let loginClone = login.clone();
+
+ // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
+ loginClone.QueryInterface(Ci.nsILoginMetaInfo);
+ if (loginClone.guid && !this._isGuidUnique(loginClone.guid)) {
+ throw new Error("specified GUID already exists");
+ } else {
+ loginClone.guid = this._uuidService.generateUUID().toString();
+ }
+
+ // Set timestamps
+ let currentTime = Date.now();
+ if (!loginClone.timeCreated) {
+ loginClone.timeCreated = currentTime;
+ }
+ if (!loginClone.timeLastUsed) {
+ loginClone.timeLastUsed = currentTime;
+ }
+ if (!loginClone.timePasswordChanged) {
+ loginClone.timePasswordChanged = currentTime;
+ }
+ if (!loginClone.timesUsed) {
+ loginClone.timesUsed = 1;
+ }
+
+ this._waitForSync(new Promise((resolve, reject) => {
+ var my_jenv;
+ try {
+ my_jenv = JNI.GetForThread();
+ let loginsAccessor = this._getLoginsAccessor(my_jenv);
+ // Allocate a local reference frame with enough space (13 for the names and 13 for the values).
+ my_jenv.contents.contents.PushLocalFrame(my_jenv, 26);
+
+ let values = this._populateContentValues(my_jenv, loginClone);
+ loginsAccessor.addLogin(values);
+
+ // Pop local reference frame to clear all the local references.
+ my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+
+ // Send a notification that a login was added.
+ this._sendNotification("addLogin", loginClone);
+ resolve(null);
+ } catch (ex) {
+ this.log("addLogin JNI failure: " + ex.name + " : " + ex.message);
+ reject(ex);
+ } finally {
+ if (my_jenv) {
+ JNI.UnloadClasses(my_jenv);
+ }
+ }
+ }));
+ },
+
+
+ /*
+ * removeLogin
+ *
+ */
+ removeLogin : function (login) {
+ let [idToDelete, storedLogin] = this._getIdForLogin(login);
+ if (!idToDelete) {
+ throw new Error("No matching logins");
+ }
+
+ this._waitForSync(new Promise((resolve, reject) => {
+ var my_jenv;
+ try {
+ my_jenv = JNI.GetForThread();
+ let loginsAccessor = this._getLoginsAccessor(my_jenv);
+ loginsAccessor.removeLogin(idToDelete);
+
+ // Send a notification that a login was removed.
+ this._sendNotification("removeLogin", storedLogin);
+ resolve(null);
+ } catch (ex) {
+ this.log("removeLogin JNI failure: " + ex.name + " : " + ex.message);
+ reject(ex);
+ } finally {
+ if (my_jenv) {
+ JNI.UnloadClasses(my_jenv);
+ }
+ }
+ }));
+ },
+
+
+ /*
+ * modifyLogin
+ *
+ */
+ modifyLogin : function (oldLogin, newLoginData) {
+ let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
+ if (!idToModify) {
+ throw new Error("No matching logins");
+ }
+
+ let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
+
+ // Check if the new GUID is duplicate.
+ if (newLogin.guid != oldStoredLogin.guid &&
+ !this._isGuidUnique(newLogin.guid)) {
+ throw new Error("specified GUID already exists");
+ }
+
+ // Look for an existing entry in case key properties changed.
+ if (!newLogin.matches(oldLogin, true)) {
+ let logins = this.findLogins({}, newLogin.hostname,
+ newLogin.formSubmitURL,
+ newLogin.httpRealm);
+
+ if (logins.some(login => newLogin.matches(login, true))) {
+ throw new Error("This login already exists.");
+ }
+ }
+
+ this._waitForSync(new Promise((resolve, reject) => {
+ var my_jenv;
+
+ try {
+ my_jenv = JNI.GetForThread();
+ let loginsAccessor = this._getLoginsAccessor(my_jenv);
+ // Allocate a local reference frame with enough space (13 for the names and 13 for the values).
+ my_jenv.contents.contents.PushLocalFrame(my_jenv, 26);
+
+ let values = this._populateContentValues(my_jenv, newLogin);
+ loginsAccessor.modifyLogin(idToModify, values);
+
+ // Pop local reference frame to clear all the local references.
+ my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+
+ // Send a notification that a login was modified.
+ this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
+ resolve(null);
+ } catch (ex) {
+ this.log("modifyLogin JNI failure: " + ex.name + " : " + ex.message);
+ reject(ex);
+ } finally {
+ if (my_jenv) {
+ JNI.UnloadClasses(my_jenv);
+ }
+ }
+ }));
+ },
+
+
+ /*
+ * getAllLogins
+ *
+ * Returns an array of nsILoginInfo.
+ */
+ getAllLogins : function (count) {
+ let [logins, ids] = this._searchLogins({});
+ this.log("_getAllLogins: returning " + logins.length + " logins.");
+ if (count) {
+ count.value = logins.length; // needed for XPCOM
+ }
+ return logins;
+ },
+
+
+ /*
+ * 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);
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+
+ /*
+ * removeAllLogins
+ *
+ * Removes all logins from storage.
+ */
+ removeAllLogins : function () {
+ this.log("Removing all logins");
+
+ // Disabled hosts kept, as one presumably doesn't want to erase those.
+ // TODO: Add these items to the deleted items table once we've sorted
+ // out the issues from bug 756701
+ this._waitForSync(new Promise((resolve, reject) => {
+ var my_jenv;
+
+ try {
+ my_jenv = JNI.GetForThread();
+ let loginsAccessor = this._getLoginsAccessor(my_jenv);
+ loginsAccessor.removeAllLogins();
+ this._sendNotification("removeAllLogins", null);
+ resolve(null);
+ } catch (ex) {
+ this.log("removeAllLogins JNI failure: " + ex.name + " : " + ex.message);
+ reject(ex);
+ } finally {
+ if (my_jenv) {
+ JNI.UnloadClasses(my_jenv);
+ }
+ }
+ }));
+ },
+
+
+ /*
+ * getAllDisabledHosts
+ *
+ */
+ getAllDisabledHosts : function (count) {
+ let disabledHosts = this._queryDisabledHosts(null);
+
+ this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
+ if (count) {
+ count.value = disabledHosts.length; // needed for XPCOM
+ }
+ return disabledHosts;
+ },
+
+
+ /*
+ * getLoginSavingEnabled
+ *
+ */
+ getLoginSavingEnabled : function (hostname) {
+ this.log("Getting login saving is enabled for " + hostname);
+ return this._queryDisabledHosts(hostname).length == 0
+ },
+
+
+ /*
+ * setLoginSavingEnabled
+ *
+ */
+ setLoginSavingEnabled : function (hostname, enabled) {
+ // Throws if there are bogus values.
+ LoginHelper.checkHostnameValue(hostname);
+ this._waitForSync(new Promise((resolve, reject) => {
+ var my_jenv;
+ try {
+ my_jenv = JNI.GetForThread();
+ let loginsAccessor = this._getLoginsAccessor(my_jenv);
+ // Allocate a local reference frame with enough space (1 String read).
+ my_jenv.contents.contents.PushLocalFrame(my_jenv, 1);
+
+ var jHostName = JNI.NewString(my_jenv, hostname);
+ loginsAccessor.setLoginSavingEnabled(jHostName, enabled);
+
+ // Pop local reference frame to clear all the local references.
+ my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+ this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
+ resolve(null);
+ } catch (ex) {
+ this.log("_setLoginSavingEnabled JNI failure: " + ex.name + " : " + ex.message);
+ reject(ex);
+ } finally {
+ if (my_jenv) {
+ JNI.UnloadClasses(my_jenv);
+ }
+ }
+ }));
+ },
+
+
+ /*
+ * findLogins
+ *
+ */
+ findLogins : function (count, hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"]) {
+ if (loginData[field]) {
+ matchData[field] = loginData[field];
+ } else {
+ matchData[field] = null;
+ }
+ }
+
+ let [logins, ids] = this._searchLogins(matchData);
+ this.log("_findLogins: returning " + logins.length + " logins");
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+
+ /*
+ * countLogins
+ *
+ */
+ countLogins : function (hostname, formSubmitURL, httpRealm) {
+ let resultLogins = this.findLogins({}, hostname, formSubmitURL, httpRealm);
+ if (resultLogins.length == 0 && formSubmitURL != null &&
+ formSubmitURL != "" && formSubmitURL != "javascript:") {
+ let formSubmitURI = Services.io.newURI(formSubmitURL, null, null);
+ let newScheme = null;
+ if (formSubmitURI.scheme == "http") {
+ newScheme = "https";
+ } else if (formSubmitURI.scheme == "https") {
+ newScheme = "http";
+ }
+ if (newScheme) {
+ let newFormSubmitURL = newScheme + "://" + formSubmitURI.hostPort;
+ resultLogins = this.findLogins({}, hostname, newFormSubmitURL, httpRealm);
+ }
+ }
+ this.log("_countLogins: counted logins: " + resultLogins.length);
+ return resultLogins.length;
+ },
+
+
+ /*
+ * uiBusy
+ */
+ get uiBusy() {
+ return false;
+ },
+
+
+ /*
+ * isLoggedIn
+ */
+ get isLoggedIn() {
+ return true;
+ },
+
+
+ /*
+ * _waitForSync
+ *
+ * Private method to wait for a promise to resolve synchronously.
+ *
+ * Returns resolved promise value or throw an error if the promised is rejected.
+ */
+ _waitForSync : function(promise) {
+ let cb = Async.makeSyncCallback();
+ Promise.resolve(promise).then(returnValue => cb.apply(null, returnValue ? returnValue : null)).catch(e => {
+ this.log("_waitForSync Promise failure: " + e.name + " : " + e.message);
+ cb.throw(e);
+ });
+ return Async.waitForSyncCallback(cb);
+ },
+
+ _getLoginsAccessor : function(my_jenv) {
+ if (!my_jenv) {
+ throw new Error('my_jenv pointer is undefined');
+ }
+
+ JNI.LoadClass(my_jenv, this._SIG.Cursor.substr(1, this._SIG.Cursor.length - 2), {
+ methods: [
+ { name: 'moveToFirst', sig: '()Z' },
+ { name: 'moveToNext', sig: '()Z' },
+ { name: 'isAfterLast', sig: '()Z' },
+ { name: 'getColumnIndex', sig: '(' + this._SIG.String + ')I' },
+ { name: 'getString', sig: '(I)' + this._SIG.String },
+ { name: 'getLong', sig: '(I)J' },
+ { name: 'close', sig: '()V' },
+ ]
+ });
+
+ JNI.LoadClass(my_jenv, this._SIG.ContentValues.substr(1, this._SIG.ContentValues.length - 2), {
+ constructors: [{
+ name: "<init>",
+ sig: "()V"
+ }],
+ methods: [
+ { name: 'put', sig: '(' + this._SIG.String + this._SIG.String + ')V' },
+ { name: 'toString', sig: '()' + this._SIG.String },
+ // { name: 'put', sig: '(' + this._SIG.String + 'J)V' },
+ ]
+ });
+
+ JNI.LoadClass(my_jenv, this._SIG.LoginsAccessor.substr(1, this._SIG.LoginsAccessor.length - 2), {
+ methods: [
+ { name: 'searchLogins',
+ sig: '(' +
+ this._SIG.String +
+ '[' + this._SIG.String +
+ ')' +
+ this._SIG.Cursor
+ },
+ { name: 'getAllDisabledHosts', sig: '()' + this._SIG.Cursor },
+ { name: 'addLogin', sig: '(' + this._SIG.ContentValues + ')V' },
+ { name: 'removeLogin', sig: '(J)V' },
+ { name: 'modifyLogin', sig: '(J'+ this._SIG.ContentValues + ')V' },
+ { name: 'removeAllLogins', sig: '()V' },
+ { name: 'getLoginsSavedEnabled', sig: '(' + this._SIG.String + ')' + this._SIG.Cursor },
+ { name: 'setLoginSavingEnabled', sig: '(' + this._SIG.String + 'Z)V' },
+ ]
+ });
+
+ var geckoAppShell = JNI.LoadClass(my_jenv, this._SIG.GeckoAppShell.substr(1, this._SIG.GeckoAppShell.length - 2), {
+ static_methods: [
+ { name: 'getLoginsAccessor', sig: '()' + this._SIG.LoginsAccessor }
+ ]
+ });
+
+ return geckoAppShell.getLoginsAccessor();
+ },
+
+
+ _populateContentValues : function(my_jenv, loginClone) {
+ if (!my_jenv) {
+ throw new Error('my_jenv pointer is undefined');
+ }
+
+ let contentValues = JNI.classes.android.content.ContentValues["new"]();
+ // Test for valid Java string for text columns.
+ var jHostName = loginClone.hostname ? JNI.NewString(my_jenv, loginClone.hostname) : this._SIG.NULL;
+ var jHttpRealm = loginClone.httpRealm ? JNI.NewString(my_jenv, loginClone.httpRealm) : this._SIG.NULL;
+ var jFormSubmitURL = loginClone.formSubmitURL ? JNI.NewString(my_jenv, loginClone.formSubmitURL) : this._SIG.NULL;
+ var jUserNameField = loginClone.usernameField ? JNI.NewString(my_jenv, loginClone.usernameField) : this._SIG.NULL;
+ var jPasswordField = loginClone.passwordField ? JNI.NewString(my_jenv, loginClone.passwordField) : this._SIG.NULL;
+ var jUserName = loginClone.username ? JNI.NewString(my_jenv, loginClone.username) : this._SIG.NULL;
+ var jPassword = loginClone.password ? JNI.NewString(my_jenv, loginClone.password) : this._SIG.NULL;
+
+ // GUID is generated in LoginsProvider if null or empty.
+ var jGUID = loginClone.guid ? JNI.NewString(my_jenv, loginClone.guid) : this._SIG.NULL;
+ var jEncType = JNI.NewString(my_jenv, loginClone.encType ? loginClone.encType : "0");
+
+ // Following is a ugly conversion of long to String but this avoid overloaded put method in ContentValues with long to Long conversion.
+ var jTimeCreated = loginClone.timeCreated ? JNI.NewString(my_jenv, "" + loginClone.timeCreated) : this._SIG.NULL;
+ var jTimeLastUsed = loginClone.timeLastUsed ? JNI.NewString(my_jenv, "" + loginClone.timeLastUsed) : this._SIG.NULL;
+ var jTimePasswordChanged = loginClone.timePasswordChanged ? JNI.NewString(my_jenv, "" + loginClone.timePasswordChanged) : this._SIG.NULL;
+ var jTimesUsed = JNI.NewString(my_jenv, loginClone.timesUsed ? "" + loginClone.timesUsed : "0");
+
+ // Keys have to be kept in sync with BrowserContract$Logins class.
+ contentValues.put(JNI.NewString(my_jenv, "hostname"), jHostName);
+ contentValues.put(JNI.NewString(my_jenv, "httpRealm"), jHttpRealm);
+ contentValues.put(JNI.NewString(my_jenv, "formSubmitURL"), jFormSubmitURL);
+ contentValues.put(JNI.NewString(my_jenv, "usernameField"), jUserNameField);
+ contentValues.put(JNI.NewString(my_jenv, "passwordField"), jPasswordField);
+ contentValues.put(JNI.NewString(my_jenv, "encryptedUsername"), jUserName);
+ contentValues.put(JNI.NewString(my_jenv, "encryptedPassword"), jPassword);
+ contentValues.put(JNI.NewString(my_jenv, "guid"), jGUID);
+ contentValues.put(JNI.NewString(my_jenv, "encType"), jEncType);
+ contentValues.put(JNI.NewString(my_jenv, "timeCreated"), jTimeCreated);
+ contentValues.put(JNI.NewString(my_jenv, "timeLastUsed"), jTimeLastUsed);
+ contentValues.put(JNI.NewString(my_jenv, "timePasswordChanged"), jTimePasswordChanged);
+ contentValues.put(JNI.NewString(my_jenv, "timesUsed"), jTimesUsed);
+
+ return contentValues;
+ },
+
+
+ /*
+ * _searchLogins
+ *
+ * Private method to perform arbitrary searches on any field.
+ *
+ * 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 selectionQuery = [], selectionParams = [];
+
+ for (let field in matchData) {
+ let value = matchData[field];
+ switch (field) {
+ // Historical compatibility requires this special case
+ case "formSubmitURL":
+ if (value) {
+ // As we also need to check for different schemes at the URI
+ // this case gets handled by filtering the result of the query.
+ break;
+ }
+ // Android CP id to _id mapping.
+ case "id":
+ if (value) {
+ selectionQuery.push("_id = ?");
+ selectionParams.push(value);
+ }
+ break;
+ // Normal cases.
+ case "hostname":
+ case "httpRealm":
+ case "usernameField":
+ case "passwordField":
+ case "encryptedUsername":
+ case "encryptedPassword":
+ case "guid":
+ case "encType":
+ case "timeCreated":
+ case "timeLastUsed":
+ case "timePasswordChanged":
+ case "timesUsed":
+ if (value) {
+ selectionQuery.push(field + " = ?");
+ selectionParams.push(value);
+ } else {
+ selectionQuery.push(field + " is null");
+ }
+ break;
+ // Fail if caller requests an unknown property.
+ default:
+ throw new Error("Unexpected field: " + field);
+ }
+ }
+
+ return this._waitForSync(new Promise((resolve, reject) => {
+ var my_jenv;
+
+ try {
+ my_jenv = JNI.GetForThread();
+ let loginsAccessor = this._getLoginsAccessor(my_jenv);
+ JNI.LoadClass(my_jenv, '[' + this._SIG.String);
+ let StringArray = JNI.classes.java.lang.String.array;
+
+ // Allocate a local reference frame with enough space (2 for the String and StringArray).
+ my_jenv.contents.contents.PushLocalFrame(my_jenv, 2);
+
+ let selectionArgs = selectionParams.length ? StringArray.new(selectionParams.length) : this._SIG.NULL;
+ if (selectionParams.length) {
+ selectionArgs.setElements(0, selectionParams);
+ }
+
+ let selection = selectionQuery.length ? JNI.NewString(my_jenv, selectionQuery.join(" AND ")) : this._SIG.NULL;
+
+ let cursor = loginsAccessor.searchLogins(selection, selectionArgs);
+
+ let logins = [], ids = [], fallbackLogins = [], fallbackIds = [];
+ if (!cursor) {
+ // Defend against null cursor.
+ resolve([[logins, ids]]);
+ return;
+ }
+
+ try {
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast()) {
+ // Allocate a local reference frame with enough space (8 String read).
+ my_jenv.contents.contents.PushLocalFrame(my_jenv, 8);
+
+ // Create the new nsLoginInfo object, push to array
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login.init(JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("hostname"))),
+ JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("formSubmitURL"))),
+ JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("httpRealm"))),
+ JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("encryptedUsername"))),
+ JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("encryptedPassword"))),
+ JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("usernameField"))),
+ JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("passwordField"))));
+ // set nsILoginMetaInfo values
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.guid = JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("guid")));
+ login.timeCreated = cursor.getLong(cursor.getColumnIndex("timeCreated"));
+ login.timeLastUsed = cursor.getLong(cursor.getColumnIndex("timeLastUsed"));
+ login.timePasswordChanged = cursor.getLong(cursor.getColumnIndex("timePasswordChanged"));
+ login.timesUsed = cursor.getLong(cursor.getColumnIndex("timesUsed"));
+
+ if (login.formSubmitURL == "" || typeof(matchData.formSubmitURL) == "undefined" ||
+ login.formSubmitURL == matchData.formSubmitURL) {
+ logins.push(login);
+ ids.push(cursor.getLong(cursor.getColumnIndex("_id")));
+ } else if (login.formSubmitURL != null &&
+ login.formSubmitURL != "javascript:" &&
+ matchData.formSubmitURL != "javascript:") {
+ let loginURI = Services.io.newURI(login.formSubmitURL, null, null);
+ let matchURI = Services.io.newURI(matchData.formSubmitURL, null, null);
+
+ if (loginURI.hostPort == matchURI.hostPort &&
+ ((loginURI.scheme == "http" && matchURI.scheme == "https") ||
+ (loginURI.scheme == "https" && matchURI.scheme == "http"))) {
+ fallbackLogins.push(login);
+ fallbackIds.push(cursor.getLong(cursor.getColumnIndex("_id")));
+ }
+ }
+ cursor.moveToNext();
+ // Pop local reference frame to clear all the local references.
+ my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+ }
+ } finally {
+ // Close the cursor.
+ cursor.close();
+ }
+
+ if (!logins.length && fallbackLogins.length) {
+ this.log("_searchLogins: returning " + fallbackLogins.length + " fallback logins");
+ resolve([[fallbackLogins, fallbackIds]]);
+ }
+ this.log("_searchLogins: returning " + logins.length + " logins");
+
+ // Pop local reference frame to clear all the local references.
+ my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+
+ resolve([[logins, ids]]);
+ } catch (ex) {
+ this.log("_searchLogins JNI failure: " + ex.name + " : " + ex.message);
+ reject(ex);
+ } finally {
+ if (my_jenv) {
+ JNI.UnloadClasses(my_jenv);
+ }
+ }
+ }));
+ },
+
+
+ /*
+ * _sendNotification
+ *
+ * Send a notification when stored data is changed.
+ */
+ _sendNotification : function (changeType, data) {
+ let dataObject = data;
+ // Can't pass a raw JS string or array though notifyObservers(). :-(
+ if (data instanceof Array) {
+ dataObject = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ for (let i = 0; i < data.length; i++)
+ dataObject.appendElement(data[i], false);
+ } else if (typeof(data) == "string") {
+ dataObject = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ dataObject.data = data;
+ }
+ Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
+ },
+
+
+ /*
+ * _getIdForLogin
+ *
+ * Returns an array with two items: [id, login]. If the login was not
+ * found, both items will be null. The returned login contains the actual
+ * stored login (useful for looking at the actual nsILoginMetaInfo values).
+ */
+ _getIdForLogin : function (login) {
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (login[field]) {
+ matchData[field] = login[field];
+ }
+ let [logins, ids] = this._searchLogins(matchData);
+
+ let id = null;
+ let foundLogin = null;
+
+ for (let i = 0; i < logins.length; i++) {
+ if (!logins[i].equals(login)) {
+ continue;
+ }
+
+ // We've found a match, set id and break
+ foundLogin = logins[i];
+ id = ids[i];
+ break;
+ }
+
+ return [id, foundLogin];
+ },
+
+
+ /*
+ * _queryDisabledHosts
+ *
+ * Returns an array of hostnames from the database according to the
+ * criteria given in the argument. If the argument hostname is null, the
+ * result array contains all hostnames
+ */
+ _queryDisabledHosts : function (hostname) {
+ return this._waitForSync(new Promise((resolve, reject) => {
+ var my_jenv;
+ try {
+ my_jenv = JNI.GetForThread();
+ let loginsAccessor = this._getLoginsAccessor(my_jenv);
+ // Allocate a local reference frame with enough space (1 for the hostname).
+ my_jenv.contents.contents.PushLocalFrame(my_jenv, 1);
+ let jHostName = hostname ? JNI.NewString(my_jenv, hostname) : this._SIG.NULL
+ let cursor = loginsAccessor.getLoginsSavedEnabled(jHostName);
+ let disabledHosts = [];
+
+ if (!cursor) {
+ // Defend against null cursor.
+ resolve([disabledHosts]);
+ return;
+ }
+
+ try {
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast()) {
+ // Allocate a local reference frame with enough space (1 String read).
+ my_jenv.contents.contents.PushLocalFrame(my_jenv, 1);
+
+ disabledHosts.push(JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("hostname"))));
+ cursor.moveToNext();
+
+ // Pop local reference frame to clear all the local references.
+ my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+ }
+ } finally {
+ // Close the cursor.
+ cursor.close();
+ }
+
+ // Pop local reference frame to clear all the local references.
+ my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+
+ resolve([disabledHosts]);
+ } catch (ex) {
+ this.log("_queryDisabledHosts JNI failure: " + ex.name + " : " + ex.message);
+ reject(ex);
+ } finally {
+ if (my_jenv) {
+ JNI.UnloadClasses(my_jenv);
+ }
+ }
+ }));
+ },
+
+
+ /*
+ * _isGuidUnique
+ *
+ * Checks to see if the specified GUID already exists.
+ */
+ _isGuidUnique : function (guid) {
+ let [logins, ids] = this._searchLogins({"guid" : guid});
+ return ids.length == 0;
+ }
+}; // end of nsLoginManagerStorage_fennecStorage implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_fennecStorage.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login storage");
+ return logger.log.bind(logger);
+});
+
+var component = [LoginManagerStorage_fennecStorage];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
\ No newline at end of file