Bug 1567175, support password manager in out of process iframes, r=MattN

Differential Revision: https://phabricator.services.mozilla.com/D47825

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Neil Deakin 2019-10-21 18:18:02 +00:00
parent 7cbc074da1
commit 7410901165
49 changed files with 901 additions and 883 deletions

View File

@ -20,16 +20,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.jsm",
});
// NOTE: Much of this logic is duplicated in BrowserCLH.js for Android.
addMessageListener("PasswordManager:fillForm", function(message) {
// intercept if ContextMenu.jsm had sent a plain object for remote targets
LoginManagerChild.receiveMessage(message, content);
});
addMessageListener("PasswordManager:fillGeneratedPassword", function(message) {
// forward message to LMC
LoginManagerChild.receiveMessage(message, content);
});
function shouldIgnoreLoginManagerEvent(event) {
let nodePrincipal = event.target.nodePrincipal;
// If we have a system or null principal then prevent any more password manager code from running and
@ -45,13 +35,15 @@ addEventListener("DOMFormBeforeSubmit", function(event) {
if (shouldIgnoreLoginManagerEvent(event)) {
return;
}
this.LoginManagerChild.forWindow(content).onDOMFormBeforeSubmit(event);
let window = event.target.ownerGlobal;
LoginManagerChild.forWindow(window).onDOMFormBeforeSubmit(event);
});
addEventListener("DOMFormHasPassword", function(event) {
if (shouldIgnoreLoginManagerEvent(event)) {
return;
}
this.LoginManagerChild.forWindow(content).onDOMFormHasPassword(event);
let window = event.target.ownerGlobal;
LoginManagerChild.forWindow(window).onDOMFormHasPassword(event);
let formLike = LoginFormFactory.createFromForm(event.originalTarget);
InsecurePasswordUtils.reportInsecurePasswords(formLike);
});
@ -59,10 +51,8 @@ addEventListener("DOMInputPasswordAdded", function(event) {
if (shouldIgnoreLoginManagerEvent(event)) {
return;
}
this.LoginManagerChild.forWindow(content).onDOMInputPasswordAdded(
event,
content
);
let window = event.target.ownerGlobal;
LoginManagerChild.forWindow(window).onDOMInputPasswordAdded(event, content);
let formLike = LoginFormFactory.createFromField(event.originalTarget);
InsecurePasswordUtils.reportInsecurePasswords(formLike);
});

View File

@ -532,7 +532,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
AboutLoginsParent: "resource:///modules/AboutLoginsParent.jsm",
AsyncPrefs: "resource://gre/modules/AsyncPrefs.jsm",
ContentClick: "resource:///modules/ContentClick.jsm",
LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
PluginManager: "resource:///actors/PluginParent.jsm",
PictureInPicture: "resource://gre/modules/PictureInPicture.jsm",
ReaderParent: "resource:///modules/ReaderParent.jsm",
@ -596,8 +595,6 @@ const listeners = {
"update-error": ["UpdateListener"],
"gmp-plugin-crash": ["PluginManager"],
"plugin-crashed": ["PluginManager"],
"passwordmgr-storage-changed": ["LoginManagerParent"],
"passwordmgr-autosaved-login-merged": ["LoginManagerParent"],
},
ppmm: {
@ -643,15 +640,6 @@ const listeners = {
"PictureInPicture:OpenToggleContextMenu": ["PictureInPicture"],
"Reader:FaviconRequest": ["ReaderParent"],
"Reader:UpdateReaderButton": ["ReaderParent"],
// PLEASE KEEP THIS LIST IN SYNC WITH THE MOBILE LISTENERS IN BrowserCLH.js
"PasswordManager:findLogins": ["LoginManagerParent"],
"PasswordManager:findRecipes": ["LoginManagerParent"],
"PasswordManager:onFormSubmit": ["LoginManagerParent"],
"PasswordManager:onGeneratedPasswordFilledOrEdited": ["LoginManagerParent"],
"PasswordManager:autoCompleteLogins": ["LoginManagerParent"],
"PasswordManager:removeLogin": ["LoginManagerParent"],
"PasswordManager:OpenPreferences": ["LoginManagerParent"],
// PLEASE KEEP THIS LIST IN SYNC WITH THE MOBILE LISTENERS IN BrowserCLH.js
"rtcpeer:CancelRequest": ["webrtcUI"],
"rtcpeer:Request": ["webrtcUI"],
"webrtc:CancelRequest": ["webrtcUI"],

View File

@ -1903,12 +1903,10 @@ var gPrivacyPane = {
*/
showPasswords() {
if (LoginHelper.managementURI) {
window.docShell.messageManager.sendAsyncMessage(
"PasswordManager:OpenPreferences",
{
entryPoint: "preferences",
}
);
let loginManager = window.getWindowGlobalChild().getActor("LoginManager");
loginManager.sendAsyncMessage("PasswordManager:OpenPreferences", {
entryPoint: "preferences",
});
return;
}
Services.telemetry.recordEvent("pwmgr", "open_management", "preferences");

View File

@ -23,11 +23,6 @@ ChromeUtils.defineModuleGetter(
"Readerable",
"resource://gre/modules/Readerable.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"LoginManagerChild",
"resource://gre/modules/LoginManagerChild.jsm"
);
XPCOMUtils.defineLazyGetter(this, "gPipNSSBundle", function() {
return Services.strings.createBundle(
@ -639,8 +634,4 @@ var AboutReaderListener = {
};
AboutReaderListener.init();
addMessageListener("PasswordManager:fillForm", function(message) {
LoginManagerChild.receiveMessage(message, content);
});
Services.obs.notifyObservers(this, "tab-content-frameloader-created");

View File

@ -75,17 +75,6 @@ BrowserCLH.prototype = {
GeckoViewUtils.addLazyGetter(this, "LoginManagerParent", {
module: "resource://gre/modules/LoginManagerParent.jsm",
mm: [
// PLEASE KEEP THIS LIST IN SYNC WITH THE DESKTOP LIST IN
// BrowserGlue.jsm
"PasswordManager:findLogins",
"PasswordManager:findRecipes",
"PasswordManager:onFormSubmit",
"PasswordManager:autoCompleteLogins",
"PasswordManager:removeLogin",
// PLEASE KEEP THIS LIST IN SYNC WITH THE DESKTOP LIST IN
// BrowserGlue.jsm
],
});
GeckoViewUtils.addLazyGetter(this, "LoginManagerChild", {
module: "resource://gre/modules/LoginManagerChild.jsm",
@ -254,17 +243,6 @@ BrowserCLH.prototype = {
},
options
);
aWindow.addEventListener(
"pageshow",
event => {
// XXXbz what about non-HTML documents??
if (ChromeUtils.getClassName(event.target) == "HTMLDocument") {
this.LoginManagerChild.forWindow(aWindow).onPageShow(event);
}
},
options
);
},
// QI

View File

@ -152,9 +152,16 @@ this.InsecurePasswordUtils = {
let isLocalIP = this._isPrincipalForLocalIPAddress(
aForm.rootElement.nodePrincipal
);
let topWindow = aForm.ownerDocument.defaultView.top;
// XXXndeakin fix this: bug 1582499 - top document not accessible in OOP frame
// So for now, just use the current document if access to top fails.
let topDocument;
try {
topDocument = aForm.ownerDocument.defaultView.top.document;
} catch (ex) {
topDocument = aForm.ownerDocument.defaultView.document;
}
let topIsLocalIP = this._isPrincipalForLocalIPAddress(
topWindow.document.nodePrincipal
topDocument.nodePrincipal
);
// Only consider the page safe if the top window has a local IP address

View File

@ -146,12 +146,12 @@ class LoginAutocompleteItem extends AutocompleteItem {
isPasswordField,
dateAndTimeFormatter,
duplicateUsernames,
messageManager,
actor,
isOriginMatched
) {
super(SHOULD_SHOW_ORIGIN ? "loginWithOrigin" : "login");
this._login = login.QueryInterface(Ci.nsILoginMetaInfo);
this._messageManager = messageManager;
this._actor = actor;
XPCOMUtils.defineLazyGetter(this, "label", () => {
let username = login.username;
@ -184,9 +184,9 @@ class LoginAutocompleteItem extends AutocompleteItem {
}
removeFromStorage() {
if (this._messageManager) {
if (this._actor) {
let vanilla = LoginHelper.loginToVanillaObject(this._login);
this._messageManager.sendAsyncMessage("PasswordManager:removeLogin", {
this._actor.sendAsyncMessage("PasswordManager:removeLogin", {
login: vanilla,
});
} else {
@ -223,7 +223,7 @@ function LoginAutoCompleteResult(
aSearchString,
matchingLogins,
formOrigin,
{ generatedPassword, isSecure, messageManager, isPasswordField, hostname }
{ generatedPassword, isSecure, actor, isPasswordField, hostname }
) {
let hidingFooterOnPWFieldAutoOpened = false;
function isFooterEnabled() {
@ -280,7 +280,7 @@ function LoginAutoCompleteResult(
isPasswordField,
dateAndTimeFormatter,
duplicateUsernames,
messageManager,
actor,
LoginHelper.isOriginMatching(login.origin, formOrigin, {
schemeUpgrades: LoginHelper.schemeUpgrades,
})
@ -438,9 +438,11 @@ LoginAutoComplete.prototype = {
let isPasswordField = aElement.type == "password";
let hostname = aElement.ownerDocument.documentURIObject.host;
let loginManagerActor = LoginManagerChild.forWindow(aElement.ownerGlobal);
let completeSearch = (
autoCompleteLookupPromise,
{ generatedPassword, logins, messageManager }
{ generatedPassword, logins }
) => {
// If the search was canceled before we got our
// results, don't bother reporting them.
@ -457,7 +459,7 @@ LoginAutoComplete.prototype = {
formOrigin,
{
generatedPassword,
messageManager,
actor: loginManagerActor,
isSecure,
isPasswordField,
hostname,
@ -505,8 +507,7 @@ LoginAutoComplete.prototype = {
previousResult = null;
}
let loginManager = LoginManagerChild.forWindow(aElement.ownerGlobal);
let acLookupPromise = (this._autoCompleteLookupPromise = loginManager._autoCompleteSearchAsync(
let acLookupPromise = (this._autoCompleteLookupPromise = loginManagerActor._autoCompleteSearchAsync(
aSearchString,
previousResult,
aElement

View File

@ -22,11 +22,6 @@ ChromeUtils.defineModuleGetter(
"LoginFormFactory",
"resource://gre/modules/LoginFormFactory.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"LoginManagerChild",
"resource://gre/modules/LoginManagerChild.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"InsecurePasswordUtils",

View File

@ -88,15 +88,11 @@ Services.cpmm.addMessageListener("clearRecipeCache", () => {
LoginRecipesContent._clearRecipeCache();
});
let gLoginManagerChildSingleton = null;
let _messages = [
"PasswordManager:loginsFound",
"PasswordManager:loginsAutoCompleted",
];
let gLastRightClickTimeStamp = Number.NEGATIVE_INFINITY;
// Input element on which enter keydown event was fired.
let gKeyDownEnterForInput = null;
const observer = {
QueryInterface: ChromeUtils.generateQI([
Ci.nsIObserver,
@ -318,10 +314,7 @@ let gAutoCompleteListener = {
}
case "FormAutoComplete:PopupClosed": {
this.onPopupClosed(
data.selectedRowStyle,
target.docShell.messageManager
);
this.onPopupClosed(data.selectedRowStyle, target);
let { chromeEventHandler } = target.docShell;
chromeEventHandler.removeEventListener("keydown", this, true);
break;
@ -345,7 +338,7 @@ let gAutoCompleteListener = {
this.keyDownEnterForInput = focusedElement;
},
onPopupClosed(selectedRowStyle, mm) {
onPopupClosed(selectedRowStyle, window) {
let focusedElement = gFormFillService.focusedInput;
let eventTarget = this.keyDownEnterForInput;
if (
@ -356,16 +349,20 @@ let gAutoCompleteListener = {
this.keyDownEnterForInput = null;
return;
}
let loginManager = window.getWindowGlobalChild().getActor("LoginManager");
let hostname = eventTarget.ownerDocument.documentURIObject.host;
mm.sendAsyncMessage("PasswordManager:OpenPreferences", {
loginManager.sendAsyncMessage("PasswordManager:OpenPreferences", {
hostname,
entryPoint: "autocomplete",
});
},
};
this.LoginManagerChild = class LoginManagerChild {
this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
constructor() {
super();
/**
* WeakMap of the root element of a LoginForm to the DeferredTask to fill its fields.
*
@ -393,70 +390,15 @@ this.LoginManagerChild = class LoginManagerChild {
* frames, to the current state used by the Login Manager.
*/
this._loginFormStateByDocument = new WeakMap();
// Map from form login requests to information about that request.
this._requests = new Map();
// Number of outstanding requests to each manager.
this._managers = new Map();
}
static forWindow(window) {
// For now, this is a singleton.
if (!gLoginManagerChildSingleton) {
gLoginManagerChildSingleton = new LoginManagerChild();
}
return gLoginManagerChildSingleton;
}
_getRandomId() {
return Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator)
.generateUUID()
.toString();
}
_takeRequest(msg) {
let data = msg.data;
let request = this._requests.get(data.requestId);
this._requests.delete(data.requestId);
let count = this._managers.get(msg.target);
if (--count === 0) {
this._managers.delete(msg.target);
for (let message of _messages) {
msg.target.removeMessageListener(message, this);
}
} else {
this._managers.set(msg.target, count);
let windowGlobal = window.getWindowGlobalChild();
if (windowGlobal) {
return windowGlobal.getActor("LoginManager");
}
return request;
}
_sendRequest(messageManager, requestData, name, messageData) {
let count;
if (!(count = this._managers.get(messageManager))) {
this._managers.set(messageManager, 1);
for (let message of _messages) {
messageManager.addMessageListener(message, this);
}
} else {
this._managers.set(messageManager, ++count);
}
let requestId = this._getRandomId();
messageData.requestId = requestId;
messageManager.sendAsyncMessage(name, messageData);
let deferred = PromiseUtils.defer();
requestData.promise = deferred;
this._requests.set(requestId, requestData);
return deferred.promise;
return null;
}
_compareAndUpdatePreviouslySentValues(
@ -500,37 +442,14 @@ this.LoginManagerChild = class LoginManagerChild {
}
receiveMessage(msg) {
if (msg.name == "PasswordManager:fillForm") {
this.fillForm({
topDocument: msg.target.content.document,
loginFormOrigin: msg.data.loginFormOrigin,
loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
recipes: msg.data.recipes,
inputElementIdentifier: msg.data.inputElementIdentifier,
});
return;
}
switch (msg.name) {
case "PasswordManager:loginsFound": {
let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
let request = this._takeRequest(msg);
request.promise.resolve({
form: request.form,
loginsFound,
case "PasswordManager:fillForm": {
this.fillForm({
loginFormOrigin: msg.data.loginFormOrigin,
loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
recipes: msg.data.recipes,
});
break;
}
case "PasswordManager:loginsAutoCompleted": {
let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
let messageManager = msg.target;
let request = this._takeRequest(msg);
request.promise.resolve({
generatedPassword: msg.data.generatedPassword,
logins: loginsFound,
messageManager,
inputElementIdentifier: msg.data.inputElementIdentifier,
originMatches: msg.data.originMatches,
});
break;
}
@ -548,11 +467,11 @@ this.LoginManagerChild = class LoginManagerChild {
msg.data.password
);
this.fillForm({
topDocument: msg.target.content.document,
loginFormOrigin: msg.data.origin,
loginsFound: [generatedLogin],
recipes: msg.data.recipes,
inputElementIdentifier: msg.data.inputElementIdentifier,
originMatches: msg.data.originMatches,
});
let inputElement = ContentDOMReference.resolve(
msg.data.inputElementIdentifier
@ -564,7 +483,22 @@ this.LoginManagerChild = class LoginManagerChild {
}
break;
}
case "PasswordManager:formIsPending": {
return this._visibleTasksByDocument.has(this.document);
}
case "PasswordManager:formProcessed": {
this.notifyObserversOfFormProcessed(msg.data.formid);
break;
}
}
return undefined;
}
notifyObserversOfFormProcessed(formid) {
Services.obs.notifyObservers(this, "passwordmgr-processed-form", formid);
}
/**
@ -577,41 +511,36 @@ this.LoginManagerChild = class LoginManagerChild {
*/
_getLoginDataFromParent(form, options) {
let doc = form.ownerDocument;
let win = doc.defaultView;
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
if (!formOrigin) {
return Promise.reject(
"_getLoginDataFromParent: A form origin is required"
);
throw new Error("_getLoginDataFromParent: A form origin is required");
}
let actionOrigin = LoginHelper.getFormActionOrigin(form);
let messageManager = win.docShell.messageManager;
// XXX Weak??
let requestData = { form };
let messageData = { formOrigin, actionOrigin, options };
return this._sendRequest(
messageManager,
requestData,
let resultPromise = this.sendQuery(
"PasswordManager:findLogins",
messageData
);
return resultPromise.then(result => {
return {
form,
loginsFound: LoginHelper.vanillaObjectsToLogins(result.logins),
recipes: result.recipes,
};
});
}
_autoCompleteSearchAsync(aSearchString, aPreviousResult, aElement) {
let doc = aElement.ownerDocument;
let form = LoginFormFactory.createFromField(aElement);
let win = doc.defaultView;
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
let actionOrigin = LoginHelper.getFormActionOrigin(form);
let autocompleteInfo = aElement.getAutocompleteInfo();
let messageManager = win.docShell.messageManager;
let previousResult = aPreviousResult
? {
searchString: aPreviousResult.searchString,
@ -619,10 +548,8 @@ this.LoginManagerChild = class LoginManagerChild {
}
: null;
let requestData = {};
let messageData = {
autocompleteInfo,
browsingContextId: win.docShell.browsingContext.id,
formOrigin,
actionOrigin,
searchString: aSearchString,
@ -635,12 +562,17 @@ this.LoginManagerChild = class LoginManagerChild {
gAutoCompleteListener.init();
}
return this._sendRequest(
messageManager,
requestData,
let resultPromise = this.sendQuery(
"PasswordManager:autoCompleteLogins",
messageData
);
return resultPromise.then(result => {
return {
generatedPassword: result.generatedPassword,
logins: LoginHelper.vanillaObjectsToLogins(result.logins),
};
});
}
setupProgressListener(window) {
@ -858,10 +790,6 @@ this.LoginManagerChild = class LoginManagerChild {
* @param {LoginForm} form to fetch the logins for then try autofill.
*/
_fetchLoginsFromParentAndFillForm(form) {
let window = form.ownerDocument.defaultView;
let messageManager = window.docShell.messageManager;
messageManager.sendAsyncMessage("LoginStats:LoginEncountered");
if (!LoginHelper.enabled) {
return;
}
@ -900,10 +828,6 @@ this.LoginManagerChild = class LoginManagerChild {
*
* @param An object with the following properties:
* {
* topDocument:
* DOM document currently associated to the the top-level window
* for which the fill is requested. This may be different from the
* document that originally caused the login UI to be displayed.
* loginFormOrigin:
* String with the origin for which the login UI was displayed.
* This must match the origin of the form used for the fill.
@ -916,14 +840,16 @@ this.LoginManagerChild = class LoginManagerChild {
* Fill recipes transmitted together with the original message.
* inputElementIdentifier:
* An identifier generated for the input element via ContentDOMReference.
* originMatches:
* True if the origin of the form matches the page URI.
* }
*/
fillForm({
topDocument,
loginFormOrigin,
loginsFound,
recipes,
inputElementIdentifier,
originMatches,
}) {
if (!inputElementIdentifier) {
log("fillForm: No input element specified");
@ -938,9 +864,7 @@ this.LoginManagerChild = class LoginManagerChild {
return;
}
if (
LoginHelper.getLoginOrigin(topDocument.documentURI) != loginFormOrigin
) {
if (!originMatches) {
if (
!inputElement ||
LoginHelper.getLoginOrigin(inputElement.ownerDocument.documentURI) !=
@ -1061,7 +985,11 @@ this.LoginManagerChild = class LoginManagerChild {
let acForm = LoginFormFactory.createFromField(acInputField);
let doc = acForm.ownerDocument;
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
let recipes = LoginRecipesContent.getRecipes(formOrigin, doc.defaultView);
let recipes = LoginRecipesContent.getRecipes(
this,
formOrigin,
doc.defaultView
);
// Make sure the username field fillForm will use is the
// same field as the autocomplete was activated on.
@ -1467,9 +1395,8 @@ this.LoginManagerChild = class LoginManagerChild {
}
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
let messageManager = win.docShell.messageManager;
let recipes = LoginRecipesContent.getRecipes(origin, win);
let recipes = LoginRecipesContent.getRecipes(this, origin, win);
// Get the appropriate fields from the form.
let [
@ -1559,7 +1486,7 @@ this.LoginManagerChild = class LoginManagerChild {
let autoFilledLogin = this.stateForDocument(doc).fillsByRootElement.get(
form.rootElement
);
messageManager.sendAsyncMessage("PasswordManager:onFormSubmit", {
this.sendAsyncMessage("PasswordManager:onFormSubmit", {
origin,
formActionOrigin,
autoFilledLoginGuid: autoFilledLogin && autoFilledLogin.guid,
@ -1639,7 +1566,7 @@ this.LoginManagerChild = class LoginManagerChild {
let origin = LoginHelper.getLoginOrigin(
passwordField.ownerDocument.documentURI
);
let recipes = LoginRecipesContent.getRecipes(origin, win);
let recipes = LoginRecipesContent.getRecipes(this, origin, win);
let [usernameField] = this._getFormFields(loginForm, false, recipes);
let username = (usernameField && usernameField.value) || "";
// Avoid prompting twice for the same value,
@ -1662,17 +1589,12 @@ this.LoginManagerChild = class LoginManagerChild {
if (win.opener) {
openerTopWindowID = win.opener.top.windowUtils.outerWindowID;
}
let messageManager = win.docShell.messageManager;
messageManager.sendAsyncMessage(
"PasswordManager:onGeneratedPasswordFilledOrEdited",
{
browsingContextId: win.docShell.browsingContext.id,
formActionOrigin,
openerTopWindowID,
password: passwordField.value,
username,
}
);
this.sendAsyncMessage("PasswordManager:onGeneratedPasswordFilledOrEdited", {
formActionOrigin,
openerTopWindowID,
password: passwordField.value,
username: (usernameField && usernameField.value) || "",
});
}
_togglePasswordFieldMasking(passwordField, unmask) {
@ -2107,10 +2029,6 @@ this.LoginManagerChild = class LoginManagerChild {
log("_fillForm succeeded");
autofillResult = AUTOFILL_RESULT.FILLED;
let win = doc.defaultView;
let messageManager = win.docShell.messageManager;
messageManager.sendAsyncMessage("LoginStats:LoginFillSuccessful");
} catch (ex) {
Cu.reportError(ex);
throw ex;
@ -2146,10 +2064,9 @@ this.LoginManagerChild = class LoginManagerChild {
usernameField.addEventListener("mousedown", observer);
}
Services.obs.notifyObservers(
form.rootElement,
"passwordmgr-processed-form"
);
this.sendAsyncMessage("PasswordManager:formProcessed", {
formid: form.rootElement.id,
});
}
}
@ -2231,7 +2148,11 @@ this.LoginManagerChild = class LoginManagerChild {
let doc = aField.ownerDocument;
let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
let recipes = LoginRecipesContent.getRecipes(formOrigin, doc.defaultView);
let recipes = LoginRecipesContent.getRecipes(
this,
formOrigin,
doc.defaultView
);
return this._getFormFields(form, false, recipes);
}

View File

@ -102,9 +102,18 @@ this.LoginManagerContextMenu = {
},
async fillGeneratedPassword(inputElementIdentifier, documentURI, browser) {
let password = LoginManagerParent.getGeneratedPassword(
inputElementIdentifier.browsingContextId
);
let browsingContextId = inputElementIdentifier.browsingContextId;
let browsingContext = BrowsingContext.get(browsingContextId);
if (!browsingContext) {
return;
}
let actor = browsingContext.currentWindowGlobal.getActor("LoginManager");
if (!actor) {
return;
}
let password = actor.getGeneratedPassword();
let origin = LoginHelper.getLoginOrigin(documentURI.spec);
log.debug("fillGeneratedPassword into:", inputElementIdentifier, origin);
@ -118,15 +127,17 @@ this.LoginManagerContextMenu = {
// Some schemes e.g. chrome aren't supported by URL
log.debug("Couldnt get recipes for formHost:", formHost, ex);
}
browser.messageManager.sendAsyncMessage(
"PasswordManager:fillGeneratedPassword",
{
password,
origin,
inputElementIdentifier,
recipes,
}
);
let browserURI = browser.browsingContext.currentWindowGlobal.documentURI;
let originMatches = LoginHelper.getLoginOrigin(browserURI) == origin;
actor.sendAsyncMessage("PasswordManager:fillGeneratedPassword", {
password,
origin,
originMatches,
inputElementIdentifier,
recipes,
});
},
/**
@ -211,7 +222,18 @@ this.LoginManagerContextMenu = {
* origin when subframes are involved.
*/
_fillTargetField(login, inputElementIdentifier, browser, formOrigin) {
LoginManagerParent.getLoginManagerParent()
let browsingContextId = inputElementIdentifier.browsingContextId;
let browsingContext = BrowsingContext.get(browsingContextId);
if (!browsingContext) {
return;
}
let actor = browsingContext.currentWindowGlobal.getActor("LoginManager");
if (!actor) {
return;
}
actor
.fillForm({
browser,
inputElementIdentifier,

View File

@ -17,11 +17,6 @@ const LoginInfo = new Components.Constructor(
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
ChromeUtils.defineModuleGetter(
this,
"DeferredTask",
"resource://gre/modules/DeferredTask.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"LoginHelper",
@ -52,7 +47,10 @@ XPCOMUtils.defineLazyPreferenceGetter(
const EXPORTED_SYMBOLS = ["LoginManagerParent"];
let gLoginManagerParentSingleton = null;
/**
* A listener for notifications to tests.
*/
let gListenerForTests = null;
/**
* A map of a principal's origin (including suffixes) to a generated password string and filled flag
@ -73,11 +71,49 @@ let gGeneratedPasswordsByPrincipalOrigin = new Map();
*/
let gRecipeManager = null;
class LoginManagerParent {
constructor() {
// Tracks the last time the user cancelled the master password prompt,
// to avoid spamming master password prompts on autocomplete searches.
this._lastMPLoginCancelled = Math.NEGATIVE_INFINITY;
/**
* Tracks the last time the user cancelled the master password prompt,
* to avoid spamming master password prompts on autocomplete searches.
*/
let gLastMPLoginCancelled = Math.NEGATIVE_INFINITY;
let gGeneratedPasswordObserver = {
addedObserver: false,
observe(subject, topic, data) {
if (
topic == "passwordmgr-autosaved-login-merged" ||
(topic == "passwordmgr-storage-changed" && data == "removeLogin")
) {
let { origin, guid } = subject;
let generatedPW = gGeneratedPasswordsByPrincipalOrigin.get(origin);
// in the case where an autosaved login removed or merged into an existing login,
// clear the guid associated with the generated-password cache entry
if (
generatedPW &&
(guid == generatedPW.storageGUID ||
topic == "passwordmgr-autosaved-login-merged")
) {
log(
"Removing storageGUID for generated-password cache entry on origin:",
origin
);
generatedPW.storageGUID = null;
}
}
},
};
Services.ppmm.addMessageListener("PasswordManager:findRecipes", message => {
let formHost = new URL(message.data.formOrigin).host;
return gRecipeManager.getRecipesForHost(formHost);
});
class LoginManagerParent extends JSWindowActorParent {
// This is used by tests to listen to form submission.
static setListenerForTests(listener) {
gListenerForTests = listener;
}
// Some unit tests need to access this.
@ -85,12 +121,14 @@ class LoginManagerParent {
return gGeneratedPasswordsByPrincipalOrigin;
}
static getLoginManagerParent() {
// For now, this is a singleton.
if (!gLoginManagerParentSingleton) {
gLoginManagerParentSingleton = new LoginManagerParent();
getRootBrowser() {
let browsingContext = null;
if (this._overrideBrowsingContextId) {
browsingContext = BrowsingContext.get(this._overrideBrowsingContextId);
} else {
browsingContext = this.browsingContext.top;
}
return gLoginManagerParentSingleton;
return browsingContext.embedderElement;
}
/**
@ -130,7 +168,7 @@ class LoginManagerParent {
// to avoid spamming them with MP prompts for autocomplete.
if (e.result == Cr.NS_ERROR_ABORT) {
log("User cancelled master password prompt.");
this._lastMPLoginCancelled = Date.now();
gLastMPLoginCancelled = Date.now();
return [];
}
throw e;
@ -153,35 +191,25 @@ class LoginManagerParent {
);
}
static receiveMessage(msg) {
LoginManagerParent.getLoginManagerParent().receiveMessage(msg);
}
// Listeners are added in BrowserGlue.jsm on desktop
// and in BrowserCLH.js on mobile.
receiveMessage(msg) {
let data = msg.data;
switch (msg.name) {
case "PasswordManager:findLogins": {
// TODO Verify msg.target's principals against the formOrigin?
this.sendLoginDataToChild(
// TODO Verify the target's principals against the formOrigin?
return this.sendLoginDataToChild(
data.formOrigin,
data.actionOrigin,
data.requestId,
msg.target.messageManager,
data.options
);
break;
}
case "PasswordManager:findRecipes": {
let formHost = new URL(data.formOrigin).host;
return gRecipeManager.getRecipesForHost(formHost);
}
case "PasswordManager:onFormSubmit": {
// TODO Verify msg.target's principals against the formOrigin?
this.onFormSubmit(msg.target, data);
let browser = this.getRootBrowser();
this.onFormSubmit(browser, data);
if (gListenerForTests) {
gListenerForTests("FormSubmit", data);
}
break;
}
@ -191,8 +219,7 @@ class LoginManagerParent {
}
case "PasswordManager:autoCompleteLogins": {
this.doAutocompleteSearch(data, msg.target);
break;
return this.doAutocompleteSearch(data);
}
case "PasswordManager:removeLogin": {
@ -202,42 +229,33 @@ class LoginManagerParent {
}
case "PasswordManager:OpenPreferences": {
LoginHelper.openPasswordManager(msg.target.ownerGlobal, {
let window = this.getRootBrowser().ownerGlobal;
LoginHelper.openPasswordManager(window, {
filterString: msg.data.hostname,
entryPoint: msg.data.entryPoint,
});
break;
}
// Used by tests to detect that a form-fill has occurred. This redirects
// to the top-level browsing context.
case "PasswordManager:formProcessed": {
let topActor = this.browsingContext.top.currentWindowGlobal.getActor(
"LoginManager"
);
topActor.sendAsyncMessage("PasswordManager:formProcessed", {
formid: data.formid,
});
if (gListenerForTests) {
gListenerForTests("FormProcessed", {});
}
break;
}
}
return undefined;
}
// Observers are added in BrowserGlue.jsm on desktop
observe(subject, topic, data) {
if (
topic == "passwordmgr-autosaved-login-merged" ||
(topic == "passwordmgr-storage-changed" && data == "removeLogin")
) {
let { origin, guid } = subject;
let generatedPW = gGeneratedPasswordsByPrincipalOrigin.get(origin);
// in the case where an autosaved login removed or merged into an existing login,
// clear the guid associated with the generated-password cache entry
if (
generatedPW &&
(guid == generatedPW.storageGUID ||
topic == "passwordmgr-autosaved-login-merged")
) {
log(
"Removing storageGUID for generated-password cache entry on origin:",
origin
);
generatedPW.storageGUID = null;
}
}
}
/**
* Trigger a login form fill and send relevant data (e.g. logins and recipes)
* to the child process (LoginManagerChild).
@ -259,9 +277,14 @@ class LoginManagerParent {
// doesn't support structured cloning.
let jsLogins = [LoginHelper.loginToVanillaObject(login)];
browser.messageManager.sendAsyncMessage("PasswordManager:fillForm", {
let browserURI = browser.currentURI.spec;
let originMatches =
LoginHelper.getLoginOrigin(browserURI) == loginFormOrigin;
this.sendAsyncMessage("PasswordManager:fillForm", {
inputElementIdentifier,
loginFormOrigin,
originMatches,
logins: jsLogins,
recipes,
});
@ -273,8 +296,6 @@ class LoginManagerParent {
async sendLoginDataToChild(
formOrigin,
actionOrigin,
requestId,
target,
{ guid, showMasterPassword }
) {
let recipes = [];
@ -290,22 +311,19 @@ class LoginManagerParent {
}
if (!showMasterPassword && !Services.logins.isLoggedIn) {
try {
target.sendAsyncMessage("PasswordManager:loginsFound", {
requestId,
logins: [],
recipes,
});
} catch (e) {
log("error sending message to target", e);
}
return;
return { logins: [], recipes };
}
// If we're currently displaying a master password prompt, defer
// processing this form until the user handles the prompt.
if (Services.logins.uiBusy) {
log("deferring sendLoginDataToChild for", formOrigin);
let uiBusyPromiseResolve;
let uiBusyPromise = new Promise(resolve => {
uiBusyPromiseResolve = resolve;
});
let self = this;
let observer = {
QueryInterface: ChromeUtils.generateQI([
@ -319,23 +337,14 @@ class LoginManagerParent {
Services.obs.removeObserver(this, "passwordmgr-crypto-login");
Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
if (topic == "passwordmgr-crypto-loginCanceled") {
target.sendAsyncMessage("PasswordManager:loginsFound", {
requestId,
logins: [],
recipes,
});
uiBusyPromise.resolve({ logins: [], recipes });
return;
}
self.sendLoginDataToChild(
formOrigin,
actionOrigin,
requestId,
target,
{
showMasterPassword,
}
);
let result = self.sendLoginDataToChild(formOrigin, actionOrigin, {
showMasterPassword,
});
uiBusyPromiseResolve(result);
},
};
@ -346,7 +355,8 @@ class LoginManagerParent {
// See bug XXX.
Services.obs.addObserver(observer, "passwordmgr-crypto-login");
Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled");
return;
return uiBusyPromise;
}
// Autocomplete results do not need to match actionOrigin or exact origin.
@ -367,50 +377,34 @@ class LoginManagerParent {
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
// doesn't support structured cloning.
let jsLogins = LoginHelper.loginsToVanillaObjects(logins);
target.sendAsyncMessage("PasswordManager:loginsFound", {
requestId,
logins: jsLogins,
recipes,
});
return { logins: jsLogins, recipes };
}
doAutocompleteSearch(
{
autocompleteInfo,
browsingContextId,
formOrigin,
actionOrigin,
searchString,
previousResult,
requestId,
isSecure,
isPasswordField,
},
target
) {
doAutocompleteSearch({
autocompleteInfo,
formOrigin,
actionOrigin,
searchString,
previousResult,
isSecure,
isPasswordField,
}) {
// Note: previousResult is a regular object, not an
// nsIAutoCompleteResult.
// Cancel if we unsuccessfully prompted for the master password too recently.
if (!Services.logins.isLoggedIn) {
let timeDiff = Date.now() - this._lastMPLoginCancelled;
if (timeDiff < this._repromptTimeout) {
let timeDiff = Date.now() - gLastMPLoginCancelled;
if (timeDiff < LoginManagerParent._repromptTimeout) {
log(
"Not searching logins for autocomplete since the master password " +
`prompt was last cancelled ${Math.round(
timeDiff / 1000
)} seconds ago.`
);
// Send an empty array to make LoginManagerChild clear the
// Return an empty array to make LoginManagerChild clear the
// outstanding request it has temporarily saved.
target.messageManager.sendAsyncMessage(
"PasswordManager:loginsAutoCompleted",
{
requestId,
logins: [],
}
);
return;
return { logins: [] };
}
}
@ -448,27 +442,22 @@ class LoginManagerParent {
return match && match.toLowerCase().startsWith(searchStringLower);
});
let browser = this.getRootBrowser();
let generatedPassword = null;
if (
isPasswordField &&
autocompleteInfo.fieldName == "new-password" &&
Services.logins.getLoginSavingEnabled(formOrigin) &&
!PrivateBrowsingUtils.isWindowPrivate(target.ownerGlobal)
(!browser || !PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal))
) {
generatedPassword = this.getGeneratedPassword(browsingContextId);
generatedPassword = this.getGeneratedPassword();
}
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
// doesn't support structured cloning.
let jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
target.messageManager.sendAsyncMessage(
"PasswordManager:loginsAutoCompleted",
{
requestId,
generatedPassword,
logins: jsLogins,
}
);
return { generatedPassword, logins: jsLogins };
}
/**
@ -478,7 +467,20 @@ class LoginManagerParent {
return BrowsingContext;
}
getGeneratedPassword(browsingContextId) {
// Set an override context within a test.
useBrowsingContext(browsingContextId = 0) {
this._overrideBrowsingContextId = browsingContextId;
}
getBrowsingContextToUse() {
if (this._overrideBrowsingContextId) {
return BrowsingContext.get(this._overrideBrowsingContextId);
}
return this.browsingContext;
}
getGeneratedPassword() {
if (
!LoginHelper.enabled ||
!LoginHelper.generationAvailable ||
@ -487,7 +489,7 @@ class LoginManagerParent {
return null;
}
let browsingContext = BrowsingContext.get(browsingContextId);
let browsingContext = this.getBrowsingContextToUse();
if (!browsingContext) {
return null;
}
@ -514,6 +516,20 @@ class LoginManagerParent {
storageGUID: null,
value: PasswordGenerator.generatePassword(),
};
// Add these observers when a password is assigned.
if (!gGeneratedPasswordObserver.addedObserver) {
Services.obs.addObserver(
gGeneratedPasswordObserver,
"passwordmgr-autosaved-login-merged"
);
Services.obs.addObserver(
gGeneratedPasswordObserver,
"passwordmgr-storage-changed"
);
gGeneratedPasswordObserver.addedObserver = true;
}
gGeneratedPasswordsByPrincipalOrigin.set(framePrincipalOrigin, generatedPW);
return generatedPW.value;
}
@ -721,7 +737,6 @@ class LoginManagerParent {
}
_onGeneratedPasswordFilledOrEdited({
browsingContextId,
formActionOrigin,
openerTopWindowID,
password,
@ -729,12 +744,20 @@ class LoginManagerParent {
}) {
log("_onGeneratedPasswordFilledOrEdited");
if (gListenerForTests) {
gListenerForTests("PasswordFilledOrEdited", {});
}
if (!password) {
log("_onGeneratedPasswordFilledOrEdited: The password field is empty");
return;
}
let browsingContext = BrowsingContext.get(browsingContextId);
let browsingContext = this.getBrowsingContextToUse();
if (!browsingContext) {
return;
}
let {
originNoSuffix,
} = browsingContext.currentWindowGlobal.documentPrincipal;
@ -901,7 +924,7 @@ class LoginManagerParent {
"_onGeneratedPasswordFilledOrEdited: not auto-saving/updating this login"
);
}
let browser = browsingContext.top.embedderElement;
let browser = this.getRootBrowser();
let prompter = this._getPrompter(browser, openerTopWindowID);
if (loginToChange) {

View File

@ -245,11 +245,12 @@ this.LoginRecipesContent = {
* Tries to fetch recipes for a given host, using a local cache if possible.
* Otherwise, the recipes are cached for later use.
*
* @param {JSWindowActor} aActor - actor making request
* @param {String} aHost (e.g. example.com:8080 [non-default port] or sub.example.com)
* @param {Object} win - the window of the host
* @return {Set} of recipes that apply to the host
*/
getRecipes(aHost, win) {
getRecipes(aActor, aHost, win) {
let recipes;
let recipeMap = this._recipeCache.get(win);
@ -261,10 +262,8 @@ this.LoginRecipesContent = {
}
}
let mm = win.docShell.messageManager;
log.warn("getRecipes: falling back to a synchronous message for:", aHost);
recipes = mm.sendSyncMessage("PasswordManager:findRecipes", {
recipes = Services.cpmm.sendSyncMessage("PasswordManager:findRecipes", {
formOrigin: aHost,
})[0];
this.cacheRecipes(aHost, win, recipes);

View File

@ -5,22 +5,6 @@ const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
const BASIC_FORM_NO_USERNAME_PAGE_PATH =
DIRECTORY_PATH + "form_basic_no_username.html";
function getSubmitMessage() {
info("getSubmitMessage");
return new Promise((resolve, reject) => {
Services.mm.addMessageListener(
"PasswordManager:onFormSubmit",
function onFormSubmit() {
Services.mm.removeMessageListener(
"PasswordManager:onFormSubmit",
onFormSubmit
);
resolve();
}
);
});
}
add_task(async function test() {
let nsLoginInfo = new Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1",
@ -57,7 +41,7 @@ add_task(async function test() {
// Convert the login object to a plain JS object for passing across process boundaries.
login = LoginHelper.loginToVanillaObject(login);
ContentTask.spawn(
await ContentTask.spawn(
tab.linkedBrowser,
{ login, usernameRequested },
async ({ login: addedLogin, usernameRequested: aUsernameRequested }) => {
@ -95,7 +79,7 @@ add_task(async function test() {
}
);
let processedPromise = getSubmitMessage();
let processedPromise = listenForTestNotification("FormSubmit");
ContentTask.spawn(tab.linkedBrowser, null, () => {
content.document.getElementById("form-basic").submit();
});

View File

@ -32,12 +32,15 @@ add_task(async function test_initialize() {
});
add_task(async function test_context_menu_username() {
let formFilled = listenForTestNotification("FormProcessed");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_ORIGIN + BASIC_FORM_PAGE_PATH,
},
async function(browser) {
await formFilled;
await openContextMenu(browser, "#form-basic-username");
let contextMenu = document.getElementById("contentAreaContextMenu");
@ -48,12 +51,15 @@ add_task(async function test_context_menu_username() {
});
add_task(async function test_context_menu_password() {
let formFilled = listenForTestNotification("FormProcessed");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_ORIGIN + BASIC_FORM_PAGE_PATH,
},
async function(browser) {
await formFilled;
await openContextMenu(browser, "#form-basic-password");
let contextMenu = document.getElementById("contentAreaContextMenu");

View File

@ -234,6 +234,8 @@ add_task(async function fill_generated_password_with_matching_logins() {
Services.logins.addLogin(login);
await storageChangedPromised;
let formFilled = listenForTestNotification("FormProcessed");
await BrowserTestUtils.withNewTab(
{
gBrowser,
@ -241,6 +243,7 @@ add_task(async function fill_generated_password_with_matching_logins() {
},
async function(browser) {
await SimpleTest.promiseFocus(browser.ownerGlobal);
await formFilled;
await ContentTask.spawn(
browser,
[passwordInputSelector],

View File

@ -3,22 +3,6 @@
const TEST_HOSTNAME = "https://example.com";
const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
function getSubmitMessage() {
info("getSubmitMessage");
return new Promise((resolve, reject) => {
Services.mm.addMessageListener(
"PasswordManager:onFormSubmit",
function onFormSubmit() {
Services.mm.removeMessageListener(
"PasswordManager:onFormSubmit",
onFormSubmit
);
resolve();
}
);
});
}
add_task(async function test_doorhanger_dismissal_un() {
let url = TEST_HOSTNAME + BASIC_FORM_PAGE_PATH;
await BrowserTestUtils.withNewTab(
@ -31,8 +15,8 @@ add_task(async function test_doorhanger_dismissal_un() {
// the password field is a three digit numberic value,
// we automatically dismiss the save logins prompt on submission.
let processedPromise = getSubmitMessage();
ContentTask.spawn(browser, null, async () => {
let processedPromise = listenForTestNotification("FormSubmit");
await ContentTask.spawn(browser, null, async () => {
content.document
.getElementById("form-basic-username")
.setUserInput("4111111111111111");
@ -64,8 +48,8 @@ add_task(async function test_doorhanger_dismissal_pw() {
// the password field is also tagged autocomplete="cc-number",
// we automatically dismiss the save logins prompt on submission.
let processedPromise = getSubmitMessage();
ContentTask.spawn(browser, null, async () => {
let processedPromise = listenForTestNotification("FormSubmit");
await ContentTask.spawn(browser, null, async () => {
content.document
.getElementById("form-basic-username")
.setUserInput("aaa");
@ -99,8 +83,8 @@ add_task(async function test_doorhanger_shown_on_un_with_invalid_ccnumber() {
// If the username field has a CC number that is invalid,
// we show the doorhanger to save logins like we usually do.
let processedPromise = getSubmitMessage();
ContentTask.spawn(browser, null, async () => {
let processedPromise = listenForTestNotification("FormSubmit");
await ContentTask.spawn(browser, null, async () => {
content.document.getElementById("form-basic-username").value =
"1234123412341234";
content.document.getElementById("form-basic-password").value = "411";
@ -144,8 +128,8 @@ add_task(async function test_doorhanger_dismissal_on_change() {
);
Services.logins.addLogin(login);
let processedPromise = getSubmitMessage();
ContentTask.spawn(browser, null, async () => {
let processedPromise = listenForTestNotification("FormSubmit");
await ContentTask.spawn(browser, null, async () => {
content.document
.getElementById("form-basic-password")
.setUserInput("111");

View File

@ -130,6 +130,8 @@ async function verifyConfirmationHint(hintElem) {
}
async function openFormInNewTab(url, formValues, taskFn) {
let formFilled = listenForTestNotification("FormProcessed");
await BrowserTestUtils.withNewTab(
{
gBrowser,
@ -137,6 +139,8 @@ async function openFormInNewTab(url, formValues, taskFn) {
},
async function(browser) {
await SimpleTest.promiseFocus(browser.ownerGlobal);
await formFilled;
await ContentTask.spawn(
browser,
formValues,

View File

@ -11,39 +11,6 @@ async function getDocumentVisibilityState(browser) {
return visibility;
}
async function addContentObserver(browser, topic) {
// add an observer.
await ContentTask.spawn(browser, [topic], function(contentTopic) {
this.gObserver = {
wasObserved: false,
observe: () => {
content.wasObserved = true;
},
};
Services.obs.addObserver(this.gObserver, contentTopic);
});
}
async function getContentObserverResult(browser, topic) {
let result = await ContentTask.spawn(browser, [topic], async function(
contentTopic
) {
const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
try {
await TestUtils.waitForCondition(() => {
return content.wasObserved;
}, `Wait for "passwordmgr-processed-form"`);
} catch (ex) {
content.wasObserved = false;
}
Services.obs.removeObserver(this.gObserver, "passwordmgr-processed-form");
return content.wasObserved;
});
return result;
}
// Waits for the master password prompt and cancels it.
function observeMasterPasswordDialog(window, result) {
let closedPromise;
@ -94,16 +61,9 @@ add_task(async function test_processed_form_fired() {
let tab1Visibility = await getDocumentVisibilityState(tab1.linkedBrowser);
is(tab1Visibility, "visible", "The first tab should be foreground");
await addContentObserver(tab1.linkedBrowser, "passwordmgr-processed-form");
let formProcessedPromise = listenForTestNotification("FormProcessed");
await BrowserTestUtils.loadURI(tab1.linkedBrowser, FORM_URL);
let result = await getContentObserverResult(
tab1.linkedBrowser,
"passwordmgr-processed-form"
);
ok(
result,
"Observer should be notified when form is loaded into a visible document"
);
await formProcessedPromise;
gBrowser.removeTab(tab1);
});
@ -124,26 +84,33 @@ testUrls.forEach(testUrl => {
tab1Visibility = await getDocumentVisibilityState(tab1.linkedBrowser);
is(tab1Visibility, "hidden", "The first tab should be backgrounded");
// we shouldn't even try to autofill while hidden, so look for the passwordmgr-processed-form
// to be observed rather than any result of filling the form
await addContentObserver(tab1.linkedBrowser, "passwordmgr-processed-form");
// we shouldn't even try to autofill while hidden, so wait for the document to be in the
// non-visible pending queue instead.
let formFilled = false;
listenForTestNotification("FormProcessed").then(() => {
formFilled = true;
});
await BrowserTestUtils.loadURI(tab1.linkedBrowser, testUrl);
result = await getContentObserverResult(
tab1.linkedBrowser,
"passwordmgr-processed-form"
);
await TestUtils.waitForCondition(() => {
let windowGlobal = tab1.linkedBrowser.browsingContext.currentWindowGlobal;
if (!windowGlobal || windowGlobal.documentURI.spec == "about:blank") {
return false;
}
let actor = windowGlobal.getActor("LoginManager");
return actor.sendQuery("PasswordManager:formIsPending");
});
ok(
!result,
!formFilled,
"Observer should not be notified when form is loaded into a hidden document"
);
// Add the observer before switching tab
await addContentObserver(tab1.linkedBrowser, "passwordmgr-processed-form");
let formProcessedPromise = listenForTestNotification("FormProcessed");
await BrowserTestUtils.switchTab(gBrowser, tab1);
result = await getContentObserverResult(
tab1.linkedBrowser,
"passwordmgr-processed-form"
);
result = await formProcessedPromise;
tab1Visibility = await getDocumentVisibilityState(tab1.linkedBrowser);
is(tab1Visibility, "visible", "The first tab should be foreground");
ok(
@ -219,14 +186,11 @@ add_task(async function test_immediate_autofill_with_masterpassword() {
// In this case we will try to autofill while hidden, so look for the passwordmgr-processed-form
// to be observed
await addContentObserver(tab1.linkedBrowser, "passwordmgr-processed-form");
let formProcessedPromise = listenForTestNotification("FormProcessed");
await BrowserTestUtils.loadURI(tab1.linkedBrowser, FORM_URL);
let wasProcessed = getContentObserverResult(
tab1.linkedBrowser,
"passwordmgr-processed-form"
);
await Promise.all([dialogObserved, wasProcessed]);
await Promise.all([formProcessedPromise, dialogObserved]);
let wasProcessed = await formProcessedPromise;
ok(
wasProcessed,
"Observer should be notified when form is loaded into a hidden document"

View File

@ -23,14 +23,18 @@ add_task(async function setup() {
add_task(async function test_http_autofill() {
for (let scheme of ["http", "https"]) {
let formFilled = listenForTestNotification("FormProcessed");
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
`${scheme}${TEST_URL_PATH}form_basic.html`
);
let [username, password] = await ContentTask.spawn(
await formFilled;
let [username, password] = await SpecialPowers.spawn(
gBrowser.selectedBrowser,
null,
[],
async function() {
let doc = content.document;
let contentUsername = doc.getElementById("form-basic-username").value;
@ -56,24 +60,25 @@ add_task(async function test_http_autofill() {
add_task(async function test_iframe_in_http_autofill() {
for (let scheme of ["http", "https"]) {
// Wait for parent and child iframe to be processed.
let formFilled = listenForTestNotification("FormProcessed", 2);
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
`${scheme}${TEST_URL_PATH}form_basic_iframe.html`
);
let [username, password] = await ContentTask.spawn(
gBrowser.selectedBrowser,
null,
await formFilled;
let [username, password] = await SpecialPowers.spawn(
gBrowser.selectedBrowser.browsingContext.getChildren()[0],
[],
async function() {
let doc = content.document;
let iframe = doc.getElementById("test-iframe");
let contentUsername = iframe.contentWindow.document.getElementById(
"form-basic-username"
).value;
let contentPassword = iframe.contentWindow.document.getElementById(
"form-basic-password"
).value;
return [contentUsername, contentPassword];
let doc = this.content.document;
return [
doc.getElementById("form-basic-username").value,
doc.getElementById("form-basic-password").value,
];
}
);
@ -94,19 +99,24 @@ add_task(async function test_iframe_in_http_autofill() {
add_task(async function test_http_action_autofill() {
for (let type of ["insecure", "secure"]) {
let formFilled = listenForTestNotification("FormProcessed");
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
`https${TEST_URL_PATH}form_cross_origin_${type}_action.html`
);
let [username, password] = await ContentTask.spawn(
await formFilled;
let [username, password] = await SpecialPowers.spawn(
gBrowser.selectedBrowser,
null,
[],
async function() {
let doc = content.document;
let contentUsername = doc.getElementById("form-basic-username").value;
let contentPassword = doc.getElementById("form-basic-password").value;
return [contentUsername, contentPassword];
let doc = this.content.document;
return [
doc.getElementById("form-basic-username").value,
doc.getElementById("form-basic-password").value,
];
}
);

View File

@ -46,7 +46,11 @@ function synthesizeDblClickOnCell(aTree, column, row) {
);
}
async function togglePasswords() {
async function togglePasswords(promptWillShow) {
let confirmPromptDone = promptWillShow
? BrowserTestUtils.waitForEvent(pwmgrdlg, "endmodalstate")
: Promise.resolve();
pwmgrdlg.document.querySelector("#togglePasswords").doCommand();
await ContentTaskUtils.waitForCondition(
() => !signonsTree.columns.getNamedColumn("passwordCol").hidden,
@ -54,6 +58,7 @@ async function togglePasswords() {
);
await new Promise(resolve => waitForFocus(resolve, pwmgrdlg));
pwmgrdlg.document.documentElement.clientWidth; // flush to ensure UI up-to-date
await confirmPromptDone;
}
async function editUsernamePromises(site, oldUsername, newUsername) {
@ -166,9 +171,9 @@ add_task(async function test_edit_multiple_logins() {
) {
addLogin(site, oldUsername, oldPassword);
await editUsernamePromises(site, oldUsername, newUsername);
await togglePasswords();
await togglePasswords(true);
await editPasswordPromises(site, oldPassword, newPassword);
await togglePasswords();
await togglePasswords(false);
}
await testLoginChange(

View File

@ -572,13 +572,7 @@ add_task(async function test_normal_autofilled_7() {
},
async function(browser) {
// Add the observer before loading the form page
let formFilled = ContentTask.spawn(browser, null, async function() {
const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
await TestUtils.topicObserved("passwordmgr-processed-form");
await Promise.resolve();
});
let formFilled = listenForTestNotification("FormProcessed");
await SimpleTest.promiseFocus(browser.ownerGlobal);
await BrowserTestUtils.loadURI(browser, form1Url);
await formFilled;
@ -600,6 +594,8 @@ add_task(async function test_private_not_autofilled_8() {
// Sanity check the HTTP login exists.
is(Services.logins.getAllLogins().length, 1, "Should have the HTTP login");
let formFilled = listenForTestNotification("FormProcessed");
await focusWindow(privateWin);
await BrowserTestUtils.withNewTab(
{
@ -607,6 +603,7 @@ add_task(async function test_private_not_autofilled_8() {
url: form1Url,
},
async function(browser) {
await formFilled;
let fieldValues = await submitFormAndGetResults(
browser,
"formsubmit.sjs",
@ -667,6 +664,8 @@ add_task(async function test_normal_autofilled_10() {
// Sanity check the HTTP login exists.
is(Services.logins.getAllLogins().length, 1, "Should have the HTTP login");
let formFilled = listenForTestNotification("FormProcessed");
await focusWindow(normalWin);
await BrowserTestUtils.withNewTab(
{
@ -674,6 +673,7 @@ add_task(async function test_normal_autofilled_10() {
url: form1Url,
},
async function(browser) {
await formFilled;
let fieldValues = await submitFormAndGetResults(
browser,
"formsubmit.sjs",

View File

@ -38,7 +38,9 @@ async function showChangePasswordDoorhanger(
formLogin,
{ notificationType = "password-change", autoSavedLoginGuid = "" } = {}
) {
let prompter = LoginManagerParent._getPrompter(browser, null);
let windowGlobal = browser.browsingContext.currentWindowGlobal;
let loginManagerActor = windowGlobal.getActor("LoginManager");
let prompter = loginManagerActor._getPrompter(browser, null);
ok(!PopupNotifications.isPanelOpen, "Check the doorhanger isnt already open");
let promiseShown = BrowserTestUtils.waitForEvent(

View File

@ -561,6 +561,30 @@ async function openPasswordContextMenu(
await popupShownPromise;
}
/**
* Listen for the login manager test notification specified by
* expectedMessage. Possible messages:
* FormProcessed - a form was processed after page load.
* FormSubmit - a form was just submitted.
* PasswordFilledOrEdited - a password was filled in or modified.
*
* The count is the number of that messages to wait for. This should
* typically be used when waiting for the FormProcessed message for a page
* that has subframes to ensure all have been handled.
*
* Returns a promise that will passed additional data specific to the message.
*/
function listenForTestNotification(expectedMessage, count = 1) {
return new Promise(resolve => {
LoginManagerParent.setListenerForTests((msg, data) => {
if (msg == expectedMessage && --count == 0) {
LoginManagerParent.setListenerForTests(null);
resolve(data);
}
});
});
}
/**
* Use the contextmenu to fill a field with a generated password
*/
@ -595,21 +619,12 @@ async function doFillGeneratedPasswordContextMenuItem(browser, passwordInput) {
await ContentTaskUtils.waitForEvent(input, "input");
}
);
let messagePromise = new Promise(resolve => {
const eventName = "PasswordManager:onGeneratedPasswordFilledOrEdited";
browser.messageManager.addMessageListener(eventName, function mgsHandler(
msg
) {
if (msg.target != browser) {
return;
}
browser.messageManager.removeMessageListener(eventName, mgsHandler);
info(
"doFillGeneratedPasswordContextMenuItem: Got onGeneratedPasswordFilledOrEdited, resolving"
);
// allow LMP to handle the message, then resolve
SimpleTest.executeSoon(resolve);
});
let passwordGeneratedPromise = listenForTestNotification(
"PasswordFilledOrEdited"
);
await new Promise(resolve => {
SimpleTest.executeSoon(resolve);
});
EventUtils.synthesizeMouseAtCenter(generatedPasswordItem, {});
@ -617,5 +632,5 @@ async function doFillGeneratedPasswordContextMenuItem(browser, passwordInput) {
"doFillGeneratedPasswordContextMenuItem: Waiting for content input event"
);
await passwordChangedPromise;
await messagePromise;
await passwordGeneratedPromise;
}

View File

@ -12,7 +12,7 @@ module.exports = {
"assert": true,
"addMessageListener": true,
"sendAsyncMessage": true,
"Assert": true,
},
"rules": {
"no-var": "off",

View File

@ -108,6 +108,11 @@ function checkAutoCompleteResults(actualValues, expectedValues, hostname, msg) {
checkArrayValues(actualValues.slice(0, -1), expectedValues, msg);
}
function getIframeBrowsingContext(window, iframeNumber = 0) {
let bc = SpecialPowers.wrap(window).getWindowGlobalChild().browsingContext;
return SpecialPowers.unwrap(bc.getChildren()[iframeNumber]);
}
/**
* Check for expected username/password in form.
* @see `checkForm` below for a similar function.
@ -131,6 +136,44 @@ function checkLoginForm(
);
}
function checkLoginFormInChildFrame(
iframeBC,
usernameFieldId,
expectedUsername,
passwordFieldId,
expectedPassword
) {
return SpecialPowers.spawn(
iframeBC,
[usernameFieldId, expectedUsername, passwordFieldId, expectedPassword],
(
usernameFieldIdF,
expectedUsernameF,
passwordFieldIdF,
expectedPasswordF
) => {
let usernameField = this.content.document.getElementById(
usernameFieldIdF
);
let passwordField = this.content.document.getElementById(
passwordFieldIdF
);
let formID = usernameField.parentNode.id;
Assert.equal(
usernameField.value,
expectedUsernameF,
"Checking " + formID + " username is: " + expectedUsernameF
);
Assert.equal(
passwordField.value,
expectedPasswordF,
"Checking " + formID + " password is: " + expectedPasswordF
);
}
);
}
/**
* Check a form for expected values. If an argument is null, a field's
* expected value will be the default value.
@ -228,12 +271,12 @@ function registerRunTests() {
form.appendChild(password);
var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
var formLikeRoot = subject;
if (formLikeRoot.id !== "observerforcer") {
if (data !== "observerforcer") {
return;
}
SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
formLikeRoot.remove();
form.remove();
SimpleTest.executeSoon(() => {
var runTestEvent = new Event("runTests");
window.dispatchEvent(runTestEvent);

View File

@ -218,14 +218,12 @@ addMessageListener("setMasterPassword", ({ enable }) => {
}
});
function onFormSubmit(message) {
sendAsyncMessage("formSubmissionProcessed", message.data, message.objects);
}
Services.mm.addMessageListener("PasswordManager:onFormSubmit", onFormSubmit);
addMessageListener("cleanup", () => {
Services.mm.removeMessageListener(
"PasswordManager:onFormSubmit",
onFormSubmit
);
LoginManagerParent.setListenerForTests((msg, data) => {
if (msg == "FormSubmit") {
sendAsyncMessage("formSubmissionProcessed", data, {});
}
});
addMessageListener("cleanup", () => {
LoginManagerParent.setListenerForTests(null);
});

View File

@ -54,17 +54,19 @@ runInParent(function addLogins() {
<pre id="test">
<script class="testbody" type="text/javascript">
let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
let iframe = document.getElementsByTagName("iframe")[0];
let iframeDoc, hostname;
let uname;
let pword;
// Restore the form to the default state.
function restoreForm() {
pword.focus();
uname.value = "";
pword.value = "";
uname.focus();
return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
this.content.document.getElementById("form-basic-password").focus();
this.content.document.getElementById("form-basic-username").value = "";
this.content.document.getElementById("form-basic-password").value = "";
this.content.document.getElementById("form-basic-username").focus();
});
}
const HTTP_FORM_URL = "http://example.com/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html";
@ -79,10 +81,11 @@ async function setup(formUrl) {
}, {once: true});
});
iframeDoc = iframe.contentDocument;
hostname = iframeDoc.documentURIObject.host;
uname = iframeDoc.getElementById("form-basic-username");
pword = iframeDoc.getElementById("form-basic-password");
await promiseFormsProcessed();
hostname = SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
return this.content.document.documentURIObject.host;
});
}
add_task(async function test_autocomplete_https_downgrade() {
@ -95,9 +98,10 @@ add_task(async function test_autocomplete_https_downgrade() {
// from a HTTP page, look for matching logins, we should never offer a login with an HTTPS scheme
// we're expecting just login2 as a match
// Make sure initial form is empty.
checkLoginForm(uname, "", pword, "");
await checkLoginFormInChildFrame(iframe, "form-basic-username", "name1", "form-basic-password", "pass1");
// Trigger autocomplete popup
restoreForm();
await restoreForm();
let popupState = await getPopupState();
is(popupState.open, false, "Check popup is initially closed");
let shownPromise = promiseACShown();
@ -106,7 +110,7 @@ add_task(async function test_autocomplete_https_downgrade() {
info("got results: " + results.join(", "));
popupState = await getPopupState();
is(popupState.selectedIndex, -1, "Check no entries are selected");
checkAutoCompleteResults(results, ["name1"], "http://example.com", "initial");
checkAutoCompleteResults(results, ["name1", "name2"], hostname, "initial");
});
</script>
</pre>

View File

@ -54,17 +54,17 @@ runInParent(function addLogins() {
<pre id="test">
<script class="testbody" type="text/javascript">
let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
let iframe = document.getElementsByTagName("iframe")[0];
let iframeDoc, hostname;
let uname;
let pword;
// Restore the form to the default state.
function restoreForm() {
pword.focus();
uname.value = "";
pword.value = "";
uname.focus();
return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
this.content.document.getElementById("form-basic-password").focus();
this.content.document.getElementById("form-basic-username").value = "";
this.content.document.getElementById("form-basic-password").value = "";
this.content.document.getElementById("form-basic-username").focus();
});
}
const HTTPS_FORM_URL = "https://example.com/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html";
@ -79,10 +79,11 @@ async function setup(formUrl = HTTPS_FORM_URL) {
}, {once: true});
});
iframeDoc = iframe.contentDocument;
hostname = iframeDoc.documentURIObject.host;
uname = iframeDoc.getElementById("form-basic-username");
pword = iframeDoc.getElementById("form-basic-password");
await promiseFormsProcessed();
hostname = SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
return this.content.document.documentURIObject.host;
});
}
add_task(async function setup_https_frame() {
@ -91,9 +92,9 @@ add_task(async function setup_https_frame() {
add_task(async function test_empty_first_entry() {
// Make sure initial form is empty.
checkLoginForm(uname, "", pword, "");
checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", "");
// Trigger autocomplete popup
restoreForm();
await restoreForm();
let popupState = await getPopupState();
is(popupState.open, false, "Check popup is initially closed");
let shownPromise = promiseACShown();
@ -107,14 +108,14 @@ add_task(async function test_empty_first_entry() {
let index0Promise = notifySelectedIndex(0);
synthesizeKey("KEY_ArrowDown");
await index0Promise;
checkLoginForm(uname, "", pword, ""); // value shouldn't update
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", ""); // value shouldn't update
synthesizeKey("KEY_Enter");
await promiseFormsProcessed();
checkLoginForm(uname, "name", pword, "pass");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name", "form-basic-password", "pass");
});
add_task(async function test_empty_second_entry() {
restoreForm();
await restoreForm();
let shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown"); // open
await shownPromise;
@ -122,16 +123,21 @@ add_task(async function test_empty_second_entry() {
synthesizeKey("KEY_ArrowDown"); // second
synthesizeKey("KEY_Enter");
await promiseFormsProcessed();
checkLoginForm(uname, "name1", pword, "pass1");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name1", "form-basic-password", "pass1");
});
add_task(async function test_search() {
restoreForm();
await restoreForm();
let shownPromise = promiseACShown();
// We need to blur for the autocomplete controller to notice the forced value below.
uname.blur();
uname.value = "name";
uname.focus();
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
let uname = this.content.document.getElementById("form-basic-username");
uname.blur();
uname.value = "name";
uname.focus();
});
sendChar("1");
synthesizeKey("KEY_ArrowDown"); // open
let results = await shownPromise;
@ -139,15 +145,14 @@ add_task(async function test_search() {
synthesizeKey("KEY_ArrowDown"); // first
synthesizeKey("KEY_Enter");
await promiseFormsProcessed();
checkLoginForm(uname, "name1", pword, "pass1");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name1", "form-basic-password", "pass1");
let popupState = await getPopupState();
is(popupState.open, false, "Check popup is now closed");
});
add_task(async function test_delete_first_entry() {
restoreForm();
uname.focus();
await restoreForm();
let shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
await shownPromise;
@ -161,7 +166,7 @@ add_task(async function test_delete_first_entry() {
// On Win/Linux, shift-backspace does not work, delete and shift-delete do.
synthesizeKey("KEY_Delete", {shiftKey: true});
await deletionPromise;
checkLoginForm(uname, "", pword, "");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", "");
let results = await notifyMenuChanged(3, "name1");
@ -174,8 +179,7 @@ add_task(async function test_delete_first_entry() {
});
add_task(async function test_delete_duplicate_entry() {
restoreForm();
uname.focus();
await restoreForm();
let shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
await shownPromise;
@ -189,7 +193,7 @@ add_task(async function test_delete_duplicate_entry() {
// On Win/Linux, shift-backspace does not work, delete and shift-delete do.
synthesizeKey("KEY_Delete", {shiftKey: true});
await deletionPromise;
checkLoginForm(uname, "", pword, "");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", "");
is(await LoginManager.countLogins("http://example.com", "http://example.com", null), 1,
"Check that the HTTP login remains");

View File

@ -1,4 +1,4 @@
<!DOCTYPE HTML>
xcod<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">

View File

@ -11,15 +11,6 @@
<body>
<script>
runChecksAfterCommonInit();
runInParent(function initLogins() {
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
login1.init("https://example.com", "", null, "autofilled", "pass1", "", "");
Services.logins.addLogin(login1);
});
</script>
<p id="display"></p>
@ -31,6 +22,20 @@ runInParent(function initLogins() {
<script class="testbody" type="text/javascript">
let win;
add_task(async function setup() {
let loginAddedPromise = promiseStorageChanged(["addLogin"]);
runInParent(function initLogins() {
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
login1.init("https://example.com", "", null, "autofilled", "pass1", "", "");
Services.logins.addLogin(login1);
});
await loginAddedPromise;
});
add_task(async function test_crossOriginBfcacheRestore() {
let processedPromise = promiseFormsProcessed();
win = window.open("form_basic.html", "loginWin");

View File

@ -9,6 +9,8 @@
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<script>
let formFilledPromise = promiseFormsProcessed();
runInParent(function initLogins() {
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
@ -32,16 +34,13 @@ runInParent(function initLogins() {
<pre id="test">
<script>
let {ContentTaskUtils} = SpecialPowers.Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
add_task(async function test_field_highlight_on_autofill() {
await formFilledPromise;
let username = document.getElementById("uname");
let password = document.getElementById("pword");
await ContentTaskUtils.waitForCondition(() => {
return document.defaultView.getComputedStyle(username).getPropertyValue("filter") !== "none";
}, "Highlight was successfully applied to the username field on page load autofill");
isnot(document.defaultView.getComputedStyle(username).getPropertyValue("filter"), "none",
"Highlight was successfully applied to the username field on page load autofill");
isnot(document.defaultView.getComputedStyle(password).getPropertyValue("filter"), "none",
"Highlight was successfully applied to the password field on page load autofill");

View File

@ -45,12 +45,15 @@ async function checkFormsWithLogin(formUrls, login, expectedUsername, expectedPa
await prepareAndProcessForm(url);
info("form was processed");
let iframeDoc = iframe.contentDocument;
let uname = iframeDoc.getElementById("form-basic-username");
let pword = iframeDoc.getElementById("form-basic-password");
info("checking form, uname: " + uname.value);
is(uname.value, expectedUsername, `username ${expectedUsername ? "filled" : "not filled"} on ${url}`);
is(pword.value, expectedPassword, `password ${expectedPassword ? "filled" : "not filled"} on ${url}`);
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [url, expectedUsername, expectedPassword],
function(urlContent, expectedUsernameContent, expectedPasswordContent) {
let doc = this.content.document;
let uname = doc.getElementById("form-basic-username");
let pword = doc.getElementById("form-basic-password");
Assert.equal(uname.value, expectedUsernameContent, `username ${expectedUsernameContent ? "filled" : "not filled"} on ${urlContent}`);
Assert.equal(pword.value, expectedPasswordContent, `password ${expectedPasswordContent ? "filled" : "not filled"} on ${urlContent}`);
});
}
}

View File

@ -28,7 +28,7 @@ let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@moz
<pre id="test">
<script class="testbody" type="text/javascript">
let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
let iframe = document.getElementsByTagName("iframe")[0];
async function prepareLoginsAndProcessForm(url, logins = []) {
await LoginManager.removeAllLogins();
@ -55,10 +55,9 @@ add_task(async function test_simpleNoDupesNoAction() {
"name2", "pass2", "uname", "pword"),
]);
let iframeDoc = iframe.contentDocument;
let uname = iframeDoc.getElementById("form-basic-username");
let pword = iframeDoc.getElementById("form-basic-password");
checkLoginForm(uname, "name2", pword, "pass2");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0),
"form-basic-username", "name2",
"form-basic-password", "pass2");
});
add_task(async function test_simpleNoDupesUpgradeOriginAndAction() {
@ -67,10 +66,8 @@ add_task(async function test_simpleNoDupesUpgradeOriginAndAction() {
"name2", "pass2", "uname", "pword"),
]);
let iframeDoc = iframe.contentDocument;
let uname = iframeDoc.getElementById("form-basic-username");
let pword = iframeDoc.getElementById("form-basic-password");
checkLoginForm(uname, "name2", pword, "pass2");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name2",
"form-basic-password", "pass2");
});
add_task(async function test_simpleNoDupesUpgradeOriginOnly() {
@ -79,10 +76,8 @@ add_task(async function test_simpleNoDupesUpgradeOriginOnly() {
"name2", "pass2", "uname", "pword"),
]);
let iframeDoc = iframe.contentDocument;
let uname = iframeDoc.getElementById("form-basic-username");
let pword = iframeDoc.getElementById("form-basic-password");
checkLoginForm(uname, "name2", pword, "pass2");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name2",
"form-basic-password", "pass2");
});
add_task(async function test_simpleNoDupesUpgradeActionOnly() {
@ -91,10 +86,8 @@ add_task(async function test_simpleNoDupesUpgradeActionOnly() {
"name2", "pass2", "uname", "pword"),
]);
let iframeDoc = iframe.contentDocument;
let uname = iframeDoc.getElementById("form-basic-username");
let pword = iframeDoc.getElementById("form-basic-password");
checkLoginForm(uname, "name2", pword, "pass2");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name2",
"form-basic-password", "pass2");
});
add_task(async function test_dedupe() {
@ -109,10 +102,8 @@ add_task(async function test_dedupe() {
"name1", "passHTTPStoHTTP", "uname", "pword"),
]);
let iframeDoc = iframe.contentDocument;
let uname = iframeDoc.getElementById("form-basic-username");
let pword = iframeDoc.getElementById("form-basic-password");
checkLoginForm(uname, "name1", pword, "passHTTPStoHTTPS");
await checkLoginFormInChildFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name1",
"form-basic-password", "passHTTPStoHTTPS");
});
</script>

View File

@ -47,7 +47,7 @@ runInParent(function addLogins() {
<pre id="test">
<script class="testbody" type="text/javascript">
let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
let iframe = document.getElementsByTagName("iframe")[0];
let iframeDoc, hostname;
add_task(async function setup() {
@ -58,8 +58,11 @@ add_task(async function setup() {
}, {once: true});
});
iframeDoc = iframe.contentDocument;
hostname = iframeDoc.documentURIObject.host;
await promiseFormsProcessed();
hostname = SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
return this.content.document.documentURIObject.host;
});
SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popupshown to occur");
});
@ -70,7 +73,9 @@ add_task(async function test_initial_focus() {
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
await promiseFormsProcessed();
is(iframeDoc.getElementById("form-basic-password").value, "pass", "Check first password filled");
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
Assert.equal(this.content.document.getElementById("form-basic-password").value, "pass", "Check first password filled");
});
let popupState = await getPopupState();
is(popupState.open, false, "Check popup is now closed");
});
@ -78,33 +83,47 @@ add_task(async function test_initial_focus() {
// This depends on the filling from the previous test.
add_task(async function test_not_reopened_if_filled() {
listenForUnexpectedPopupShown();
let usernameField = iframeDoc.getElementById("form-basic-username");
usernameField.focus();
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
this.content.document.getElementById("form-basic-username").focus();
});
info("Waiting to see if a popupshown occurs");
await new Promise(resolve => setTimeout(resolve, 1000));
// cleanup
gPopupShownExpected = true;
iframeDoc.getElementById("form-basic-submit").focus();
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
this.content.document.getElementById("form-basic-submit").focus();
});
});
add_task(async function test_reopened_after_edit_not_matching_saved() {
let usernameField = iframeDoc.getElementById("form-basic-username");
usernameField.value = "nam";
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
this.content.document.getElementById("form-basic-username").value = "nam";
});
let shownPromise = promiseACShown();
usernameField.focus();
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
this.content.document.getElementById("form-basic-username").focus();
});
await shownPromise;
iframeDoc.getElementById("form-basic-submit").focus();
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
this.content.document.getElementById("form-basic-submit").focus();
});
});
add_task(async function test_not_reopened_after_selecting() {
let formFillController = SpecialPowers.Cc["@mozilla.org/satchel/form-fill-controller;1"].
getService(SpecialPowers.Ci.nsIFormFillController);
let usernameField = iframeDoc.getElementById("form-basic-username");
usernameField.value = "";
iframeDoc.getElementById("form-basic-password").value = "";
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
this.content.document.getElementById("form-basic-username").value = "";
this.content.document.getElementById("form-basic-password").value = "";
});
listenForUnexpectedPopupShown();
formFillController.markAsLoginManagerField(usernameField);
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
let formFillController = SpecialPowers.Cc["@mozilla.org/satchel/form-fill-controller;1"].
getService(SpecialPowers.Ci.nsIFormFillController);
let usernameField = this.content.document.getElementById("form-basic-username");
formFillController.markAsLoginManagerField(usernameField);
});
info("Waiting to see if a popupshown occurs");
await new Promise(resolve => setTimeout(resolve, 1000));

View File

@ -9,9 +9,6 @@
</head>
<body>
<script type="application/javascript">
const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerChild.jsm");
const { LoginManagerChild } = LMCBackstagePass;
SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive");
let loadPromise = new Promise(resolve => {

View File

@ -149,13 +149,15 @@ add_task(async function test_3() {
info("filled");
// check contents of iframe1 fields
var u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
var p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
is(u.value, "user1", "checking expected user to have been filled in");
is(p.value, "pass1", "checking expected pass to have been filled in");
info("clearing fields to not cause a submission when the next document is loaded");
u.value = "";
p.value = "";
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
var u = this.content.document.getElementById("userfield");
var p = this.content.document.getElementById("passfield");
Assert.equal(u.value, "user1", "checking expected user to have been filled in");
Assert.equal(p.value, "pass1", "checking expected pass to have been filled in");
u.value = "";
p.value = "";
});
ok(await isLoggedIn(), "should be logged in");
logoutMasterPassword();
@ -189,10 +191,12 @@ add_task(async function test_4() {
await promptDone;
// check contents of iframe1 fields
var u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
var p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
is(u.value, "", "checking expected empty user");
is(p.value, "", "checking expected empty pass");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
var u = this.content.document.getElementById("userfield");
var p = this.content.document.getElementById("passfield");
Assert.equal(u.value, "", "checking expected empty user");
Assert.equal(p.value, "", "checking expected empty pass");
});
ok(!await isLoggedIn(), "should be logged out");
@ -227,10 +231,12 @@ add_task(async function test_4() {
// is already waiting)
// check contents of iframe2 fields
u = SpecialPowers.wrap(iframe2).contentDocument.getElementById("userfield");
p = SpecialPowers.wrap(iframe2).contentDocument.getElementById("passfield");
is(u.value, "", "checking expected empty user");
is(p.value, "", "checking expected empty pass");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 1), [], function() {
var u = this.content.document.getElementById("userfield");
var p = this.content.document.getElementById("passfield");
Assert.equal(u.value, "", "checking expected empty user");
Assert.equal(p.value, "", "checking expected empty pass");
});
// XXX check that there's 1 MP window open
ok(!await isLoggedIn(), "should be logged out");
@ -254,24 +260,28 @@ add_task(async function test_4() {
ok(await isLoggedIn(), "should be logged in");
// check contents of iframe1 fields
u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
is(u.value, "user2", "checking expected user to have been filled in");
is(p.value, "pass2", "checking expected pass to have been filled in");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
var u = this.content.document.getElementById("userfield");
var p = this.content.document.getElementById("passfield");
Assert.equal(u.value, "user2", "checking expected user to have been filled in");
Assert.equal(p.value, "pass2", "checking expected pass to have been filled in");
info("clearing fields to not cause a submission when the next document is loaded");
u.value = "";
p.value = "";
// clearing fields to not cause a submission when the next document is loaded
u.value = "";
p.value = "";
});
// check contents of iframe2 fields
u = SpecialPowers.wrap(iframe2).contentDocument.getElementById("userfield");
p = SpecialPowers.wrap(iframe2).contentDocument.getElementById("passfield");
is(u.value, "user1", "checking expected user to have been filled in");
is(p.value, "pass1", "checking expected pass to have been filled in");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 1), [], function() {
var u = this.content.document.getElementById("userfield");
var p = this.content.document.getElementById("passfield");
Assert.equal(u.value, "user1", "checking expected user to have been filled in");
Assert.equal(p.value, "pass1", "checking expected pass to have been filled in");
info("clearing fields to not cause a submission when the next document is loaded");
u.value = "";
p.value = "";
// clearing fields to not cause a submission when the next document is loaded
u.value = "";
p.value = "";
});
});
// XXX do a test5ABC with clicking cancel?

View File

@ -10,9 +10,6 @@
</head>
<body>
<script type="application/javascript">
const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerChild.jsm");
const { LoginManagerChild } = LMCBackstagePass;
let readyPromise = registerRunTests();
function add2logins() {
@ -59,12 +56,13 @@ async function loadFormIntoIframe(origin, html) {
});
loginFrame.src = origin + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
await loadedPromise;
let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
// eslint-disable-next-line no-unsanitized/property
frameDoc.documentElement.innerHTML = html;
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [html], function(contentHtml) {
// eslint-disable-next-line no-unsanitized/property
this.content.document.documentElement.innerHTML = contentHtml;
});
// Wait for the form to be processed before trying to submit.
await promiseFormsProcessed();
return frameDoc;
}
add_task(async function setup() {
@ -146,16 +144,21 @@ function getSubmitMessage() {
add_task(async function test_new_logins() {
for (let tc of TESTCASES) {
info("Starting testcase: " + JSON.stringify(tc));
let frameDoc = await loadFormIntoIframe(DEFAULT_ORIGIN, `<form id="form1" onsubmit="return false;">
await loadFormIntoIframe(DEFAULT_ORIGIN, `<form id="form1" onsubmit="return false;">
<input type="text" name="uname" value="${tc.username}">
<input type="password" name="pword" value="thepassword">
<button type="submit" id="submitBtn">Submit</button>
</form>`);
is(frameDoc.querySelector("[name='uname']").value, tc.username, "Checking for filled username");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [tc], function(testcase) {
let doc = this.content.document;
Assert.equal(doc.querySelector("[name='uname']").value, testcase.username, "Checking for filled username");
});
// Check data sent via PasswordManager:onFormSubmit
let processedPromise = getSubmitMessage();
frameDoc.getElementById("submitBtn").click();
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
this.content.document.getElementById("submitBtn").click();
});
let submittedResult = await processedPromise;
info("Got submittedResult: " + JSON.stringify(submittedResult));
@ -182,21 +185,26 @@ add_task(async function test_no_autofill_munged_username_matching_password() {
let timesUsed = bulletLogin.timesUsed;
let guid = bulletLogin.guid;
let frameDoc = await loadFormIntoIframe(ORG_ORIGIN, `<form id="form1" onsubmit="return false;">
await loadFormIntoIframe(ORG_ORIGIN, `<form id="form1" onsubmit="return false;">
<input type="text" name="uname" value="">
<input type="password" name="pword" value="">
<button type="submit" id="submitBtn">Submit</button>
</form>`);
is(frameDoc.querySelector("[name='uname']").value, "", "Check username didn't get autofilled");
frameDoc.querySelector("[name='uname']").setUserInput("real••••user");
frameDoc.querySelector("[name='pword']").setUserInput("pass1");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
let doc = this.content.document;
Assert.equal(doc.querySelector("[name='uname']").value, "", "Check username didn't get autofilled");
doc.querySelector("[name='uname']").setUserInput("real••••user");
doc.querySelector("[name='pword']").setUserInput("pass1");
});
// we shouldn't get the save password doorhanger...
let popupShownPromise = promiseNoUnexpectedPopupShown();
// Check data sent via PasswordManager:onFormSubmit
let processedPromise = getSubmitMessage();
frameDoc.getElementById("submitBtn").click();
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
this.content.document.getElementById("submitBtn").click();
});
let submittedResult = await processedPromise;
info("Got submittedResult: " + JSON.stringify(submittedResult));
@ -227,20 +235,25 @@ add_task(async function test_autofill_munged_username_matching_password() {
let timesUsed = bulletLogin.timesUsed;
let guid = bulletLogin.guid;
let frameDoc = await loadFormIntoIframe(ORG_ORIGIN, `<form id="form1" onsubmit="return false;">
await loadFormIntoIframe(ORG_ORIGIN, `<form id="form1" onsubmit="return false;">
<input type="text" name="uname" value="">
<input type="password" name="pword" value="">
<button type="submit" id="submitBtn">Submit</button>
</form>`);
is(frameDoc.querySelector("[name='uname']").value, "real••••user", "Check username did get autofilled");
frameDoc.querySelector("[name='pword']").setUserInput("pass1");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
let doc = this.content.document;
Assert.equal(doc.querySelector("[name='uname']").value, "real••••user", "Check username did get autofilled");
doc.querySelector("[name='pword']").setUserInput("pass1");
});
// we shouldn't get the save/update password doorhanger as it didn't change
let popupShownPromise = promiseNoUnexpectedPopupShown();
// Check data sent via PasswordManager:onFormSubmit
let processedPromise = getSubmitMessage();
frameDoc.getElementById("submitBtn").click();
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
this.content.document.getElementById("submitBtn").click();
});
let submittedResult = await processedPromise;
info("Got submittedResult: " + JSON.stringify(submittedResult));

View File

@ -31,12 +31,14 @@ async function loadFormIntoIframe(origin, html) {
});
loginFrame.src = origin + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
await loadedPromise;
let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
// eslint-disable-next-line no-unsanitized/property
frameDoc.documentElement.innerHTML = html;
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [html], function(contentHtml) {
// eslint-disable-next-line no-unsanitized/property
this.content.document.documentElement.innerHTML = contentHtml;
});
// Wait for the form to be processed before trying to submit.
await promiseFormsProcessed();
return frameDoc;
}
add_task(async function setup() {
@ -93,20 +95,25 @@ function getSubmitMessage() {
add_task(async function test_password_lengths() {
for (let tc of TESTCASES) {
info("Starting testcase: " + tc.testName + ", " + JSON.stringify([tc.pword1, tc.pword2]));
let frameDoc = await loadFormIntoIframe(DEFAULT_ORIGIN, `<form id="form1" onsubmit="return false;">
await loadFormIntoIframe(DEFAULT_ORIGIN, `<form id="form1" onsubmit="return false;">
<input type="text" name="uname" value="myname">
<input type="password" name="pword1" value="">
<input type="password" name="pword2" value="">
<button type="submit" id="submitBtn">Submit</button>
</form>`);
is(frameDoc.querySelector("[name='uname']").value, "myname", "Checking for filled username");
frameDoc.querySelector("[name='pword1']").setUserInput(tc.pword1);
frameDoc.querySelector("[name='pword2']").setUserInput(tc.pword2);
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [tc], function(testcase) {
let doc = this.content.document;
Assert.equal(doc.querySelector("[name='uname']").value, "myname", "Checking for filled username");
doc.querySelector("[name='pword1']").setUserInput(testcase.pword1);
doc.querySelector("[name='pword2']").setUserInput(testcase.pword2);
});
// Check data sent via PasswordManager:onFormSubmit
let processedPromise = getSubmitMessage();
frameDoc.getElementById("submitBtn").click();
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
this.content.document.getElementById("submitBtn").click();
});
let submittedResult = await processedPromise;
info("Got submittedResult: " + JSON.stringify(submittedResult));

View File

@ -216,25 +216,33 @@
checkPromptState(promptState, expectedPromptState);
}
let iframe1Doc = await iframe1DocPromise;
let iframe2aDoc = await iframe2aDocPromise;
let iframe2bDoc = await iframe2bDocPromise;
await iframe1DocPromise;
await iframe2aDocPromise;
await iframe2bDocPromise;
let authok1 = iframe1Doc.getElementById("ok").textContent;
let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
let doc = this.content.document;
let authok1 = doc.getElementById("ok").textContent;
let proxyok1 = doc.getElementById("proxy").textContent;
Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1");
Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
});
let authok2a = iframe2aDoc.getElementById("ok").textContent;
let proxyok2a = iframe2aDoc.getElementById("proxy").textContent;
await SpecialPowers.spawn(getIframeBrowsingContext(window, 1), [], function() {
let doc = this.content.document;
let authok2a = doc.getElementById("ok").textContent;
let proxyok2a = doc.getElementById("proxy").textContent;
Assert.equal(authok2a, "PASS", "WWW Authorization OK, frame2a");
Assert.equal(proxyok2a, "PASS", "Proxy Authorization OK, frame2a");
});
let authok2b = iframe2bDoc.getElementById("ok").textContent;
let proxyok2b = iframe2bDoc.getElementById("proxy").textContent;
is(authok1, "PASS", "WWW Authorization OK, frame1");
is(authok2a, "PASS", "WWW Authorization OK, frame2a");
is(authok2b, "PASS", "WWW Authorization OK, frame2b");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
is(proxyok2a, "PASS", "Proxy Authorization OK, frame2a");
is(proxyok2b, "PASS", "Proxy Authorization OK, frame2b");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 2), [], function() {
let doc = this.content.document;
let authok2b = doc.getElementById("ok").textContent;
let proxyok2b = doc.getElementById("proxy").textContent;
Assert.equal(authok2b, "PASS", "WWW Authorization OK, frame2b");
Assert.equal(proxyok2b, "PASS", "Proxy Authorization OK, frame2b");
});
});
add_task(async function test_threeSubframesWithSameProxyAndHTTPAuth() {
@ -275,21 +283,26 @@
});
await handlePrompt(state, action);
let iframe1Doc = await iframe1DocPromise;
function checkIframe(frame) {
let doc = SpecialPowers.wrap(frame).contentDocument;
await iframe1DocPromise;
function checkIframe(frameid) {
let doc = this.content.document;
let authok = doc.getElementById("ok").textContent;
let proxyok = doc.getElementById("proxy").textContent;
is(authok, "PASS", "WWW Authorization OK, " + frame.id);
is(proxyok, "PASS", "Proxy Authorization OK, " + frame.id);
Assert.equal(authok, "PASS", "WWW Authorization OK, " + frameid);
Assert.equal(proxyok, "PASS", "Proxy Authorization OK, " + frameid);
}
checkIframe(iframe1Doc.getElementById("iframe1"));
checkIframe(iframe1Doc.getElementById("iframe2"));
checkIframe(iframe1Doc.getElementById("iframe3"));
let parentIFrameBC = SpecialPowers.wrap(window).getWindowGlobalChild().
browsingContext.getChildren()[0];
let childIFrame = SpecialPowers.unwrap(parentIFrameBC.getChildren()[0]);
await SpecialPowers.spawn(childIFrame, ["iframe1"], checkIframe);
childIFrame = SpecialPowers.unwrap(parentIFrameBC.getChildren()[1]);
await SpecialPowers.spawn(childIFrame, ["iframe2"], checkIframe);
childIFrame = SpecialPowers.unwrap(parentIFrameBC.getChildren()[2]);
await SpecialPowers.spawn(childIFrame, ["iframe3"], checkIframe);
});
add_task(async function test_oneFrameWithUnauthenticatedProxy() {
@ -372,13 +385,16 @@
};
await handlePrompt(state, action);
let iframe1Doc = await iframe1DocPromise;
await iframe1DocPromise;
let authok1 = iframe1Doc.getElementById("ok").textContent;
let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
let doc = this.content.document;
let authok1 = doc.getElementById("ok").textContent;
let proxyok1 = doc.getElementById("proxy").textContent;
is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
Assert.equal(authok1, "FAIL", "WWW Authorization FAILED, frame1");
Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
});
});
add_task(async function test_hugePayloadCancelled() {
@ -464,16 +480,19 @@
};
await handlePrompt(state, action);
let iframe1Doc = await iframe1DocPromise;
await iframe1DocPromise;
let authok1 = iframe1Doc.getElementById("ok").textContent;
let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
let footnote = iframe1Doc.getElementById("footnote").textContent;
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
let doc = this.content.document;
let authok1 = doc.getElementById("ok").textContent;
let proxyok1 = doc.getElementById("proxy").textContent;
let footnote = doc.getElementById("footnote").textContent;
is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
is(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
Assert.equal(authok1, "FAIL", "WWW Authorization FAILED, frame1");
Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
Assert.equal(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
});
});
add_task(async function test_hugeProxySuccessWWWSuccess() {
@ -504,15 +523,19 @@
};
await handlePrompt(state, action);
let iframe1Doc = await iframe1DocPromise;
let authok1 = iframe1Doc.getElementById("ok").textContent;
let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
let footnote = iframe1Doc.getElementById("footnote").textContent;
await iframe1DocPromise;
is(authok1, "PASS", "WWW Authorization OK, frame1");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
is(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
let doc = this.content.document;
let authok1 = doc.getElementById("ok").textContent;
let proxyok1 = doc.getElementById("proxy").textContent;
let footnote = doc.getElementById("footnote").textContent;
Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1");
Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
Assert.equal(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
});
});
add_task(async function test_cancelSome() {
@ -575,15 +598,19 @@
};
await handlePrompt(state, action);
let iframe1Doc = await iframe1DocPromise;
let authok1 = iframe1Doc.getElementById("ok").textContent;
let proxyok1 = iframe1Doc.getElementById("proxy").textContent;
let footnote = iframe1Doc.getElementById("footnote").textContent;
await iframe1DocPromise;
await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
let doc = this.content.document;
let authok1 = doc.getElementById("ok").textContent;
let proxyok1 = doc.getElementById("proxy").textContent;
let footnote = doc.getElementById("footnote").textContent;
Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1");
Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
Assert.equal(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
});
is(authok1, "PASS", "WWW Authorization OK, frame1");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
is(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
});
</script>
</head>

View File

@ -98,8 +98,8 @@ add_task(async function test_iframe() {
iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
await promptDone;
await iframeLoaded;
checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
iframe.contentDocument);
await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
iframe);
state = {
msg: "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest2”",
@ -127,8 +127,8 @@ add_task(async function test_iframe() {
iframe.src = "authenticate.sjs?user=mochiuser2&pass=mochipass2&realm=mochitest2";
await promptDone;
await iframeLoaded;
checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
iframe.contentDocument);
await checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
iframe);
// Now make a load that requests the realm from test 1000. It was
// already provided there, so auth will *not* be prompted for -- the
@ -136,8 +136,8 @@ add_task(async function test_iframe() {
iframeLoaded = onloadPromiseFor("iframe");
iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
await iframeLoaded;
checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
iframe.contentDocument);
await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
iframe);
// Same realm we've already authenticated to, but with a different
// expected password (to trigger an auth prompt, and change-password
@ -167,8 +167,8 @@ add_task(async function test_iframe() {
iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1-new";
await promptDone;
await iframeLoaded;
checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1-new"},
iframe.contentDocument);
await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1-new"},
iframe);
await promptShownPromise;
// Same as last test, but for a realm we haven't already authenticated
@ -199,8 +199,8 @@ add_task(async function test_iframe() {
iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-new&realm=mochitest3";
await promptDone;
await iframeLoaded;
checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-new"},
iframe.contentDocument);
await checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-new"},
iframe);
await promptShownPromise;
// Housekeeping: Delete login4 to test the save prompt in the next test.
@ -247,8 +247,8 @@ add_task(async function test_iframe() {
iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-old&realm=mochitest3";
await promptDone;
await iframeLoaded;
checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-old"},
iframe.contentDocument);
await checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-old"},
iframe);
await promptShownPromise;
});
@ -281,8 +281,8 @@ add_task(async function test_schemeUpgrade() {
"?user=httpUser&pass=httpPass&realm=schemeUpgrade";
await promptDone;
await iframeLoaded;
checkEchoedAuthInfo({user: "httpUser", pass: "httpPass"},
SpecialPowers.wrap(iframe).contentDocument);
await checkEchoedAuthInfo({user: "httpUser", pass: "httpPass"},
iframe);
});
add_task(async function test_schemeDowngrade() {
@ -345,8 +345,8 @@ add_task(async function test_schemeUpgrade_dedupe() {
"?user=dedupeUser&pass=httpsPass&realm=schemeUpgradeDedupe";
await promptDone;
await iframeLoaded;
checkEchoedAuthInfo({user: "dedupeUser", pass: "httpsPass"},
SpecialPowers.wrap(iframe).contentDocument);
await checkEchoedAuthInfo({user: "dedupeUser", pass: "httpsPass"},
iframe);
});
</script>
</pre>

View File

@ -20,14 +20,20 @@
// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
isTabModal = false;
runInParent(() => {
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
add_task(async function setup() {
let loginAddedPromise = promiseStorageChanged(["addLogin"]);
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
login.init("http://mochi.test:8888", null, "mochitest",
"mochiuser1", "mochipass1", "", "");
Services.logins.addLogin(login);
runInParent(() => {
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
login.init("http://mochi.test:8888", null, "mochitest",
"mochiuser1", "mochipass1", "", "");
Services.logins.addLogin(login);
});
await loginAddedPromise;
});
add_task(async function test_sandbox_xhr() {

View File

@ -25,6 +25,9 @@ function waitForFills(fillCount) {
}
add_task(async function setup() {
// This test should run without any existing loaded recipes interfering.
await resetRecipes();
if (document.readyState !== "complete") {
await new Promise((resolve) => {
document.onreadystatechange = () => {

View File

@ -14,6 +14,7 @@ add_task(async function test_doAutocompleteSearch_generated_noLogins() {
Services.prefs.setBoolPref("signon.generation.enabled", true);
let LMP = new LoginManagerParent();
LMP.useBrowsingContext(123);
ok(LMP.doAutocompleteSearch, "doAutocompleteSearch exists");
@ -26,7 +27,6 @@ add_task(async function test_doAutocompleteSearch_generated_noLogins() {
fieldName: "new-password",
canAutomaticallyPersist: false,
},
browsingContextId: 123,
formOrigin: "https://example.com",
actionOrigin: "https://mozilla.org",
searchString: "",
@ -36,21 +36,6 @@ add_task(async function test_doAutocompleteSearch_generated_noLogins() {
isPasswordField: true,
};
let sendMessageStub = sinon.stub();
let fakeBrowser = {
messageManager: {
sendAsyncMessage: sendMessageStub,
},
ownerGlobal: {
docShell: {
// eslint-disable-next-line mozilla/use-chromeutils-generateqi
QueryInterface() {
return { usePrivateBrowsing: false };
},
},
},
};
sinon
.stub(LoginManagerParent._browsingContextGlobal, "get")
.withArgs(123)
@ -64,72 +49,48 @@ add_task(async function test_doAutocompleteSearch_generated_noLogins() {
};
});
LMP.doAutocompleteSearch(arg1, fakeBrowser);
ok(sendMessageStub.calledOnce, "sendAsyncMessage was called");
let msg1 = sendMessageStub.firstCall.args[1];
equal(msg1.requestId, arg1.requestId, "requestId matches");
equal(msg1.logins.length, 0, "no logins");
ok(msg1.generatedPassword, "has a generated password");
equal(
msg1.generatedPassword.length,
LoginTestUtils.generation.LENGTH,
"generated password length"
);
sendMessageStub.resetHistory();
let result1 = LMP.doAutocompleteSearch(arg1);
equal(result1.logins.length, 0, "no logins");
ok(result1.generatedPassword, "has a generated password");
equal(result1.generatedPassword.length, 15, "generated password length");
info("repeat the search and ensure the same password was used");
LMP.doAutocompleteSearch(arg1, fakeBrowser);
ok(sendMessageStub.calledOnce, "sendAsyncMessage was called");
let msg2 = sendMessageStub.firstCall.args[1];
equal(msg2.requestId, arg1.requestId, "requestId matches");
equal(msg2.logins.length, 0, "no logins");
let result2 = LMP.doAutocompleteSearch(arg1);
equal(result2.logins.length, 0, "no logins");
equal(
msg2.generatedPassword,
msg1.generatedPassword,
result2.generatedPassword,
result1.generatedPassword,
"same generated password"
);
sendMessageStub.resetHistory();
info("Check cases where a password shouldn't be generated");
LMP.doAutocompleteSearch(
{ ...arg1, ...{ isPasswordField: false } },
fakeBrowser
);
ok(sendMessageStub.calledOnce, "sendAsyncMessage was called");
let msg = sendMessageStub.firstCall.args[1];
equal(msg.requestId, arg1.requestId, "requestId matches");
let result3 = LMP.doAutocompleteSearch({
...arg1,
...{ isPasswordField: false },
});
equal(
msg.generatedPassword,
result3.generatedPassword,
null,
"no generated password when not a pw. field"
);
sendMessageStub.resetHistory();
let arg1_2 = { ...arg1 };
arg1_2.autocompleteInfo.fieldName = "";
LMP.doAutocompleteSearch(arg1_2, fakeBrowser);
ok(sendMessageStub.calledOnce, "sendAsyncMessage was called");
msg = sendMessageStub.firstCall.args[1];
equal(msg.requestId, arg1.requestId, "requestId matches");
let result4 = LMP.doAutocompleteSearch(arg1_2);
equal(
msg.generatedPassword,
result4.generatedPassword,
null,
"no generated password when not autocomplete=new-password"
);
sendMessageStub.resetHistory();
LMP.doAutocompleteSearch(
{ ...arg1, ...{ browsingContextId: 999 } },
fakeBrowser
);
ok(sendMessageStub.calledOnce, "sendAsyncMessage was called");
msg = sendMessageStub.firstCall.args[1];
equal(msg.requestId, arg1.requestId, "requestId matches");
let result5 = LMP.doAutocompleteSearch({
...arg1,
...{ browsingContextId: 999 },
});
equal(
msg.generatedPassword,
result5.generatedPassword,
null,
"no generated password with a missing browsingContextId"
);
sendMessageStub.resetHistory();
});

View File

@ -15,6 +15,7 @@ add_task(async function test_getGeneratedPassword() {
Services.prefs.setBoolPref("signon.generation.enabled", true);
let LMP = new LoginManagerParent();
LMP.useBrowsingContext(99);
ok(LMP.getGeneratedPassword, "LMP.getGeneratedPassword exists");
equal(
@ -23,7 +24,7 @@ add_task(async function test_getGeneratedPassword() {
"Empty cache to start"
);
equal(LMP.getGeneratedPassword(99), null, "Null with no BrowsingContext");
equal(LMP.getGeneratedPassword(), null, "Null with no BrowsingContext");
ok(
LoginManagerParent._browsingContextGlobal,
@ -51,7 +52,7 @@ add_task(async function test_getGeneratedPassword() {
"Checking BrowsingContext.get(99) stub"
);
let password1 = LMP.getGeneratedPassword(99);
let password1 = LMP.getGeneratedPassword();
notEqual(password1, null, "Check password was returned");
equal(
password1.length,
@ -70,7 +71,7 @@ add_task(async function test_getGeneratedPassword() {
password1,
"Cache key and value"
);
let password2 = LMP.getGeneratedPassword(99);
let password2 = LMP.getGeneratedPassword();
equal(
password1,
password2,
@ -91,7 +92,7 @@ add_task(async function test_getGeneratedPassword() {
},
};
});
let password3 = LMP.getGeneratedPassword(99);
let password3 = LMP.getGeneratedPassword();
notEqual(
password2,
password3,
@ -106,19 +107,20 @@ add_task(async function test_getGeneratedPassword() {
info("Now checks cases where null should be returned");
Services.prefs.setBoolPref("signon.rememberSignons", false);
equal(LMP.getGeneratedPassword(99), null, "Prevented when pwmgr disabled");
equal(LMP.getGeneratedPassword(), null, "Prevented when pwmgr disabled");
Services.prefs.setBoolPref("signon.rememberSignons", true);
Services.prefs.setBoolPref("signon.generation.available", false);
equal(LMP.getGeneratedPassword(99), null, "Prevented when unavailable");
equal(LMP.getGeneratedPassword(), null, "Prevented when unavailable");
Services.prefs.setBoolPref("signon.generation.available", true);
Services.prefs.setBoolPref("signon.generation.enabled", false);
equal(LMP.getGeneratedPassword(99), null, "Prevented when disabled");
equal(LMP.getGeneratedPassword(), null, "Prevented when disabled");
Services.prefs.setBoolPref("signon.generation.enabled", true);
LMP.useBrowsingContext(123);
equal(
LMP.getGeneratedPassword(123),
LMP.getGeneratedPassword(),
null,
"Prevented when browsingContext is missing"
);

View File

@ -147,15 +147,13 @@ function checkEditTelemetryRecorded(expectedCount, msg) {
}
function startTestConditions(contextId) {
LMP.useBrowsingContext(contextId);
ok(
LMP._onGeneratedPasswordFilledOrEdited,
"LMP._onGeneratedPasswordFilledOrEdited exists"
);
equal(
LMP.getGeneratedPassword(contextId),
null,
"Null with no BrowsingContext"
);
equal(LMP.getGeneratedPassword(), null, "Null with no BrowsingContext");
equal(
LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().size,
0,
@ -528,7 +526,7 @@ add_task(
add_task(
async function test_onGeneratedPasswordFilledOrEdited_withSavedEmptyUsername() {
startTestConditions();
startTestConditions(99);
let login0Props = Object.assign({}, loginTemplate, {
username: "",
password: "qweqweq",
@ -714,7 +712,7 @@ add_task(
add_task(
async function test_onGeneratedPasswordFilledOrEdited_withEmptyUsernameDifferentFormActionOrigin() {
startTestConditions();
startTestConditions(99);
let login0Props = Object.assign({}, loginTemplate, {
username: "",
password: "qweqweq",
@ -772,7 +770,7 @@ add_task(
add_task(
async function test_onGeneratedPasswordFilledOrEdited_withSavedUsername() {
startTestConditions();
startTestConditions(99);
let login0Props = Object.assign({}, loginTemplate, {
username: "previoususer",
password: "qweqweq",

View File

@ -4,6 +4,9 @@ module.exports = {
"extends": [
"plugin:mozilla/mochitest-test"
],
"globals": {
"Assert": true,
},
"rules": {
// ownerGlobal doesn't exist in content privileged windows.
"mozilla/use-ownerGlobal": "off",

View File

@ -187,15 +187,23 @@ function checkPromptState(promptState, expectedState) {
}
}
function checkEchoedAuthInfo(expectedState, doc) {
// The server echos back the HTTP auth info it received.
let username = doc.getElementById("user").textContent;
let password = doc.getElementById("pass").textContent;
let authok = doc.getElementById("ok").textContent;
function checkEchoedAuthInfo(expectedState, browsingContext) {
return SpecialPowers.spawn(
browsingContext,
[expectedState.user, expectedState.pass],
(expectedUser, expectedPass) => {
let doc = this.content.document;
is(authok, "PASS", "Checking for successful authentication");
is(username, expectedState.user, "Checking for echoed username");
is(password, expectedState.pass, "Checking for echoed password");
// The server echos back the HTTP auth info it received.
let username = doc.getElementById("user").textContent;
let password = doc.getElementById("pass").textContent;
let authok = doc.getElementById("ok").textContent;
Assert.equal(authok, "PASS", "Checking for successful authentication");
Assert.equal(username, expectedUser, "Checking for echoed username");
Assert.equal(password, expectedPass, "Checking for echoed password");
}
);
}
/**

View File

@ -145,8 +145,8 @@ add_task(async function runTestAuth() {
iframe_prompt.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
await promptDone;
await iframe3Loaded;
checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
iframe_prompt.contentDocument);
await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
iframe_prompt);
// Cross-origin subresourse test.
@ -182,8 +182,8 @@ add_task(async function runTestAuth() {
iframe_prompt.src = "http://example.com/tests/toolkit/components/prompts/test/authenticate.sjs?user=mochiuser2&pass=mochipass2&realm=mochitest";
await promptDone;
await iframe3Loaded;
checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
SpecialPowers.wrap(iframe_prompt.contentWindow).document);
await checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
iframe_prompt);
});
function dispatchMouseEvent(target, type) {

View File

@ -275,6 +275,33 @@ let ACTORS = {
allFrames: true,
},
LoginManager: {
parent: {
moduleURI: "resource://gre/modules/LoginManagerParent.jsm",
messages: [
"PasswordManager:findLogins",
"PasswordManager:onFormSubmit",
"PasswordManager:onGeneratedPasswordFilledOrEdited",
"PasswordManager:insecureLoginFormPresent",
"PasswordManager:autoCompleteLogins",
"PasswordManager:removeLogin",
"PasswordManager:OpenPreferences",
"PasswordManager:formProcessed",
],
},
child: {
moduleURI: "resource://gre/modules/LoginManagerChild.jsm",
messages: [
"PasswordManager:fillForm",
"PasswordManager:fillGeneratedPassword",
"FormAutoComplete:PopupOpened",
"FormAutoComplete:PopupClosed",
],
},
allFrames: true,
},
Select: {
parent: {
moduleURI: "resource://gre/actors/SelectParent.jsm",