Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
import { PromptUtils } from "resource://gre/modules/PromptUtils.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetter(
lazy,
"gPrompterService",
"@mozilla.org/login-manager/prompter;1",
Ci.nsILoginManagerPrompter
);
/* eslint-disable block-scoped-var, no-var */
ChromeUtils.defineESModuleGetters(lazy, {
});
const LoginInfo = Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1",
"nsILoginInfo",
"init"
);
/**
* A helper module to prevent modal auth prompt abuse.
*/
const PromptAbuseHelper = {
getBaseDomainOrFallback(hostname) {
try {
return Services.eTLD.getBaseDomainFromHost(hostname);
} catch (e) {
return hostname;
}
},
incrementPromptAbuseCounter(baseDomain, browser) {
if (!browser) {
return;
}
if (!browser.authPromptAbuseCounter) {
browser.authPromptAbuseCounter = {};
}
if (!browser.authPromptAbuseCounter[baseDomain]) {
browser.authPromptAbuseCounter[baseDomain] = 0;
}
browser.authPromptAbuseCounter[baseDomain] += 1;
},
resetPromptAbuseCounter(baseDomain, browser) {
if (!browser || !browser.authPromptAbuseCounter) {
return;
}
browser.authPromptAbuseCounter[baseDomain] = 0;
},
hasReachedAbuseLimit(baseDomain, browser) {
if (!browser || !browser.authPromptAbuseCounter) {
return false;
}
let abuseCounter = browser.authPromptAbuseCounter[baseDomain];
// Allow for setting -1 to turn the feature off.
if (this.abuseLimit < 0) {
return false;
}
return !!abuseCounter && abuseCounter >= this.abuseLimit;
},
};
XPCOMUtils.defineLazyPreferenceGetter(
PromptAbuseHelper,
"abuseLimit",
"prompts.authentication_dialog_abuse_limit"
);
/**
* Implements nsIPromptFactory
*
* Invoked by [toolkit/components/prompts/src/Prompter.sys.mjs]
*/
export function LoginManagerAuthPromptFactory() {
Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
}
LoginManagerAuthPromptFactory.prototype = {
classID: Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
QueryInterface: ChromeUtils.generateQI([
"nsIPromptFactory",
"nsIObserver",
"nsISupportsWeakReference",
]),
// Tracks pending auth prompts per top level browser and hash key.
// browser -> hashkey -> prompt
// This enables us to consolidate auth prompts with the same browser and
// hashkey (level, origin, realm).
_pendingPrompts: new WeakMap(),
_pendingSavePrompts: new WeakMap(),
// We use a separate bucket for when we don't have a browser.
// _noBrowser -> hashkey -> prompt
_noBrowser: {},
// Promise used to defer prompts if the password manager isn't ready when
// they're called.
_uiBusyPromise: null,
_uiBusyResolve: null,
observe(_subject, topic, _data) {
this.log(`Observed topic: ${topic}.`);
if (topic == "passwordmgr-crypto-login") {
// Show the deferred prompters.
this._uiBusyResolve?.();
}
},
getPrompt(aWindow, aIID) {
var prompt = new LoginManagerAuthPrompter().QueryInterface(aIID);
prompt.init(aWindow, this);
return prompt;
},
getPendingPrompt(browser, hashKey) {
// If there is already a matching auth prompt which has no browser
// associated we can reuse it. This way we avoid showing tab level prompts
// when there is already a pending window prompt.
let pendingNoBrowserPrompt = this._pendingPrompts
.get(this._noBrowser)
?.get(hashKey);
if (pendingNoBrowserPrompt) {
return pendingNoBrowserPrompt;
}
return this._pendingPrompts.get(browser)?.get(hashKey);
},
_dismissPendingSavePrompt(browser) {
this._pendingSavePrompts.get(browser)?.dismiss();
this._pendingSavePrompts.delete(browser);
},
_setPendingSavePrompt(browser, prompt) {
this._pendingSavePrompts.set(browser, prompt);
},
_setPendingPrompt(prompt, hashKey) {
let browser = prompt.prompter.browser || this._noBrowser;
let hashToPrompt = this._pendingPrompts.get(browser);
if (!hashToPrompt) {
hashToPrompt = new Map();
this._pendingPrompts.set(browser, hashToPrompt);
}
hashToPrompt.set(hashKey, prompt);
},
_removePendingPrompt(prompt, hashKey) {
let browser = prompt.prompter.browser || this._noBrowser;
let hashToPrompt = this._pendingPrompts.get(browser);
if (!hashToPrompt) {
return;
}
hashToPrompt.delete(hashKey);
if (!hashToPrompt.size) {
this._pendingPrompts.delete(browser);
}
},
async _waitForLoginsUI(prompt) {
await this._uiBusyPromise;
let [origin, httpRealm] = prompt.prompter._getAuthTarget(
prompt.channel,
prompt.authInfo
);
// No UI to wait for.
if (!Services.logins.uiBusy) {
return;
}
let hasLogins = Services.logins.countLogins(origin, null, httpRealm) > 0;
if (
!hasLogins &&
lazy.LoginHelper.schemeUpgrades &&
origin.startsWith("https://")
) {
let httpOrigin = origin.replace(/^https:\/\//, "http://");
hasLogins = Services.logins.countLogins(httpOrigin, null, httpRealm) > 0;
}
// We don't depend on saved logins.
if (!hasLogins) {
return;
}
this.log("Waiting for primary password UI.");
this._uiBusyPromise = new Promise(resolve => {
this._uiBusyResolve = resolve;
});
await this._uiBusyPromise;
},
async _doAsyncPrompt(prompt, hashKey) {
this._setPendingPrompt(prompt, hashKey);
// UI might be busy due to the primary password dialog. Wait for it to close.
await this._waitForLoginsUI(prompt);
let ok = false;
let promptAborted = false;
try {
this.log(`Performing the prompt for ${hashKey}.`);
ok = await prompt.prompter.promptAuthInternal(
prompt.channel,
prompt.level,
prompt.authInfo
);
} catch (e) {
if (
e instanceof Components.Exception &&
e.result == Cr.NS_ERROR_NOT_AVAILABLE
) {
this.log("Bypassed, UI is not available in this context.");
// Prompts throw NS_ERROR_NOT_AVAILABLE if they're aborted.
promptAborted = true;
} else {
console.error("LoginManagerAuthPrompter: _doAsyncPrompt", e);
}
}
this._removePendingPrompt(prompt, hashKey);
// Handle callbacks
for (var consumer of prompt.consumers) {
if (!consumer.callback) {
// Not having a callback means that consumer didn't provide it
// or canceled the notification
continue;
}
this.log(`Calling back to callback: ${consumer.callback} ok: ${ok}.`);
try {
if (ok) {
consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
} else {
consumer.callback.onAuthCancelled(consumer.context, !promptAborted);
}
} catch (e) {
/* Throw away exceptions caused by callback */
}
}
},
}; // end of LoginManagerAuthPromptFactory implementation
ChromeUtils.defineLazyGetter(
LoginManagerAuthPromptFactory.prototype,
"log",
() => {
let logger = lazy.LoginHelper.createLogger("LoginManagerAuthPromptFactory");
return logger.log.bind(logger);
}
);
/* ==================== LoginManagerAuthPrompter ==================== */
/**
* Implements interfaces for prompting the user to enter/save/change auth info.
*
* nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
*
* Note this implementation no longer provides `nsIAuthPrompt.promptPassword()`
* and `nsIAuthPrompt.promptUsernameAndPassword()`. Use their async
* counterparts `asyncPromptPassword` and `asyncPromptUsernameAndPassword`
* instead.
*
* nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
* (eg HTTP Authenticate, FTP login).
*
* nsILoginManagerAuthPrompter: Used by consumers to indicate which tab/window a
* prompt should appear on.
*/
export function LoginManagerAuthPrompter() {}
LoginManagerAuthPrompter.prototype = {
classID: Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
QueryInterface: ChromeUtils.generateQI([
"nsIAuthPrompt",
"nsIAuthPrompt2",
"nsILoginManagerAuthPrompter",
]),
_factory: null,
_chromeWindow: null,
_browser: null,
__strBundle: null, // String bundle for L10N
get _strBundle() {
if (!this.__strBundle) {
this.__strBundle = Services.strings.createBundle(
);
if (!this.__strBundle) {
throw new Error("String bundle for Login Manager not present!");
}
}
return this.__strBundle;
},
__ellipsis: null,
get _ellipsis() {
if (!this.__ellipsis) {
this.__ellipsis = "\u2026";
try {
this.__ellipsis = Services.prefs.getComplexValue(
"intl.ellipsis",
Ci.nsIPrefLocalizedString
).data;
} catch (e) {}
}
return this.__ellipsis;
},
// Whether we are in private browsing mode
get _inPrivateBrowsing() {
if (this._chromeWindow) {
return PrivateBrowsingUtils.isWindowPrivate(this._chromeWindow);
}
// If we don't that we're in private browsing mode if the caller did
// not provide a window. The callers which really care about this
// will indeed pass down a window to us, and for those who don't,
// we can just assume that we don't want to save the entered login
// information.
this.log("We have no chromeWindow so assume we're in a private context.");
return true;
},
get _allowRememberLogin() {
if (!this._inPrivateBrowsing) {
return true;
}
return lazy.LoginHelper.privateBrowsingCaptureEnabled;
},
/* ---------- nsIAuthPrompt prompts ---------- */
/**
* Wrapper around the prompt service prompt. Saving random fields here
* doesn't really make sense and therefore isn't implemented.
*/
prompt(
aDialogTitle,
aText,
aPasswordRealm,
aSavePassword,
aDefaultText,
aResult
) {
if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) {
throw new Components.Exception(
"prompt only supports SAVE_PASSWORD_NEVER",
Cr.NS_ERROR_NOT_IMPLEMENTED
);
}
if (aDefaultText) {
aResult.value = aDefaultText;
}
return Services.prompt.prompt(
this._chromeWindow,
aDialogTitle,
aText,
aResult,
null,
{}
);
},
/**
* Looks up a username and password in the database. Will prompt the user
* with a dialog, even if a username and password are found.
*/
async asyncPromptUsernameAndPassword(
aDialogTitle,
aText,
aPasswordRealm,
aSavePassword,
aUsername,
aPassword
) {
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
throw new Components.Exception(
"asyncPromptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
Cr.NS_ERROR_NOT_IMPLEMENTED
);
}
let foundLogins = null;
let canRememberLogin = false;
var selectedLogin = null;
var [origin, realm] = this._getRealmInfo(aPasswordRealm);
// If origin is null, we can't save this login.
if (origin) {
if (this._allowRememberLogin) {
canRememberLogin =
aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
Services.logins.getLoginSavingEnabled(origin);
}
// Look for existing logins.
// We don't use searchLoginsAsync here and in asyncPromptPassword
// because of bug 1848682
let matchData = lazy.LoginHelper.newPropertyBag({
origin,
httpRealm: realm,
});
foundLogins = Services.logins.searchLogins(matchData);
// XXX Like the original code, we can't deal with multiple
// account selection. (bug 227632)
if (foundLogins.length) {
selectedLogin = foundLogins[0];
// If the caller provided a username, try to use it. If they
// provided only a password, this will try to find a password-only
// login (or return null if none exists).
if (aUsername.value) {
selectedLogin = this._repickSelectedLogin(
foundLogins,
aUsername.value
);
}
if (selectedLogin) {
aUsername.value = selectedLogin.username;
// If the caller provided a password, prefer it.
if (!aPassword.value) {
aPassword.value = selectedLogin.password;
}
}
}
}
let autofilled = !!aPassword.value;
var ok = Services.prompt.promptUsernameAndPassword(
this._chromeWindow,
aDialogTitle,
aText,
aUsername,
aPassword
);
if (!ok || !canRememberLogin) {
return ok;
}
if (!aPassword.value) {
this.log("No password entered, so won't offer to save.");
return ok;
}
// XXX We can't prompt with multiple logins yet (bug 227632), so
// the entered login might correspond to an existing login
// other than the one we originally selected.
selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
// If we didn't find an existing login, or if the username
// changed, save as a new login.
let newLogin = new LoginInfo(
origin,
null,
realm,
aUsername.value,
aPassword.value
);
if (!selectedLogin) {
// add as new
this.log(`New login seen for: ${realm}.`);
await Services.logins.addLoginAsync(newLogin);
} else if (aPassword.value != selectedLogin.password) {
// update password
this.log(`Updating password for ${realm}.`);
this._updateLogin(selectedLogin, newLogin);
} else {
this.log("Login unchanged, no further action needed.");
Services.logins.recordPasswordUse(
selectedLogin,
this._inPrivateBrowsing,
"prompt_login",
autofilled
);
}
return ok;
},
/**
* If a password is found in the database for the password realm, it is
* returned straight away without displaying a dialog.
*
* If a password is not found in the database, the user will be prompted
* with a dialog with a text field and ok/cancel buttons. If the user
* allows it, then the password will be saved in the database.
*/
async asyncPromptPassword(
aDialogTitle,
aText,
aPasswordRealm,
aSavePassword,
aPassword
) {
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
throw new Components.Exception(
"promptPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
Cr.NS_ERROR_NOT_IMPLEMENTED
);
}
var [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
username = decodeURIComponent(username);
let canRememberLogin = false;
// If origin is null, we can't save this login.
if (origin && !this._inPrivateBrowsing) {
canRememberLogin =
aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
Services.logins.getLoginSavingEnabled(origin);
if (!aPassword.value) {
// Look for existing logins.
let matchData = lazy.LoginHelper.newPropertyBag({
origin,
httpRealm: realm,
});
let foundLogins = Services.logins.searchLogins(matchData);
// XXX Like the original code, we can't deal with multiple
// account selection (bug 227632). We can deal with finding the
// account based on the supplied username - but in this case we'll
// just return the first match.
for (var i = 0; i < foundLogins.length; ++i) {
if (foundLogins[i].username == username) {
aPassword.value = foundLogins[i].password;
// wallet returned straight away, so this mimics that code
return true;
}
}
}
}
var ok = Services.prompt.promptPassword(
this._chromeWindow,
aDialogTitle,
aText,
aPassword
);
if (ok && canRememberLogin && aPassword.value) {
let newLogin = new LoginInfo(
origin,
null,
realm,
username,
aPassword.value
);
this.log(`New login seen for ${realm}.`);
await Services.logins.addLoginAsync(newLogin);
}
return ok;
},
/* ---------- nsIAuthPrompt helpers ---------- */
/**
* Given aRealmString, such as "http://user@example.com/foo", returns an
* array of:
* - the formatted origin
* - the realm (origin + path)
* - the username, if present
*
* If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
* channels, e.g. "example.com:80 (httprealm)", null is returned for all
* arguments to let callers know the login can't be saved because we don't
* know whether it's http or https.
*/
_getRealmInfo(aRealmString) {
var httpRealm = /^.+ \(.+\)$/;
if (httpRealm.test(aRealmString)) {
return [null, null, null];
}
var uri = Services.io.newURI(aRealmString);
var pathname = "";
if (uri.pathQueryRef != "/") {
pathname = uri.pathQueryRef;
}
var formattedOrigin = this._getFormattedOrigin(uri);
return [formattedOrigin, formattedOrigin + pathname, uri.username];
},
async promptAuthInternal(aChannel, aLevel, aAuthInfo) {
var selectedLogin = null;
var epicfail = false;
var canAutologin = false;
var foundLogins;
let autofilled = false;
try {
// If the user submits a login but it fails, we need to remove the
// notification prompt that was displayed. Conveniently, the user will
// be prompted for authentication again, which brings us here.
this._factory._dismissPendingSavePrompt(this._browser);
var [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
// Looks for existing logins to prefill the prompt with.
foundLogins = await Services.logins.searchLoginsAsync({
origin,
httpRealm,
schemeUpgrades: lazy.LoginHelper.schemeUpgrades,
});
this.log(`Found ${foundLogins.length} matching logins.`);
let resolveBy = ["scheme", "timePasswordChanged"];
foundLogins = lazy.LoginHelper.dedupeLogins(
foundLogins,
["username"],
resolveBy,
origin
);
this.log(`${foundLogins.length} matching logins remain after deduping.`);
// XXX Can't select from multiple accounts yet. (bug 227632)
if (foundLogins.length) {
selectedLogin = foundLogins[0];
this._SetAuthInfo(
aAuthInfo,
selectedLogin.username,
selectedLogin.password
);
autofilled = true;
// Allow automatic proxy login
if (
aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
!(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
Services.prefs.getBoolPref("signon.autologin.proxy") &&
!PrivateBrowsingUtils.permanentPrivateBrowsing
) {
this.log("Autologin enabled, skipping auth prompt.");
canAutologin = true;
}
}
var canRememberLogin = Services.logins.getLoginSavingEnabled(origin);
if (!this._allowRememberLogin) {
canRememberLogin = false;
}
} catch (e) {
// Ignore any errors and display the prompt anyway.
epicfail = true;
console.error("LoginManagerAuthPrompter: Epic fail in promptAuth:", e);
}
var ok = canAutologin;
let browser = this._browser;
let baseDomain;
// We might not have a browser or browser.currentURI.host could fail
// (e.g. on about:blank). Fall back to the subresource hostname in that case.
try {
let topLevelHost = browser.currentURI.host;
baseDomain = PromptAbuseHelper.getBaseDomainOrFallback(topLevelHost);
} catch (e) {
baseDomain = PromptAbuseHelper.getBaseDomainOrFallback(origin);
}
if (!ok) {
if (PromptAbuseHelper.hasReachedAbuseLimit(baseDomain, browser)) {
this.log("Blocking auth dialog, due to exceeding dialog bloat limit.");
return false;
}
// Set up a counter for ensuring that the basic auth prompt can not
// be abused for DOS-style attacks. With this counter, each eTLD+1
// per browser will get a limited number of times a user can
// cancel the prompt until we stop showing it.
PromptAbuseHelper.incrementPromptAbuseCounter(baseDomain, browser);
if (this._chromeWindow) {
PromptUtils.fireDialogEvent(
this._chromeWindow,
"DOMWillOpenModalDialog",
this._browser
);
}
ok = await Services.prompt.asyncPromptAuth(
this._browser?.browsingContext,
Ci.nsIPrompt.MODAL_TYPE_TAB,
aChannel,
aLevel,
aAuthInfo
);
}
let [username, password] = this._GetAuthInfo(aAuthInfo);
// Reset the counter state if the user replied to a prompt and actually
// tried to login (vs. simply clicking any button to get out).
if (ok && (username || password)) {
PromptAbuseHelper.resetPromptAbuseCounter(baseDomain, browser);
}
if (!ok || !canRememberLogin || epicfail) {
return ok;
}
try {
if (!password) {
this.log("No password entered, so won't offer to save.");
return ok;
}
// XXX We can't prompt with multiple logins yet (bug 227632), so
// the entered login might correspond to an existing login
// other than the one we originally selected.
selectedLogin = this._repickSelectedLogin(foundLogins, username);
// If we didn't find an existing login, or if the username
// changed, save as a new login.
let newLogin = new LoginInfo(origin, null, httpRealm, username, password);
if (!selectedLogin) {
this.log(`New login seen for origin: ${origin}.`);
let promptBrowser = lazy.LoginHelper.getBrowserForPrompt(browser);
let savePrompt = lazy.gPrompterService.promptToSavePassword(
promptBrowser,
newLogin
);
this._factory._setPendingSavePrompt(promptBrowser, savePrompt);
} else if (password != selectedLogin.password) {
this.log(`Updating password for origin: ${origin}.`);
let promptBrowser = lazy.LoginHelper.getBrowserForPrompt(browser);
let savePrompt = lazy.gPrompterService.promptToChangePassword(
promptBrowser,
selectedLogin,
newLogin
);
this._factory._setPendingSavePrompt(promptBrowser, savePrompt);
} else {
this.log("Login unchanged, no further action needed.");
Services.logins.recordPasswordUse(
selectedLogin,
this._inPrivateBrowsing,
"auth_login",
autofilled
);
}
} catch (e) {
console.error("LoginManagerAuthPrompter: Fail2 in promptAuth:", e);
}
return ok;
},
/* ---------- nsIAuthPrompt2 prompts ---------- */
/**
* Implementation of nsIAuthPrompt2.
*
* @param {nsIChannel} aChannel
* @param {int} aLevel
* @param {nsIAuthInformation} aAuthInfo
*/
promptAuth(aChannel, aLevel, aAuthInfo) {
let closed = false;
let result = false;
this.promptAuthInternal(aChannel, aLevel, aAuthInfo)
.then(ok => (result = ok))
.finally(() => (closed = true));
Services.tm.spinEventLoopUntilOrQuit(
"LoginManagerAuthPrompter.sys.mjs:promptAuth",
() => closed
);
return result;
},
asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) {
var cancelable = null;
try {
// If the user submits a login but it fails, we need to remove the
// notification prompt that was displayed. Conveniently, the user will
// be prompted for authentication again, which brings us here.
this._factory._dismissPendingSavePrompt(this._browser);
cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
let [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
let hashKey = aLevel + "|" + origin + "|" + httpRealm;
let pendingPrompt = this._factory.getPendingPrompt(
this._browser,
hashKey
);
if (pendingPrompt) {
this.log(
`Prompt bound to an existing one in the queue, callback: ${aCallback}.`
);
pendingPrompt.consumers.push(cancelable);
return cancelable;
}
this.log(`Adding new async prompt, callback: ${aCallback}.`);
let asyncPrompt = {
consumers: [cancelable],
channel: aChannel,
authInfo: aAuthInfo,
level: aLevel,
prompter: this,
};
this._factory._doAsyncPrompt(asyncPrompt, hashKey);
} catch (e) {
console.error("LoginManagerAuthPrompter: asyncPromptAuth:", e);
console.error("Falling back to promptAuth");
// Fail the prompt operation to let the consumer fall back
// to synchronous promptAuth method
throw e;
}
return cancelable;
},
/* ---------- nsILoginManagerAuthPrompter prompts ---------- */
init(aWindow = null, aFactory = null) {
if (!aWindow) {
// There may be no applicable window e.g. in a Sandbox or JSM.
this._chromeWindow = null;
this._browser = null;
} else if (aWindow.isChromeWindow) {
this._chromeWindow = aWindow;
// needs to be set explicitly using setBrowser
this._browser = null;
} else {
let { win, browser } = this._getChromeWindow(aWindow);
this._chromeWindow = win;
this._browser = browser;
}
this._factory = aFactory || null;
},
set browser(aBrowser) {
this._browser = aBrowser;
},
get browser() {
return this._browser;
},
/* ---------- Internal Methods ---------- */
_updateLogin(login, aNewLogin) {
var now = Date.now();
var propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
Ci.nsIWritablePropertyBag
);
propBag.setProperty("formActionOrigin", aNewLogin.formActionOrigin);
propBag.setProperty("origin", aNewLogin.origin);
propBag.setProperty("password", aNewLogin.password);
propBag.setProperty("username", aNewLogin.username);
// Explicitly set the password change time here (even though it would
// be changed automatically), to ensure that it's exactly the same
// value as timeLastUsed.
propBag.setProperty("timePasswordChanged", now);
propBag.setProperty("timeLastUsed", now);
propBag.setProperty("timesUsedIncrement", 1);
// Note that we don't call `recordPasswordUse` so we won't potentially record
// both a use and a save/update. See bug 1640096.
Services.logins.modifyLogin(login, propBag);
},
/**
* Given a content DOM window, returns the chrome window and browser it's in.
*/
_getChromeWindow(aWindow) {
let browser = aWindow.docShell.chromeEventHandler;
if (!browser) {
return null;
}
let chromeWin = browser.ownerGlobal;
if (!chromeWin) {
return null;
}
return { win: chromeWin, browser };
},
/**
* The user might enter a login that isn't the one we prefilled, but
* is the same as some other existing login. So, pick a login with a
* matching username, or return null.
*/
_repickSelectedLogin(foundLogins, username) {
for (var i = 0; i < foundLogins.length; i++) {
if (foundLogins[i].username == username) {
return foundLogins[i];
}
}
return null;
},
/**
* Can be called as:
* _getLocalizedString("key1");
* _getLocalizedString("key2", ["arg1"]);
* _getLocalizedString("key3", ["arg1", "arg2"]);
* (etc)
*
* Returns the localized string for the specified key,
* formatted if required.
*
*/
_getLocalizedString(key, formatArgs) {
if (formatArgs) {
return this._strBundle.formatStringFromName(key, formatArgs);
}
return this._strBundle.GetStringFromName(key);
},
/**
* Sanitizes the specified username, by stripping quotes and truncating if
* it's too long. This helps prevent an evil site from messing with the
* "save password?" prompt too much.
*/
_sanitizeUsername(username) {
if (username.length > 30) {
username = username.substring(0, 30);
username += this._ellipsis;
}
return username.replace(/['"]/g, "");
},
/**
* The aURI parameter may either be a string uri, or an nsIURI instance.
*
* Returns the origin to use in a nsILoginInfo object (for example,
*/
_getFormattedOrigin(aURI) {
let uri;
if (aURI instanceof Ci.nsIURI) {
uri = aURI;
} else {
uri = Services.io.newURI(aURI);
}
return uri.scheme + "://" + uri.displayHostPort;
},
/**
* Converts a login's origin field (a URL) to a short string for
* prompting purposes. Eg, "http://foo.com" --> "foo.com", or
* "ftp://www.site.co.uk" --> "site.co.uk".
*/
_getShortDisplayHost(aURIString) {
var displayHost;
var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
Ci.nsIIDNService
);
try {
var uri = Services.io.newURI(aURIString);
var baseDomain = Services.eTLD.getBaseDomain(uri);
displayHost = idnService.convertToDisplayIDN(baseDomain, {});
} catch (e) {
this.log(`Couldn't process supplied URIString ${aURIString}.`);
}
if (!displayHost) {
displayHost = aURIString;
}
return displayHost;
},
/**
* Returns the origin and realm for which authentication is being
* requested, in the format expected to be used with nsILoginInfo.
*/
_getAuthTarget(aChannel, aAuthInfo) {
var origin, realm;
// If our proxy is demanding authentication, don't use the
// channel's actual destination.
if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
this.log("getAuthTarget is for proxy auth.");
if (!(aChannel instanceof Ci.nsIProxiedChannel)) {
throw new Error("proxy auth needs nsIProxiedChannel");
}
var info = aChannel.proxyInfo;
if (!info) {
throw new Error("proxy auth needs nsIProxyInfo");
}
// Proxies don't have a scheme, but we'll use "moz-proxy://"
// so that it's more obvious what the login is for.
var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
Ci.nsIIDNService
);
origin =
"moz-proxy://" +
idnService.convertUTF8toACE(info.host) +
":" +
info.port;
realm = aAuthInfo.realm;
if (!realm) {
realm = origin;
}
return [origin, realm];
}
origin = this._getFormattedOrigin(aChannel.URI);
// If a HTTP WWW-Authenticate header specified a realm, that value
// will be available here. If it wasn't set or wasn't HTTP, we'll use
// the formatted origin instead.
realm = aAuthInfo.realm;
if (!realm) {
realm = origin;
}
return [origin, realm];
},
/**
* Returns [username, password] as extracted from aAuthInfo (which
* holds this info after having prompted the user).
*
* If the authentication was for a Windows domain, we'll prepend the
* return username with the domain. (eg, "domain\user")
*/
_GetAuthInfo(aAuthInfo) {
var username, password;
var flags = aAuthInfo.flags;
if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain) {
username = aAuthInfo.domain + "\\" + aAuthInfo.username;
} else {
username = aAuthInfo.username;
}
password = aAuthInfo.password;
return [username, password];
},
/**
* Given a username (possibly in DOMAIN\user form) and password, parses the
* domain out of the username if necessary and sets domain, username and
* password on the auth information object.
*/
_SetAuthInfo(aAuthInfo, username, password) {
var flags = aAuthInfo.flags;
if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
// Domain is separated from username by a backslash
var idx = username.indexOf("\\");
if (idx == -1) {
aAuthInfo.username = username;
} else {
aAuthInfo.domain = username.substring(0, idx);
aAuthInfo.username = username.substring(idx + 1);
}
} else {
aAuthInfo.username = username;
}
aAuthInfo.password = password;
},
_newAsyncPromptConsumer(aCallback, aContext) {
return {
QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
callback: aCallback,
context: aContext,
cancel() {
this.callback.onAuthCancelled(this.context, false);
this.callback = null;
this.context = null;
},
};
},
}; // end of LoginManagerAuthPrompter implementation
ChromeUtils.defineLazyGetter(LoginManagerAuthPrompter.prototype, "log", () => {
let logger = lazy.LoginHelper.createLogger("LoginManagerAuthPrompter");
return logger.log.bind(logger);
});