Backed out 2 changesets (bug 1686989, bug 1684469) for auth and dialog box failures. CLOSED TREE

Backed out changeset 111af4c2bf6b (bug 1684469)
Backed out changeset 81794f8a220b (bug 1686989)
This commit is contained in:
Csoregi Natalia 2021-01-25 21:47:43 +02:00
Родитель 1f192d6edb
Коммит 086251db75
19 изменённых файлов: 348 добавлений и 709 удалений

Просмотреть файл

@ -1302,9 +1302,7 @@
if (newBrowser.hasAttribute("tabDialogShowing")) {
newBrowser.tabDialogBox.focus();
return;
}
if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
} else if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
// If there's a tabmodal prompt showing, focus it.
let prompts = newBrowser.tabModalPromptBox.listPrompts();
let prompt = prompts[prompts.length - 1];

Просмотреть файл

@ -6,4 +6,3 @@ skip-if = verify && debug && (os == 'linux')
[browser_multiplePrompts.js]
[browser_openPromptInBackgroundTab.js]
support-files = openPromptOffTimeout.html
[browser_promptFocus.js]

Просмотреть файл

@ -1,169 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { PromptTestUtils } = ChromeUtils.import(
"resource://testing-common/PromptTestUtils.jsm"
);
// MacOS has different default focus behavior for prompts.
const isMacOS = Services.appinfo.OS === "Darwin";
/**
* Tests that prompts are focused when switching tabs.
*/
add_task(async function test_tabdialogbox_tab_switch_focus() {
// Open 3 tabs
let tabPromises = [];
for (let i = 0; i < 3; i += 1) {
tabPromises.push(
BrowserTestUtils.openNewForegroundTab(
gBrowser,
"http://example.com",
true
)
);
}
// Wait for tabs to be ready
let tabs = await Promise.all(tabPromises);
let [tabA, tabB, tabC] = tabs;
// Spawn two prompts, which have different default focus as determined by
// CommonDialog#setDefaultFocus.
let openPromise = PromptTestUtils.waitForPrompt(tabA.linkedBrowser, {
modalType: Services.prompt.MODAL_TYPE_TAB,
promptType: "confirm",
});
Services.prompt.asyncConfirm(
tabA.linkedBrowser.browsingContext,
Services.prompt.MODAL_TYPE_TAB,
null,
"prompt A"
);
let promptA = await openPromise;
openPromise = PromptTestUtils.waitForPrompt(tabB.linkedBrowser, {
modalType: Services.prompt.MODAL_TYPE_TAB,
promptType: "promptPassword",
});
Services.prompt.asyncPromptPassword(
tabB.linkedBrowser.browsingContext,
Services.prompt.MODAL_TYPE_TAB,
null,
"prompt B",
"",
null,
false
);
let promptB = await openPromise;
// Switch tabs and check if the correct element was focused.
// Switch back to the third tab which doesn't have a prompt.
await BrowserTestUtils.switchTab(gBrowser, tabC);
is(
Services.focus.focusedElement,
tabC.linkedBrowser,
"Tab without prompt should have focus on browser."
);
// Switch to first tab which has prompt
await BrowserTestUtils.switchTab(gBrowser, tabA);
if (isMacOS) {
is(
Services.focus.focusedElement,
promptA.ui.infoBody,
"Tab with prompt should have focus on body."
);
} else {
is(
Services.focus.focusedElement,
promptA.ui.button0,
"Tab with prompt should have focus on default button."
);
}
await PromptTestUtils.handlePrompt(promptA);
// Switch to second tab which has prompt
await BrowserTestUtils.switchTab(gBrowser, tabB);
is(
Services.focus.focusedElement,
promptB.ui.password1Textbox,
"Tab with password prompt should have focus on password field."
);
await PromptTestUtils.handlePrompt(promptB);
// Cleanup
tabs.forEach(tab => {
BrowserTestUtils.removeTab(tab);
});
});
/**
* Tests that an alert prompt has focus on the default element.
* @param {CommonDialog} prompt - Prompt to test focus for.
* @param {number} index - Index of the prompt to log.
*/
function testAlertPromptFocus(prompt, index) {
if (isMacOS) {
is(
Services.focus.focusedElement,
prompt.ui.infoBody,
`Prompt #${index} should have focus on body.`
);
} else {
is(
Services.focus.focusedElement,
prompt.ui.button0,
`Prompt #${index} should have focus on default button.`
);
}
}
/**
* Test that we set the correct focus when queuing multiple prompts.
*/
add_task(async function test_tabdialogbox_prompt_queue_focus() {
await BrowserTestUtils.withNewTab(gBrowser, async browser => {
const PROMPT_COUNT = 10;
let firstPromptPromise = PromptTestUtils.waitForPrompt(browser, {
modalType: Services.prompt.MODAL_TYPE_TAB,
promptType: "alert",
});
for (let i = 0; i < PROMPT_COUNT; i += 1) {
Services.prompt.asyncAlert(
browser.browsingContext,
Services.prompt.MODAL_TYPE_TAB,
null,
"prompt " + i
);
}
// Close prompts one by one and check focus.
let nextPromptPromise = firstPromptPromise;
for (let i = 0; i < PROMPT_COUNT; i += 1) {
let p = await nextPromptPromise;
testAlertPromptFocus(p, i);
if (i < PROMPT_COUNT - 1) {
nextPromptPromise = PromptTestUtils.waitForPrompt(browser, {
modalType: Services.prompt.MODAL_TYPE_TAB,
promptType: "alert",
});
}
await PromptTestUtils.handlePrompt(p);
}
// All prompts are closed, focus should be back on the browser.
is(
Services.focus.focusedElement,
browser,
"Tab without prompts should have focus on browser."
);
});
});

