/* 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); });