diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index 05bfbf378a8..e12575c75ac 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -34,13 +34,22 @@ export interface AutoFillOptions { inlineMenuFillType?: InlineMenuFillType; } -export interface FormData { +interface FormDataBase { form: AutofillForm; password: AutofillField; - username: AutofillField; passwords: AutofillField[]; } +export interface FormDataWithUsername extends FormDataBase { + username: AutofillField; +} + +export interface FormDataPasswordOnly extends FormDataBase { + username: null; +} + +export type FormData = FormDataWithUsername | FormDataPasswordOnly; + export interface GenerateFillScriptOptions { skipUsernameOnlyFill: boolean; onlyEmptyFields: boolean; diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 13e97766594..a8ed5975509 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -189,7 +189,7 @@ describe("AutofillService", () => { sender: AutofillMessageSender.collectPageDetailsFromTabObservable, tab, }, - null, + undefined, true, ); }); @@ -631,6 +631,25 @@ describe("AutofillService", () => { }, ]); }); + + it("includes forms with password but no detectable username field with username: null", () => { + const passwordInputField = createAutofillFieldMock({ + opid: "password-field", + type: "password", + form: "validFormId", + elementNumber: 1, + }); + pageDetailsMock.fields = [passwordInputField]; + jest.spyOn(AutofillService, "loadPasswordFields").mockReturnValueOnce([passwordInputField]); + + const formData = autofillService.getFormsWithPasswordFields(pageDetailsMock); + + expect(formData).toHaveLength(1); + expect(formData[0].form).toBe(pageDetailsMock.forms.validFormId); + expect(formData[0].password).toBe(passwordInputField); + expect(formData[0].passwords).toEqual([passwordInputField]); + expect(formData[0].username).toBeNull(); + }); }); describe("doAutoFill", () => { diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 010f5ea0f27..b7d57525663 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { filter, firstValueFrom, @@ -75,7 +73,7 @@ import { export default class AutofillService implements AutofillServiceInterface { private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout; - private openPasswordRepromptPopoutDebounce: number | NodeJS.Timeout; + private openPasswordRepromptPopoutDebounce?: ReturnType; private currentlyOpeningPasswordRepromptPopout = false; private autofillScriptPortsSet = new Set(); static searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); @@ -106,7 +104,7 @@ export default class AutofillService implements AutofillServiceInterface { */ collectPageDetailsFromTab$(tab: chrome.tabs.Tab): Observable { /** Replay Subject that can be utilized when `messages$` may not emit the page details. */ - const pageDetailsFallback$ = new ReplaySubject<[]>(1); + const pageDetailsFallback$ = new ReplaySubject(1); const pageDetailsFromTab$ = this.messageListener .messages$(COLLECT_PAGE_DETAILS_RESPONSE_COMMAND) @@ -117,14 +115,17 @@ export default class AutofillService implements AutofillServiceInterface { message.sender === AutofillMessageSender.collectPageDetailsFromTabObservable, ), scan( - (acc, message) => [ - ...acc, - { - frameId: message.webExtSender.frameId, - tab: message.tab, - details: message.details, - }, - ], + (acc: PageDetail[], message): PageDetail[] => + message.webExtSender?.frameId === undefined + ? acc + : [ + ...acc, + { + frameId: message.webExtSender.frameId, + tab: message.tab, + details: message.details, + }, + ], [] as PageDetail[], ), ); @@ -136,7 +137,7 @@ export default class AutofillService implements AutofillServiceInterface { command: AutofillMessageCommand.collectPageDetails, sender: AutofillMessageSender.collectPageDetailsFromTabObservable, }, - null, + undefined, true, ).catch(() => { // When `tabSendMessage` throws an error the `pageDetailsFromTab$` will not emit, @@ -152,7 +153,7 @@ export default class AutofillService implements AutofillServiceInterface { "moz-extension://", "chrome-extension://", "safari-web-extension://", - ].some((protocol) => tab.url.startsWith(protocol)); + ].some((protocol) => tab.url?.startsWith(protocol)); if (!tab.url || urlHasBrowserProtocol) { pageDetailsFallback$.next([]); } @@ -162,7 +163,7 @@ export default class AutofillService implements AutofillServiceInterface { // Create a timeout observable that emits an empty array if pageDetailsFromTab$ hasn't emitted within 1 second. const pageDetailsTimeout$ = timer(1000).pipe( - map((): any => []), + map(() => [] as PageDetail[]), takeUntil(sharedPageDetailsFromTab$), ); @@ -230,6 +231,10 @@ export default class AutofillService implements AutofillServiceInterface { frameId = 0, triggeringOnPageLoad = true, ): Promise { + const tabId = tab.id; + if (tabId === undefined) { + return; + } // Autofill user settings loaded from state can await the active account state indefinitely // if not guarded by an active account check (e.g. the user is logged in) const activeAccount = await firstValueFrom(this.accountService.activeAccount$); @@ -249,7 +254,7 @@ export default class AutofillService implements AutofillServiceInterface { if (!triggeringOnPageLoad) { await this.scriptInjectorService.inject({ - tabId: tab.id, + tabId, injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" }, }); } @@ -258,7 +263,7 @@ export default class AutofillService implements AutofillServiceInterface { for (const injectedScript of injectedScripts) { await this.scriptInjectorService.inject({ - tabId: tab.id, + tabId, injectDetails: { file: `content/${injectedScript}`, runAt: "document_start", @@ -276,7 +281,7 @@ export default class AutofillService implements AutofillServiceInterface { * @param activeAccount - The active account */ private async getBootstrapAutofillContentScript( - activeAccount: { id: UserId | undefined } & AccountInfo, + activeAccount: ({ id: UserId | undefined } & AccountInfo) | null | undefined, ): Promise { let inlineMenuVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; @@ -367,7 +372,7 @@ export default class AutofillService implements AutofillServiceInterface { formData.push({ form: pageDetails.forms[formKey], password: formPasswordFields[0], - username: uf, + username: uf ?? null, passwords: formPasswordFields, }); } @@ -411,20 +416,24 @@ export default class AutofillService implements AutofillServiceInterface { */ async doAutoFill(options: AutoFillOptions): Promise { const tab = options.tab; - if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) { + const tabUrl = tab?.url; + if (!tabUrl || !options.cipher || !options.pageDetails || !options.pageDetails.length) { throw new Error("Nothing to autofill."); } let totp: string | null = null; const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - const canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id), - ); + let canAccessPremium = false; + if (activeAccount?.id) { + canAccessPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id), + ); + } const defaultUriMatch = await this.getDefaultUriMatchStrategy(); if (!canAccessPremium) { - options.cipher.login.totp = null; + options.cipher.login.totp = undefined; } let didAutofill = false; @@ -450,7 +459,7 @@ export default class AutofillService implements AutofillServiceInterface { allowTotpAutofill: options.allowTotpAutofill || false, autoSubmitLogin: options.autoSubmitLogin || false, cipher: options.cipher, - tabUrl: tab.url, + tabUrl, defaultUriMatch: defaultUriMatch, focusedFieldOpid: options.focusedFieldOpid, inlineMenuFillType: options.inlineMenuFillType, @@ -473,7 +482,7 @@ export default class AutofillService implements AutofillServiceInterface { fillScript.properties.delay_between_operations = 20; didAutofill = true; - if (!options.skipLastUsed) { + if (!options.skipLastUsed && activeAccount?.id) { await this.cipherService.updateLastUsedDate(options.cipher.id, activeAccount.id); } @@ -544,21 +553,27 @@ export default class AutofillService implements AutofillServiceInterface { return null; } + if (!tab.url) { + return null; + } + const tabUrl = tab.url; if (fromCommand) { - cipher = await this.cipherService.getNextCipherForUrl(tab.url, activeUserId); + cipher = await this.cipherService.getNextCipherForUrl(tabUrl, activeUserId); } else { const lastLaunchedCipher = await this.cipherService.getLastLaunchedForUrl( - tab.url, + tabUrl, activeUserId, true, ); + const lastLaunched = lastLaunchedCipher?.localData?.lastLaunched; if ( lastLaunchedCipher && - Date.now().valueOf() - lastLaunchedCipher.localData?.lastLaunched?.valueOf() < 30000 + lastLaunched && + Date.now().valueOf() - lastLaunched.valueOf() < 30000 ) { cipher = lastLaunchedCipher; } else { - cipher = await this.cipherService.getLastUsedForUrl(tab.url, activeUserId, true); + cipher = await this.cipherService.getLastUsedForUrl(tabUrl, activeUserId, true); } } @@ -568,7 +583,7 @@ export default class AutofillService implements AutofillServiceInterface { if (await this.isPasswordRepromptRequired(cipher, tab)) { if (fromCommand) { - this.cipherService.updateLastUsedIndexForUrl(tab.url); + this.cipherService.updateLastUsedIndexForUrl(tabUrl); } return null; @@ -589,7 +604,7 @@ export default class AutofillService implements AutofillServiceInterface { // Update last used index as autofill has succeeded if (fromCommand) { - this.cipherService.updateLastUsedIndexForUrl(tab.url); + this.cipherService.updateLastUsedIndexForUrl(tabUrl); } return totpCode; @@ -740,7 +755,7 @@ export default class AutofillService implements AutofillServiceInterface { return null; } - let fillScript = new AutofillScript(); + const fillScript = new AutofillScript(); const filledFields: { [id: string]: AutofillField } = {}; const fields = options.cipher.fields; @@ -748,14 +763,17 @@ export default class AutofillService implements AutofillServiceInterface { const fieldNames: string[] = []; fields.forEach((f) => { - if (AutofillService.hasValue(f.name)) { + if (f.name != null && AutofillService.hasValue(f.name)) { fieldNames.push(f.name.toLowerCase()); } }); pageDetails.fields.forEach((field) => { - // eslint-disable-next-line - if (filledFields.hasOwnProperty(field.opid)) { + const fieldOpid = field.opid; + if (fieldOpid == null) { + return; + } + if (Object.prototype.hasOwnProperty.call(filledFields, fieldOpid)) { return; } @@ -773,40 +791,40 @@ export default class AutofillService implements AutofillServiceInterface { const matchingField: FieldView = fields[matchingIndex]; let val: string; if (matchingField.type === FieldType.Linked) { + if (matchingField.linkedId == null) { + return; + } // Assumption: Linked Field is not being used to autofill a boolean value val = options.cipher.linkedFieldValue(matchingField.linkedId) as string; } else { - val = matchingField.value; - if (val == null && matchingField.type === FieldType.Boolean) { - val = "false"; + const rawVal = matchingField.value; + if (rawVal == null) { + if (matchingField.type === FieldType.Boolean) { + val = "false"; + } else { + return; + } + } else { + val = rawVal; } } - filledFields[field.opid] = field; + filledFields[fieldOpid] = field; AutofillService.fillByOpid(fillScript, field, val); } }); } + let result: AutofillScript | null = null; switch (options.cipher.type) { case CipherType.Login: - fillScript = await this.generateLoginFillScript( - fillScript, - pageDetails, - filledFields, - options, - ); + result = await this.generateLoginFillScript(fillScript, pageDetails, filledFields, options); break; case CipherType.Card: - fillScript = await this.generateCardFillScript( - fillScript, - pageDetails, - filledFields, - options, - ); + result = await this.generateCardFillScript(fillScript, pageDetails, filledFields, options); break; case CipherType.Identity: - fillScript = await this.generateIdentityFillScript( + result = await this.generateIdentityFillScript( fillScript, pageDetails, filledFields, @@ -817,7 +835,7 @@ export default class AutofillService implements AutofillServiceInterface { return null; } - return fillScript; + return result; } /** @@ -842,12 +860,17 @@ export default class AutofillService implements AutofillServiceInterface { const passwords: AutofillField[] = []; const usernames = new Map(); const totps: AutofillField[] = []; - let pf: AutofillField = null; - let username: AutofillField = null; - let totp: AutofillField = null; + let pf: AutofillField | null = null; + let username: AutofillField | null = null; + let totp: AutofillField | null = null; const login = options.cipher.login; - fillScript.savedUrls = - login?.uris?.filter((u) => u.match != UriMatchStrategy.Never).map((u) => u.uri) ?? []; + const loginURIs = login?.uris ?? []; + fillScript.savedUrls = loginURIs.reduce((acc, savedURI) => { + if (savedURI.match != UriMatchStrategy.Never && savedURI.uri != null) { + acc.push(savedURI.uri); + } + return acc; + }, []); fillScript.untrustedIframe = await this.inUntrustedIframe(pageDetails.url, options); @@ -874,9 +897,9 @@ export default class AutofillService implements AutofillServiceInterface { const prioritizedPasswordFields = loginPasswordFields.length > 0 ? loginPasswordFields : registrationPasswordFields; - const focusedField = - options.focusedFieldOpid && - pageDetails.fields.find((f) => f.opid === options.focusedFieldOpid); + const focusedField = options.focusedFieldOpid + ? pageDetails.fields.find((f) => f.opid === options.focusedFieldOpid) + : undefined; const focusedForm = focusedField?.form; const isFocusedTotpField = @@ -908,10 +931,12 @@ export default class AutofillService implements AutofillServiceInterface { ? true : focusedForm != null ? pf.form === focusedForm - : focusedUsernameField && - pf.form == null && - this.findUsernameField(pageDetails, pf, false, false, true)?.opid === - focusedUsernameField.opid; + : !!( + focusedUsernameField && + pf.form == null && + this.findUsernameField(pageDetails, pf, false, false, true)?.opid === + focusedUsernameField.opid + ); const getUsernameForPassword = ( pf: AutofillField, @@ -924,10 +949,17 @@ export default class AutofillService implements AutofillServiceInterface { return this.findUsernameField(pageDetails, pf, false, false, withoutForm); }; - if (focusedUsernameField && !prioritizedPasswordFields.some(passwordMatchesFocused)) { + if ( + focusedUsernameField && + focusedUsernameField.opid != null && + !prioritizedPasswordFields.some(passwordMatchesFocused) + ) { if (!Object.prototype.hasOwnProperty.call(filledFields, focusedUsernameField.opid)) { filledFields[focusedUsernameField.opid] = focusedUsernameField; - AutofillService.fillByOpid(fillScript, focusedUsernameField, login.username); + const usernameVal = login.username; + if (usernameVal != null) { + AutofillService.fillByOpid(fillScript, focusedUsernameField, usernameVal); + } if (options.autoSubmitLogin && focusedUsernameField.form) { fillScript.autosubmit = [focusedUsernameField.form]; } @@ -954,7 +986,7 @@ export default class AutofillService implements AutofillServiceInterface { if (login.username) { username = getUsernameForPassword(pf, false); - if (username) { + if (username?.opid != null) { usernames.set(username.opid, username); } } @@ -1010,7 +1042,7 @@ export default class AutofillService implements AutofillServiceInterface { if (firstPasswordField) { if (login.username && firstPasswordField.elementNumber > 0) { username = getUsernameForPassword(firstPasswordField, true); - if (username) { + if (username?.opid != null) { usernames.set(username.opid, username); } } @@ -1068,46 +1100,68 @@ export default class AutofillService implements AutofillServiceInterface { const usernamesToFill = focusedUsernameField ? [focusedUsernameField] : [...usernames.values()]; usernamesToFill.forEach((u) => { - if (Object.prototype.hasOwnProperty.call(filledFields, u.opid)) { + if (u.opid == null) { + return; + } + const uOpid = u.opid; + if (Object.prototype.hasOwnProperty.call(filledFields, uOpid)) { return; } - filledFields[u.opid] = u; - AutofillService.fillByOpid(fillScript, u, login.username); - formElementsSet.add(u.form); + filledFields[uOpid] = u; + const usernameVal = login.username; + if (usernameVal != null) { + AutofillService.fillByOpid(fillScript, u, usernameVal); + } + if (u.form != null) { + formElementsSet.add(u.form); + } }); passwords.forEach((p) => { + if (p.opid == null) { + return; + } + const pOpid = p.opid; // eslint-disable-next-line - if (filledFields.hasOwnProperty(p.opid)) { + if (filledFields.hasOwnProperty(pOpid)) { return; } - filledFields[p.opid] = p; - AutofillService.fillByOpid(fillScript, p, login.password); - formElementsSet.add(p.form); + filledFields[pOpid] = p; + if (login.password != null) { + AutofillService.fillByOpid(fillScript, p, login.password); + } + if (p.form != null) { + formElementsSet.add(p.form); + } }); if (options.autoSubmitLogin && formElementsSet.size) { fillScript.autosubmit = Array.from(formElementsSet); } - if (options.allowTotpAutofill && login?.totp) { + const loginTotp = login?.totp; + if (options.allowTotpAutofill && typeof loginTotp === "string") { await Promise.all( totps.map(async (t, i) => { + if (t.opid == null) { + return; + } + if (Object.prototype.hasOwnProperty.call(filledFields, t.opid)) { return; } filledFields[t.opid] = t; - const totpResponse = await firstValueFrom(this.totpService.getCode$(login.totp)); - let totpValue = totpResponse.code; - - if (totpValue.length == totps.length) { - totpValue = totpValue.charAt(i); + const totpResponse = await firstValueFrom(this.totpService.getCode$(loginTotp)); + const totpValue = totpResponse.code; + if (totpValue == null) { + return; } - AutofillService.fillByOpid(fillScript, t, totpValue); + const totpChar = totpValue.length === totps.length ? totpValue.charAt(i) : totpValue; + AutofillService.fillByOpid(fillScript, t, totpChar); }), ); } @@ -1216,23 +1270,23 @@ export default class AutofillService implements AutofillServiceInterface { this.makeScriptAction(fillScript, card, fillFields, filledFields, "brand"); // There is an expiration month field and the cipher has an expiration month value - if (fillFields.expMonth && AutofillService.hasValue(card.expMonth)) { + if (fillFields.expMonth && card.expMonth != null && AutofillService.hasValue(card.expMonth)) { let expMonth: string = card.expMonth; if (fillFields.expMonth.selectInfo && fillFields.expMonth.selectInfo.options) { - let index: number = null; + let index: number | null = null; const siOptions = fillFields.expMonth.selectInfo.options; if (siOptions.length === 12) { - index = parseInt(card.expMonth, null) - 1; + index = parseInt(card.expMonth, 10) - 1; } else if (siOptions.length === 13) { if ( siOptions[0][0] != null && siOptions[0][0] !== "" && (siOptions[12][0] == null || siOptions[12][0] === "") ) { - index = parseInt(card.expMonth, null) - 1; + index = parseInt(card.expMonth, 10) - 1; } else { - index = parseInt(card.expMonth, null); + index = parseInt(card.expMonth, 10); } } @@ -1250,12 +1304,14 @@ export default class AutofillService implements AutofillServiceInterface { expMonth = "0" + expMonth; } - filledFields[fillFields.expMonth.opid] = fillFields.expMonth; - AutofillService.fillByOpid(fillScript, fillFields.expMonth, expMonth); + if (fillFields.expMonth.opid != null) { + filledFields[fillFields.expMonth.opid] = fillFields.expMonth; + AutofillService.fillByOpid(fillScript, fillFields.expMonth, expMonth); + } } // There is an expiration year field and the cipher has an expiration year value - if (fillFields.expYear && AutofillService.hasValue(card.expYear)) { + if (fillFields.expYear && card.expYear != null && AutofillService.hasValue(card.expYear)) { let expYear: string = card.expYear; if (fillFields.expYear.selectInfo && fillFields.expYear.selectInfo.options) { for (let i = 0; i < fillFields.expYear.selectInfo.options.length; i++) { @@ -1275,7 +1331,7 @@ export default class AutofillService implements AutofillServiceInterface { const colonIndex = o[1].indexOf(":"); if (colonIndex > -1 && o[1].length > colonIndex + 1) { const val = o[1].substring(colonIndex + 2); - if (val != null && val.trim() !== "" && val === card.expYear) { + if (val.trim() !== "" && val === card.expYear) { expYear = o[1]; break; } @@ -1286,35 +1342,44 @@ export default class AutofillService implements AutofillServiceInterface { fillFields.expYear.maxLength === 4 ) { if (expYear.length === 2) { - expYear = normalizeExpiryYearFormat(expYear); + const normalized = normalizeExpiryYearFormat(expYear); + if (normalized != null) { + expYear = normalized; + } } } else if ( this.fieldAttrsContain(fillFields.expYear, "yy") || fillFields.expYear.maxLength === 2 ) { if (expYear.length === 4) { - expYear = expYear.substr(2); + expYear = expYear.substring(2); } } - filledFields[fillFields.expYear.opid] = fillFields.expYear; - AutofillService.fillByOpid(fillScript, fillFields.expYear, expYear); + if (fillFields.expYear.opid != null) { + filledFields[fillFields.expYear.opid] = fillFields.expYear; + AutofillService.fillByOpid(fillScript, fillFields.expYear, expYear); + } } // There is a single expiry date field (combined values) and the cipher has both expiration month and year if ( fillFields.exp && + card.expMonth != null && AutofillService.hasValue(card.expMonth) && + card.expYear != null && AutofillService.hasValue(card.expYear) ) { const combinedExpiryFillValue = this.generateCombinedExpiryValue(card, fillFields.exp); - this.makeScriptActionWithValue( - fillScript, - combinedExpiryFillValue, - fillFields.exp, - filledFields, - ); + if (combinedExpiryFillValue != null) { + this.makeScriptActionWithValue( + fillScript, + combinedExpiryFillValue, + fillFields.exp, + filledFields, + ); + } } return fillScript; @@ -1385,7 +1450,7 @@ export default class AutofillService implements AutofillServiceInterface { * @param {CardView} cardCipher * @param {AutofillField} field */ - private generateCombinedExpiryValue(cardCipher: CardView, field: AutofillField): string { + private generateCombinedExpiryValue(cardCipher: CardView, field: AutofillField): string | null { /* Some expectations of the passed stored card cipher view: @@ -1420,7 +1485,7 @@ export default class AutofillService implements AutofillServiceInterface { expiryDateFormatPatterns, ] = this.getExpectedExpiryDateFormat(field); - if (expectedExpiryDateFormat) { + if (expectedExpiryDateFormat && expiryDateFormatPatterns) { const { Month, MonthShort, Year } = expiryDateFormatPatterns; const expiryDateDelimitersPattern = "\\" + CardExpiryDateDelimiters.join("\\"); @@ -1451,6 +1516,9 @@ export default class AutofillService implements AutofillServiceInterface { useYearFull = containsLongYearPattern.test(expectedExpiryDateFormat); } + if (cardCipher.expMonth == null || cardCipher.expYear == null) { + return null; + } const month = useMonthPadding ? // Ensure zero-padding ("0" + cardCipher.expMonth).slice(-2) @@ -1532,7 +1600,7 @@ export default class AutofillService implements AutofillServiceInterface { pageDetails: AutofillPageDetails, filledFields: { [id: string]: AutofillField }, options: GenerateFillScriptOptions, - ): AutofillScript { + ): AutofillScript | null { const identity = options.cipher.identity; if (!identity) { return null; @@ -1645,7 +1713,8 @@ export default class AutofillService implements AutofillServiceInterface { "password", ...AutoFillConstants.ExcludedAutofillTypes, ]) || - AutoFillConstants.ExcludedIdentityAutocompleteTypes.has(field.autoCompleteType) || + (field.autoCompleteType != null && + AutoFillConstants.ExcludedIdentityAutocompleteTypes.has(field.autoCompleteType)) || !field.viewable ); } @@ -1660,9 +1729,10 @@ export default class AutofillService implements AutofillServiceInterface { const keywords: Set = new Set(); for (let index = 0; index < IdentityAutoFillConstants.IdentityAttributes.length; index++) { const attribute = IdentityAutoFillConstants.IdentityAttributes[index]; - if (field[attribute]) { + const value = field[attribute]; + if (value != null && typeof value === "string") { keywords.add( - field[attribute] + value .trim() .toLowerCase() .replace(/[^a-zA-Z0-9]+/g, ""), @@ -2125,12 +2195,13 @@ export default class AutofillService implements AutofillServiceInterface { private static isSearchField(field: AutofillField) { const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder]; for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) { - if (!matchFieldAttributeValues[attrIndex]) { + const attributeValue = matchFieldAttributeValues[attrIndex]; + if (!attributeValue) { continue; } // Separate camel case words and case them to lower case values - const camelCaseSeparatedFieldAttribute = matchFieldAttributeValues[attrIndex] + const camelCaseSeparatedFieldAttribute = attributeValue .replace(/([a-z])([A-Z])/g, "$1 $2") .toLowerCase(); // Split the attribute by non-alphabetical characters to get the keywords @@ -2151,7 +2222,7 @@ export default class AutofillService implements AutofillServiceInterface { return true; } - if (this.isExcludedType(field.type, excludedTypes)) { + if (field.type != null && this.isExcludedType(field.type, excludedTypes)) { return true; } @@ -2211,12 +2282,10 @@ export default class AutofillService implements AutofillServiceInterface { fieldProp?: string, ) { fieldProp = fieldProp || dataProp; - this.makeScriptActionWithValue( - fillScript, - cipherData[dataProp], - fillFields[fieldProp], - filledFields, - ); + const field = fillFields[fieldProp]; + if (field != null) { + this.makeScriptActionWithValue(fillScript, cipherData[dataProp], field, filledFields); + } } /** @@ -2348,7 +2417,8 @@ export default class AutofillService implements AutofillServiceInterface { const testedValues = [f.htmlID, f.htmlName, f.placeholder]; for (let i = 0; i < testedValues.length; i++) { - if (AutofillService.valueIsLikePassword(testedValues[i])) { + const value = testedValues[i]; + if (value != null && AutofillService.valueIsLikePassword(value)) { return true; } } @@ -2396,7 +2466,7 @@ export default class AutofillService implements AutofillServiceInterface { form.htmlName?.toLowerCase?.(), passwordField?.htmlID?.toLowerCase?.(), passwordField?.htmlName?.toLowerCase?.(), - ].filter(Boolean); + ].filter((value): value is string => typeof value === "string"); return formIdentifierValues.some((value) => AutoFillConstants.RegistrationKeywords.some((keyword) => value.includes(keyword)), @@ -2421,8 +2491,8 @@ export default class AutofillService implements AutofillServiceInterface { canBeReadOnly: boolean, withoutForm: boolean, ): AutofillField | null { - let usernameField: AutofillField = null; - let usernameFieldInSameForm: AutofillField = null; + let usernameField: AutofillField | null = null; + let usernameFieldInSameForm: AutofillField | null = null; for (let i = 0; i < pageDetails.fields.length; i++) { const f = pageDetails.fields[i]; @@ -2486,7 +2556,7 @@ export default class AutofillService implements AutofillServiceInterface { canBeReadOnly: boolean, withoutForm: boolean, ): AutofillField | null { - let totpField: AutofillField = null; + let totpField: AutofillField | null = null; for (let i = 0; i < pageDetails.fields.length; i++) { const f = pageDetails.fields[i]; if (AutofillService.forCustomFieldsOnly(f)) { @@ -2761,8 +2831,8 @@ export default class AutofillService implements AutofillServiceInterface { filledFields: { [id: string]: AutofillField }, fillScript: AutofillScript, ): AutofillScript { - let lastField: AutofillField = null; - let lastPasswordField: AutofillField = null; + let lastField: AutofillField | null = null; + let lastPasswordField: AutofillField | null = null; for (const opid in filledFields) { // eslint-disable-next-line @@ -2776,9 +2846,9 @@ export default class AutofillService implements AutofillServiceInterface { } // Prioritize password field over others. - if (lastPasswordField) { + if (lastPasswordField?.opid != null) { fillScript.script.push(["focus_by_opid", lastPasswordField.opid]); - } else if (lastField) { + } else if (lastField?.opid != null) { fillScript.script.push(["focus_by_opid", lastField.opid]); } @@ -2886,10 +2956,10 @@ export default class AutofillService implements AutofillServiceInterface { * @param newSettingValue - The current setting value */ private async handleInlineMenuVisibilitySettingsChange( - oldSettingValue: InlineMenuVisibilitySetting | boolean, - newSettingValue: InlineMenuVisibilitySetting | boolean, + oldSettingValue: InlineMenuVisibilitySetting | boolean | undefined, + newSettingValue: InlineMenuVisibilitySetting | boolean | undefined, ) { - if (oldSettingValue == null || oldSettingValue === newSettingValue) { + if (oldSettingValue == null || newSettingValue == null || oldSettingValue === newSettingValue) { return; }