1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
9 FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
12 ChromeUtils.defineLazyGetter(
15 () => new Localization(["toolkit/formautofill/formAutofill.ftl"], true)
18 class ProfileAutoCompleteResult {
27 { resultCode = null, isSecure = true, isInputAutofilled = false }
30 this.QueryInterface = ChromeUtils.generateQI(["nsIAutoCompleteResult"]);
32 // The user's query string
33 this.searchString = searchString;
34 // The field name of the focused input.
35 this._focusedFieldName = focusedFieldDetail.fieldName;
36 // The content dom reference id of the focused input.
37 this._focusedElementId = focusedFieldDetail.elementId;
38 // The matching profiles contains the information for filling forms.
39 this._matchingProfiles = matchingProfiles;
40 // The default item that should be entered if none is selected
41 this.defaultIndex = 0;
42 // The reason the search failed
43 this.errorDescription = "";
44 // The value used to determine whether the form is secure or not.
45 this._isSecure = isSecure;
46 // The value to indicate whether the focused input has been autofilled or not.
47 this._isInputAutofilled = isInputAutofilled;
48 // All fillable field names in the form including the field name of the currently-focused input.
49 this._allFieldNames = [
50 ...this._matchingProfiles.reduce((fieldSet, curProfile) => {
51 for (let field of Object.keys(curProfile)) {
57 ].filter(field => allFieldNames.includes(field));
59 this._fillCategories = fillCategories;
61 // Force return success code if the focused field is auto-filled in order
62 // to show clear form button popup.
63 if (isInputAutofilled) {
64 resultCode = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
66 // The result code of this result object.
68 this.searchResult = resultCode;
70 this.searchResult = matchingProfiles.length
71 ? Ci.nsIAutoCompleteResult.RESULT_SUCCESS
72 : Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
75 // An array of primary and secondary labels for each profile.
76 this._popupLabels = this._generateLabels(
77 this._focusedFieldName,
79 this._matchingProfiles,
85 for (const group of [this._popupLabels, this.externalEntries]) {
86 if (index < group.length) {
89 index -= group.length;
92 throw Components.Exception(
93 "Index out of range.",
94 Cr.NS_ERROR_ILLEGAL_VALUE
99 * @returns {number} The number of results
102 return this._popupLabels.length + this.externalEntries.length;
106 * Get the secondary label based on the focused field name and related field names
109 * @param {string} _focusedFieldName The field name of the focused input
110 * @param {Array<object>} _allFieldNames The field names in the same section
111 * @param {object} _profile The profile providing the labels to show.
112 * @returns {string} The secondary label
114 _getSecondaryLabel(_focusedFieldName, _allFieldNames, _profile) {
126 * Get the value of the result at the given index.
128 * Always return empty string for form autofill feature to suppress
129 * AutoCompleteController from autofilling, as we'll populate the
132 * @param {number} index The index of the result requested
133 * @returns {string} The result at the specified index
141 const item = this.getAt(index);
142 return typeof item == "string" ? item : item.primary || item.label;
146 * Retrieves a comment (metadata instance)
148 * @param {number} index The index of the comment requested
149 * @returns {string} The comment at the specified index
151 getCommentAt(index) {
152 const item = this.getAt(index);
153 if (item.style == "status") {
154 return JSON.stringify(item);
159 focusElementId: this._focusedElementId,
163 const type = this.getTypeOfIndex(index);
166 data.fillMessageName = "FormAutofill:ClearForm";
169 data.fillMessageName = "FormAutofill:OpenPreferences";
172 data.noLearnMore = true;
179 data.fillMessageName = "FormAutofill:FillForm";
180 data.fillMessageData.profile = this._matchingProfiles[index];
185 return JSON.stringify({ ...item, ...data });
189 * Retrieves a style hint specific to a particular index.
191 * @param {number} index The index of the style hint requested
192 * @returns {string} The style hint at the specified index
195 const itemStyle = this.getAt(index).style;
200 switch (this.getTypeOfIndex(index)) {
206 return "insecureWarning";
213 * Retrieves an image url.
215 * @param {number} index The index of the image url requested
216 * @returns {string} The image url at the specified index
219 return this.getAt(index).image ?? "";
225 * @param {number} index The index of the result requested
226 * @returns {string} The result at the specified index
228 getFinalCompleteValueAt(index) {
229 return this.getValueAt(index);
233 * Returns true if the value at the given index is removable
235 * @param {number} _index The index of the result to remove
236 * @returns {boolean} True if the value is removable
238 isRemovableAt(_index) {
243 * Removes a result from the resultset
245 * @param {number} _index The index of the result to remove
247 removeValueAt(_index) {
248 // There is no plan to support removing profiles via autocomplete.
252 * Returns a type string that identifies te type of row at the given index.
254 * @param {number} index The index of the result requested
255 * @returns {string} The type at the specified index
257 getTypeOfIndex(index) {
258 if (this._isInputAutofilled && index == 0) {
262 if (index == this._popupLabels.length - 1) {
270 export class AddressResult extends ProfileAutoCompleteResult {
271 _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
272 // We group similar fields into the same field name so we won't pick another
273 // field in the same group as the secondary label.
274 const GROUP_FIELDS = {
275 name: ["name", "given-name", "additional-name", "family-name"],
282 "country-name": ["country", "country-name"],
294 const secondaryLabelOrder = [
295 "street-address", // Street address
297 "address-level3", // Townland / Neighborhood / Village
298 "address-level2", // City/Town
299 "organization", // Company or organization name
300 "address-level1", // Province/State (Standardized code if possible)
301 "country-name", // Country name
302 "postal-code", // Postal code
303 "tel", // Phone number
304 "email", // Email address
307 for (let field in GROUP_FIELDS) {
308 if (GROUP_FIELDS[field].includes(focusedFieldName)) {
309 focusedFieldName = field;
314 for (const currentFieldName of secondaryLabelOrder) {
315 if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
319 let matching = GROUP_FIELDS[currentFieldName]
320 ? allFieldNames.some(fieldName =>
321 GROUP_FIELDS[currentFieldName].includes(fieldName)
323 : allFieldNames.includes(currentFieldName);
327 currentFieldName == "street-address" &&
328 profile["-moz-street-address-one-line"]
330 return profile["-moz-street-address-one-line"];
332 return profile[currentFieldName];
336 return ""; // Nothing matched.
339 _generateLabels(focusedFieldName, allFieldNames, profiles, fillCategories) {
340 const manageLabel = lazy.l10n.formatValueSync(
341 "autofill-manage-addresses-label"
345 primary: manageLabel,
349 if (this._isInputAutofilled) {
350 const clearLabel = lazy.l10n.formatValueSync("autofill-clear-form-label");
357 labels.push(footerItem);
361 const focusedCategory =
362 lazy.FormAutofillUtils.getCategoryFromFieldName(focusedFieldName);
365 for (let idx = 0; idx < profiles.length; idx++) {
366 const profile = profiles[idx];
368 let primary = profile[focusedFieldName];
369 // Skip results without a primary label.
375 focusedFieldName == "street-address" &&
376 profile["-moz-street-address-one-line"]
378 primary = profile["-moz-street-address-one-line"];
381 const status = this.getStatusNote(fillCategories[idx], focusedCategory);
382 const secondary = this._getSecondaryLabel(
387 // Exclude empty chunks.
388 const ariaLabel = [primary, secondary, status]
389 .filter(chunk => !!chunk)
400 const allCategories =
401 lazy.FormAutofillUtils.getCategoriesFromFieldNames(allFieldNames);
403 if (allCategories?.length) {
407 status: this.getStatusNote(allCategories, focusedCategory),
410 labels.push(statusItem);
413 labels.push(footerItem);
418 getStatusNote(categories, focusedCategory) {
419 if (!categories || !categories.length) {
423 // If the length of categories is 1, that means all the fillable fields are in the same
424 // category. We will change the way to inform user according to this flag. When the value
425 // is true, we show "Also autofills ...", otherwise, show "Autofills ..." only.
426 let hasExtraCategories = categories.length > 1;
427 // Show the categories in certain order to conform with the spec.
428 let orderedCategoryList = [
435 let showCategories = hasExtraCategories
436 ? orderedCategoryList.filter(
438 categories.includes(category) && category != focusedCategory
440 : [orderedCategoryList.find(category => category == focusedCategory)];
442 let formatter = new Intl.ListFormat(undefined, {
446 let categoriesText = showCategories.map(category =>
447 lazy.l10n.formatValueSync("autofill-category-" + category)
449 categoriesText = formatter.format(categoriesText);
451 let statusTextTmplKey = hasExtraCategories
452 ? "autofill-phishing-warningmessage-extracategory"
453 : "autofill-phishing-warningmessage";
454 return lazy.l10n.formatValueSync(statusTextTmplKey, {
455 categories: categoriesText,
460 export class CreditCardResult extends ProfileAutoCompleteResult {
461 _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
462 const GROUP_FIELDS = {
466 "cc-additional-name",
469 "cc-exp": ["cc-exp", "cc-exp-month", "cc-exp-year"],
472 const secondaryLabelOrder = [
473 "cc-number", // Credit card number
474 "cc-name", // Full name
475 "cc-exp", // Expiration date
478 for (let field in GROUP_FIELDS) {
479 if (GROUP_FIELDS[field].includes(focusedFieldName)) {
480 focusedFieldName = field;
485 for (const currentFieldName of secondaryLabelOrder) {
486 if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
490 let matching = GROUP_FIELDS[currentFieldName]
491 ? allFieldNames.some(fieldName =>
492 GROUP_FIELDS[currentFieldName].includes(fieldName)
494 : allFieldNames.includes(currentFieldName);
497 if (currentFieldName == "cc-number") {
498 return lazy.CreditCard.formatMaskedNumber(profile[currentFieldName]);
500 return profile[currentFieldName];
504 return ""; // Nothing matched.
507 _generateLabels(focusedFieldName, allFieldNames, profiles, _fillCategories) {
508 if (!this._isSecure) {
510 lazy.FormAutofillUtils.brandBundle.GetStringFromName("brandShortName");
513 lazy.FormAutofillUtils.stringBundle.formatStringFromName(
514 "insecureFieldWarningDescription",
520 const manageLabel = lazy.l10n.formatValueSync(
521 "autofill-manage-payment-methods-label"
525 primary: manageLabel,
528 if (this._isInputAutofilled) {
529 const clearLabel = lazy.l10n.formatValueSync("autofill-clear-form-label");
536 labels.push(footerItem);
540 // Skip results without a primary label.
541 let labels = profiles
543 return !!profile[focusedFieldName];
546 let primary = profile[focusedFieldName];
548 if (focusedFieldName == "cc-number") {
549 primary = lazy.CreditCard.formatMaskedNumber(primary);
551 const secondary = this._getSecondaryLabel(
556 // The card type is displayed visually using an image. For a11y, we need
557 // to expose it as text. We do this using aria-label. However,
558 // aria-label overrides the text content, so we must include that also.
559 const ccType = profile["cc-type"];
560 const image = lazy.CreditCard.getCreditCardLogo(ccType);
561 const ccTypeL10nId = lazy.CreditCard.getNetworkL10nId(ccType);
562 const ccTypeName = ccTypeL10nId
563 ? lazy.l10n.formatValueSync(ccTypeL10nId)
564 : (ccType ?? ""); // Unknown card type
567 primary.toString().replaceAll("*", ""),
570 .filter(chunk => !!chunk) // Exclude empty chunks.
573 primary: primary.toString().replaceAll("*", "•"),
574 secondary: secondary.toString().replaceAll("*", "•"),
580 labels.push(footerItem);
585 getTypeOfIndex(index) {
586 if (!this._isSecure) {
590 return super.getTypeOfIndex(index);