Просмотреть файл

@ -4,7 +4,7 @@ support-files =
[browser_tabdialogbox_content_prompts.js]
[browser_tabdialogbox_navigation.js]
[browser_tabdialogbox_focus.js]
[browser_tabdialogbox_tab_switch_focus.js]
[browser_subdialog_esc.js]
support-files =
loadDelayedReply.sjs

Просмотреть файл

@ -74,90 +74,6 @@ add_task(async function test_tabdialogbox_tab_switch_focus() {
});
});
/**
* Tests that if we're showing multiple tab dialogs they are focused in the
* correct order and custom focus handlers are called.
*/
add_task(async function test_tabdialogbox_multiple_focus() {
await BrowserTestUtils.withNewTab(gBrowser, async browser => {
let dialogBox = gBrowser.getTabDialogBox(browser);
let dialogAClose = dialogBox.open(
TEST_DIALOG_PATH,
{},
{
testCustomFocusHandler: true,
}
);
let dialogBClose = dialogBox.open(TEST_DIALOG_PATH);
let dialogCClose = dialogBox.open(
TEST_DIALOG_PATH,
{},
{
testCustomFocusHandler: true,
}
);
let dialogs = dialogBox._dialogManager._dialogs;
let [dialogA, dialogB, dialogC] = dialogs;
// Wait until all dialogs are ready
await Promise.all(dialogs.map(dialog => dialog._dialogReady));
// Dialog A's custom focus target should be focused
let dialogElementA = dialogA._frame.contentDocument.querySelector(
"#custom-focus-el"
);
is(
Services.focus.focusedElement,
dialogElementA,
"Dialog A custom focus target is focused"
);
// Close top dialog
dialogA.close();
await dialogAClose;
// Dialog B's first focus target should be focused
let dialogElementB = dialogB._frame.contentDocument.querySelector(
"#textbox"
);
is(
Services.focus.focusedElement,
dialogElementB,
"Dialog B default focus target is focused"
);
// close top dialog
dialogB.close();
await dialogBClose;
// Dialog C's custom focus target should be focused
let dialogElementC = dialogC._frame.contentDocument.querySelector(
"#custom-focus-el"
);
is(
Services.focus.focusedElement,
dialogElementC,
"Dialog C custom focus target is focused"
);
// Close last dialog
dialogC.close();
await dialogCClose;
is(
dialogBox._dialogManager._dialogs.length,
0,
"All dialogs should be closed"
);
is(
Services.focus.focusedElement,
browser,
"Focus should be back on the browser"
);
});
});
/**
* Tests that other dialogs are still visible if one dialog is hidden.
*/

Просмотреть файл

@ -14,17 +14,6 @@
function acceptSubdialog() {
window.arguments[0].acceptCount++;
}
document.addEventListener("DOMContentLoaded", () => {
if (!window.arguments) {
return;
}
let [options] = window.arguments;
if (options?.testCustomFocusHandler) {
document.subDialogSetDefaultFocus = () => {
document.getElementById("custom-focus-el").focus();
}
}
}, {once: true})
</script>
<description id="desc">A sample sub-dialog for testing</description>
@ -36,8 +25,6 @@
<html:option>Bar</html:option>
</html:select>
<html:input id="custom-focus-el" value="Custom Focus Test" />
<separator class="thin"/>
<button oncommand="window.close();" label="Close" />

Просмотреть файл

