245 lines
6.7 KiB
JavaScript
245 lines
6.7 KiB
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/. */
|
|
|
|
/**
|
|
* LoginManagerStorage implementation for GeckoView
|
|
*/
|
|
|
|
import { LoginManagerStorage_json } from "resource://gre/modules/storage-json.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.sys.mjs",
|
|
LoginEntry: "resource://gre/modules/GeckoViewAutocomplete.sys.mjs",
|
|
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
|
|
});
|
|
|
|
export class LoginManagerStorage extends LoginManagerStorage_json {
|
|
static #storage = null;
|
|
|
|
static create(callback) {
|
|
if (!LoginManagerStorage.#storage) {
|
|
LoginManagerStorage.#storage = new LoginManagerStorage();
|
|
LoginManagerStorage.#storage.initialize().then(callback);
|
|
} else if (callback) {
|
|
callback();
|
|
}
|
|
|
|
return LoginManagerStorage.#storage;
|
|
}
|
|
|
|
get _crypto() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
initialize() {
|
|
try {
|
|
return Promise.resolve();
|
|
} catch (e) {
|
|
this.log("Initialization failed:", e);
|
|
throw new Error("Initialization failed");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method used by regression tests only. It is called before
|
|
* replacing this storage module with a new instance.
|
|
*/
|
|
terminate() {}
|
|
|
|
async addLoginsAsync(_logins, _continueOnDuplicates = false) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
removeLogin(_login) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
modifyLogin(_oldLogin, _newLoginData) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
recordPasswordUse(login) {
|
|
lazy.GeckoViewAutocomplete.onLoginPasswordUsed(
|
|
lazy.LoginEntry.fromLoginInfo(login)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a promise resolving to an array of all saved logins that can be decrypted.
|
|
*
|
|
* @resolve {nsILoginInfo[]}
|
|
*/
|
|
getAllLogins(includeDeleted) {
|
|
return this._getLoginsAsync({}, includeDeleted);
|
|
}
|
|
|
|
async searchLoginsAsync(matchData, includeDeleted) {
|
|
this.log(
|
|
`Searching for matching saved logins for origin: ${matchData.origin}`
|
|
);
|
|
return this._getLoginsAsync(matchData, includeDeleted);
|
|
}
|
|
|
|
_baseHostnameFromOrigin(origin) {
|
|
if (!origin) {
|
|
return null;
|
|
}
|
|
|
|
let originURI = Services.io.newURI(origin);
|
|
try {
|
|
return Services.eTLD.getBaseDomain(originURI);
|
|
} catch (ex) {
|
|
if (ex.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
|
|
// `getBaseDomain` cannot handle IP addresses and `nsIURI` cannot return
|
|
// IPv6 hostnames with the square brackets so use `URL.hostname`.
|
|
return new URL(origin).hostname;
|
|
} else if (ex.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
|
|
return originURI.asciiHost;
|
|
}
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
async _getLoginsAsync(matchData, includeDeleted) {
|
|
let baseHostname = this._baseHostnameFromOrigin(matchData.origin);
|
|
|
|
// Query all logins for the eTLD+1 and then filter the logins in _searchLogins
|
|
// so that we can handle the logic for scheme upgrades, subdomains, etc.
|
|
// Convert from the new shape to one which supports the legacy getters used
|
|
// by _searchLogins.
|
|
let candidateLogins = await lazy.GeckoViewAutocomplete.fetchLogins(
|
|
baseHostname
|
|
).catch(_ => {
|
|
// No GV delegate is attached.
|
|
});
|
|
|
|
if (!candidateLogins) {
|
|
// May be undefined if there is no delegate attached to handle the request.
|
|
// Ignore the request.
|
|
return [];
|
|
}
|
|
|
|
let realMatchData = {};
|
|
let options = {};
|
|
|
|
if (matchData.guid) {
|
|
// Enforce GUID-based filtering when available, since the origin of the
|
|
// login may not match the origin of the form in the case of scheme
|
|
// upgrades.
|
|
realMatchData = { guid: matchData.guid };
|
|
} else {
|
|
for (let [name, value] of Object.entries(matchData)) {
|
|
switch (name) {
|
|
// Some property names aren't field names but are special options to
|
|
// affect the search.
|
|
case "acceptDifferentSubdomains":
|
|
case "schemeUpgrades": {
|
|
options[name] = value;
|
|
break;
|
|
}
|
|
default: {
|
|
realMatchData[name] = value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const [logins] = this._searchLogins(
|
|
realMatchData,
|
|
includeDeleted,
|
|
options,
|
|
candidateLogins.map(this._vanillaLoginToStorageLogin)
|
|
);
|
|
return logins;
|
|
}
|
|
|
|
/**
|
|
* Convert a modern decrypted vanilla login object to one expected from logins.json.
|
|
*
|
|
* The storage login is usually encrypted but not in this case, this aligns
|
|
* with the `_decryptLogins` method being a no-op.
|
|
*
|
|
* @param {object} vanillaLogin using `origin`/`formActionOrigin`/`username` properties.
|
|
* @returns {object} a vanilla login for logins.json using
|
|
* `hostname`/`formSubmitURL`/`encryptedUsername`.
|
|
*/
|
|
_vanillaLoginToStorageLogin(vanillaLogin) {
|
|
return {
|
|
...vanillaLogin,
|
|
hostname: vanillaLogin.origin,
|
|
formSubmitURL: vanillaLogin.formActionOrigin,
|
|
encryptedUsername: vanillaLogin.username,
|
|
encryptedPassword: vanillaLogin.password,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Use `searchLoginsAsync` instead.
|
|
*/
|
|
searchLogins(_matchData) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
/**
|
|
* Removes all logins from storage.
|
|
*/
|
|
removeAllLogins() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
countLogins(_origin, _formActionOrigin, _httpRealm) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
get uiBusy() {
|
|
return false;
|
|
}
|
|
|
|
get isLoggedIn() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* GeckoView will encrypt the login itself.
|
|
*/
|
|
_encryptLogin(login) {
|
|
return login;
|
|
}
|
|
|
|
/**
|
|
* GeckoView logins are already decrypted before this component receives them
|
|
* so this method is a no-op for this backend.
|
|
* @see _vanillaLoginToStorageLogin
|
|
*/
|
|
_decryptLogins(logins) {
|
|
return logins;
|
|
}
|
|
|
|
/**
|
|
* Sync metadata, which isn't supported by GeckoView.
|
|
*/
|
|
async getSyncID() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
async setSyncID(_syncID) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
async getLastSync() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
async setLastSync(_timestamp) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
}
|
|
|
|
ChromeUtils.defineLazyGetter(LoginManagerStorage.prototype, "log", () => {
|
|
let logger = lazy.LoginHelper.createLogger("Login storage");
|
|
return logger.log.bind(logger);
|
|
});
|