mirror of
https://github.com/bitwarden/clients.git
synced 2026-07-04 21:05:54 +08:00
[PM-29528]Remove @ts-strict-ignore in autofill.service.ts (#19032)
* [PM-29528]Remove @ts-strict-ignore in autofill.service.ts * remove redundant explicit checks * implement a discriminated union to account for forms with no username * add reducer and move up const for field opid * Update apps/browser/src/autofill/services/autofill.service.ts Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com> * revert suggestion --------- Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>
This commit is contained in:
parent
cd2b708982
commit
25dda0c78c
@ -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;
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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<typeof setTimeout>;
|
||||
private currentlyOpeningPasswordRepromptPopout = false;
|
||||
private autofillScriptPortsSet = new Set<chrome.runtime.Port>();
|
||||
static searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
|
||||
@ -106,7 +104,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
*/
|
||||
collectPageDetailsFromTab$(tab: chrome.tabs.Tab): Observable<PageDetail[]> {
|
||||
/** Replay Subject that can be utilized when `messages$` may not emit the page details. */
|
||||
const pageDetailsFallback$ = new ReplaySubject<[]>(1);
|
||||
const pageDetailsFallback$ = new ReplaySubject<PageDetail[]>(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<void> {
|
||||
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<string> {
|
||||
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<string | null> {
|
||||
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<string, AutofillField>();
|
||||
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<string[]>((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<string> = 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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user