mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Backed out changeset ca4caf0d721c (bug 853549) for valgrind testfailures
This commit is contained in:
parent
34e7a08682
commit
74c34d7401
@ -388,7 +388,7 @@
|
||||
#endif
|
||||
@BINPATH@/components/SiteSpecificUserAgent.js
|
||||
@BINPATH@/components/SiteSpecificUserAgent.manifest
|
||||
@BINPATH@/components/storage-json.js
|
||||
@BINPATH@/components/storage-mozStorage.js
|
||||
@BINPATH@/components/crypto-SDR.js
|
||||
@BINPATH@/components/jsconsole-clhandler.manifest
|
||||
@BINPATH@/components/jsconsole-clhandler.js
|
||||
|
@ -1129,18 +1129,6 @@ var gBrowserInit = {
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// Load the Login Manager data from disk off the main thread, some time
|
||||
// after startup. If the data is required before the timeout, for example
|
||||
// because a restored page contains a password field, it will be loaded on
|
||||
// the main thread, and this initialization request will be ignored.
|
||||
setTimeout(function() {
|
||||
try {
|
||||
Services.logins;
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// The object handling the downloads indicator is also initialized here in the
|
||||
// delayed startup function, but the actual indicator element is not loaded
|
||||
// unless there are downloads to be displayed.
|
||||
|
@ -390,7 +390,7 @@
|
||||
@BINPATH@/components/nsLoginInfo.js
|
||||
@BINPATH@/components/nsLoginManager.js
|
||||
@BINPATH@/components/nsLoginManagerPrompter.js
|
||||
@BINPATH@/components/storage-json.js
|
||||
@BINPATH@/components/storage-mozStorage.js
|
||||
@BINPATH@/components/crypto-SDR.js
|
||||
@BINPATH@/components/jsconsole-clhandler.manifest
|
||||
@BINPATH@/components/jsconsole-clhandler.js
|
||||
|
@ -113,6 +113,11 @@ function PasswordStore(name, engine) {
|
||||
Store.call(this, name, engine);
|
||||
this._nsLoginInfo = new Components.Constructor(
|
||||
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "DBConnection", function() {
|
||||
return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.mozIStorageConnection);
|
||||
});
|
||||
}
|
||||
PasswordStore.prototype = {
|
||||
__proto__: Store.prototype,
|
||||
@ -157,6 +162,21 @@ PasswordStore.prototype = {
|
||||
return null;
|
||||
},
|
||||
|
||||
applyIncomingBatch: function applyIncomingBatch(records) {
|
||||
if (!this.DBConnection) {
|
||||
return Store.prototype.applyIncomingBatch.call(this, records);
|
||||
}
|
||||
|
||||
return Utils.runInTransaction(this.DBConnection, function() {
|
||||
return Store.prototype.applyIncomingBatch.call(this, records);
|
||||
}, this);
|
||||
},
|
||||
|
||||
applyIncoming: function applyIncoming(record) {
|
||||
Store.prototype.applyIncoming.call(this, record);
|
||||
this._sleep(0); // Yield back to main thread after synchronous operation.
|
||||
},
|
||||
|
||||
getAllIDs: function PasswordStore__getAllIDs() {
|
||||
let items = {};
|
||||
let logins = Services.logins.getAllLogins({});
|
||||
|
@ -150,6 +150,22 @@ this.Utils = {
|
||||
};
|
||||
},
|
||||
|
||||
runInTransaction: function(db, callback, thisObj) {
|
||||
let hasTransaction = false;
|
||||
try {
|
||||
db.beginTransaction();
|
||||
hasTransaction = true;
|
||||
} catch(e) { /* om nom nom exceptions */ }
|
||||
|
||||
try {
|
||||
return callback.call(thisObj);
|
||||
} finally {
|
||||
if (hasTransaction) {
|
||||
db.commitTransaction();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GUIDs are 9 random bytes encoded with base64url (RFC 4648).
|
||||
* That makes them 12 characters long with 72 bits of entropy.
|
||||
|
@ -1,233 +0,0 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* Contains functions shared by different Login Manager components.
|
||||
*
|
||||
* This JavaScript module exists in order to share code between the different
|
||||
* XPCOM components that constitute the Login Manager, including implementations
|
||||
* of nsILoginManager and nsILoginManagerStorage.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"LoginHelper",
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// LoginHelper
|
||||
|
||||
/**
|
||||
* Contains functions shared by different Login Manager components.
|
||||
*/
|
||||
this.LoginHelper = {
|
||||
/**
|
||||
* Due to the way the signons2.txt file is formatted, we need to make
|
||||
* sure certain field values or characters do not cause the file to
|
||||
* be parsed incorrectly. Reject hostnames that we can't store correctly.
|
||||
*
|
||||
* @throws String with English message in case validation failed.
|
||||
*/
|
||||
checkHostnameValue: function (aHostname)
|
||||
{
|
||||
// Nulls are invalid, as they don't round-trip well. Newlines are also
|
||||
// invalid for any field stored as plaintext, and a hostname made of a
|
||||
// single dot cannot be stored in the legacy format.
|
||||
if (aHostname == "." ||
|
||||
aHostname.indexOf("\r") != -1 ||
|
||||
aHostname.indexOf("\n") != -1 ||
|
||||
aHostname.indexOf("\0") != -1) {
|
||||
throw "Invalid hostname";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Due to the way the signons2.txt file is formatted, we need to make
|
||||
* sure certain field values or characters do not cause the file to
|
||||
* be parsed incorrectly. Reject logins that we can't store correctly.
|
||||
*
|
||||
* @throws String with English message in case validation failed.
|
||||
*/
|
||||
checkLoginValues: function (aLogin)
|
||||
{
|
||||
function badCharacterPresent(l, c)
|
||||
{
|
||||
return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
|
||||
(l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
|
||||
l.hostname.indexOf(c) != -1 ||
|
||||
l.usernameField.indexOf(c) != -1 ||
|
||||
l.passwordField.indexOf(c) != -1);
|
||||
}
|
||||
|
||||
// Nulls are invalid, as they don't round-trip well.
|
||||
// Mostly not a formatting problem, although ".\0" can be quirky.
|
||||
if (badCharacterPresent(aLogin, "\0")) {
|
||||
throw "login values can't contain nulls";
|
||||
}
|
||||
|
||||
// In theory these nulls should just be rolled up into the encrypted
|
||||
// values, but nsISecretDecoderRing doesn't use nsStrings, so the
|
||||
// nulls cause truncation. Check for them here just to avoid
|
||||
// unexpected round-trip surprises.
|
||||
if (aLogin.username.indexOf("\0") != -1 ||
|
||||
aLogin.password.indexOf("\0") != -1) {
|
||||
throw "login values can't contain nulls";
|
||||
}
|
||||
|
||||
// Newlines are invalid for any field stored as plaintext.
|
||||
if (badCharacterPresent(aLogin, "\r") ||
|
||||
badCharacterPresent(aLogin, "\n")) {
|
||||
throw "login values can't contain newlines";
|
||||
}
|
||||
|
||||
// A line with just a "." can have special meaning.
|
||||
if (aLogin.usernameField == "." ||
|
||||
aLogin.formSubmitURL == ".") {
|
||||
throw "login values can't be periods";
|
||||
}
|
||||
|
||||
// A hostname with "\ \(" won't roundtrip.
|
||||
// eg host="foo (", realm="bar" --> "foo ( (bar)"
|
||||
// vs host="foo", realm=" (bar" --> "foo ( (bar)"
|
||||
if (aLogin.hostname.indexOf(" (") != -1) {
|
||||
throw "bad parens in hostname";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new login object that results by modifying the given object with
|
||||
* the provided data.
|
||||
*
|
||||
* @param aOldStoredLogin
|
||||
* Existing nsILoginInfo object to modify.
|
||||
* @param aNewLoginData
|
||||
* The new login values, either as nsILoginInfo or nsIProperyBag.
|
||||
*
|
||||
* @return The newly created nsILoginInfo object.
|
||||
*
|
||||
* @throws String with English message in case validation failed.
|
||||
*/
|
||||
buildModifiedLogin: function (aOldStoredLogin, aNewLoginData)
|
||||
{
|
||||
function bagHasProperty(aPropName)
|
||||
{
|
||||
try {
|
||||
aNewLoginData.getProperty(aPropName);
|
||||
return true;
|
||||
} catch (ex) { }
|
||||
return false;
|
||||
}
|
||||
|
||||
aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
let newLogin;
|
||||
if (aNewLoginData instanceof Ci.nsILoginInfo) {
|
||||
// Clone the existing login to get its nsILoginMetaInfo, then init it
|
||||
// with the replacement nsILoginInfo data from the new login.
|
||||
newLogin = aOldStoredLogin.clone();
|
||||
newLogin.init(aNewLoginData.hostname,
|
||||
aNewLoginData.formSubmitURL, aNewLoginData.httpRealm,
|
||||
aNewLoginData.username, aNewLoginData.password,
|
||||
aNewLoginData.usernameField, aNewLoginData.passwordField);
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// Automatically update metainfo when password is changed.
|
||||
if (newLogin.password != aOldStoredLogin.password) {
|
||||
newLogin.timePasswordChanged = Date.now();
|
||||
}
|
||||
} else if (aNewLoginData instanceof Ci.nsIPropertyBag) {
|
||||
// Clone the existing login, along with all its properties.
|
||||
newLogin = aOldStoredLogin.clone();
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// Automatically update metainfo when password is changed.
|
||||
// (Done before the main property updates, lest the caller be
|
||||
// explicitly updating both .password and .timePasswordChanged)
|
||||
if (bagHasProperty("password")) {
|
||||
let newPassword = aNewLoginData.getProperty("password");
|
||||
if (newPassword != aOldStoredLogin.password) {
|
||||
newLogin.timePasswordChanged = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
let propEnum = aNewLoginData.enumerator;
|
||||
while (propEnum.hasMoreElements()) {
|
||||
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
||||
switch (prop.name) {
|
||||
// nsILoginInfo
|
||||
case "hostname":
|
||||
case "httpRealm":
|
||||
case "formSubmitURL":
|
||||
case "username":
|
||||
case "password":
|
||||
case "usernameField":
|
||||
case "passwordField":
|
||||
// nsILoginMetaInfo
|
||||
case "guid":
|
||||
case "timeCreated":
|
||||
case "timeLastUsed":
|
||||
case "timePasswordChanged":
|
||||
case "timesUsed":
|
||||
newLogin[prop.name] = prop.value;
|
||||
break;
|
||||
|
||||
// Fake property, allows easy incrementing.
|
||||
case "timesUsedIncrement":
|
||||
newLogin.timesUsed += prop.value;
|
||||
break;
|
||||
|
||||
// Fail if caller requests setting an unknown property.
|
||||
default:
|
||||
throw "Unexpected propertybag item: " + prop.name;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw "newLoginData needs an expected interface!";
|
||||
}
|
||||
|
||||
// Sanity check the login
|
||||
if (newLogin.hostname == null || newLogin.hostname.length == 0) {
|
||||
throw "Can't add a login with a null or empty hostname.";
|
||||
}
|
||||
|
||||
// For logins w/o a username, set to "", not null.
|
||||
if (newLogin.username == null) {
|
||||
throw "Can't add a login with a null username.";
|
||||
}
|
||||
|
||||
if (newLogin.password == null || newLogin.password.length == 0) {
|
||||
throw "Can't add a login with a null or empty password.";
|
||||
}
|
||||
|
||||
if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
|
||||
// We have a form submit URL. Can't have a HTTP realm.
|
||||
if (newLogin.httpRealm != null) {
|
||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||
}
|
||||
} else if (newLogin.httpRealm) {
|
||||
// We have a HTTP realm. Can't have a form submit URL.
|
||||
if (newLogin.formSubmitURL != null) {
|
||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||
}
|
||||
} else {
|
||||
// Need one or the other!
|
||||
throw "Can't add a login without a httpRealm or formSubmitURL.";
|
||||
}
|
||||
|
||||
// Throws if there are bogus values.
|
||||
this.checkLoginValues(newLogin);
|
||||
|
||||
return newLogin;
|
||||
},
|
||||
};
|
@ -1,180 +0,0 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* Provides an object that has a method to import login-related data from the
|
||||
* previous SQLite storage format.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"LoginImport",
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||
"resource://gre/modules/Sqlite.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
"resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// LoginImport
|
||||
|
||||
/**
|
||||
* Provides an object that has a method to import login-related data from the
|
||||
* previous SQLite storage format.
|
||||
*
|
||||
* @param aStore
|
||||
* LoginStore object where imported data will be added.
|
||||
* @param aPath
|
||||
* String containing the file path of the SQLite login database.
|
||||
*/
|
||||
this.LoginImport = function (aStore, aPath)
|
||||
{
|
||||
this.store = aStore;
|
||||
this.path = aPath;
|
||||
}
|
||||
|
||||
this.LoginImport.prototype = {
|
||||
/**
|
||||
* LoginStore object where imported data will be added.
|
||||
*/
|
||||
store: null,
|
||||
|
||||
/**
|
||||
* String containing the file path of the SQLite login database.
|
||||
*/
|
||||
path: null,
|
||||
|
||||
/**
|
||||
* Imports login-related data from the previous SQLite storage format.
|
||||
*/
|
||||
import: Task.async(function* () {
|
||||
// We currently migrate data directly from the database to the JSON store at
|
||||
// first run, then we set a preference to prevent repeating the import.
|
||||
// Thus, merging with existing data is not a use case we support. This
|
||||
// restriction might be removed to support re-importing passwords set by an
|
||||
// old version by flipping the import preference and restarting.
|
||||
if (this.store.data.logins.length > 0 ||
|
||||
this.store.data.disabledHosts.length > 0) {
|
||||
throw new Error("Unable to import saved passwords because some data " +
|
||||
"has already been imported or saved.");
|
||||
}
|
||||
|
||||
// When a timestamp is not specified, we will use the same reference time.
|
||||
let referenceTimeMs = Date.now();
|
||||
|
||||
let connection = yield Sqlite.openConnection({ path: this.path });
|
||||
try {
|
||||
let schemaVersion = yield connection.getSchemaVersion();
|
||||
|
||||
// We support importing database schema versions from 3 onwards.
|
||||
// Version 3 was implemented in bug 316084 (Firefox 3.6, March 2009).
|
||||
// Version 4 was implemented in bug 465636 (Firefox 4, March 2010).
|
||||
// Version 5 was implemented in bug 718817 (Firefox 13, February 2012).
|
||||
if (schemaVersion < 3) {
|
||||
throw new Error("Unable to import saved passwords because " +
|
||||
"the existing profile is too old.");
|
||||
}
|
||||
|
||||
let rows = yield connection.execute("SELECT * FROM moz_logins");
|
||||
for (let row of rows) {
|
||||
try {
|
||||
let hostname = row.getResultByName("hostname");
|
||||
let httpRealm = row.getResultByName("httpRealm");
|
||||
let formSubmitURL = row.getResultByName("formSubmitURL");
|
||||
let usernameField = row.getResultByName("usernameField");
|
||||
let passwordField = row.getResultByName("passwordField");
|
||||
let encryptedUsername = row.getResultByName("encryptedUsername");
|
||||
let encryptedPassword = row.getResultByName("encryptedPassword");
|
||||
|
||||
// The "guid" field was introduced in schema version 2, and the
|
||||
// "enctype" field was introduced in schema version 3. We don't
|
||||
// support upgrading from older versions of the database.
|
||||
let guid = row.getResultByName("guid");
|
||||
let encType = row.getResultByName("encType");
|
||||
|
||||
// The time and count fields were introduced in schema version 4.
|
||||
let timeCreated = null;
|
||||
let timeLastUsed = null;
|
||||
let timePasswordChanged = null;
|
||||
let timesUsed = null;
|
||||
try {
|
||||
timeCreated = row.getResultByName("timeCreated");
|
||||
timeLastUsed = row.getResultByName("timeLastUsed");
|
||||
timePasswordChanged = row.getResultByName("timePasswordChanged");
|
||||
timesUsed = row.getResultByName("timesUsed");
|
||||
} catch (ex) { }
|
||||
|
||||
// These columns may be null either because they were not present in
|
||||
// the database or because the record was created on a new schema
|
||||
// version by an old application version.
|
||||
if (!timeCreated) {
|
||||
timeCreated = referenceTimeMs;
|
||||
}
|
||||
if (!timeLastUsed) {
|
||||
timeLastUsed = referenceTimeMs;
|
||||
}
|
||||
if (!timePasswordChanged) {
|
||||
timePasswordChanged = referenceTimeMs;
|
||||
}
|
||||
if (!timesUsed) {
|
||||
timesUsed = 1;
|
||||
}
|
||||
|
||||
this.store.data.logins.push({
|
||||
id: this.store.data.nextId++,
|
||||
hostname: hostname,
|
||||
httpRealm: httpRealm,
|
||||
formSubmitURL: formSubmitURL,
|
||||
usernameField: usernameField,
|
||||
passwordField: passwordField,
|
||||
encryptedUsername: encryptedUsername,
|
||||
encryptedPassword: encryptedPassword,
|
||||
guid: guid,
|
||||
encType: encType,
|
||||
timeCreated: timeCreated,
|
||||
timeLastUsed: timeLastUsed,
|
||||
timePasswordChanged: timePasswordChanged,
|
||||
timesUsed: timesUsed,
|
||||
});
|
||||
} catch (ex) {
|
||||
Cu.reportError("Error importing login: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
rows = yield connection.execute("SELECT * FROM moz_disabledHosts");
|
||||
for (let row of rows) {
|
||||
try {
|
||||
let id = row.getResultByName("id");
|
||||
let hostname = row.getResultByName("hostname");
|
||||
|
||||
this.store.data.disabledHosts.push({
|
||||
id: this.store.data.nextId++,
|
||||
hostname: hostname,
|
||||
});
|
||||
} catch (ex) {
|
||||
Cu.reportError("Error importing disabled host: " + ex);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
yield connection.close();
|
||||
}
|
||||
}),
|
||||
};
|
@ -1,301 +0,0 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* Handles serialization of login-related data and persistence into a file.
|
||||
*
|
||||
* This modules handles the raw data stored in JavaScript serializable objects,
|
||||
* and contains no special validation or query logic, that is handled entirely
|
||||
* by "storage.js" instead.
|
||||
*
|
||||
* The data can be manipulated only after it has been loaded from disk. The
|
||||
* load process can happen asynchronously, through the "load" method, or
|
||||
* synchronously, through "ensureDataReady". After any modification, the
|
||||
* "saveSoon" method must be called to flush the data to disk asynchronously.
|
||||
*
|
||||
* The raw data should be manipulated synchronously, without waiting for the
|
||||
* event loop or for promise resolution, so that the saved file is always
|
||||
* consistent. This synchronous approach also simplifies the query and update
|
||||
* logic. For example, it is possible to find an object and modify it
|
||||
* immediately without caring whether other code modifies it in the meantime.
|
||||
*
|
||||
* An asynchronous shutdown observer makes sure that data is always saved before
|
||||
* the browser is closed. The data cannot be modified during shutdown.
|
||||
*
|
||||
* The file is stored in JSON format, without indentation, using UTF-8 encoding.
|
||||
* With indentation applied, the file would look like this:
|
||||
*
|
||||
* {
|
||||
* "logins": [
|
||||
* {
|
||||
* "id": 2,
|
||||
* "hostname": "http://www.example.com",
|
||||
* "httpRealm": null,
|
||||
* "formSubmitURL": "http://www.example.com/submit-url",
|
||||
* "usernameField": "username_field",
|
||||
* "passwordField": "password_field",
|
||||
* "encryptedUsername": "...",
|
||||
* "encryptedPassword": "...",
|
||||
* "guid": "...",
|
||||
* "encType": 1,
|
||||
* "timeCreated": 1262304000000,
|
||||
* "timeLastUsed": 1262304000000,
|
||||
* "timePasswordChanged": 1262476800000,
|
||||
* "timesUsed": 1
|
||||
* },
|
||||
* {
|
||||
* "id": 4,
|
||||
* (...)
|
||||
* }
|
||||
* ],
|
||||
* "disabledHosts": [
|
||||
* "http://www.example.org",
|
||||
* "http://www.example.net"
|
||||
* ],
|
||||
* "nextId": 10,
|
||||
* "version": 1
|
||||
* }
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"LoginStore",
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||
"resource://gre/modules/AsyncShutdown.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
|
||||
"resource://gre/modules/DeferredTask.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm")
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
|
||||
return new TextDecoder();
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
|
||||
return new TextEncoder();
|
||||
});
|
||||
|
||||
const FileInputStream =
|
||||
Components.Constructor("@mozilla.org/network/file-input-stream;1",
|
||||
"nsIFileInputStream", "init");
|
||||
|
||||
/**
|
||||
* Delay between a change to the login data and the related save operation.
|
||||
*/
|
||||
const kSaveDelayMs = 1500;
|
||||
|
||||
/**
|
||||
* Current data version assigned by the code that last touched the data.
|
||||
*
|
||||
* This number should be updated only when it is important to understand whether
|
||||
* an old version of the code has touched the data, for example to execute an
|
||||
* update logic. In most cases, this number should not be changed, in
|
||||
* particular when no special one-time update logic is needed.
|
||||
*
|
||||
* For example, this number should NOT be changed when a new optional field is
|
||||
* added to a login entry.
|
||||
*/
|
||||
const kDataVersion = 1;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// LoginStore
|
||||
|
||||
/**
|
||||
* Handles serialization of login-related data and persistence into a file.
|
||||
*
|
||||
* @param aPath
|
||||
* String containing the file path where data should be saved.
|
||||
*/
|
||||
function LoginStore(aPath)
|
||||
{
|
||||
this.path = aPath;
|
||||
|
||||
this._saver = new DeferredTask(() => this.save(), kSaveDelayMs);
|
||||
AsyncShutdown.profileBeforeChange.addBlocker("Login store: writing data",
|
||||
() => this._saver.finalize());
|
||||
}
|
||||
|
||||
LoginStore.prototype = {
|
||||
/**
|
||||
* String containing the file path where data should be saved.
|
||||
*/
|
||||
path: "",
|
||||
|
||||
/**
|
||||
* Serializable object containing the login-related data. This is populated
|
||||
* directly with the data loaded from the file, and is saved without
|
||||
* modifications.
|
||||
*
|
||||
* This contains one property for each list.
|
||||
*/
|
||||
data: null,
|
||||
|
||||
/**
|
||||
* True when data has been loaded.
|
||||
*/
|
||||
dataReady: false,
|
||||
|
||||
/**
|
||||
* Loads persistent data from the file to memory.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation finished successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
load: function ()
|
||||
{
|
||||
return Task.spawn(function () {
|
||||
try {
|
||||
let bytes = yield OS.File.read(this.path);
|
||||
|
||||
// If synchronous loading happened in the meantime, exit now.
|
||||
if (this.dataReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = JSON.parse(gTextDecoder.decode(bytes));
|
||||
} catch (ex) {
|
||||
// If an exception occurred because the file did not exist, we should
|
||||
// just start with new data. Other errors may indicate that the file is
|
||||
// corrupt, thus we move it to a backup location before allowing it to
|
||||
// be overwritten by an empty file.
|
||||
if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
|
||||
Cu.reportError(ex);
|
||||
|
||||
// Move the original file to a backup location, ignoring errors.
|
||||
try {
|
||||
let openInfo = yield OS.File.openUnique(this.path + ".corrupt",
|
||||
{ humanReadable: true });
|
||||
yield openInfo.file.close();
|
||||
yield OS.File.move(this.path, openInfo.path);
|
||||
} catch (e2) {
|
||||
Cu.reportError(e2);
|
||||
}
|
||||
}
|
||||
|
||||
// In any case, initialize a new object to host the data.
|
||||
this.data = {
|
||||
nextId: 1,
|
||||
};
|
||||
}
|
||||
|
||||
this._processLoadedData();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads persistent data from the file to memory, synchronously.
|
||||
*/
|
||||
ensureDataReady: function ()
|
||||
{
|
||||
if (this.dataReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// This reads the file and automatically detects the UTF-8 encoding.
|
||||
let inputStream = new FileInputStream(new FileUtils.File(this.path),
|
||||
FileUtils.MODE_RDONLY,
|
||||
FileUtils.PERMS_FILE, 0)
|
||||
try {
|
||||
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
||||
this.data = json.decodeFromStream(inputStream,
|
||||
inputStream.available());
|
||||
} finally {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (ex) {
|
||||
// If an exception occurred because the file did not exist, we should just
|
||||
// start with new data. Other errors may indicate that the file is
|
||||
// corrupt, thus we move it to a backup location before allowing it to be
|
||||
// overwritten by an empty file.
|
||||
if (!(ex instanceof Components.Exception &&
|
||||
ex.result == Cr.NS_ERROR_FILE_NOT_FOUND)) {
|
||||
Cu.reportError(ex);
|
||||
// Move the original file to a backup location, ignoring errors.
|
||||
try {
|
||||
let originalFile = new FileUtils.File(this.path);
|
||||
let backupFile = originalFile.clone();
|
||||
backupFile.leafName += ".corrupt";
|
||||
backupFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE,
|
||||
FileUtils.PERMS_FILE);
|
||||
backupFile.remove(false);
|
||||
originalFile.moveTo(backupFile.parent, backupFile.leafName);
|
||||
} catch (e2) {
|
||||
Cu.reportError(e2);
|
||||
}
|
||||
}
|
||||
|
||||
// In any case, initialize a new object to host the data.
|
||||
this.data = {
|
||||
nextId: 1,
|
||||
};
|
||||
}
|
||||
|
||||
this._processLoadedData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronously work on the data just loaded into memory.
|
||||
*/
|
||||
_processLoadedData: function ()
|
||||
{
|
||||
// Create any arrays that are not present in the saved file.
|
||||
if (!this.data.logins) {
|
||||
this.data.logins = [];
|
||||
}
|
||||
if (!this.data.disabledHosts) {
|
||||
this.data.disabledHosts = [];
|
||||
}
|
||||
|
||||
// Indicate that the current version of the code has touched the file.
|
||||
this.data.version = kDataVersion;
|
||||
|
||||
this.dataReady = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the data changed, this triggers asynchronous serialization.
|
||||
*/
|
||||
saveSoon: function () this._saver.arm(),
|
||||
|
||||
/**
|
||||
* DeferredTask that handles the save operation.
|
||||
*/
|
||||
_saver: null,
|
||||
|
||||
/**
|
||||
* Saves persistent data from memory to the file.
|
||||
*
|
||||
* If an error occurs, the previous file is not deleted.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation finished successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
save: function ()
|
||||
{
|
||||
return Task.spawn(function () {
|
||||
// Create or overwrite the file.
|
||||
let bytes = gTextEncoder.encode(JSON.stringify(this.data));
|
||||
yield OS.File.writeAtomic(this.path, bytes,
|
||||
{ tmpPath: this.path + ".tmp" });
|
||||
}.bind(this));
|
||||
},
|
||||
};
|
@ -23,31 +23,18 @@ XPIDL_MODULE = 'loginmgr'
|
||||
EXTRA_COMPONENTS += [
|
||||
'crypto-SDR.js',
|
||||
'nsLoginInfo.js',
|
||||
'nsLoginManager.js',
|
||||
'nsLoginManagerPrompter.js',
|
||||
'passwordmgr.manifest',
|
||||
]
|
||||
|
||||
EXTRA_PP_COMPONENTS += [
|
||||
'nsLoginManager.js',
|
||||
'passwordmgr.manifest',
|
||||
'storage-mozStorage.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'InsecurePasswordUtils.jsm',
|
||||
'LoginHelper.jsm',
|
||||
'LoginManagerContent.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['OS_TARGET'] == 'Android':
|
||||
EXTRA_COMPONENTS += [
|
||||
'storage-mozStorage.js',
|
||||
]
|
||||
else:
|
||||
EXTRA_COMPONENTS += [
|
||||
'storage-json.js',
|
||||
]
|
||||
EXTRA_JS_MODULES += [
|
||||
'LoginImport.jsm',
|
||||
'LoginStore.jsm',
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
@ -125,11 +125,7 @@ LoginManager.prototype = {
|
||||
|
||||
|
||||
_initStorage : function () {
|
||||
#ifdef ANDROID
|
||||
var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
|
||||
#else
|
||||
var contractID = "@mozilla.org/login-manager/storage/json;1";
|
||||
#endif
|
||||
try {
|
||||
var catMan = Cc["@mozilla.org/categorymanager;1"].
|
||||
getService(Ci.nsICategoryManager);
|
||||
|
@ -6,12 +6,7 @@ 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}
|
||||
#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}
|
||||
|
@ -1,654 +0,0 @@
|
||||
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* vim: set sw=4 ts=4 et lcs=trail\:.,tab\:>~ : */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* nsILoginManagerStorage implementation for the JSON back-end.
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
|
||||
"resource://gre/modules/LoginImport.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||
"resource://gre/modules/LoginStore.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// LoginManagerStorage_json
|
||||
|
||||
this.LoginManagerStorage_json = function () {}
|
||||
|
||||
this.LoginManagerStorage_json.prototype = {
|
||||
classID : Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"),
|
||||
QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
|
||||
|
||||
__crypto : null, // nsILoginManagerCrypto service
|
||||
get _crypto() {
|
||||
if (!this.__crypto)
|
||||
this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
|
||||
getService(Ci.nsILoginManagerCrypto);
|
||||
return this.__crypto;
|
||||
},
|
||||
|
||||
/*
|
||||
* log
|
||||
*
|
||||
* Internal function for logging debug messages to the Error Console.
|
||||
*/
|
||||
log : function (message) {
|
||||
if (!this._debug)
|
||||
return;
|
||||
dump("PwMgr json: " + message + "\n");
|
||||
Services.console.logStringMessage("PwMgr json: " + message);
|
||||
},
|
||||
_debug : false,
|
||||
|
||||
|
||||
/*
|
||||
* initialize
|
||||
*
|
||||
*/
|
||||
initialize : function () {
|
||||
this._debug = Services.prefs.getBoolPref("signon.debug");
|
||||
|
||||
try {
|
||||
// Force initialization of the crypto module.
|
||||
// See bug 717490 comment 17.
|
||||
this._crypto;
|
||||
|
||||
// Set the reference to LoginStore synchronously.
|
||||
let jsonPath = OS.Path.join(OS.Constants.Path.profileDir,
|
||||
"logins.json");
|
||||
this._store = new LoginStore(jsonPath);
|
||||
|
||||
return Task.spawn(function () {
|
||||
// Load the data asynchronously.
|
||||
this.log("Opening database at " + this._store.path);
|
||||
yield this._store.load();
|
||||
|
||||
// The import from previous versions operates the first time
|
||||
// that this built-in storage back-end is used. This may be
|
||||
// later than expected, in case add-ons have registered an
|
||||
// alternate storage that disabled the default one.
|
||||
try {
|
||||
if (Services.prefs.getBoolPref("signon.importedFromSqlite")) {
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
// If the preference does not exist, we need to import.
|
||||
}
|
||||
|
||||
// Import only happens asynchronously.
|
||||
let sqlitePath = OS.Path.join(OS.Constants.Path.profileDir,
|
||||
"signons.sqlite");
|
||||
if (yield OS.File.exists(sqlitePath)) {
|
||||
let loginImport = new LoginImport(this._store, sqlitePath);
|
||||
// Failures during import, for example due to a corrupt
|
||||
// file or a schema version that is too old, will not
|
||||
// prevent us from marking the operation as completed.
|
||||
// At the next startup, we will not try the import again.
|
||||
yield loginImport.import().catch(Cu.reportError);
|
||||
this._store.saveSoon();
|
||||
}
|
||||
|
||||
// We won't attempt import again on next startup.
|
||||
Services.prefs.setBoolPref("signon.importedFromSqlite", true);
|
||||
}.bind(this)).catch(Cu.reportError);
|
||||
} catch (e) {
|
||||
this.log("Initialization failed: " + e);
|
||||
throw "Initialization failed";
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* terminate
|
||||
*
|
||||
* Internal method used by regression tests only. It is called before
|
||||
* replacing this storage module with a new instance.
|
||||
*/
|
||||
terminate : function () {
|
||||
this._store._saver.disarm();
|
||||
return this._store.save();
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* addLogin
|
||||
*
|
||||
*/
|
||||
addLogin : function (login) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let encUsername, encPassword;
|
||||
|
||||
// Throws if there are bogus values.
|
||||
LoginHelper.checkLoginValues(login);
|
||||
|
||||
[encUsername, encPassword, encType] = this._encryptLogin(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) {
|
||||
if (!this._isGuidUnique(loginClone.guid))
|
||||
throw "specified GUID already exists";
|
||||
} else {
|
||||
loginClone.guid = gUUIDGenerator.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._store.data.logins.push({
|
||||
id: this._store.data.nextId++,
|
||||
hostname: loginClone.hostname,
|
||||
httpRealm: loginClone.httpRealm,
|
||||
formSubmitURL: loginClone.formSubmitURL,
|
||||
usernameField: loginClone.usernameField,
|
||||
passwordField: loginClone.passwordField,
|
||||
encryptedUsername: encUsername,
|
||||
encryptedPassword: encPassword,
|
||||
guid: loginClone.guid,
|
||||
encType: encType,
|
||||
timeCreated: loginClone.timeCreated,
|
||||
timeLastUsed: loginClone.timeLastUsed,
|
||||
timePasswordChanged: loginClone.timePasswordChanged,
|
||||
timesUsed: loginClone.timesUsed
|
||||
});
|
||||
this._store.saveSoon();
|
||||
|
||||
// Send a notification that a login was added.
|
||||
this._sendNotification("addLogin", loginClone);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* removeLogin
|
||||
*
|
||||
*/
|
||||
removeLogin : function (login) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let [idToDelete, storedLogin] = this._getIdForLogin(login);
|
||||
if (!idToDelete)
|
||||
throw "No matching logins";
|
||||
|
||||
let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
|
||||
if (foundIndex != -1) {
|
||||
this._store.data.logins.splice(foundIndex, 1);
|
||||
this._store.saveSoon();
|
||||
}
|
||||
|
||||
this._sendNotification("removeLogin", storedLogin);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* modifyLogin
|
||||
*
|
||||
*/
|
||||
modifyLogin : function (oldLogin, newLoginData) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
||||
if (!idToModify)
|
||||
throw "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 "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 "This login already exists.";
|
||||
}
|
||||
|
||||
// Get the encrypted value of the username and password.
|
||||
let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
|
||||
|
||||
for (let loginItem of this._store.data.logins) {
|
||||
if (loginItem.id == idToModify) {
|
||||
loginItem.hostname = newLogin.hostname;
|
||||
loginItem.httpRealm = newLogin.httpRealm;
|
||||
loginItem.formSubmitURL = newLogin.formSubmitURL;
|
||||
loginItem.usernameField = newLogin.usernameField;
|
||||
loginItem.passwordField = newLogin.passwordField;
|
||||
loginItem.encryptedUsername = encUsername;
|
||||
loginItem.encryptedPassword = encPassword;
|
||||
loginItem.guid = newLogin.guid;
|
||||
loginItem.encType = encType;
|
||||
loginItem.timeCreated = newLogin.timeCreated;
|
||||
loginItem.timeLastUsed = newLogin.timeLastUsed;
|
||||
loginItem.timePasswordChanged = newLogin.timePasswordChanged;
|
||||
loginItem.timesUsed = newLogin.timesUsed;
|
||||
this._store.saveSoon();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* getAllLogins
|
||||
*
|
||||
* Returns an array of nsILoginInfo.
|
||||
*/
|
||||
getAllLogins : function (count) {
|
||||
let [logins, ids] = this._searchLogins({});
|
||||
|
||||
// decrypt entries for caller.
|
||||
logins = this._decryptLogins(logins);
|
||||
|
||||
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);
|
||||
|
||||
// Decrypt entries found for the caller.
|
||||
logins = this._decryptLogins(logins);
|
||||
|
||||
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) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let conditions = [];
|
||||
|
||||
function match(aLogin) {
|
||||
for (let field in matchData) {
|
||||
let value = matchData[field];
|
||||
switch (field) {
|
||||
// Historical compatibility requires this special case
|
||||
case "formSubmitURL":
|
||||
if (value != null) {
|
||||
if (aLogin.formSubmitURL != "" && aLogin.formSubmitURL != value) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Normal cases.
|
||||
case "hostname":
|
||||
case "httpRealm":
|
||||
case "id":
|
||||
case "usernameField":
|
||||
case "passwordField":
|
||||
case "encryptedUsername":
|
||||
case "encryptedPassword":
|
||||
case "guid":
|
||||
case "encType":
|
||||
case "timeCreated":
|
||||
case "timeLastUsed":
|
||||
case "timePasswordChanged":
|
||||
case "timesUsed":
|
||||
if (value == null && aLogin[field]) {
|
||||
return false;
|
||||
} else if (aLogin[field] != value) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
// Fail if caller requests an unknown property.
|
||||
default:
|
||||
throw "Unexpected field: " + field;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let foundLogins = [], foundIds = [];
|
||||
for (let loginItem of this._store.data.logins) {
|
||||
if (match(loginItem)) {
|
||||
// Create the new nsLoginInfo object, push to array
|
||||
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
|
||||
createInstance(Ci.nsILoginInfo);
|
||||
login.init(loginItem.hostname, loginItem.formSubmitURL,
|
||||
loginItem.httpRealm, loginItem.encryptedUsername,
|
||||
loginItem.encryptedPassword, loginItem.usernameField,
|
||||
loginItem.passwordField);
|
||||
// set nsILoginMetaInfo values
|
||||
login.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
login.guid = loginItem.guid;
|
||||
login.timeCreated = loginItem.timeCreated;
|
||||
login.timeLastUsed = loginItem.timeLastUsed;
|
||||
login.timePasswordChanged = loginItem.timePasswordChanged;
|
||||
login.timesUsed = loginItem.timesUsed;
|
||||
foundLogins.push(login);
|
||||
foundIds.push(loginItem.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.log("_searchLogins: returning " + foundLogins.length + " logins");
|
||||
return [foundLogins, foundIds];
|
||||
},
|
||||
|
||||
/*
|
||||
* removeAllLogins
|
||||
*
|
||||
* Removes all logins from storage.
|
||||
*
|
||||
* Disabled hosts are kept, as one presumably doesn't want to erase those.
|
||||
*/
|
||||
removeAllLogins : function () {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
this.log("Removing all logins");
|
||||
this._store.data.logins = [];
|
||||
this._store.saveSoon();
|
||||
|
||||
this._sendNotification("removeAllLogins", null);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* getAllDisabledHosts
|
||||
*
|
||||
*/
|
||||
getAllDisabledHosts : function (count) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let disabledHosts = this._store.data.disabledHosts.slice(0);
|
||||
|
||||
this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
|
||||
if (count)
|
||||
count.value = disabledHosts.length; // needed for XPCOM
|
||||
return disabledHosts;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* getLoginSavingEnabled
|
||||
*
|
||||
*/
|
||||
getLoginSavingEnabled : function (hostname) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
this.log("Getting login saving is enabled for " + hostname);
|
||||
return this._store.data.disabledHosts.indexOf(hostname) == -1;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* setLoginSavingEnabled
|
||||
*
|
||||
*/
|
||||
setLoginSavingEnabled : function (hostname, enabled) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
// Throws if there are bogus values.
|
||||
LoginHelper.checkHostnameValue(hostname);
|
||||
|
||||
this.log("Setting login saving enabled for " + hostname + " to " + enabled);
|
||||
let foundIndex = this._store.data.disabledHosts.indexOf(hostname);
|
||||
if (enabled) {
|
||||
if (foundIndex != -1) {
|
||||
this._store.data.disabledHosts.splice(foundIndex, 1);
|
||||
this._store.saveSoon();
|
||||
}
|
||||
} else {
|
||||
if (foundIndex == -1) {
|
||||
this._store.data.disabledHosts.push(hostname);
|
||||
this._store.saveSoon();
|
||||
}
|
||||
}
|
||||
|
||||
this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* findLogins
|
||||
*
|
||||
*/
|
||||
findLogins : function (count, hostname, formSubmitURL, httpRealm) {
|
||||
let loginData = {
|
||||
hostname: hostname,
|
||||
formSubmitURL: formSubmitURL,
|
||||
httpRealm: httpRealm
|
||||
};
|
||||
let matchData = { };
|
||||
for each (let 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 = this._decryptLogins(logins);
|
||||
|
||||
this.log("_findLogins: returning " + logins.length + " logins");
|
||||
count.value = logins.length; // needed for XPCOM
|
||||
return logins;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* countLogins
|
||||
*
|
||||
*/
|
||||
countLogins : function (hostname, formSubmitURL, httpRealm) {
|
||||
let count = {};
|
||||
let loginData = {
|
||||
hostname: hostname,
|
||||
formSubmitURL: formSubmitURL,
|
||||
httpRealm: httpRealm
|
||||
};
|
||||
let matchData = { };
|
||||
for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
||||
if (loginData[field] != '')
|
||||
matchData[field] = loginData[field];
|
||||
let [logins, ids] = this._searchLogins(matchData);
|
||||
|
||||
this.log("_countLogins: counted logins: " + logins.length);
|
||||
return logins.length;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* uiBusy
|
||||
*/
|
||||
get uiBusy() {
|
||||
return this._crypto.uiBusy;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* isLoggedIn
|
||||
*/
|
||||
get isLoggedIn() {
|
||||
return this._crypto.isLoggedIn;
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _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 each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
||||
if (login[field] != '')
|
||||
matchData[field] = login[field];
|
||||
let [logins, ids] = this._searchLogins(matchData);
|
||||
|
||||
let id = null;
|
||||
let foundLogin = null;
|
||||
|
||||
// The specified login isn't encrypted, so we need to ensure
|
||||
// the logins we're comparing with are decrypted. We decrypt one entry
|
||||
// at a time, lest _decryptLogins return fewer entries and screw up
|
||||
// indices between the two.
|
||||
for (let i = 0; i < logins.length; i++) {
|
||||
let [decryptedLogin] = this._decryptLogins([logins[i]]);
|
||||
|
||||
if (!decryptedLogin || !decryptedLogin.equals(login))
|
||||
continue;
|
||||
|
||||
// We've found a match, set id and break
|
||||
foundLogin = decryptedLogin;
|
||||
id = ids[i];
|
||||
break;
|
||||
}
|
||||
|
||||
return [id, foundLogin];
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _isGuidUnique
|
||||
*
|
||||
* Checks to see if the specified GUID already exists.
|
||||
*/
|
||||
_isGuidUnique : function (guid) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
return this._store.data.logins.every(l => l.guid != guid);
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _encryptLogin
|
||||
*
|
||||
* Returns the encrypted username, password, and encrypton type for the specified
|
||||
* login. Can throw if the user cancels a master password entry.
|
||||
*/
|
||||
_encryptLogin : function (login) {
|
||||
let encUsername = this._crypto.encrypt(login.username);
|
||||
let encPassword = this._crypto.encrypt(login.password);
|
||||
let encType = this._crypto.defaultEncType;
|
||||
|
||||
return [encUsername, encPassword, encType];
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _decryptLogins
|
||||
*
|
||||
* Decrypts username and password fields in the provided array of
|
||||
* logins.
|
||||
*
|
||||
* The entries specified by the array will be decrypted, if possible.
|
||||
* An array of successfully decrypted logins will be returned. The return
|
||||
* value should be given to external callers (since still-encrypted
|
||||
* entries are useless), whereas internal callers generally don't want
|
||||
* to lose unencrypted entries (eg, because the user clicked Cancel
|
||||
* instead of entering their master password)
|
||||
*/
|
||||
_decryptLogins : function (logins) {
|
||||
let result = [];
|
||||
|
||||
for each (let login in logins) {
|
||||
try {
|
||||
login.username = this._crypto.decrypt(login.username);
|
||||
login.password = this._crypto.decrypt(login.password);
|
||||
} catch (e) {
|
||||
// If decryption failed (corrupt entry?), just skip it.
|
||||
// Rethrow other errors (like canceling entry of a master pw)
|
||||
if (e.result == Cr.NS_ERROR_FAILURE)
|
||||
continue;
|
||||
throw e;
|
||||
}
|
||||
result.push(login);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManagerStorage_json]);
|
@ -13,10 +13,9 @@ 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");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
|
||||
/**
|
||||
* Object that manages a database transaction properly so consumers don't have
|
||||
@ -244,7 +243,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
let encUsername, encPassword;
|
||||
|
||||
// Throws if there are bogus values.
|
||||
LoginHelper.checkLoginValues(login);
|
||||
this._checkLoginValues(login);
|
||||
|
||||
[encUsername, encPassword, encType] = this._encryptLogin(login);
|
||||
|
||||
@ -356,16 +355,109 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
||||
if (!idToModify)
|
||||
throw "No matching logins";
|
||||
oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
|
||||
let newLogin;
|
||||
if (newLoginData instanceof Ci.nsILoginInfo) {
|
||||
// Clone the existing login to get its nsILoginMetaInfo, then init it
|
||||
// with the replacement nsILoginInfo data from the new login.
|
||||
newLogin = oldStoredLogin.clone();
|
||||
newLogin.init(newLoginData.hostname,
|
||||
newLoginData.formSubmitURL, newLoginData.httpRealm,
|
||||
newLoginData.username, newLoginData.password,
|
||||
newLoginData.usernameField, newLoginData.passwordField);
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// Check if the new GUID is duplicate.
|
||||
if (newLogin.guid != oldStoredLogin.guid &&
|
||||
!this._isGuidUnique(newLogin.guid))
|
||||
{
|
||||
throw "specified GUID already exists";
|
||||
// Automatically update metainfo when password is changed.
|
||||
if (newLogin.password != oldLogin.password)
|
||||
newLogin.timePasswordChanged = Date.now();
|
||||
} else if (newLoginData instanceof Ci.nsIPropertyBag) {
|
||||
function _bagHasProperty(aPropName) {
|
||||
try {
|
||||
newLoginData.getProperty(aPropName);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Clone the existing login, along with all its properties.
|
||||
newLogin = oldStoredLogin.clone();
|
||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// Automatically update metainfo when password is changed.
|
||||
// (Done before the main property updates, lest the caller be
|
||||
// explicitly updating both .password and .timePasswordChanged)
|
||||
if (_bagHasProperty("password")) {
|
||||
let newPassword = newLoginData.getProperty("password");
|
||||
if (newPassword != oldLogin.password)
|
||||
newLogin.timePasswordChanged = Date.now();
|
||||
}
|
||||
|
||||
let propEnum = newLoginData.enumerator;
|
||||
while (propEnum.hasMoreElements()) {
|
||||
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
||||
switch (prop.name) {
|
||||
// nsILoginInfo properties...
|
||||
case "hostname":
|
||||
case "httpRealm":
|
||||
case "formSubmitURL":
|
||||
case "username":
|
||||
case "password":
|
||||
case "usernameField":
|
||||
case "passwordField":
|
||||
// nsILoginMetaInfo properties...
|
||||
case "guid":
|
||||
case "timeCreated":
|
||||
case "timeLastUsed":
|
||||
case "timePasswordChanged":
|
||||
case "timesUsed":
|
||||
newLogin[prop.name] = prop.value;
|
||||
if (prop.name == "guid" && !this._isGuidUnique(newLogin.guid))
|
||||
throw "specified GUID already exists";
|
||||
break;
|
||||
|
||||
// Fake property, allows easy incrementing.
|
||||
case "timesUsedIncrement":
|
||||
newLogin.timesUsed += prop.value;
|
||||
break;
|
||||
|
||||
// Fail if caller requests setting an unknown property.
|
||||
default:
|
||||
throw "Unexpected propertybag item: " + prop.name;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw "newLoginData needs an expected interface!";
|
||||
}
|
||||
|
||||
// Sanity check the login
|
||||
if (newLogin.hostname == null || newLogin.hostname.length == 0)
|
||||
throw "Can't add a login with a null or empty hostname.";
|
||||
|
||||
// For logins w/o a username, set to "", not null.
|
||||
if (newLogin.username == null)
|
||||
throw "Can't add a login with a null username.";
|
||||
|
||||
if (newLogin.password == null || newLogin.password.length == 0)
|
||||
throw "Can't add a login with a null or empty password.";
|
||||
|
||||
if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
|
||||
// We have a form submit URL. Can't have a HTTP realm.
|
||||
if (newLogin.httpRealm != null)
|
||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||
} else if (newLogin.httpRealm) {
|
||||
// We have a HTTP realm. Can't have a form submit URL.
|
||||
if (newLogin.formSubmitURL != null)
|
||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||
} else {
|
||||
// Need one or the other!
|
||||
throw "Can't add a login without a httpRealm or formSubmitURL.";
|
||||
}
|
||||
|
||||
// Throws if there are bogus values.
|
||||
this._checkLoginValues(newLogin);
|
||||
|
||||
// Look for an existing entry in case key properties changed.
|
||||
if (!newLogin.matches(oldLogin, true)) {
|
||||
let logins = this.findLogins({}, newLogin.hostname,
|
||||
@ -573,6 +665,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
*
|
||||
*/
|
||||
storeDeletedLogin : function(aLogin) {
|
||||
#ifdef ANDROID
|
||||
let stmt = null;
|
||||
try {
|
||||
this.log("Storing " + aLogin.guid + " in deleted passwords\n");
|
||||
@ -587,6 +680,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
if (stmt)
|
||||
stmt.reset();
|
||||
}
|
||||
#endif
|
||||
},
|
||||
|
||||
|
||||
@ -620,7 +714,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
}
|
||||
|
||||
this._sendNotification("removeAllLogins", null);
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
@ -653,7 +747,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
*/
|
||||
setLoginSavingEnabled : function (hostname, enabled) {
|
||||
// Throws if there are bogus values.
|
||||
LoginHelper.checkHostnameValue(hostname);
|
||||
this._checkHostnameValue(hostname);
|
||||
|
||||
this.log("Setting login saving enabled for " + hostname + " to " + enabled);
|
||||
let query;
|
||||
@ -694,8 +788,8 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
};
|
||||
let matchData = { };
|
||||
for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
||||
if (loginData[field] != '')
|
||||
matchData[field] = loginData[field];
|
||||
if (loginData[field] != '')
|
||||
matchData[field] = loginData[field];
|
||||
let [logins, ids] = this._searchLogins(matchData);
|
||||
|
||||
// Decrypt entries found for the caller.
|
||||
@ -884,6 +978,70 @@ LoginManagerStorage_mozStorage.prototype = {
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _checkLoginValues
|
||||
*
|
||||
* Due to the way the signons2.txt file is formatted, we need to make
|
||||
* sure certain field values or characters do not cause the file to
|
||||
* be parse incorrectly. Reject logins that we can't store correctly.
|
||||
*/
|
||||
_checkLoginValues : function (aLogin) {
|
||||
function badCharacterPresent(l, c) {
|
||||
return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
|
||||
(l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
|
||||
l.hostname.indexOf(c) != -1 ||
|
||||
l.usernameField.indexOf(c) != -1 ||
|
||||
l.passwordField.indexOf(c) != -1);
|
||||
}
|
||||
|
||||
// Nulls are invalid, as they don't round-trip well.
|
||||
// Mostly not a formatting problem, although ".\0" can be quirky.
|
||||
if (badCharacterPresent(aLogin, "\0"))
|
||||
throw "login values can't contain nulls";
|
||||
|
||||
// In theory these nulls should just be rolled up into the encrypted
|
||||
// values, but nsISecretDecoderRing doesn't use nsStrings, so the
|
||||
// nulls cause truncation. Check for them here just to avoid
|
||||
// unexpected round-trip surprises.
|
||||
if (aLogin.username.indexOf("\0") != -1 ||
|
||||
aLogin.password.indexOf("\0") != -1)
|
||||
throw "login values can't contain nulls";
|
||||
|
||||
// Newlines are invalid for any field stored as plaintext.
|
||||
if (badCharacterPresent(aLogin, "\r") ||
|
||||
badCharacterPresent(aLogin, "\n"))
|
||||
throw "login values can't contain newlines";
|
||||
|
||||
// A line with just a "." can have special meaning.
|
||||
if (aLogin.usernameField == "." ||
|
||||
aLogin.formSubmitURL == ".")
|
||||
throw "login values can't be periods";
|
||||
|
||||
// A hostname with "\ \(" won't roundtrip.
|
||||
// eg host="foo (", realm="bar" --> "foo ( (bar)"
|
||||
// vs host="foo", realm=" (bar" --> "foo ( (bar)"
|
||||
if (aLogin.hostname.indexOf(" (") != -1)
|
||||
throw "bad parens in hostname";
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _checkHostnameValue
|
||||
*
|
||||
* Legacy storage prohibited newlines and nulls in hostnames, so we'll keep
|
||||
* that standard here. Throws on illegal format.
|
||||
*/
|
||||
_checkHostnameValue : function (hostname) {
|
||||
// File format prohibits certain values. Also, nulls
|
||||
// won't round-trip with getAllDisabledHosts().
|
||||
if (hostname == "." ||
|
||||
hostname.indexOf("\r") != -1 ||
|
||||
hostname.indexOf("\n") != -1 ||
|
||||
hostname.indexOf("\0") != -1)
|
||||
throw "Invalid hostname";
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* _isGuidUnique
|
||||
*
|
||||
|
@ -17,14 +17,12 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
|
||||
"resource://gre/modules/DownloadPaths.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
"resource://gre/modules/commonjs/sdk/core/promise.js");
|
||||
|
||||
const LoginInfo =
|
||||
Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
|
||||
@ -45,46 +43,6 @@ function run_test()
|
||||
// Some of these functions are already implemented in other parts of the source
|
||||
// tree, see bug 946708 about sharing more code.
|
||||
|
||||
// While the previous test file should have deleted all the temporary files it
|
||||
// used, on Windows these might still be pending deletion on the physical file
|
||||
// system. Thus, start from a new base number every time, to make a collision
|
||||
// with a file that is still pending deletion highly unlikely.
|
||||
let gFileCounter = Math.floor(Math.random() * 1000000);
|
||||
|
||||
/**
|
||||
* Returns a reference to a temporary file, that is guaranteed not to exist, and
|
||||
* to have never been created before.
|
||||
*
|
||||
* @param aLeafName
|
||||
* Suggested leaf name for the file to be created.
|
||||
*
|
||||
* @return nsIFile pointing to a non-existent file in a temporary directory.
|
||||
*
|
||||
* @note It is not enough to delete the file if it exists, or to delete the file
|
||||
* after calling nsIFile.createUnique, because on Windows the delete
|
||||
* operation in the file system may still be pending, preventing a new
|
||||
* file with the same name to be created.
|
||||
*/
|
||||
function getTempFile(aLeafName)
|
||||
{
|
||||
// Prepend a serial number to the extension in the suggested leaf name.
|
||||
let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
|
||||
let leafName = base + "-" + gFileCounter + ext;
|
||||
gFileCounter++;
|
||||
|
||||
// Get a file reference under the temporary directory for this test file.
|
||||
let file = FileUtils.getFile("TmpD", [leafName]);
|
||||
do_check_false(file.exists());
|
||||
|
||||
do_register_cleanup(function () {
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
});
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows waiting for an observer notification once.
|
||||
*
|
||||
|
@ -1,248 +0,0 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests the LoginImport object.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
|
||||
"resource://gre/modules/LoginImport.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||
"resource://gre/modules/LoginStore.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||
"resource://gre/modules/Sqlite.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gLoginManagerCrypto",
|
||||
"@mozilla.org/login-manager/crypto/SDR;1",
|
||||
"nsILoginManagerCrypto");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
/**
|
||||
* Creates empty login data tables in the given SQLite connection, resembling
|
||||
* the most recent schema version (excluding indices).
|
||||
*/
|
||||
function promiseCreateDatabaseSchema(aConnection)
|
||||
{
|
||||
return Task.spawn(function () {
|
||||
yield aConnection.setSchemaVersion(5);
|
||||
yield aConnection.execute("CREATE TABLE moz_logins (" +
|
||||
"id INTEGER PRIMARY KEY," +
|
||||
"hostname TEXT NOT NULL," +
|
||||
"httpRealm TEXT," +
|
||||
"formSubmitURL TEXT," +
|
||||
"usernameField TEXT NOT NULL," +
|
||||
"passwordField TEXT NOT NULL," +
|
||||
"encryptedUsername TEXT NOT NULL," +
|
||||
"encryptedPassword TEXT NOT NULL," +
|
||||
"guid TEXT," +
|
||||
"encType INTEGER," +
|
||||
"timeCreated INTEGER," +
|
||||
"timeLastUsed INTEGER," +
|
||||
"timePasswordChanged INTEGER," +
|
||||
"timesUsed INTEGER)");
|
||||
yield aConnection.execute("CREATE TABLE moz_disabledHosts (" +
|
||||
"id INTEGER PRIMARY KEY," +
|
||||
"hostname TEXT UNIQUE)");
|
||||
yield aConnection.execute("CREATE TABLE moz_deleted_logins (" +
|
||||
"id INTEGER PRIMARY KEY," +
|
||||
"guid TEXT," +
|
||||
"timeDeleted INTEGER)");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new entry in the database resembling the given nsILoginInfo object.
|
||||
*/
|
||||
function promiseInsertLoginInfo(aConnection, aLoginInfo)
|
||||
{
|
||||
aLoginInfo.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
|
||||
// We can't use the aLoginInfo object directly in the execute statement
|
||||
// because the bind code in Sqlite.jsm doesn't allow objects with extra
|
||||
// properties beyond those being binded. So we might as well use an array as
|
||||
// it is simpler.
|
||||
let values = [
|
||||
aLoginInfo.hostname,
|
||||
aLoginInfo.httpRealm,
|
||||
aLoginInfo.formSubmitURL,
|
||||
aLoginInfo.usernameField,
|
||||
aLoginInfo.passwordField,
|
||||
gLoginManagerCrypto.encrypt(aLoginInfo.username),
|
||||
gLoginManagerCrypto.encrypt(aLoginInfo.password),
|
||||
aLoginInfo.guid,
|
||||
aLoginInfo.encType,
|
||||
aLoginInfo.timeCreated,
|
||||
aLoginInfo.timeLastUsed,
|
||||
aLoginInfo.timePasswordChanged,
|
||||
aLoginInfo.timesUsed,
|
||||
];
|
||||
|
||||
return aConnection.execute("INSERT INTO moz_logins (hostname, " +
|
||||
"httpRealm, formSubmitURL, usernameField, " +
|
||||
"passwordField, encryptedUsername, " +
|
||||
"encryptedPassword, guid, encType, timeCreated, " +
|
||||
"timeLastUsed, timePasswordChanged, timesUsed) " +
|
||||
"VALUES (?" + ",?".repeat(12) + ")", values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new disabled host entry in the database.
|
||||
*/
|
||||
function promiseInsertDisabledHost(aConnection, aHostname)
|
||||
{
|
||||
return aConnection.execute("INSERT INTO moz_disabledHosts (hostname) " +
|
||||
"VALUES (?)", [aHostname]);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Tests
|
||||
|
||||
/**
|
||||
* Imports login data from a SQLite file constructed using the test data.
|
||||
*/
|
||||
add_task(function test_import()
|
||||
{
|
||||
let store = new LoginStore(getTempFile("test-import.json").path);
|
||||
let loginsSqlite = getTempFile("test-logins.sqlite").path;
|
||||
|
||||
// Prepare the logins to be imported, including the nsILoginMetaInfo data.
|
||||
let loginList = TestData.loginList();
|
||||
for (let loginInfo of loginList) {
|
||||
loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
loginInfo.guid = gUUIDGenerator.generateUUID().toString();
|
||||
loginInfo.timeCreated = Date.now();
|
||||
loginInfo.timeLastUsed = Date.now();
|
||||
loginInfo.timePasswordChanged = Date.now();
|
||||
loginInfo.timesUsed = 1;
|
||||
}
|
||||
|
||||
// Create and populate the SQLite database first.
|
||||
let connection = yield Sqlite.openConnection({ path: loginsSqlite });
|
||||
try {
|
||||
yield promiseCreateDatabaseSchema(connection);
|
||||
for (let loginInfo of loginList) {
|
||||
yield promiseInsertLoginInfo(connection, loginInfo);
|
||||
}
|
||||
yield promiseInsertDisabledHost(connection, "http://www.example.com");
|
||||
yield promiseInsertDisabledHost(connection, "https://www.example.org");
|
||||
} finally {
|
||||
yield connection.close();
|
||||
}
|
||||
|
||||
// The "load" method must be called before importing data.
|
||||
yield store.load();
|
||||
yield new LoginImport(store, loginsSqlite).import();
|
||||
|
||||
// Verify that every login in the test data has a matching imported row.
|
||||
do_check_eq(loginList.length, store.data.logins.length);
|
||||
do_check_true(loginList.every(function (loginInfo) {
|
||||
return store.data.logins.some(function (loginDataItem) {
|
||||
let username = gLoginManagerCrypto.decrypt(loginDataItem.encryptedUsername);
|
||||
let password = gLoginManagerCrypto.decrypt(loginDataItem.encryptedPassword);
|
||||
return loginDataItem.hostname == loginInfo.hostname &&
|
||||
loginDataItem.httpRealm == loginInfo.httpRealm &&
|
||||
loginDataItem.formSubmitURL == loginInfo.formSubmitURL &&
|
||||
loginDataItem.usernameField == loginInfo.usernameField &&
|
||||
loginDataItem.passwordField == loginInfo.passwordField &&
|
||||
username == loginInfo.username &&
|
||||
password == loginInfo.password &&
|
||||
loginDataItem.guid == loginInfo.guid &&
|
||||
loginDataItem.encType == loginInfo.encType &&
|
||||
loginDataItem.timeCreated == loginInfo.timeCreated &&
|
||||
loginDataItem.timeLastUsed == loginInfo.timeLastUsed &&
|
||||
loginDataItem.timePasswordChanged == loginInfo.timePasswordChanged &&
|
||||
loginDataItem.timesUsed == loginInfo.timesUsed;
|
||||
});
|
||||
}));
|
||||
|
||||
// Verify that disabled hosts have been imported.
|
||||
do_check_eq(store.data.disabledHosts.length, 2);
|
||||
do_check_true(store.data.disabledHosts.some(
|
||||
dataItem => dataItem.hostname == "http://www.example.com"));
|
||||
do_check_true(store.data.disabledHosts.some(
|
||||
dataItem => dataItem.hostname == "https://www.example.org"));
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests imports of NULL values due to a downgraded database.
|
||||
*/
|
||||
add_task(function test_import_downgraded()
|
||||
{
|
||||
let store = new LoginStore(getTempFile("test-import-downgraded.json").path);
|
||||
let loginsSqlite = getTempFile("test-logins-downgraded.sqlite").path;
|
||||
let loginList = TestData.loginList();
|
||||
|
||||
// Create and populate the SQLite database first.
|
||||
let connection = yield Sqlite.openConnection({ path: loginsSqlite });
|
||||
try {
|
||||
yield promiseCreateDatabaseSchema(connection);
|
||||
yield connection.setSchemaVersion(3);
|
||||
yield promiseInsertLoginInfo(connection, TestData.formLogin({
|
||||
guid: gUUIDGenerator.generateUUID().toString(),
|
||||
timeCreated: null,
|
||||
timeLastUsed: null,
|
||||
timePasswordChanged: null,
|
||||
timesUsed: 0,
|
||||
}));
|
||||
} finally {
|
||||
yield connection.close();
|
||||
}
|
||||
|
||||
// The "load" method must be called before importing data.
|
||||
yield store.load();
|
||||
yield new LoginImport(store, loginsSqlite).import();
|
||||
|
||||
// Verify that the missing metadata was generated correctly.
|
||||
let loginItem = store.data.logins[0];
|
||||
let creationTime = loginItem.timeCreated;
|
||||
LoginTest.assertTimeIsAboutNow(creationTime);
|
||||
do_check_eq(loginItem.timeLastUsed, creationTime);
|
||||
do_check_eq(loginItem.timePasswordChanged, creationTime);
|
||||
do_check_eq(loginItem.timesUsed, 1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that importing from a SQLite file with database version 2 fails.
|
||||
*/
|
||||
add_task(function test_import_v2()
|
||||
{
|
||||
let store = new LoginStore(getTempFile("test-import-v2.json").path);
|
||||
let loginsSqlite = do_get_file("data/signons-v2.sqlite").path;
|
||||
|
||||
// The "load" method must be called before importing data.
|
||||
yield store.load();
|
||||
try {
|
||||
yield new LoginImport(store, loginsSqlite).import();
|
||||
do_throw("The operation should have failed.");
|
||||
} catch (ex) { }
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a SQLite file, with database version 3.
|
||||
*/
|
||||
add_task(function test_import_v3()
|
||||
{
|
||||
let store = new LoginStore(getTempFile("test-import-v3.json").path);
|
||||
let loginsSqlite = do_get_file("data/signons-v3.sqlite").path;
|
||||
|
||||
// The "load" method must be called before importing data.
|
||||
yield store.load();
|
||||
yield new LoginImport(store, loginsSqlite).import();
|
||||
|
||||
// We only execute basic integrity checks.
|
||||
do_check_eq(store.data.logins[0].usernameField, "u1");
|
||||
do_check_eq(store.data.disabledHosts.length, 0);
|
||||
});
|
@ -1,208 +0,0 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests the LoginStore object.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||
"resource://gre/modules/LoginStore.jsm");
|
||||
|
||||
const TEST_STORE_FILE_NAME = "test-logins.json";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Tests
|
||||
|
||||
/**
|
||||
* Saves login data to a file, then reloads it.
|
||||
*/
|
||||
add_task(function test_save_reload()
|
||||
{
|
||||
let storeForSave = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
// The "load" method must be called before preparing the data to be saved.
|
||||
yield storeForSave.load();
|
||||
|
||||
let rawLoginData = {
|
||||
id: storeForSave.data.nextId++,
|
||||
hostname: "http://www.example.com",
|
||||
httpRealm: null,
|
||||
formSubmitURL: "http://www.example.com/submit-url",
|
||||
usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345),
|
||||
passwordField: "field_" + String.fromCharCode(421, 259, 349, 537),
|
||||
encryptedUsername: "(test)",
|
||||
encryptedPassword: "(test)",
|
||||
guid: "(test)",
|
||||
encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
|
||||
timeCreated: Date.now(),
|
||||
timeLastUsed: Date.now(),
|
||||
timePasswordChanged: Date.now(),
|
||||
timesUsed: 1,
|
||||
};
|
||||
storeForSave.data.logins.push(rawLoginData);
|
||||
|
||||
storeForSave.data.disabledHosts.push("http://www.example.org");
|
||||
|
||||
yield storeForSave.save();
|
||||
|
||||
// Test the asynchronous initialization path.
|
||||
let storeForLoad = new LoginStore(storeForSave.path);
|
||||
yield storeForLoad.load();
|
||||
|
||||
do_check_eq(storeForLoad.data.logins.length, 1);
|
||||
do_check_matches(storeForLoad.data.logins[0], rawLoginData);
|
||||
do_check_eq(storeForLoad.data.disabledHosts.length, 1);
|
||||
do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
|
||||
|
||||
// Test the synchronous initialization path.
|
||||
storeForLoad = new LoginStore(storeForSave.path);
|
||||
storeForLoad.ensureDataReady();
|
||||
|
||||
do_check_eq(storeForLoad.data.logins.length, 1);
|
||||
do_check_matches(storeForLoad.data.logins[0], rawLoginData);
|
||||
do_check_eq(storeForLoad.data.disabledHosts.length, 1);
|
||||
do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that loading from a missing file results in empty arrays.
|
||||
*/
|
||||
add_task(function test_load_empty()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
do_check_false(yield OS.File.exists(store.path));
|
||||
|
||||
yield store.load();
|
||||
|
||||
do_check_false(yield OS.File.exists(store.path));
|
||||
|
||||
do_check_eq(store.data.logins.length, 0);
|
||||
do_check_eq(store.data.disabledHosts.length, 0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that saving empty data still overwrites any existing file.
|
||||
*/
|
||||
add_task(function test_save_empty()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
yield store.load();
|
||||
|
||||
let createdFile = yield OS.File.open(store.path, { create: true });
|
||||
yield createdFile.close();
|
||||
|
||||
yield store.save();
|
||||
|
||||
do_check_true(yield OS.File.exists(store.path));
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads data from a string in a predefined format. The purpose of this test is
|
||||
* to verify that the JSON format used in previous versions can be loaded.
|
||||
*/
|
||||
add_task(function test_load_string_predefined()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
let string = "{\"logins\":[{" +
|
||||
"\"id\":1," +
|
||||
"\"hostname\":\"http://www.example.com\"," +
|
||||
"\"httpRealm\":null," +
|
||||
"\"formSubmitURL\":\"http://www.example.com/submit-url\"," +
|
||||
"\"usernameField\":\"usernameField\"," +
|
||||
"\"passwordField\":\"passwordField\"," +
|
||||
"\"encryptedUsername\":\"(test)\"," +
|
||||
"\"encryptedPassword\":\"(test)\"," +
|
||||
"\"guid\":\"(test)\"," +
|
||||
"\"encType\":1," +
|
||||
"\"timeCreated\":1262304000000," +
|
||||
"\"timeLastUsed\":1262390400000," +
|
||||
"\"timePasswordChanged\":1262476800000," +
|
||||
"\"timesUsed\":1}],\"disabledHosts\":[" +
|
||||
"\"http://www.example.org\"]}";
|
||||
|
||||
yield OS.File.writeAtomic(store.path,
|
||||
new TextEncoder().encode(string),
|
||||
{ tmpPath: store.path + ".tmp" });
|
||||
|
||||
yield store.load();
|
||||
|
||||
do_check_eq(store.data.logins.length, 1);
|
||||
do_check_matches(store.data.logins[0], {
|
||||
id: 1,
|
||||
hostname: "http://www.example.com",
|
||||
httpRealm: null,
|
||||
formSubmitURL: "http://www.example.com/submit-url",
|
||||
usernameField: "usernameField",
|
||||
passwordField: "passwordField",
|
||||
encryptedUsername: "(test)",
|
||||
encryptedPassword: "(test)",
|
||||
guid: "(test)",
|
||||
encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
|
||||
timeCreated: 1262304000000,
|
||||
timeLastUsed: 1262390400000,
|
||||
timePasswordChanged: 1262476800000,
|
||||
timesUsed: 1,
|
||||
});
|
||||
|
||||
do_check_eq(store.data.disabledHosts.length, 1);
|
||||
do_check_eq(store.data.disabledHosts[0], "http://www.example.org");
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads login data from a malformed JSON string.
|
||||
*/
|
||||
add_task(function test_load_string_malformed()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
|
||||
"\"id\":1,";
|
||||
|
||||
yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
|
||||
{ tmpPath: store.path + ".tmp" });
|
||||
|
||||
yield store.load();
|
||||
|
||||
// A backup file should have been created.
|
||||
do_check_true(yield OS.File.exists(store.path + ".corrupt"));
|
||||
yield OS.File.remove(store.path + ".corrupt");
|
||||
|
||||
// The store should be ready to accept new data.
|
||||
do_check_eq(store.data.logins.length, 0);
|
||||
do_check_eq(store.data.disabledHosts.length, 0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads login data from a malformed JSON string, using the synchronous
|
||||
* initialization path.
|
||||
*/
|
||||
add_task(function test_load_string_malformed_sync()
|
||||
{
|
||||
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||
|
||||
let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
|
||||
"\"id\":1,";
|
||||
|
||||
yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
|
||||
{ tmpPath: store.path + ".tmp" });
|
||||
|
||||
store.ensureDataReady();
|
||||
|
||||
// A backup file should have been created.
|
||||
do_check_true(yield OS.File.exists(store.path + ".corrupt"));
|
||||
yield OS.File.remove(store.path + ".corrupt");
|
||||
|
||||
// The store should be ready to accept new data.
|
||||
do_check_eq(store.data.logins.length, 0);
|
||||
do_check_eq(store.data.disabledHosts.length, 0);
|
||||
});
|
@ -3,17 +3,6 @@ head = head.js
|
||||
tail =
|
||||
support-files = data/**
|
||||
|
||||
# Test JSON file access and import from SQLite, not applicable to Android.
|
||||
[test_module_LoginImport.js]
|
||||
skip-if = os == "android"
|
||||
[test_module_LoginStore.js]
|
||||
skip-if = os == "android"
|
||||
|
||||
# Test SQLite database backup and migration, applicable to Android only.
|
||||
[test_storage_mozStorage.js]
|
||||
skip-if = os != "android"
|
||||
|
||||
# The following tests apply to any storage back-end.
|
||||
[test_disabled_hosts.js]
|
||||
[test_legacy_empty_formSubmitURL.js]
|
||||
[test_legacy_validation.js]
|
||||
@ -23,3 +12,4 @@ skip-if = os != "android"
|
||||
[test_logins_search.js]
|
||||
[test_notifications.js]
|
||||
[test_storage.js]
|
||||
[test_storage_mozStorage.js]
|
||||
|
Loading…
Reference in New Issue
Block a user