@ -440,6 +440,9 @@ class PromptFactory {
asyncPromptAuth() {
return this.callProxy("asyncPromptAuth", arguments);
}
asyncPromptAuthBC() {
return this.callProxy("asyncPromptAuth", arguments);
}
}
PromptFactory.prototype.classID = Components.ID(
@ -755,21 +758,48 @@ class PromptDelegate {
return this._fillAuthInfo(aAuthInfo, aCheckState, result);
}
async asyncPromptAuth(aChannel, aLevel, aAuthInfo, aCheckMsg, aCheckState) {
const check = {
value: aCheckState,
asyncPromptAuth(
aChannel,
aCallback,
aContext,
aLevel,
aAuthInfo,
aCheckMsg,
aCheckState
) {
let responded = false;
const callback = result => {
// OK: result && result.password !== undefined
// Cancel: result && result.password === undefined
// Error: !result
if (responded) {
return;
}
responded = true;
if (this._fillAuthInfo(aAuthInfo, aCheckState, result)) {
aCallback.onAuthAvailable(aContext, aAuthInfo);
} else {
aCallback.onAuthCancelled(aContext, /* userCancel */ true);
}
};
const result = await this._prompter.asyncShowPromptPromise(
this._prompter.asyncShowPrompt(
this._addCheck(
aCheckMsg,
check,
aCheckState,
this._getAuthMsg(aChannel, aLevel, aAuthInfo)
)
),
callback
);
// OK: result && result.password !== undefined
// Cancel: result && result.password === undefined
// Error: !result
return this._fillAuthInfo(aAuthInfo, check, result);
return {
QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
cancel() {
if (responded) {
return;
}
responded = true;
aCallback.onAuthCancelled(aContext, /* userCancel */ false);
},
};
}
_getAuthText(aChannel, aAuthInfo) {

Просмотреть файл

@ -94,12 +94,6 @@ class GeckoViewPrompter {
return result;
}
asyncShowPromptPromise(aMsg) {
return new Promise(resolve => {
this.asyncShowPrompt(aMsg, resolve);
});
}
asyncShowPrompt(aMsg, aCallback) {
let handled = false;
const onResponse = response => {

Просмотреть файл

@ -59,7 +59,6 @@ interface nsIAuthPrompt2 : nsISupports
*
* @note Exceptions thrown from this function will be treated like a
* return value of false.
* @deprecated use asyncPromptAuth
*/
boolean promptAuth(in nsIChannel aChannel,
in uint32_t level,

Просмотреть файл

@ -94,7 +94,9 @@ XPCOMUtils.defineLazyPreferenceGetter(
* Invoked by [toolkit/components/prompts/src/Prompter.jsm]
*/
function LoginManagerAuthPromptFactory() {
Services.obs.addObserver(this, "quit-application-granted", true);
Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true);
}
LoginManagerAuthPromptFactory.prototype = {
@ -105,23 +107,20 @@ LoginManagerAuthPromptFactory.prototype = {
"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(),
// 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,
_asyncPrompts: {},
_asyncPromptInProgress: false,
observe(subject, topic, data) {
this.log("Observed: " + topic);
if (topic == "passwordmgr-crypto-login") {
// Show the deferred prompters.
this._uiBusyPromise?.resolve();
if (topic == "quit-application-granted") {
this._cancelPendingPrompts();
} else if (topic == "passwordmgr-crypto-login") {
// Start processing the deferred prompters.
this._doAsyncPrompt();
} else if (topic == "passwordmgr-crypto-loginCanceled") {
// User canceled a Master Password prompt, so go ahead and cancel
// all pending auth prompts to avoid nagging over and over.
this._cancelPendingPrompts();
}
},
@ -131,114 +130,143 @@ LoginManagerAuthPromptFactory.prototype = {
return prompt;
},
getPendingPrompt(browser, hashKey) {
return this._pendingPrompts.get(browser || this._noBrowser)?.get(hashKey);
},
_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) {
_doAsyncPrompt() {
if (this._asyncPromptInProgress) {
this.log("_doAsyncPrompt bypassed, already in progress");
return;
}
hashToPrompt.delete(hashKey);
if (!hashToPrompt.size) {
this._pendingPrompts.delete(browser);
// Find the first prompt key we have in the queue
var hashKey = null;
for (hashKey in this._asyncPrompts) {
break;
}
},
async _waitForLoginsUI(prompt) {
await this._uiBusyPromise;
if (!hashKey) {
this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
return;
}
let [origin, httpRealm] = prompt.prompter._getAuthTarget(
// If login manger has logins for this host, defer prompting if we're
// already waiting on a master password entry.
var prompt = this._asyncPrompts[hashKey];
var prompter = prompt.prompter;
var [origin, httpRealm] = 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 &&
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 master password UI");
this._uiBusyPromise = new Promise();
await this._uiBusyPromise;
},
async _doAsyncPrompt(prompt, hashKey) {
this._setPendingPrompt(prompt, hashKey);
// UI might be busy due to the master password dialog. Wait for it to close.
await this._waitForLoginsUI(prompt);
let ok = false;
let promptAborted = false;
try {
this.log("_doAsyncPrompt - performing the prompt for '" + hashKey + "'");
ok = await prompt.prompter.promptAuthInternal(
prompt.channel,
prompt.level,
prompt.authInfo
);
} catch (e) {
if (Services.logins.uiBusy) {
let hasLogins = Services.logins.countLogins(origin, null, httpRealm) > 0;
if (
e instanceof Components.Exception &&
e.result == Cr.NS_ERROR_NOT_AVAILABLE
!hasLogins &&
LoginHelper.schemeUpgrades &&
origin.startsWith("https://")
) {
this.log(
"_doAsyncPrompt bypassed, UI is not available in this context"
);
// Prompts throw NS_ERROR_NOT_AVAILABLE if they're aborted.
promptAborted = true;
} else {
Cu.reportError("LoginManagerAuthPrompter: _doAsyncPrompt " + e + "\n");
let httpOrigin = origin.replace(/^https:\/\//, "http://");
hasLogins =
Services.logins.countLogins(httpOrigin, null, httpRealm) > 0;
}
if (hasLogins) {
this.log("_doAsyncPrompt:run bypassed, master password UI busy");
return;
}
}
this._removePendingPrompt(prompt, hashKey);
var self = this;
// 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
var runnable = {
cancel: false,
run() {
var ok = false;
if (!this.cancel) {
try {
self.log(
"_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'"
);
ok = prompter.promptAuth(
prompt.channel,
prompt.level,
prompt.authInfo
);
} catch (e) {
if (
e instanceof Components.Exception &&
e.result == Cr.NS_ERROR_NOT_AVAILABLE
) {
self.log(
"_doAsyncPrompt:run bypassed, UI is not available in this context"
);
} else {
Cu.reportError(
"LoginManagerAuthPrompter: _doAsyncPrompt:run: " + e + "\n"
);
}
}
delete self._asyncPrompts[hashKey];
prompt.inProgress = false;
self._asyncPromptInProgress = false;
}
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;
}
self.log("Calling back to " + consumer.callback + " ok=" + ok);
try {
if (ok) {
consumer.callback.onAuthAvailable(
consumer.context,
prompt.authInfo
);
} else {
consumer.callback.onAuthCancelled(consumer.context, !this.cancel);
}
} catch (e) {
/* Throw away exceptions caused by callback */
}
}
self._doAsyncPrompt();
},
};
this._asyncPromptInProgress = true;
prompt.inProgress = true;
Services.tm.dispatchToMainThread(runnable);
this.log("_doAsyncPrompt:run dispatched");
},
_cancelPendingPrompts() {
this.log("Canceling all pending prompts...");
var asyncPrompts = this._asyncPrompts;
this.__proto__._asyncPrompts = {};
for (var hashKey in asyncPrompts) {
let prompt = asyncPrompts[hashKey];
// Watch out! If this prompt is currently prompting, let it handle
// notifying the callbacks of success/failure, since it's already
// asking the user for input. Reusing a callback can be crashy.
if (prompt.inProgress) {
this.log("skipping a prompt in progress");
continue;
}
this.log("Calling back to " + consumer.callback + " ok=" + ok);
try {
if (ok) {
consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
} else {
consumer.callback.onAuthCancelled(consumer.context, !promptAborted);
for (var consumer of prompt.consumers) {
if (!consumer.callback) {
continue;
}
this.log("Canceling async auth prompt callback " + consumer.callback);
try {
consumer.callback.onAuthCancelled(consumer.context, true);
} catch (e) {
/* Just ignore exceptions from the callback */
}
} catch (e) {
/* Throw away exceptions caused by callback */
}
}
},
@ -609,7 +637,16 @@ LoginManagerAuthPrompter.prototype = {
return [formattedOrigin, formattedOrigin + pathname, uri.username];
},
async promptAuthInternal(aChannel, aLevel, aAuthInfo) {
/* ---------- nsIAuthPrompt2 prompts ---------- */
/**
* Implementation of nsIAuthPrompt2.
*
* @param {nsIChannel} aChannel
* @param {int} aLevel
* @param {nsIAuthInformation} aAuthInfo
*/
promptAuth(aChannel, aLevel, aAuthInfo) {
var selectedLogin = null;
var checkbox = { value: false };
var checkboxLabel = null;
@ -719,16 +756,29 @@ LoginManagerAuthPrompter.prototype = {
this._browser
);
}
ok = await Services.prompt.asyncPromptAuth(
this._browser?.browsingContext,
LoginManagerAuthPrompter.promptAuthModalType,
aChannel,
aLevel,
aAuthInfo,
checkboxLabel,
checkbox
);
if (this._browser) {
ok = Services.prompt.promptAuthBC(
this._browser.browsingContext,
LoginManagerAuthPrompter.promptAuthModalType,
aChannel,
aLevel,
aAuthInfo,
checkboxLabel,
checkbox
);
} else {
// Can't tab prompt without browser. Fallback to window prompt.
// For cases where this._chromeWindow is defined, this will keep our
// parent relationship intact as opposed to passing null above.
ok = Services.prompt.promptAuth(
this._chromeWindow,
aChannel,
aLevel,
aAuthInfo,
checkboxLabel,
checkbox
);
}
}
let [username, password] = this._GetAuthInfo(aAuthInfo);
@ -818,25 +868,6 @@ LoginManagerAuthPrompter.prototype = {
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.spinEventLoopUntilOrShutdown(() => closed);
return result;
},
asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) {
var cancelable = null;
@ -850,33 +881,32 @@ LoginManagerAuthPrompter.prototype = {
cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
let [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
var [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
let hashKey = aLevel + "|" + origin + "|" + httpRealm;
var hashKey = aLevel + "|" + origin + "|" + httpRealm;
this.log("Async prompt key = " + hashKey);
let pendingPrompt = this._factory.getPendingPrompt(
this._browser,
hashKey
);
if (pendingPrompt) {
var asyncPrompt = this._factory._asyncPrompts[hashKey];
if (asyncPrompt) {
this.log(
"Prompt bound to an existing one in the queue, callback = " +
aCallback
);
pendingPrompt.consumers.push(cancelable);
asyncPrompt.consumers.push(cancelable);
return cancelable;
}
this.log("Adding new async prompt, callback = " + aCallback);
let asyncPrompt = {
this.log("Adding new prompt to the queue, callback = " + aCallback);
asyncPrompt = {
consumers: [cancelable],
channel: aChannel,
authInfo: aAuthInfo,
level: aLevel,
inProgress: false,
prompter: this,
};
this._factory._doAsyncPrompt(asyncPrompt, hashKey);
this._factory._asyncPrompts[hashKey] = asyncPrompt;
this._factory._doAsyncPrompt();
} catch (e) {
Cu.reportError(
"LoginManagerAuthPrompter: " +
@ -918,10 +948,6 @@ LoginManagerAuthPrompter.prototype = {
this._browser = aBrowser;
},
get browser() {
return this._browser;
},
set openerBrowser(aOpenerBrowser) {
this._openerBrowser = aOpenerBrowser;
},

Просмотреть файл

@ -11,22 +11,6 @@ function handleRequest(request, response)
var expected_user = "test", expected_pass = "testpass", realm = "mochitest";
// user=xxx
match = /[^_]user=([^&]*)/.exec(query);
if (match)
expected_user = match[1];
// pass=xxx
match = /[^_]pass=([^&]*)/.exec(query);
if (match)
expected_pass = match[1];
// realm=xxx
match = /[^_]realm=([^&]*)/.exec(query);
if (match)
realm = match[1];
// Look for an authentication header, if any, in the request.
//
// EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

Просмотреть файл

@ -116,5 +116,3 @@ support-files =
[browser_username_select_dialog.js]
support-files =
subtst_notifications_change_p.html
[browser_basicAuth_multiTab.js]
skip-if = os == "android"

Просмотреть файл

@ -1,141 +0,0 @@
/* 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/. */
"use-strict";
/**
* Tests that can show multiple auth prompts in different tabs and handle them
* correctly.
*/
const ORIGIN1 = "https://example.com";
const ORIGIN2 = "https://example.org";
const AUTH_PATH =
"/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs";
/**
* Opens a tab and navigates to the auth test path.
* @param {String} origin - Origin to open with test path.
* @param {Object} authOptions - Authentication options to pass to server and
* test for.
* @param {String} authOptions.user - Expected username.
* @param {String} authOptions.pass - Expected password.
* @param {String} authOptions.realm - Realm to return on auth request.
* @returns {Object} - An object containing passed origin and authOptions,
* opened tab, a promise which resolves once the tab loads, a promise which
* resolves once the prompt has been opened.
*/
function openTabWithAuthPrompt(origin, authOptions) {
let tab = BrowserTestUtils.addTab(gBrowser);
let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
modalType: Services.prompt.MODAL_TYPE_TAB,
promptType: "promptUserAndPass",
});
let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
let url = new URL(origin + AUTH_PATH);
Object.entries(authOptions).forEach(([key, value]) =>
url.searchParams.append(key, value)
);
info("Loading " + url.toString());
BrowserTestUtils.loadURI(tab.linkedBrowser, url.toString());
return { origin, tab, authOptions, loadPromise, promptPromise };
}
/**
* Waits for tab to load and tests for expected auth state.
* @param {boolean} expectAuthed - true: auth success, false otherwise.
* @param {Object} tabInfo - Information about the tab as generated by
* openTabWithAuthPrompt.
*/
async function testTabAuthed(expectAuthed, { tab, loadPromise, authOptions }) {
// Wait for tab to load after auth.
await loadPromise;
ok(true, "Tab loads after auth");
// Fetch auth results from body (set by authenticate.sjs).
let { loginResult, user, pass } = await SpecialPowers.spawn(
tab.linkedBrowser,
[],
() => {
let result = {};
result.loginResult = content.document.getElementById("ok").innerText;
result.user = content.document.getElementById("user").innerText;
result.pass = content.document.getElementById("pass").innerText;
return result;
}
);
is(loginResult == "PASS", expectAuthed, "Site has expected auth state");
is(user, expectAuthed ? authOptions.user : "", "Sent correct user");
is(pass, expectAuthed ? authOptions.pass : "", "Sent correct password");
}
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
// This test relies on tab auth prompts.
set: [["prompts.modalType.httpAuth", Services.prompt.MODAL_TYPE_TAB]],
});
});
add_task(async function test() {
let tabA = openTabWithAuthPrompt(ORIGIN1, {
user: "userA",
pass: "passA",
realm: "realmA",
});
// Tab B and C share realm and credentials.
// However, since the auth happens in separate tabs we should get two prompts.
let tabB = openTabWithAuthPrompt(ORIGIN2, {
user: "userB",
pass: "passB",
realm: "realmB",
});
let tabC = openTabWithAuthPrompt(ORIGIN2, {
user: "userB",
pass: "passB",
realm: "realmB",
});
let tabs = [tabA, tabB, tabC];
info(`Opening ${tabs.length} tabs with auth prompts`);
let prompts = await Promise.all(tabs.map(tab => tab.promptPromise));
is(prompts.length, tabs.length, "Should have one prompt per tab");
for (let i = 0; i < prompts.length; i++) {
ok(
prompts[i].args.text.startsWith(tabs[i].origin),
"Prompt matches the tabs origin"
);
}
// Interact with the prompts. This is deliberately done out of order
// (no FIFO, LIFO).
let [promptA, promptB, promptC] = prompts;
// Accept prompt B with correct login details.
await PromptTestUtils.handlePrompt(promptB, {
loginInput: tabB.authOptions.user,
passwordInput: tabB.authOptions.pass,
});
await testTabAuthed(true, tabB);
// Accept prompt A with correct login details
await PromptTestUtils.handlePrompt(promptA, {
loginInput: tabA.authOptions.user,
passwordInput: tabA.authOptions.pass,
});
await testTabAuthed(true, tabA);
// Cancel prompt C
await PromptTestUtils.handlePrompt(promptC, {
buttonNumClick: 1,
});
await testTabAuthed(false, tabC);
// Cleanup tabs
tabs.forEach(({ tab }) => BrowserTestUtils.removeTab(tab));
});

Просмотреть файл

@ -60,8 +60,6 @@ function commonDialogOnLoad() {
Dialog.onButton3();
window.close();
});
document.subDialogSetDefaultFocus = isInitialFocus =>
Dialog.setDefaultFocus(isInitialFocus);
Dialog.onLoad(dialog);
// resize the window to the content

Просмотреть файл

@ -18,9 +18,6 @@ const { AppConstants } = ChromeUtils.import(
function CommonDialog(args, ui) {
this.args = args;
this.ui = ui;
this.initialFocusPromise = new Promise(resolve => {
this.initialFocusResolver = resolve;
});
}
CommonDialog.prototype = {
@ -32,8 +29,6 @@ CommonDialog.prototype = {
iconClass: undefined,
soundID: undefined,
focusTimer: null,
initialFocusPromise: null,
initialFocusResolver: null,
/**
* @param [commonDialogEl] - Dialog element from commonDialog.xhtml,
@ -207,12 +202,21 @@ CommonDialog.prototype = {
button.setAttribute("default", "true");
}
let isEmbedded =
commonDialogEl && this.ui.prompt.docShell.chromeEventHandler;
if (!isEmbedded && !this.ui.promptContainer?.hidden) {
// Set default focus and select textbox contents if applicable. If we're
// embedded SubDialogManager will call setDefaultFocus for us.
this.setDefaultFocus(true);
let focusReady;
if (!this.ui.promptContainer || !this.ui.promptContainer.hidden) {
// Set default focus and select textbox contents if applicable.
if (commonDialogEl && this.ui.prompt.docShell.chromeEventHandler) {
// We're embedded. Delay focus until onload, to after when our embedder
// (SubDialog) has focused the frame.
focusReady = new Promise(resolve =>
this.ui.prompt.addEventListener("load", resolve, { once: true })
).then(() => {
this.setDefaultFocus(true);
});
} else {
this.setDefaultFocus(true);
}
}
if (this.args.enableDelay) {
@ -236,11 +240,10 @@ CommonDialog.prototype = {
}
if (commonDialogEl) {
if (isEmbedded) {
// If we delayed default focus above, wait for it to be ready before
// sending the notification.
await this.initialFocusPromise;
}
// If we delayed default focus above, wait for it to be ready before
// sending the notification.
await focusReady;
// ui.prompt is the window object of the dialog.
Services.obs.notifyObservers(this.ui.prompt, "common-dialog-loaded");
} else {
// ui.promptContainer is the <tabmodalprompt> element.
@ -320,10 +323,6 @@ CommonDialog.prototype = {
} else {
this.ui.loginTextbox.focus();
}
if (isInitialLoad) {
this.initialFocusResolver();
}
},
onCheckbox() {

Просмотреть файл

@ -691,25 +691,75 @@ Prompter.prototype = {
},
/**
* Requests a username and a password. Shows a dialog with username and
* password field, depending on flags also a domain field.
* Asynchronously prompt the user for a username and password.
* This has largely the same semantics as promptUsernameAndPassword(),
* but returns immediately after calling and returns the entered
* data in a callback.
*
* @param {mozIDOMWindowProxy} domWin - The parent window or null.
* @param {nsIChannel} channel - The channel that requires authentication.
* @param {nsIAuthPromptCallback} callback - Called once the prompt has been
* closed.
* @param {nsISupports} context
* @param {Number} level - Security level of the credential transmission.
* Any of nsIAuthPrompt2.<LEVEL_NONE|LEVEL_PW_ENCRYPTED|LEVEL_SECURE>
* @param {nsIAuthInformation} authInfo
* @param {String} checkLabel - Text to appear with the checkbox.
* If null, check box will not be shown.
* @param {Object} checkValue - Contains the initial checked state of the
* checkbox when this method is called and the final checked state
* after the callback.
* @returns {nsICancelable} Interface to cancel prompt.
*/
asyncPromptAuth(
domWin,
channel,
callback,
context,
level,
authInfo,
checkLabel,
checkValue
) {
let p = this.pickPrompter({ domWin });
return p.asyncPromptAuth(
channel,
callback,
context,
level,
authInfo,
checkLabel,
checkValue
);
},
/**
* Asynchronously prompt the user for a username and password.
* This has largely the same semantics as promptUsernameAndPassword(),
* but returns immediately after calling and returns the entered
* data in a callback.
*
* @param {BrowsingContext} browsingContext - The browsing context the
* prompt should be opened for.
* @param {Number} modalType - The modal type of the prompt.
* nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
* @param {nsIChannel} channel - The channel that requires authentication.
* @param {nsIAuthPromptCallback} callback - Called once the prompt has been
* closed.
* @param {nsISupports} context
* @param {Number} level - Security level of the credential transmission.
* Any of nsIAuthPrompt2.<LEVEL_NONE|LEVEL_PW_ENCRYPTED|LEVEL_SECURE>
* @param {nsIAuthInformation} authInfo - Authentication information object.
* @param {nsIAuthInformation} authInfo
* @param {String} checkLabel - Text to appear with the checkbox.
* If null, check box will not be shown.
* @param {Object} checkValue - Initial checked state of the checkbox.
* @returns {Promise<nsIPropertyBag<{ ok: Boolean }>>}
* A promise which resolves when the prompt is dismissed.
* @param {Object} checkValue - Contains the initial checked state of the
* checkbox when this method is called and the final checked state
* after the callback.
* @returns {nsICancelable} Interface to cancel prompt.
*/
asyncPromptAuth(browsingContext, modalType, ...promptArgs) {
let p = this.pickPrompter({ browsingContext, modalType, async: true });
return p.promptAuth(...promptArgs);
asyncPromptAuthBC(browsingContext, modalType, ...promptArgs) {
let p = this.pickPrompter({ browsingContext, modalType });
return p.asyncPromptAuth(...promptArgs);
},
};
@ -1596,12 +1646,12 @@ class ModalPrompter {
let [username, password] = PromptUtils.getAuthInfo(authInfo);
let userParam = this.async ? username : { value: username };
let passParam = this.async ? password : { value: password };
let userParam = { value: username };
let passParam = { value: password };
let result;
let ok;
if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) {
result = this.nsIPrompt_promptPassword(
ok = this.nsIPrompt_promptPassword(
null,
message,
passParam,
@ -1609,7 +1659,7 @@ class ModalPrompter {
checkValue
);
} else {
result = this.nsIPrompt_promptUsernameAndPassword(
ok = this.nsIPrompt_promptUsernameAndPassword(
null,
message,
userParam,
@ -1619,25 +1669,10 @@ class ModalPrompter {
);
}
// For the async case result is an nsIPropertyBag with prompt results.
if (this.async) {
return result.then(bag => {
let ok = bag.getProperty("ok");
if (ok) {
let username = bag.getProperty("user");
let password = bag.getProperty("pass");
PromptUtils.setAuthInfo(authInfo, username, password);
}
return ok;
});
}
// For the sync case result is the "ok" boolean which indicates whether
// the user has confirmed the dialog.
if (result) {
if (ok) {
PromptUtils.setAuthInfo(authInfo, userParam.value, passParam.value);
}
return result;
return ok;
}
asyncPromptAuth(

Просмотреть файл

@ -160,30 +160,19 @@ let PromptTestUtils = {
let dialog;
await TestUtils.topicObserved(topic, subject => {
// If we are not given a browser, use the currently selected browser of the window
let browser =
parentBrowser || subject.ownerGlobal.gBrowser.selectedBrowser;
if (isCommonDialog(modalType)) {
// Is not associated with given parent window, skip
if (parentWindow && subject.opener !== parentWindow) {
return false;
}
// For tab prompts, ensure that the associated browser matches.
if (modalType == Services.prompt.MODAL_TYPE_TAB) {
let dialogBox = parentWindow.gBrowser.getTabDialogBox(browser);
let hasMatchingDialog = dialogBox._dialogManager._dialogs.some(
d => d._frame?.browsingContext == subject.browsingContext
);
if (!hasMatchingDialog) {
return false;
}
}
// subject is the window object of the prompt which has a Dialog object
// attached.
dialog = subject.Dialog;
} else {
// If we are not given a browser, use the currently selected browser of the window
let browser =
parentBrowser || subject.ownerGlobal.gBrowser.selectedBrowser;
// subject is the tabprompt dom node
// Get the full prompt object which has the dialog object
let prompt = browser.tabModalPromptBox.getPrompt(subject);

Просмотреть файл

@ -627,18 +627,29 @@ interface nsIPromptService : nsISupports
in nsIAuthInformation authInfo,
in wstring checkboxLabel,
inout boolean checkValue);
nsICancelable asyncPromptAuth(in mozIDOMWindowProxy aParent,
in nsIChannel aChannel,
in nsIAuthPromptCallback aCallback,
in nsISupports aContext,
in uint32_t level,
in nsIAuthInformation authInfo,
in wstring checkboxLabel,
inout boolean checkValue);
/**
* Async version of promptAuthBC
* Like asyncPromptAuth, but with a BrowsingContext as parent.
*
* @return A promise which resolves when the prompt is dismissed.
*
* @resolves nsIPropertyBag { ok: boolean }
* @param aBrowsingContext
* The browsing context the prompt should be opened for.
* @param modalType
* Whether the prompt should be window, tab or content modal.
*/
Promise asyncPromptAuth(in BrowsingContext aBrowsingContext,
in unsigned long modalType,
in nsIChannel aChannel,
in uint32_t level,
in nsIAuthInformation authInfo,
in wstring checkboxLabel,
in boolean checkValue);
nsICancelable asyncPromptAuthBC(in BrowsingContext aBrowsingContext,
in unsigned long modalType,
in nsIChannel aChannel,
in nsIAuthPromptCallback aCallback,
in nsISupports aContext,
in uint32_t level,
in nsIAuthInformation authInfo,
in wstring checkboxLabel,
inout boolean checkValue);
};

Просмотреть файл

@ -755,22 +755,7 @@ SubDialog.prototype = {
this._untrapFocus();
},
/**
* Focus the dialog content.
* If the embedded document defines a custom focus handler it will be called.
* Otherwise we will focus the first focusable element in the content window.
* @param {boolean} [isInitialFocus] - Whether the dialog is focused for the
* first time after opening.
*/
focus(isInitialFocus = false) {
// If the content window has its own focus logic, hand off the focus call.
let focusHandler = this._frame?.contentDocument?.subDialogSetDefaultFocus;
if (focusHandler) {
focusHandler(isInitialFocus);
return;
}
// Handle focus ourselves. Try to move the focus to the first element in
// the content window.
focus() {
let fm = Services.focus;
let focusedElement = fm.moveFocus(
this._frame.contentWindow,
@ -786,6 +771,7 @@ SubDialog.prototype = {
},
_trapFocus() {
this.focus();
// Attach a system event listener so the dialog can cancel keydown events.
// See Bug 1669990.
this._box.addEventListener("keydown", this, { mozSystemGroup: true });
@ -904,19 +890,22 @@ class SubDialogManager {
this._dialogStack.hidden = false;
this._dialogStack.classList.remove("temporarilyHidden");
this._topLevelPrevActiveElement = doc.activeElement;
// Mark the top dialog according to the array insertion order.
// This is needed because when multiple dialog are opening, their callbacks
// don't necessarily arrive in order.
this._preloadDialog.isTop = true;
} else if (this._orderType === SubDialogManager.ORDER_STACK) {
this._preloadDialog.isTop = true;
this._dialogs[this._dialogs.length - 1].isTop = false;
}
this._dialogs.push(this._preloadDialog);
this._preloadDialog.open(
aURL,
{
features,
closingCallback,
closedCallback,
sizeTo,
},
{ features, closingCallback, closedCallback, sizeTo },
...aParams
);
this._dialogs.push(this._preloadDialog);
let openedDialog = this._preloadDialog;
@ -984,19 +973,20 @@ class SubDialogManager {
}
_onDialogOpen(dialog) {
// The first dialog is always on top
if (this._dialogs.length === 1) {
return;
}
let lowerDialogs = [];
if (dialog == this._topDialog) {
dialog.focus(true);
} else {
if (!dialog.isTop) {
// Opening dialog is not on top, hide it
lowerDialogs.push(dialog);
}
// For stack order, hide the previous top
if (
this._dialogs.length &&
this._orderType === SubDialogManager.ORDER_STACK
) {
if (this._orderType === SubDialogManager.ORDER_STACK) {
let index = this._dialogs.indexOf(dialog);
if (index > 0) {
lowerDialogs.push(this._dialogs[index - 1]);
@ -1016,11 +1006,7 @@ class SubDialogManager {
if (this._topDialog) {
// The prevActiveElement is only set for stacked dialogs
if (this._topDialog._prevActiveElement) {
this._topDialog._prevActiveElement.focus();
} else {
this._topDialog.focus(true);
}
this._topDialog._prevActiveElement?.focus();
this._topDialog._overlay.setAttribute("topmost", true);
this._topDialog._addDialogEventListeners(false);
this._dialogStack.hidden = false;