Bug 1310049 - Refactor FormLikeFactory to its own module for use by Form Autofill. r=steveck

This introduces LoginFormFactory which wraps FormLikeFactory for use with login-specific contexts.

MozReview-Commit-ID: 6rPz5JOy3Yp

--HG--
rename : toolkit/components/passwordmgr/LoginManagerContent.jsm => toolkit/modules/FormLikeFactory.jsm
extra : rebase_source : b2a28803def0dce3de4a01db5bdbc3217c5d0f83
This commit is contained in:
Matthew Noorenberghe 2016-10-18 13:31:09 -07:00
Родитель d9a7400119
Коммит e911e68e21
10 изменённых файлов: 189 добавлений и 124 удалений

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

@ -24,7 +24,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
"resource:///modules/ContentLinkHandler.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
"resource://gre/modules/LoginManagerContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory",
"resource://gre/modules/LoginManagerContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
"resource://gre/modules/InsecurePasswordUtils.jsm");
@ -65,12 +65,12 @@ addMessageListener("RemoteLogins:fillForm", function(message) {
});
addEventListener("DOMFormHasPassword", function(event) {
LoginManagerContent.onDOMFormHasPassword(event, content);
let formLike = FormLikeFactory.createFromForm(event.target);
let formLike = LoginFormFactory.createFromForm(event.target);
InsecurePasswordUtils.checkForInsecurePasswords(formLike);
});
addEventListener("DOMInputPasswordAdded", function(event) {
LoginManagerContent.onDOMInputPasswordAdded(event, content);
let formLike = FormLikeFactory.createFromField(event.target);
let formLike = LoginFormFactory.createFromField(event.target);
InsecurePasswordUtils.checkForInsecurePasswords(formLike);
});
addEventListener("pageshow", function(event) {

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

@ -43,7 +43,7 @@ this.InsecurePasswordUtils = {
* or on insecure web pages. If insecure password fields are present,
* a log message is sent to the web console to warn developers.
*
* @param {FormLike} aForm A form-like object. @See {FormLikeFactory}
* @param {FormLike} aForm A form-like object. @See {LoginFormFactory}
*/
checkForInsecurePasswords(aForm) {
if (this._formRootsWarned.has(aForm.rootElement) ||

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

@ -5,7 +5,7 @@
"use strict";
this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
"FormLikeFactory",
"LoginFormFactory",
"UserAutoCompleteResult" ];
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
@ -17,6 +17,8 @@ Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
"resource://gre/modules/FormLikeFactory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
"resource://gre/modules/LoginRecipes.jsm");
@ -49,7 +51,7 @@ var observer = {
// can grab form data before it might be modified (see bug 257781).
try {
let formLike = FormLikeFactory.createFromForm(formElement);
let formLike = LoginFormFactory.createFromForm(formElement);
LoginManagerContent._onFormSubmit(formLike);
} catch (e) {
log("Caught error in onFormSubmit(", e.lineNumber, "):", e.message);
@ -144,7 +146,7 @@ var LoginManagerContent = {
* WeakMap of the root element of a FormLike to the FormLike representing its fields.
*
* This is used to be able to lookup an existing FormLike for a given root element since multiple
* calls to FormLikeFactory won't give the exact same object. When batching fills we don't always
* calls to LoginFormFactory won't give the exact same object. When batching fills we don't always
* want to use the most recent list of elements for a FormLike since we may end up doing multiple
* fills for the same set of elements when a field gets added between arming and running the
* DeferredTask.
@ -284,7 +286,7 @@ var LoginManagerContent = {
_autoCompleteSearchAsync(aSearchString, aPreviousResult,
aElement, aRect) {
let doc = aElement.ownerDocument;
let form = FormLikeFactory.createFromField(aElement);
let form = LoginFormFactory.createFromField(aElement);
let win = doc.defaultView;
let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
@ -338,7 +340,7 @@ var LoginManagerContent = {
}
let form = event.target;
let formLike = FormLikeFactory.createFromForm(form);
let formLike = LoginFormFactory.createFromForm(form);
log("onDOMFormHasPassword:", form, formLike);
this._fetchLoginsFromParentAndFillForm(formLike, window);
},
@ -358,7 +360,7 @@ var LoginManagerContent = {
// Capture within a <form> but without a submit event is bug 1287202.
this.setupProgressListener(window);
let formLike = FormLikeFactory.createFromField(pwField);
let formLike = LoginFormFactory.createFromField(pwField);
log("onDOMInputPasswordAdded:", pwField, formLike);
let deferredTask = this._deferredPasswordAddedTasksByRootElement.get(formLike.rootElement);
@ -508,7 +510,7 @@ var LoginManagerContent = {
inputElement,
};
let form = FormLikeFactory.createFromField(inputElement);
let form = LoginFormFactory.createFromField(inputElement);
if (inputElement.type == "password") {
clobberUsername = false;
}
@ -543,7 +545,7 @@ var LoginManagerContent = {
if (!LoginHelper.isUsernameFieldType(acInputField))
return;
var acForm = FormLikeFactory.createFromField(acInputField);
var acForm = LoginFormFactory.createFromField(acInputField);
if (!acForm)
return;
@ -649,7 +651,7 @@ var LoginManagerContent = {
);
if (pwOverrideField) {
// The field from the password override may be in a different FormLike.
let formLike = FormLikeFactory.createFromField(pwOverrideField);
let formLike = LoginFormFactory.createFromField(pwOverrideField);
pwFields = [{
index : [...formLike.elements].indexOf(pwOverrideField),
element : pwOverrideField,
@ -1134,7 +1136,7 @@ var LoginManagerContent = {
!aField.ownerDocument) {
return null;
}
let form = FormLikeFactory.createFromField(aField);
let form = LoginFormFactory.createFromField(aField);
let doc = aField.ownerDocument;
let messageManager = messageManagerFromWindow(doc.defaultView);
@ -1299,35 +1301,17 @@ UserAutoCompleteResult.prototype = {
* A factory to generate FormLike objects that represent a set of login fields
* which aren't necessarily marked up with a <form> element.
*/
var FormLikeFactory = {
_propsFromForm: [
"autocomplete",
"ownerDocument",
],
var LoginFormFactory = {
/**
* Create a FormLike object from a <form>.
* Create a LoginForm object from a <form>.
*
* @param {HTMLFormElement} aForm
* @return {FormLike}
* @return {LoginForm}
* @throws Error if aForm isn't an HTMLFormElement
*/
createFromForm(aForm) {
if (!(aForm instanceof Ci.nsIDOMHTMLFormElement)) {
throw new Error("createFromForm: aForm must be a nsIDOMHTMLFormElement");
}
let formLike = {
action: LoginUtils._getActionOrigin(aForm),
elements: [...aForm.elements],
rootElement: aForm,
};
for (let prop of this._propsFromForm) {
formLike[prop] = aForm[prop];
}
this._addToJSONProperty(formLike);
let formLike = FormLikeFactory.createFromForm(aForm);
formLike.action = LoginUtils._getActionOrigin(aForm);
let state = LoginManagerContent.stateForDocument(formLike.ownerDocument);
state.loginFormRootElements.add(formLike.rootElement);
@ -1338,19 +1322,19 @@ var FormLikeFactory = {
},
/**
* Create a FormLike object from a password or username field.
* Create a LoginForm object from a password or username field.
*
* If the field is in a <form>, construct the FormLike from the form.
* Otherwise, create a FormLike with a rootElement (wrapper) according to
* heuristics. Currently all <input> not in a <form> are one FormLike but this
* If the field is in a <form>, construct the LoginForm from the form.
* Otherwise, create a LoginForm with a rootElement (wrapper) according to
* heuristics. Currently all <input> not in a <form> are one LoginForm but this
* shouldn't be relied upon as the heuristics may change to detect multiple
* "forms" (e.g. registration and login) on one page with a <form>.
*
* Note that two FormLikes created from the same field won't return the same FormLike object.
* Use the `rootElement` property on the FormLike as a key instead.
* Note that two LoginForms created from the same field won't return the same LoginForm object.
* Use the `rootElement` property on the LoginForm as a key instead.
*
* @param {HTMLInputElement} aField - a password or username field in a document
* @return {FormLike}
* @return {LoginForm}
* @throws Error if aField isn't a password or username field in a document
*/
createFromField(aField) {
@ -1364,23 +1348,9 @@ var FormLikeFactory = {
return this.createFromForm(aField.form);
}
let doc = aField.ownerDocument;
log("Created non-form FormLike for rootElement:", doc.documentElement);
let elements = [];
for (let el of doc.documentElement.querySelectorAll("input")) {
if (!el.form) {
elements.push(el);
}
}
let formLike = {
action: LoginUtils._getPasswordOrigin(doc.baseURI),
autocomplete: "on",
// Exclude elements inside the rootElement that are already in a <form> as
// they will be handled by their own FormLike.
elements,
ownerDocument: doc,
rootElement: doc.documentElement,
};
let formLike = FormLikeFactory.createFromField(aField);
formLike.action = LoginUtils._getPasswordOrigin(aField.ownerDocument.baseURI);
log("Created non-form FormLike for rootElement:", aField.ownerDocument.documentElement);
let state = LoginManagerContent.stateForDocument(formLike.ownerDocument);
state.loginFormRootElements.add(formLike.rootElement);
@ -1389,59 +1359,6 @@ var FormLikeFactory = {
LoginManagerContent._formLikeByRootElement.set(formLike.rootElement, formLike);
this._addToJSONProperty(formLike);
return formLike;
},
/**
* Add a `toJSON` property to a FormLike so logging which ends up going
* through dump doesn't include usless garbage from DOM objects.
*/
_addToJSONProperty(aFormLike) {
function prettyElementOutput(aElement) {
let idText = aElement.id ? "#" + aElement.id : "";
let classText = "";
for (let className of aElement.classList) {
classText += "." + className;
}
return `<${aElement.nodeName + idText + classText}>`;
}
Object.defineProperty(aFormLike, "toJSON", {
value: () => {
let cleansed = {};
for (let key of Object.keys(aFormLike)) {
let value = aFormLike[key];
let cleansedValue = value;
switch (key) {
case "elements": {
cleansedValue = [];
for (let element of value) {
cleansedValue.push(prettyElementOutput(element));
}
break;
}
case "ownerDocument": {
cleansedValue = {
location: {
href: value.location.href,
},
};
break;
}
case "rootElement": {
cleansedValue = prettyElementOutput(value);
break;
}
}
cleansed[key] = cleansedValue;
}
return cleansed;
}
});
},
};

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

@ -11,7 +11,7 @@
<body>
<script type="application/javascript;version=1.8">
const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
@ -143,7 +143,7 @@ add_task(function* test() {
frameDoc.documentElement.innerHTML = tc.document;
let inputForFormLike = frameDoc.querySelectorAll("input")[tc.inputIndexForFormLike];
let formLike = FormLikeFactory.createFromField(inputForFormLike);
let formLike = LoginFormFactory.createFromField(inputForFormLike);
info("Calling _onFormSubmit with FormLike");
let processedPromise = getSubmitMessage();

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

@ -11,7 +11,7 @@
<body>
<script type="application/javascript;version=1.8">
const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));

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

@ -11,7 +11,7 @@
<body>
<script type="application/javascript;version=1.8">
const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive");

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

@ -9,7 +9,7 @@
Cu.importGlobalProperties(["URL"]);
const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
const TESTCASES = [
{
description: "1 password field outside of a <form>",
@ -123,7 +123,7 @@ for (let tc of TESTCASES) {
let input = document.querySelector("input");
MockDocument.mockOwnerDocumentProperty(input, document, "http://localhost:8080/test/");
let formLike = FormLikeFactory.createFromField(input);
let formLike = LoginFormFactory.createFromField(input);
let actual = LoginManagerContent._getFormFields(formLike,
testcase.skipEmptyFields,

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

@ -1,11 +1,11 @@
/*
* Test for LoginManagerContent._getPasswordFields using FormLikeFactory.
* Test for LoginManagerContent._getPasswordFields using LoginFormFactory.
*/
"use strict";
const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
const TESTCASES = [
{
description: "Empty document",
@ -97,7 +97,7 @@ for (let tc of TESTCASES) {
let mapRootElementToFormLike = new Map();
for (let input of document.querySelectorAll("input")) {
let formLike = FormLikeFactory.createFromField(input);
let formLike = LoginFormFactory.createFromField(input);
let existingFormLike = mapRootElementToFormLike.get(formLike.rootElement);
if (!existingFormLike) {
mapRootElementToFormLike.set(formLike.rootElement, formLike);
@ -119,7 +119,7 @@ for (let tc of TESTCASES) {
testcase.skipEmptyFields);
if (formLikeFromInput.rootElement instanceof Ci.nsIDOMHTMLFormElement) {
let formLikeFromForm = FormLikeFactory.createFromForm(formLikeFromInput.rootElement);
let formLikeFromForm = LoginFormFactory.createFromForm(formLikeFromInput.rootElement);
do_print("Checking that the FormLike created for the <form> matches" +
" the one from a password field");
formLikeEqual(formLikeFromInput, formLikeFromForm);

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

@ -0,0 +1,147 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["FormLikeFactory"];
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
/**
* A factory to generate FormLike objects that represent a set of related fields
* which aren't necessarily marked up with a <form> element. FormLike's emulate
* the properties of an HTMLFormElement which are relevant to form tasks.
*/
let FormLikeFactory = {
_propsFromForm: [
"action",
"autocomplete",
"ownerDocument",
],
/**
* Create a FormLike object from a <form>.
*
* @param {HTMLFormElement} aForm
* @return {FormLike}
* @throws Error if aForm isn't an HTMLFormElement
*/
createFromForm(aForm) {
if (!(aForm instanceof Ci.nsIDOMHTMLFormElement)) {
throw new Error("createFromForm: aForm must be a nsIDOMHTMLFormElement");
}
let formLike = {
elements: [...aForm.elements],
rootElement: aForm,
};
for (let prop of this._propsFromForm) {
formLike[prop] = aForm[prop];
}
this._addToJSONProperty(formLike);
return formLike;
},
/**
* Create a FormLike object from an <input> in a document.
*
* If the field is in a <form>, construct the FormLike from the form.
* Otherwise, create a FormLike with a rootElement (wrapper) according to
* heuristics. Currently all <input> not in a <form> are one FormLike but this
* shouldn't be relied upon as the heuristics may change to detect multiple
* "forms" (e.g. registration and login) on one page with a <form>.
*
* Note that two FormLikes created from the same field won't return the same FormLike object.
* Use the `rootElement` property on the FormLike as a key instead.
*
* @param {HTMLInputElement} aField - a field in a document
* @return {FormLike}
* @throws Error if aField isn't a password or username field in a document
*/
createFromField(aField) {
if (!(aField instanceof Ci.nsIDOMHTMLInputElement) ||
!aField.ownerDocument) {
throw new Error("createFromField requires a field in a document");
}
if (aField.form) {
return this.createFromForm(aField.form);
}
let doc = aField.ownerDocument;
let elements = [];
for (let el of doc.documentElement.querySelectorAll("input")) {
if (!el.form) {
elements.push(el);
}
}
let formLike = {
action: doc.baseURI,
autocomplete: "on",
// Exclude elements inside the rootElement that are already in a <form> as
// they will be handled by their own FormLike.
elements,
ownerDocument: doc,
rootElement: doc.documentElement,
};
this._addToJSONProperty(formLike);
return formLike;
},
/**
* Add a `toJSON` property to a FormLike so logging which ends up going
* through dump doesn't include usless garbage from DOM objects.
*/
_addToJSONProperty(aFormLike) {
function prettyElementOutput(aElement) {
let idText = aElement.id ? "#" + aElement.id : "";
let classText = "";
for (let className of aElement.classList) {
classText += "." + className;
}
return `<${aElement.nodeName + idText + classText}>`;
}
Object.defineProperty(aFormLike, "toJSON", {
value: () => {
let cleansed = {};
for (let key of Object.keys(aFormLike)) {
let value = aFormLike[key];
let cleansedValue = value;
switch (key) {
case "elements": {
cleansedValue = [];
for (let element of value) {
cleansedValue.push(prettyElementOutput(element));
}
break;
}
case "ownerDocument": {
cleansedValue = {
location: {
href: value.location.href,
},
};
break;
}
case "rootElement": {
cleansedValue = prettyElementOutput(value);
break;
}
}
cleansed[key] = cleansedValue;
}
return cleansed;
}
});
},
};

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

@ -43,6 +43,7 @@ EXTRA_JS_MODULES += [
'Finder.jsm',
'FinderHighlighter.jsm',
'FinderIterator.jsm',
'FormLikeFactory.jsm',
'Geometry.jsm',
'GMPInstallManager.jsm',
'GMPUtils.jsm',