gecko-dev/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm

451 строка
13 KiB
JavaScript

/* 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";
var EXPORTED_SYMBOLS = ["AddressResult", "CreditCardResult"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { FormAutofill } = ChromeUtils.import(
"resource://formautofill/FormAutofill.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FormAutofillUtils",
"resource://formautofill/FormAutofillUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"CreditCard",
"resource://gre/modules/CreditCard.jsm"
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"insecureWarningEnabled",
"security.insecure_field_warning.contextual.enabled"
);
this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
class ProfileAutoCompleteResult {
constructor(
searchString,
focusedFieldName,
allFieldNames,
matchingProfiles,
{ resultCode = null, isSecure = true, isInputAutofilled = false }
) {
log.debug("Constructing new ProfileAutoCompleteResult:", [...arguments]);
// nsISupports
this.QueryInterface = ChromeUtils.generateQI([Ci.nsIAutoCompleteResult]);
// The user's query string
this.searchString = searchString;
// The field name of the focused input.
this._focusedFieldName = focusedFieldName;
// The matching profiles contains the information for filling forms.
this._matchingProfiles = matchingProfiles;
// The default item that should be entered if none is selected
this.defaultIndex = 0;
// The reason the search failed
this.errorDescription = "";
// The value used to determine whether the form is secure or not.
this._isSecure = isSecure;
// The value to indicate whether the focused input has been autofilled or not.
this._isInputAutofilled = isInputAutofilled;
// All fillable field names in the form including the field name of the currently-focused input.
this._allFieldNames = [
...this._matchingProfiles.reduce((fieldSet, curProfile) => {
for (let field of Object.keys(curProfile)) {
fieldSet.add(field);
}
return fieldSet;
}, new Set()),
].filter(field => allFieldNames.includes(field));
// Force return success code if the focused field is auto-filled in order
// to show clear form button popup.
if (isInputAutofilled) {
resultCode = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
}
// The result code of this result object.
if (resultCode) {
this.searchResult = resultCode;
} else if (matchingProfiles.length > 0) {
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
} else {
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
}
// An array of primary and secondary labels for each profile.
this._popupLabels = this._generateLabels(
this._focusedFieldName,
this._allFieldNames,
this._matchingProfiles
);
}
/**
* @returns {number} The number of results
*/
get matchCount() {
return this._popupLabels.length;
}
_checkIndexBounds(index) {
if (index < 0 || index >= this._popupLabels.length) {
throw Components.Exception(
"Index out of range.",
Cr.NS_ERROR_ILLEGAL_VALUE
);
}
}
/**
* Get the secondary label based on the focused field name and related field names
* in the same form.
* @param {string} focusedFieldName The field name of the focused input
* @param {Array<Object>} allFieldNames The field names in the same section
* @param {object} profile The profile providing the labels to show.
* @returns {string} The secondary label
*/
_getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
return "";
}
_generateLabels(focusedFieldName, allFieldNames, profiles) {}
/**
* Get the value of the result at the given index.
*
* Always return empty string for form autofill feature to suppress
* AutoCompleteController from autofilling, as we'll populate the
* fields on our own.
*
* @param {number} index The index of the result requested
* @returns {string} The result at the specified index
*/
getValueAt(index) {
this._checkIndexBounds(index);
return "";
}
getLabelAt(index) {
this._checkIndexBounds(index);
let label = this._popupLabels[index];
if (typeof label == "string") {
return label;
}
return JSON.stringify(label);
}
/**
* Retrieves a comment (metadata instance)
* @param {number} index The index of the comment requested
* @returns {string} The comment at the specified index
*/
getCommentAt(index) {
this._checkIndexBounds(index);
return JSON.stringify(this._matchingProfiles[index]);
}
/**
* Retrieves a style hint specific to a particular index.
* @param {number} index The index of the style hint requested
* @returns {string} The style hint at the specified index
*/
getStyleAt(index) {
this._checkIndexBounds(index);
if (index == this.matchCount - 1) {
return "autofill-footer";
}
if (this._isInputAutofilled) {
return "autofill-clear-button";
}
return "autofill-profile";
}
/**
* Retrieves an image url.
* @param {number} index The index of the image url requested
* @returns {string} The image url at the specified index
*/
getImageAt(index) {
this._checkIndexBounds(index);
return "";
}
/**
* Retrieves a result
* @param {number} index The index of the result requested
* @returns {string} The result at the specified index
*/
getFinalCompleteValueAt(index) {
return this.getValueAt(index);
}
/**
* Removes a result from the resultset
* @param {number} index The index of the result to remove
* @param {boolean} removeFromDatabase TRUE for removing data from DataBase
* as well.
*/
removeValueAt(index, removeFromDatabase) {
// There is no plan to support removing profiles via autocomplete.
}
}
class AddressResult extends ProfileAutoCompleteResult {
constructor(...args) {
super(...args);
}
_getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
// We group similar fields into the same field name so we won't pick another
// field in the same group as the secondary label.
const GROUP_FIELDS = {
name: ["name", "given-name", "additional-name", "family-name"],
"street-address": [
"street-address",
"address-line1",
"address-line2",
"address-line3",
],
"country-name": ["country", "country-name"],
tel: [
"tel",
"tel-country-code",
"tel-national",
"tel-area-code",
"tel-local",
"tel-local-prefix",
"tel-local-suffix",
],
};
const secondaryLabelOrder = [
"street-address", // Street address
"name", // Full name
"address-level3", // Townland / Neighborhood / Village
"address-level2", // City/Town
"organization", // Company or organization name
"address-level1", // Province/State (Standardized code if possible)
"country-name", // Country name
"postal-code", // Postal code
"tel", // Phone number
"email", // Email address
];
for (let field in GROUP_FIELDS) {
if (GROUP_FIELDS[field].includes(focusedFieldName)) {
focusedFieldName = field;
break;
}
}
for (const currentFieldName of secondaryLabelOrder) {
if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
continue;
}
let matching = GROUP_FIELDS[currentFieldName]
? allFieldNames.some(fieldName =>
GROUP_FIELDS[currentFieldName].includes(fieldName)
)
: allFieldNames.includes(currentFieldName);
if (matching) {
if (
currentFieldName == "street-address" &&
profile["-moz-street-address-one-line"]
) {
return profile["-moz-street-address-one-line"];
}
return profile[currentFieldName];
}
}
return ""; // Nothing matched.
}
_generateLabels(focusedFieldName, allFieldNames, profiles) {
if (this._isInputAutofilled) {
return [
{ primary: "", secondary: "" }, // Clear button
{ primary: "", secondary: "" }, // Footer
];
}
// Skip results without a primary label.
let labels = profiles
.filter(profile => {
return !!profile[focusedFieldName];
})
.map(profile => {
let primaryLabel = profile[focusedFieldName];
if (
focusedFieldName == "street-address" &&
profile["-moz-street-address-one-line"]
) {
primaryLabel = profile["-moz-street-address-one-line"];
}
return {
primary: primaryLabel,
secondary: this._getSecondaryLabel(
focusedFieldName,
allFieldNames,
profile
),
};
});
// Add an empty result entry for footer. Its content will come from
// the footer binding, so don't assign any value to it.
// The additional properties: categories and focusedCategory are required of
// the popup to generate autofill hint on the footer.
labels.push({
primary: "",
secondary: "",
categories: FormAutofillUtils.getCategoriesFromFieldNames(
this._allFieldNames
),
focusedCategory: FormAutofillUtils.getCategoryFromFieldName(
this._focusedFieldName
),
});
return labels;
}
}
class CreditCardResult extends ProfileAutoCompleteResult {
constructor(...args) {
super(...args);
}
_getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
const GROUP_FIELDS = {
"cc-name": [
"cc-name",
"cc-given-name",
"cc-additional-name",
"cc-family-name",
],
"cc-exp": ["cc-exp", "cc-exp-month", "cc-exp-year"],
};
const secondaryLabelOrder = [
"cc-number", // Credit card number
"cc-name", // Full name
"cc-exp", // Expiration date
];
for (let field in GROUP_FIELDS) {
if (GROUP_FIELDS[field].includes(focusedFieldName)) {
focusedFieldName = field;
break;
}
}
for (const currentFieldName of secondaryLabelOrder) {
if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
continue;
}
let matching = GROUP_FIELDS[currentFieldName]
? allFieldNames.some(fieldName =>
GROUP_FIELDS[currentFieldName].includes(fieldName)
)
: allFieldNames.includes(currentFieldName);
if (matching) {
if (currentFieldName == "cc-number") {
let { affix, label } = CreditCard.formatMaskedNumber(
profile[currentFieldName]
);
return affix + label;
}
return profile[currentFieldName];
}
}
return ""; // Nothing matched.
}
_generateLabels(focusedFieldName, allFieldNames, profiles) {
if (!this._isSecure) {
if (!insecureWarningEnabled) {
return [];
}
let brandName = FormAutofillUtils.brandBundle.GetStringFromName(
"brandShortName"
);
return [
FormAutofillUtils.stringBundle.formatStringFromName(
"insecureFieldWarningDescription",
[brandName]
),
];
}
if (this._isInputAutofilled) {
return [
{ primary: "", secondary: "" }, // Clear button
{ primary: "", secondary: "" }, // Footer
];
}
// Skip results without a primary label.
let labels = profiles
.filter(profile => {
return !!profile[focusedFieldName];
})
.map(profile => {
let primaryAffix;
let primary = profile[focusedFieldName];
if (focusedFieldName == "cc-number") {
let { affix, label } = CreditCard.formatMaskedNumber(primary);
primaryAffix = affix;
primary = label;
}
return {
primaryAffix,
primary,
secondary: this._getSecondaryLabel(
focusedFieldName,
allFieldNames,
profile
),
};
});
// Add an empty result entry for footer.
labels.push({ primary: "", secondary: "" });
return labels;
}
getStyleAt(index) {
this._checkIndexBounds(index);
if (!this._isSecure && insecureWarningEnabled) {
return "autofill-insecureWarning";
}
return super.getStyleAt(index);
}
getImageAt(index) {
this._checkIndexBounds(index);
return "chrome://formautofill/content/icon-credit-card-generic.svg";
}
}