bitwarden-estensione-browser/apps/browser/src/autofill/services/autofill.service.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1495 lines
47 KiB
TypeScript
Raw Normal View History

import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
2022-06-14 17:10:53 +02:00
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
2022-06-14 17:10:53 +02:00
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { EventType } from "@bitwarden/common/enums/eventType";
import { FieldType } from "@bitwarden/common/enums/fieldType";
import { UriMatchType } from "@bitwarden/common/enums/uriMatchType";
import { Utils } from "@bitwarden/common/misc/utils";
[SG-998] and [SG-999] Vault and Autofill team refactor (#4542) * Move DeprecatedVaultFilterService to vault folder * [libs] move VaultItemsComponent * [libs] move AddEditComponent * [libs] move AddEditCustomFields * [libs] move attachmentsComponent * [libs] folderAddEditComponent * [libs] IconComponent * [libs] PasswordRepormptComponent * [libs] PremiumComponent * [libs] ViewCustomFieldsComponent * [libs] ViewComponent * [libs] PasswordRepromptService * [libs] Move FolderService and FolderApiService abstractions * [libs] FolderService imports * [libs] PasswordHistoryComponent * [libs] move Sync and SyncNotifier abstractions * [libs] SyncService imports * [libs] fix file casing for passwordReprompt abstraction * [libs] SyncNotifier import fix * [libs] CipherServiceAbstraction * [libs] PasswordRepromptService abstraction * [libs] Fix file casing for angular passwordReprompt service * [libs] fix file casing for SyncNotifierService * [libs] CipherRepromptType * [libs] rename CipherRepromptType * [libs] CipherType * [libs] Rename CipherType * [libs] CipherData * [libs] FolderData * [libs] PasswordHistoryData * [libs] AttachmentData * [libs] CardData * [libs] FieldData * [libs] IdentityData * [libs] LocalData * [libs] LoginData * [libs] SecureNoteData * [libs] LoginUriData * [libs] Domain classes * [libs] SecureNote * [libs] Request models * [libs] Response models * [libs] View part 1 * [libs] Views part 2 * [libs] Move folder services * [libs] Views fixes * [libs] Move sync services * [libs] cipher service * [libs] Types * [libs] Sync file casing * [libs] Fix folder service import * [libs] Move spec files * [libs] casing fixes on spec files * [browser] Autofill background, clipboard, commands * [browser] Fix ContextMenusBackground casing * [browser] Rename fix * [browser] Autofill content * [browser] autofill.js * [libs] enpass importer spec fix * [browser] autofill models * [browser] autofill manifest path updates * [browser] Autofill notification files * [browser] autofill services * [browser] Fix file casing * [browser] Vault popup loose components * [browser] Vault components * [browser] Manifest fixes * [browser] Vault services * [cli] vault commands and models * [browser] File capitilization fixes * [desktop] Vault components and services * [web] vault loose components * [web] Vault components * [browser] Fix misc-utils import * [libs] Fix psono spec imports * [fix] Add comments to address lint rules
2023-01-31 22:08:37 +01:00
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
[SG-998] and [SG-999] Vault and Autofill team refactor (#4542) * Move DeprecatedVaultFilterService to vault folder * [libs] move VaultItemsComponent * [libs] move AddEditComponent * [libs] move AddEditCustomFields * [libs] move attachmentsComponent * [libs] folderAddEditComponent * [libs] IconComponent * [libs] PasswordRepormptComponent * [libs] PremiumComponent * [libs] ViewCustomFieldsComponent * [libs] ViewComponent * [libs] PasswordRepromptService * [libs] Move FolderService and FolderApiService abstractions * [libs] FolderService imports * [libs] PasswordHistoryComponent * [libs] move Sync and SyncNotifier abstractions * [libs] SyncService imports * [libs] fix file casing for passwordReprompt abstraction * [libs] SyncNotifier import fix * [libs] CipherServiceAbstraction * [libs] PasswordRepromptService abstraction * [libs] Fix file casing for angular passwordReprompt service * [libs] fix file casing for SyncNotifierService * [libs] CipherRepromptType * [libs] rename CipherRepromptType * [libs] CipherType * [libs] Rename CipherType * [libs] CipherData * [libs] FolderData * [libs] PasswordHistoryData * [libs] AttachmentData * [libs] CardData * [libs] FieldData * [libs] IdentityData * [libs] LocalData * [libs] LoginData * [libs] SecureNoteData * [libs] LoginUriData * [libs] Domain classes * [libs] SecureNote * [libs] Request models * [libs] Response models * [libs] View part 1 * [libs] Views part 2 * [libs] Move folder services * [libs] Views fixes * [libs] Move sync services * [libs] cipher service * [libs] Types * [libs] Sync file casing * [libs] Fix folder service import * [libs] Move spec files * [libs] casing fixes on spec files * [browser] Autofill background, clipboard, commands * [browser] Fix ContextMenusBackground casing * [browser] Rename fix * [browser] Autofill content * [browser] autofill.js * [libs] enpass importer spec fix * [browser] autofill models * [browser] autofill manifest path updates * [browser] Autofill notification files * [browser] autofill services * [browser] Fix file casing * [browser] Vault popup loose components * [browser] Vault components * [browser] Manifest fixes * [browser] Vault services * [cli] vault commands and models * [browser] File capitilization fixes * [desktop] Vault components and services * [web] vault loose components * [web] Vault components * [browser] Fix misc-utils import * [libs] Fix psono spec imports * [fix] Add comments to address lint rules
2023-01-31 22:08:37 +01:00
import { BrowserApi } from "../../browser/browserApi";
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
import AutofillScript from "../models/autofill-script";
[Account Switching] Base changes for account switching (#2250) * Pull in jslib * Create new state models * Create browser specific stateService * Remove registration deprecated services, register stateService * Replace usage of deprecated services (user, constants) * Add missing properties to BrowserGroupingsComponentState * Remove StorageService from initFactory * Clear the correct state * Add null check when restoring send-grouping state * add remember email * Initialize stateservice in services.module * Fix 'lock now' not working * Comment to remove setting defaults on install * Pull jslib * Remove setting defaults on install * Bump jslib * Pass the current userId to services when logging out * Bump jslib * Override vaultTimeout default on account addition * Pull latest jslib * Retrieve vaultTimeout from stateService * Record activity per Account * Add userId to logout and add fallback if not present * Register AccountFactory * Pass userId in messages * Base changes for account switching di fixes (#2280) * [bug] Null checks on Account init * [bug] Use same stateService instance for all operations We override the stateService in browser, but currently don't pull the background service into popup and allow jslib to create its own instance of the base StateService for jslib services. This causes a split in in memory state between the three isntances that results in many errors, namely locking not working. * [chore] Update jslib * Pull in jslib * Pull in jslib * Pull in latest jslib to multiple stateservice inits * Check vault states before executing processReload * Adjust iterator * Update native messaging to include the userId (#2290) * Re-Add UserVerificationService * Fix email not being remembered by base component * Improve readability of reloadProcess * Removed unneeded null check * Fix constructor dependency (stateService) * Added missing await * Simplify dependency registration * Fixed typos * Reverted back to simple loop * Use vaultTimeoutService to retrieve Timeout Co-authored-by: Addison Beck <abeck@bitwarden.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
2022-01-27 22:22:51 +01:00
import {
AutoFillOptions,
AutofillService as AutofillServiceInterface,
PageDetail,
FormData,
} from "./abstractions/autofill.service";
import {
AutoFillConstants,
CreditCardAutoFillConstants,
IdentityAutoFillConstants,
} from "./autofill-constants";
2022-01-04 16:48:57 +01:00
export interface GenerateFillScriptOptions {
skipUsernameOnlyFill: boolean;
onlyEmptyFields: boolean;
onlyVisibleFields: boolean;
fillNewPassword: boolean;
cipher: CipherView;
tabUrl: string;
defaultUriMatch: UriMatchType;
}
2018-01-11 20:45:27 +01:00
export default class AutofillService implements AutofillServiceInterface {
constructor(
private cipherService: CipherService,
[PS-1854] Split services between background and visualizations (#4075) * Elevate Map <-> Record JSON helpers to Utils * Build Account from a StateService provided AccountDeserializer * Allow Manifest V2 usage of session sync Expands use of SessionSyncer to all Subject types. Correctly handles replay buffer for each type to ignore the flood of data upon subscription to each Subject type. * Create browser-synced Policy Service * Move BrowserFolderService * Libs account serialization improvements * Serialize Browser Accounts * Separate StateService in background/visualizations Visualizer state services share storages with background page, which nicely emulates mv3 synchronization through session/local storage. There should not be multithreading issues since all of these services are still running through a single thread, we just now have multiple places we are reading/writing data from. Smaller improvements * Rename browser's state service to BrowserStateService * Remove unused WithPrototype decorator :celebrate: * Removed conversion on withPrototypeForArrayMembers. It's reasonable to think that if the type is maintained, it doesn't need conversion. Eventually, we should be able to remove the withPrototypeForArrayMembers decorator as well, but that will require a bit more work on (de)serialization of the Accounts.data property. * Make Record <-> Map idempotent Should we get in a situation where we _think_ an object has been jsonified, but hasn't been, we need to correctly deal with the object received to create our target. * Check all requirements while duck typing * Name client services after the client * Use union type to limit initialize options * Fixup usages of `initializeAs` * Add OrganizationService to synced services Co-Authored-By: Daniel James Smith <djsmith85@users.noreply.github.com> * Add Settings service to synced services Co-Authored-By: Daniel James Smith <djsmith85@users.noreply.github.com> * Add missing BrowserStateService * Fix factories to use browser-specific service overides * Fix org-service registration in services.module * Revert "Add missing BrowserStateService" This reverts commit 81cf384e872718e86e84f9c54731e78a083e1ee3. * Fix session syncer tests * Fix synced item metadata tests * Early return null json objects * Prefer abstract service dependencies * Prefer minimal browser service overrides * [SG-632] - Change forwarded providers radio buttons list to dropdown (#4045) * SG-632 - Changed forwarded providers list of radio buttons to dropdown * SG-632 - Added role attributes to improve accessibility. * SG-632 - Added sorting to array and empty option * SG-632 - Fix styling to match standards. * rename cipehrs component to vault items component (#4081) * Update the version hash for the QA Web build artifact to follow SemVer syntax (#4102) * Remove extra call to toJSON() (#4101) Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith@web.de> Co-authored-by: Carlos Gonçalves <carlosmaccam@gmail.com> Co-authored-by: Jake Fink <jfink@bitwarden.com> Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
2022-11-23 23:26:57 +01:00
private stateService: BrowserStateService,
private totpService: TotpService,
private eventCollectionService: EventCollectionService,
private logService: LogService,
private settingsService: SettingsService
) {}
getFormsWithPasswordFields(pageDetails: AutofillPageDetails): FormData[] {
const formData: FormData[] = [];
const passwordFields = AutofillService.loadPasswordFields(
pageDetails,
true,
true,
false,
false
);
if (passwordFields.length === 0) {
return formData;
}
for (const formKey in pageDetails.forms) {
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (!pageDetails.forms.hasOwnProperty(formKey)) {
continue;
}
const formPasswordFields = passwordFields.filter((pf) => formKey === pf.form);
if (formPasswordFields.length > 0) {
let uf = this.findUsernameField(pageDetails, formPasswordFields[0], false, false, false);
if (uf == null) {
// not able to find any viewable username fields. maybe there are some "hidden" ones?
uf = this.findUsernameField(pageDetails, formPasswordFields[0], true, true, false);
2017-11-06 20:16:06 +01:00
}
formData.push({
form: pageDetails.forms[formKey],
password: formPasswordFields[0],
username: uf,
passwords: formPasswordFields,
});
}
}
2017-11-06 20:16:06 +01:00
return formData;
}
2017-11-06 20:16:06 +01:00
/**
* Autofills a given tab with a given login item
* @param options Instructions about the autofill operation, including tab and login item
* @returns The TOTP code of the successfully autofilled login, if any
*/
async doAutoFill(options: AutoFillOptions): Promise<string> {
const tab = options.tab;
if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) {
throw new Error("Nothing to auto-fill.");
2017-11-06 20:16:06 +01:00
}
let totpPromise: Promise<string> = null;
[Account Switching] Base changes for account switching (#2250) * Pull in jslib * Create new state models * Create browser specific stateService * Remove registration deprecated services, register stateService * Replace usage of deprecated services (user, constants) * Add missing properties to BrowserGroupingsComponentState * Remove StorageService from initFactory * Clear the correct state * Add null check when restoring send-grouping state * add remember email * Initialize stateservice in services.module * Fix 'lock now' not working * Comment to remove setting defaults on install * Pull jslib * Remove setting defaults on install * Bump jslib * Pass the current userId to services when logging out * Bump jslib * Override vaultTimeout default on account addition * Pull latest jslib * Retrieve vaultTimeout from stateService * Record activity per Account * Add userId to logout and add fallback if not present * Register AccountFactory * Pass userId in messages * Base changes for account switching di fixes (#2280) * [bug] Null checks on Account init * [bug] Use same stateService instance for all operations We override the stateService in browser, but currently don't pull the background service into popup and allow jslib to create its own instance of the base StateService for jslib services. This causes a split in in memory state between the three isntances that results in many errors, namely locking not working. * [chore] Update jslib * Pull in jslib * Pull in jslib * Pull in latest jslib to multiple stateservice inits * Check vault states before executing processReload * Adjust iterator * Update native messaging to include the userId (#2290) * Re-Add UserVerificationService * Fix email not being remembered by base component * Improve readability of reloadProcess * Removed unneeded null check * Fix constructor dependency (stateService) * Added missing await * Simplify dependency registration * Fixed typos * Reverted back to simple loop * Use vaultTimeoutService to retrieve Timeout Co-authored-by: Addison Beck <abeck@bitwarden.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
2022-01-27 22:22:51 +01:00
const canAccessPremium = await this.stateService.getCanAccessPremium();
const defaultUriMatch = (await this.stateService.getDefaultUriMatch()) ?? UriMatchType.Domain;
let didAutofill = false;
options.pageDetails.forEach((pd) => {
// make sure we're still on correct tab
if (pd.tab.id !== tab.id || pd.tab.url !== tab.url) {
return;
}
const fillScript = this.generateFillScript(pd.details, {
skipUsernameOnlyFill: options.skipUsernameOnlyFill || false,
onlyEmptyFields: options.onlyEmptyFields || false,
onlyVisibleFields: options.onlyVisibleFields || false,
fillNewPassword: options.fillNewPassword || false,
cipher: options.cipher,
tabUrl: tab.url,
defaultUriMatch: defaultUriMatch,
});
if (!fillScript || !fillScript.script || !fillScript.script.length) {
return;
}
if (
fillScript.untrustedIframe &&
options.allowUntrustedIframe != undefined &&
!options.allowUntrustedIframe
) {
this.logService.info("Auto-fill on page load was blocked due to an untrusted iframe.");
return;
}
// Add a small delay between operations
fillScript.properties.delay_between_operations = 20;
didAutofill = true;
if (!options.skipLastUsed) {
this.cipherService.updateLastUsedDate(options.cipher.id);
}
BrowserApi.tabSendMessage(
tab,
{
command: "fillForm",
fillScript: fillScript,
url: tab.url,
},
{ frameId: pd.frameId }
);
if (
options.cipher.type !== CipherType.Login ||
totpPromise ||
!options.cipher.login.totp ||
(!canAccessPremium && !options.cipher.organizationUseTotp)
) {
return;
}
totpPromise = this.stateService.getDisableAutoTotpCopy().then((disabled) => {
if (!disabled) {
return this.totpService.getCode(options.cipher.login.totp);
2017-11-06 20:16:06 +01:00
}
return null;
});
});
if (didAutofill) {
this.eventCollectionService.collect(EventType.Cipher_ClientAutofilled, options.cipher.id);
if (totpPromise != null) {
return await totpPromise;
} else {
return null;
}
} else {
throw new Error("Did not auto-fill.");
}
}
2017-11-06 20:16:06 +01:00
/**
* Autofills the specified tab with the next login item from the cache
* @param pageDetails The data scraped from the page
* @param tab The tab to be autofilled
* @param fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
* @returns The TOTP code of the successfully autofilled login, if any
*/
async doAutoFillOnTab(
pageDetails: PageDetail[],
tab: chrome.tabs.Tab,
fromCommand: boolean
): Promise<string> {
let cipher: CipherView;
if (fromCommand) {
cipher = await this.cipherService.getNextCipherForUrl(tab.url);
} else {
const lastLaunchedCipher = await this.cipherService.getLastLaunchedForUrl(tab.url, true);
if (
lastLaunchedCipher &&
Date.now().valueOf() - lastLaunchedCipher.localData?.lastLaunched?.valueOf() < 30000
) {
cipher = lastLaunchedCipher;
} else {
cipher = await this.cipherService.getLastUsedForUrl(tab.url, true);
}
}
2017-11-06 20:16:06 +01:00
if (cipher == null || cipher.reprompt !== CipherRepromptType.None) {
return null;
}
2017-11-06 20:16:06 +01:00
const totpCode = await this.doAutoFill({
tab: tab,
cipher: cipher,
pageDetails: pageDetails,
skipLastUsed: !fromCommand,
skipUsernameOnlyFill: !fromCommand,
onlyEmptyFields: !fromCommand,
onlyVisibleFields: !fromCommand,
fillNewPassword: fromCommand,
allowUntrustedIframe: fromCommand,
});
// Update last used index as autofill has succeed
if (fromCommand) {
this.cipherService.updateLastUsedIndexForUrl(tab.url);
}
2020-05-05 22:59:20 +02:00
return totpCode;
}
2017-11-06 20:16:06 +01:00
/**
* Autofills the active tab with the next login item from the cache
* @param pageDetails The data scraped from the page
* @param fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
* @returns The TOTP code of the successfully autofilled login, if any
*/
async doAutoFillActiveTab(pageDetails: PageDetail[], fromCommand: boolean): Promise<string> {
const tab = await this.getActiveTab();
if (!tab || !tab.url) {
return;
}
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
}
// Helpers
2017-11-06 20:16:06 +01:00
private async getActiveTab(): Promise<chrome.tabs.Tab> {
const tab = await BrowserApi.getTabFromCurrentWindow();
if (!tab) {
throw new Error("No tab found.");
}
2017-11-06 20:16:06 +01:00
return tab;
}
2017-11-06 20:16:06 +01:00
private generateFillScript(
pageDetails: AutofillPageDetails,
options: GenerateFillScriptOptions
): AutofillScript {
if (!pageDetails || !options.cipher) {
return null;
2017-11-06 20:16:06 +01:00
}
let fillScript = new AutofillScript(pageDetails.documentUUID);
const filledFields: { [id: string]: AutofillField } = {};
const fields = options.cipher.fields;
2017-11-06 20:16:06 +01:00
if (fields && fields.length) {
const fieldNames: string[] = [];
2020-10-26 23:13:09 +01:00
fields.forEach((f) => {
if (AutofillService.hasValue(f.name)) {
fieldNames.push(f.name.toLowerCase());
2017-11-06 20:16:06 +01:00
}
});
2017-11-06 20:16:06 +01:00
pageDetails.fields.forEach((field) => {
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (filledFields.hasOwnProperty(field.opid)) {
return;
}
2021-05-05 05:57:50 +02:00
if (!field.viewable && field.tagName !== "span") {
return;
}
const matchingIndex = this.findMatchingFieldIndex(field, fieldNames);
if (matchingIndex > -1) {
const matchingField: FieldView = fields[matchingIndex];
let val: string;
if (matchingField.type === FieldType.Linked) {
// 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";
}
}
2017-11-06 20:16:06 +01:00
filledFields[field.opid] = field;
AutofillService.fillByOpid(fillScript, field, val);
}
});
2017-11-06 20:16:06 +01:00
}
switch (options.cipher.type) {
case CipherType.Login:
fillScript = this.generateLoginFillScript(fillScript, pageDetails, filledFields, options);
break;
case CipherType.Card:
fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options);
break;
case CipherType.Identity:
fillScript = this.generateIdentityFillScript(
fillScript,
pageDetails,
filledFields,
options
);
break;
default:
return null;
}
2017-11-06 20:16:06 +01:00
return fillScript;
}
private generateLoginFillScript(
fillScript: AutofillScript,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: GenerateFillScriptOptions
): AutofillScript {
if (!options.cipher.login) {
return null;
}
2017-11-06 20:16:06 +01:00
const passwords: AutofillField[] = [];
const usernames: AutofillField[] = [];
let pf: AutofillField = null;
let username: AutofillField = null;
const login = options.cipher.login;
fillScript.savedUrls =
login?.uris?.filter((u) => u.match != UriMatchType.Never).map((u) => u.uri) ?? [];
2017-11-06 20:16:06 +01:00
const inIframe = pageDetails.url !== options.tabUrl;
fillScript.untrustedIframe =
inIframe && !this.iframeUrlMatches(pageDetails.url, options.cipher, options.defaultUriMatch);
if (!login.password || login.password === "") {
// No password for this login. Maybe they just wanted to auto-fill some custom fields?
fillScript = AutofillService.setFillScriptForFocus(filledFields, fillScript);
return fillScript;
2017-11-06 20:16:06 +01:00
}
let passwordFields = AutofillService.loadPasswordFields(
pageDetails,
false,
false,
options.onlyEmptyFields,
options.fillNewPassword
);
if (!passwordFields.length && !options.onlyVisibleFields) {
// not able to find any viewable password fields. maybe there are some "hidden" ones?
passwordFields = AutofillService.loadPasswordFields(
pageDetails,
true,
true,
options.onlyEmptyFields,
options.fillNewPassword
);
}
2017-11-06 20:16:06 +01:00
for (const formKey in pageDetails.forms) {
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (!pageDetails.forms.hasOwnProperty(formKey)) {
continue;
}
2017-11-06 20:16:06 +01:00
const passwordFieldsForForm: AutofillField[] = [];
passwordFields.forEach((passField) => {
if (formKey === passField.form) {
passwordFieldsForForm.push(passField);
2017-11-06 20:16:06 +01:00
}
});
2017-11-06 20:16:06 +01:00
passwordFields.forEach((passField) => {
pf = passField;
passwords.push(pf);
2017-11-06 20:16:06 +01:00
if (login.username) {
username = this.findUsernameField(pageDetails, pf, false, false, false);
2017-11-06 20:16:06 +01:00
if (!username && !options.onlyVisibleFields) {
// not able to find any viewable username fields. maybe there are some "hidden" ones?
username = this.findUsernameField(pageDetails, pf, true, true, false);
}
2017-11-06 20:16:06 +01:00
if (username) {
usernames.push(username);
}
}
});
}
2017-11-06 20:16:06 +01:00
if (passwordFields.length && !passwords.length) {
// The page does not have any forms with password fields. Use the first password field on the page and the
// input field just before it as the username.
2017-11-06 20:16:06 +01:00
pf = passwordFields[0];
passwords.push(pf);
2017-11-06 20:16:06 +01:00
if (login.username && pf.elementNumber > 0) {
username = this.findUsernameField(pageDetails, pf, false, false, true);
2017-11-06 20:16:06 +01:00
if (!username && !options.onlyVisibleFields) {
// not able to find any viewable username fields. maybe there are some "hidden" ones?
username = this.findUsernameField(pageDetails, pf, true, true, true);
2017-11-06 20:16:06 +01:00
}
if (username) {
usernames.push(username);
2017-11-06 20:16:06 +01:00
}
}
2017-11-06 20:16:06 +01:00
}
if (!passwordFields.length && !options.skipUsernameOnlyFill) {
// No password fields on this page. Let's try to just fuzzy fill the username.
pageDetails.fields.forEach((f) => {
if (
f.viewable &&
(f.type === "text" || f.type === "email" || f.type === "tel") &&
AutofillService.fieldIsFuzzyMatch(f, AutoFillConstants.UsernameFieldNames)
) {
usernames.push(f);
2017-11-06 20:16:06 +01:00
}
});
}
2017-11-06 20:16:06 +01:00
usernames.forEach((u) => {
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (filledFields.hasOwnProperty(u.opid)) {
return;
}
filledFields[u.opid] = u;
AutofillService.fillByOpid(fillScript, u, login.username);
});
passwords.forEach((p) => {
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (filledFields.hasOwnProperty(p.opid)) {
return;
}
filledFields[p.opid] = p;
AutofillService.fillByOpid(fillScript, p, login.password);
});
fillScript = AutofillService.setFillScriptForFocus(filledFields, fillScript);
return fillScript;
}
private generateCardFillScript(
fillScript: AutofillScript,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: GenerateFillScriptOptions
): AutofillScript {
if (!options.cipher.card) {
return null;
}
const fillFields: { [id: string]: AutofillField } = {};
pageDetails.fields.forEach((f) => {
if (AutofillService.forCustomFieldsOnly(f)) {
return;
}
2017-11-06 20:16:06 +01:00
if (this.isExcludedType(f.type, AutoFillConstants.ExcludedAutofillTypes)) {
return;
}
2017-11-24 21:49:13 +01:00
for (let i = 0; i < CreditCardAutoFillConstants.CardAttributes.length; i++) {
const attr = CreditCardAutoFillConstants.CardAttributes[i];
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (!f.hasOwnProperty(attr) || !f[attr] || !f.viewable) {
continue;
2017-11-24 21:49:13 +01:00
}
// ref https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill
// ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/
if (
!fillFields.cardholderName &&
AutofillService.isFieldMatch(
f[attr],
CreditCardAutoFillConstants.CardHolderFieldNames,
CreditCardAutoFillConstants.CardHolderFieldNameValues
)
) {
fillFields.cardholderName = f;
break;
} else if (
!fillFields.number &&
AutofillService.isFieldMatch(
f[attr],
CreditCardAutoFillConstants.CardNumberFieldNames,
CreditCardAutoFillConstants.CardNumberFieldNameValues
)
) {
fillFields.number = f;
break;
} else if (
!fillFields.exp &&
AutofillService.isFieldMatch(
f[attr],
CreditCardAutoFillConstants.CardExpiryFieldNames,
CreditCardAutoFillConstants.CardExpiryFieldNameValues
)
) {
fillFields.exp = f;
break;
} else if (
!fillFields.expMonth &&
AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryMonthFieldNames)
) {
fillFields.expMonth = f;
break;
} else if (
!fillFields.expYear &&
AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryYearFieldNames)
) {
fillFields.expYear = f;
break;
} else if (
!fillFields.code &&
AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.CVVFieldNames)
) {
fillFields.code = f;
break;
} else if (
!fillFields.brand &&
AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.CardBrandFieldNames)
) {
fillFields.brand = f;
break;
}
}
});
const card = options.cipher.card;
this.makeScriptAction(fillScript, card, fillFields, filledFields, "cardholderName");
this.makeScriptAction(fillScript, card, fillFields, filledFields, "number");
this.makeScriptAction(fillScript, card, fillFields, filledFields, "code");
this.makeScriptAction(fillScript, card, fillFields, filledFields, "brand");
if (fillFields.expMonth && AutofillService.hasValue(card.expMonth)) {
let expMonth: string = card.expMonth;
if (fillFields.expMonth.selectInfo && fillFields.expMonth.selectInfo.options) {
let index: number = null;
const siOptions = fillFields.expMonth.selectInfo.options;
if (siOptions.length === 12) {
index = parseInt(card.expMonth, null) - 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;
} else {
index = parseInt(card.expMonth, null);
}
2017-11-06 20:16:06 +01:00
}
if (index != null) {
const option = siOptions[index];
if (option.length > 1) {
expMonth = option[1];
}
2018-03-16 02:16:40 +01:00
}
} else if (
(this.fieldAttrsContain(fillFields.expMonth, "mm") ||
fillFields.expMonth.maxLength === 2) &&
expMonth.length === 1
) {
expMonth = "0" + expMonth;
}
filledFields[fillFields.expMonth.opid] = fillFields.expMonth;
AutofillService.fillByOpid(fillScript, fillFields.expMonth, expMonth);
}
2018-03-16 02:16:40 +01:00
if (fillFields.expYear && 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++) {
const o: [string, string] = fillFields.expYear.selectInfo.options[i];
if (o[0] === card.expYear || o[1] === card.expYear) {
expYear = o[1];
break;
}
if (
o[1].length === 2 &&
card.expYear.length === 4 &&
o[1] === card.expYear.substring(2)
) {
expYear = o[1];
break;
}
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) {
expYear = o[1];
break;
2018-03-16 02:16:40 +01:00
}
}
}
} else if (
this.fieldAttrsContain(fillFields.expYear, "yyyy") ||
fillFields.expYear.maxLength === 4
) {
if (expYear.length === 2) {
expYear = "20" + expYear;
}
} else if (
this.fieldAttrsContain(fillFields.expYear, "yy") ||
fillFields.expYear.maxLength === 2
) {
if (expYear.length === 4) {
expYear = expYear.substr(2);
}
}
2018-03-16 02:16:40 +01:00
filledFields[fillFields.expYear.opid] = fillFields.expYear;
AutofillService.fillByOpid(fillScript, fillFields.expYear, expYear);
2018-03-16 02:16:40 +01:00
}
if (
fillFields.exp &&
AutofillService.hasValue(card.expMonth) &&
AutofillService.hasValue(card.expYear)
) {
const fullMonth = ("0" + card.expMonth).slice(-2);
let fullYear: string = card.expYear;
let partYear: string = null;
if (fullYear.length === 2) {
partYear = fullYear;
fullYear = "20" + fullYear;
} else if (fullYear.length === 4) {
partYear = fullYear.substr(2, 2);
}
let exp: string = null;
for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) {
if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.MonthAbbr[i] +
"/" +
CreditCardAutoFillConstants.YearAbbrShort[i]
) &&
partYear != null
) {
exp = fullMonth + "/" + partYear;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.MonthAbbr[i] +
"/" +
CreditCardAutoFillConstants.YearAbbrLong[i]
)
) {
exp = fullMonth + "/" + fullYear;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.YearAbbrShort[i] +
"/" +
CreditCardAutoFillConstants.MonthAbbr[i]
) &&
partYear != null
) {
exp = partYear + "/" + fullMonth;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.YearAbbrLong[i] +
"/" +
CreditCardAutoFillConstants.MonthAbbr[i]
)
) {
exp = fullYear + "/" + fullMonth;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.MonthAbbr[i] +
"-" +
CreditCardAutoFillConstants.YearAbbrShort[i]
) &&
partYear != null
) {
exp = fullMonth + "-" + partYear;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.MonthAbbr[i] +
"-" +
CreditCardAutoFillConstants.YearAbbrLong[i]
)
) {
exp = fullMonth + "-" + fullYear;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.YearAbbrShort[i] +
"-" +
CreditCardAutoFillConstants.MonthAbbr[i]
) &&
partYear != null
) {
exp = partYear + "-" + fullMonth;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.YearAbbrLong[i] +
"-" +
CreditCardAutoFillConstants.MonthAbbr[i]
)
) {
exp = fullYear + "-" + fullMonth;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.YearAbbrShort[i] + CreditCardAutoFillConstants.MonthAbbr[i]
) &&
partYear != null
) {
exp = partYear + fullMonth;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.YearAbbrLong[i] + CreditCardAutoFillConstants.MonthAbbr[i]
)
) {
exp = fullYear + fullMonth;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.MonthAbbr[i] + CreditCardAutoFillConstants.YearAbbrShort[i]
) &&
partYear != null
) {
exp = fullMonth + partYear;
} else if (
this.fieldAttrsContain(
fillFields.exp,
CreditCardAutoFillConstants.MonthAbbr[i] + CreditCardAutoFillConstants.YearAbbrLong[i]
)
) {
exp = fullMonth + fullYear;
2017-11-06 20:16:06 +01:00
}
if (exp != null) {
break;
}
}
2017-11-06 20:16:06 +01:00
if (exp == null) {
exp = fullYear + "-" + fullMonth;
}
this.makeScriptActionWithValue(fillScript, exp, fillFields.exp, filledFields);
}
return fillScript;
}
2017-11-06 20:16:06 +01:00
/**
* Determines whether to warn the user about filling an iframe
* @param pageUrl The url of the page/iframe, usually from AutofillPageDetails
* @param tabUrl The url of the tab, usually from the message sender (should not come from a content script because
* that is likely to be incorrect in the case of iframes)
* @param loginItem The cipher to be filled
* @returns `true` if the iframe is untrusted and the warning should be shown, `false` otherwise
*/
iframeUrlMatches(pageUrl: string, loginItem: CipherView, defaultUriMatch: UriMatchType): boolean {
// Check the pageUrl against cipher URIs using the configured match detection.
// If we are in this function at all, it is assumed that the tabUrl already matches a URL for `loginItem`,
// need to verify the pageUrl also matches one of the saved URIs using the match detection selected.
const uriMatched = loginItem.login.uris?.some((uri) =>
this.uriMatches(uri, pageUrl, defaultUriMatch)
);
return uriMatched;
}
// TODO should this be put in a common place (Utils maybe?) to be used both here and by CipherService?
private uriMatches(uri: LoginUriView, url: string, defaultUriMatch: UriMatchType): boolean {
const matchType = uri.match ?? defaultUriMatch;
const matchDomains = [Utils.getDomain(url)];
const equivalentDomains = this.settingsService.getEquivalentDomains(url);
if (equivalentDomains != null) {
matchDomains.push(...equivalentDomains);
}
switch (matchType) {
case UriMatchType.Domain:
if (url != null && uri.domain != null && matchDomains.includes(uri.domain)) {
if (Utils.DomainMatchBlacklist.has(uri.domain)) {
const domainUrlHost = Utils.getHost(url);
if (!Utils.DomainMatchBlacklist.get(uri.domain).has(domainUrlHost)) {
return true;
}
} else {
return true;
}
}
break;
case UriMatchType.Host: {
const urlHost = Utils.getHost(url);
if (urlHost != null && urlHost === Utils.getHost(uri.uri)) {
return true;
}
break;
}
case UriMatchType.Exact:
if (url === uri.uri) {
return true;
}
break;
case UriMatchType.StartsWith:
if (url.startsWith(uri.uri)) {
return true;
}
break;
case UriMatchType.RegularExpression:
try {
const regex = new RegExp(uri.uri, "i");
if (regex.test(url)) {
return true;
}
} catch (e) {
this.logService.error(e);
return false;
}
break;
case UriMatchType.Never:
default:
break;
}
return false;
}
private fieldAttrsContain(field: AutofillField, containsVal: string) {
if (!field) {
return false;
}
2017-11-06 20:16:06 +01:00
let doesContain = false;
CreditCardAutoFillConstants.CardAttributesExtended.forEach((attr) => {
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (doesContain || !field.hasOwnProperty(attr) || !field[attr]) {
return;
}
let val = field[attr];
val = val.replace(/ /g, "").toLowerCase();
doesContain = val.indexOf(containsVal) > -1;
});
return doesContain;
}
private generateIdentityFillScript(
fillScript: AutofillScript,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: GenerateFillScriptOptions
): AutofillScript {
if (!options.cipher.identity) {
return null;
}
2017-11-06 20:16:06 +01:00
const fillFields: { [id: string]: AutofillField } = {};
2017-11-06 20:16:06 +01:00
pageDetails.fields.forEach((f) => {
if (AutofillService.forCustomFieldsOnly(f)) {
return;
}
2017-11-06 20:16:06 +01:00
if (this.isExcludedType(f.type, AutoFillConstants.ExcludedAutofillTypes)) {
return;
}
2017-11-06 20:16:06 +01:00
for (let i = 0; i < IdentityAutoFillConstants.IdentityAttributes.length; i++) {
const attr = IdentityAutoFillConstants.IdentityAttributes[i];
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (!f.hasOwnProperty(attr) || !f[attr] || !f.viewable) {
continue;
2017-11-06 20:16:06 +01:00
}
// ref https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill
// ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/
if (
!fillFields.name &&
AutofillService.isFieldMatch(
f[attr],
IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues
)
) {
fillFields.name = f;
break;
} else if (
!fillFields.firstName &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.FirstnameFieldNames)
) {
fillFields.firstName = f;
break;
} else if (
!fillFields.middleName &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.MiddlenameFieldNames)
) {
fillFields.middleName = f;
break;
} else if (
!fillFields.lastName &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.LastnameFieldNames)
) {
fillFields.lastName = f;
break;
} else if (
!fillFields.title &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.TitleFieldNames)
) {
fillFields.title = f;
break;
} else if (
!fillFields.email &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.EmailFieldNames)
) {
fillFields.email = f;
break;
} else if (
!fillFields.address &&
AutofillService.isFieldMatch(
f[attr],
IdentityAutoFillConstants.AddressFieldNames,
IdentityAutoFillConstants.AddressFieldNameValues
)
) {
fillFields.address = f;
break;
} else if (
!fillFields.address1 &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address1FieldNames)
) {
fillFields.address1 = f;
break;
} else if (
!fillFields.address2 &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address2FieldNames)
) {
fillFields.address2 = f;
break;
} else if (
!fillFields.address3 &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address3FieldNames)
) {
fillFields.address3 = f;
break;
} else if (
!fillFields.postalCode &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PostalCodeFieldNames)
) {
fillFields.postalCode = f;
break;
} else if (
!fillFields.city &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CityFieldNames)
) {
fillFields.city = f;
break;
} else if (
!fillFields.state &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.StateFieldNames)
) {
fillFields.state = f;
break;
} else if (
!fillFields.country &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CountryFieldNames)
) {
fillFields.country = f;
break;
} else if (
!fillFields.phone &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PhoneFieldNames)
) {
fillFields.phone = f;
break;
} else if (
!fillFields.username &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.UserNameFieldNames)
) {
fillFields.username = f;
break;
} else if (
!fillFields.company &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CompanyFieldNames)
) {
fillFields.company = f;
break;
2017-11-06 20:16:06 +01:00
}
}
});
const identity = options.cipher.identity;
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "title");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "firstName");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "middleName");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "lastName");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address1");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address2");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address3");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "city");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "postalCode");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "company");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "email");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "phone");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "username");
let filledState = false;
if (fillFields.state && identity.state && identity.state.length > 2) {
const stateLower = identity.state.toLowerCase();
const isoState =
IdentityAutoFillConstants.IsoStates[stateLower] ||
IdentityAutoFillConstants.IsoProvinces[stateLower];
if (isoState) {
filledState = true;
this.makeScriptActionWithValue(fillScript, isoState, fillFields.state, filledFields);
}
2017-11-06 20:16:06 +01:00
}
if (!filledState) {
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "state");
}
let filledCountry = false;
if (fillFields.country && identity.country && identity.country.length > 2) {
const countryLower = identity.country.toLowerCase();
const isoCountry = IdentityAutoFillConstants.IsoCountries[countryLower];
if (isoCountry) {
filledCountry = true;
this.makeScriptActionWithValue(fillScript, isoCountry, fillFields.country, filledFields);
}
}
if (!filledCountry) {
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "country");
}
if (fillFields.name && (identity.firstName || identity.lastName)) {
let fullName = "";
if (AutofillService.hasValue(identity.firstName)) {
fullName = identity.firstName;
}
if (AutofillService.hasValue(identity.middleName)) {
if (fullName !== "") {
fullName += " ";
}
fullName += identity.middleName;
}
if (AutofillService.hasValue(identity.lastName)) {
if (fullName !== "") {
fullName += " ";
2017-11-06 20:16:06 +01:00
}
fullName += identity.lastName;
}
2017-11-06 20:16:06 +01:00
this.makeScriptActionWithValue(fillScript, fullName, fillFields.name, filledFields);
2017-11-06 20:16:06 +01:00
}
if (fillFields.address && AutofillService.hasValue(identity.address1)) {
let address = "";
if (AutofillService.hasValue(identity.address1)) {
address = identity.address1;
}
if (AutofillService.hasValue(identity.address2)) {
if (address !== "") {
address += ", ";
}
address += identity.address2;
}
if (AutofillService.hasValue(identity.address3)) {
if (address !== "") {
address += ", ";
2017-11-06 20:16:06 +01:00
}
address += identity.address3;
}
2017-11-06 20:16:06 +01:00
this.makeScriptActionWithValue(fillScript, address, fillFields.address, filledFields);
2017-11-06 20:16:06 +01:00
}
return fillScript;
}
private isExcludedType(type: string, excludedTypes: string[]) {
return excludedTypes.indexOf(type) > -1;
}
private static isFieldMatch(
value: string,
options: string[],
containsOptions?: string[]
): boolean {
value = value
.trim()
.toLowerCase()
.replace(/[^a-zA-Z0-9]+/g, "");
for (let i = 0; i < options.length; i++) {
let option = options[i];
const checkValueContains = containsOptions == null || containsOptions.indexOf(option) > -1;
option = option.toLowerCase().replace(/-/g, "");
if (value === option || (checkValueContains && value.indexOf(option) > -1)) {
return true;
}
}
return false;
}
private makeScriptAction(
fillScript: AutofillScript,
cipherData: any,
fillFields: { [id: string]: AutofillField },
filledFields: { [id: string]: AutofillField },
dataProp: string,
fieldProp?: string
) {
fieldProp = fieldProp || dataProp;
this.makeScriptActionWithValue(
fillScript,
cipherData[dataProp],
fillFields[fieldProp],
filledFields
);
}
private makeScriptActionWithValue(
fillScript: AutofillScript,
dataValue: any,
field: AutofillField,
filledFields: { [id: string]: AutofillField }
) {
let doFill = false;
if (AutofillService.hasValue(dataValue) && field) {
if (field.type === "select-one" && field.selectInfo && field.selectInfo.options) {
for (let i = 0; i < field.selectInfo.options.length; i++) {
const option = field.selectInfo.options[i];
for (let j = 0; j < option.length; j++) {
if (
AutofillService.hasValue(option[j]) &&
option[j].toLowerCase() === dataValue.toLowerCase()
) {
doFill = true;
if (option.length > 1) {
dataValue = option[1];
}
break;
2017-11-13 19:07:59 +01:00
}
}
2017-11-13 19:07:59 +01:00
if (doFill) {
break;
}
}
} else {
doFill = true;
}
2017-11-13 19:07:59 +01:00
}
if (doFill) {
filledFields[field.opid] = field;
AutofillService.fillByOpid(fillScript, field, dataValue);
}
}
static loadPasswordFields(
pageDetails: AutofillPageDetails,
canBeHidden: boolean,
canBeReadOnly: boolean,
mustBeEmpty: boolean,
fillNewPassword: boolean
) {
const arr: AutofillField[] = [];
pageDetails.fields.forEach((f) => {
if (AutofillService.forCustomFieldsOnly(f)) {
return;
}
const isPassword = f.type === "password";
const valueIsLikePassword = (value: string) => {
if (value == null) {
return false;
}
// Removes all whitespace, _ and - characters
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
const cleanedValue = value.toLowerCase().replace(/[\s_\-]/g, "");
if (cleanedValue.indexOf("password") < 0) {
return false;
2017-11-06 20:16:06 +01:00
}
2017-11-13 19:07:59 +01:00
if (AutoFillConstants.PasswordFieldIgnoreList.some((i) => cleanedValue.indexOf(i) > -1)) {
return false;
2017-11-06 20:16:06 +01:00
}
return true;
};
const isLikePassword = () => {
if (f.type !== "text") {
return false;
}
if (valueIsLikePassword(f.htmlID)) {
return true;
}
if (valueIsLikePassword(f.htmlName)) {
return true;
}
if (valueIsLikePassword(f.placeholder)) {
return true;
}
return false;
};
if (
!f.disabled &&
(canBeReadOnly || !f.readonly) &&
(isPassword || isLikePassword()) &&
(canBeHidden || f.viewable) &&
(!mustBeEmpty || f.value == null || f.value.trim() === "") &&
(fillNewPassword || f.autoCompleteType !== "new-password")
) {
arr.push(f);
}
});
return arr;
}
private findUsernameField(
pageDetails: AutofillPageDetails,
passwordField: AutofillField,
canBeHidden: boolean,
canBeReadOnly: boolean,
withoutForm: boolean
) {
let usernameField: AutofillField = null;
for (let i = 0; i < pageDetails.fields.length; i++) {
const f = pageDetails.fields[i];
if (AutofillService.forCustomFieldsOnly(f)) {
continue;
}
if (f.elementNumber >= passwordField.elementNumber) {
break;
}
if (
!f.disabled &&
(canBeReadOnly || !f.readonly) &&
(withoutForm || f.form === passwordField.form) &&
(canBeHidden || f.viewable) &&
(f.type === "text" || f.type === "email" || f.type === "tel")
) {
usernameField = f;
if (this.findMatchingFieldIndex(f, AutoFillConstants.UsernameFieldNames) > -1) {
// We found an exact match. No need to keep looking.
break;
}
}
2017-11-06 20:16:06 +01:00
}
return usernameField;
}
private findMatchingFieldIndex(field: AutofillField, names: string[]): number {
for (let i = 0; i < names.length; i++) {
if (names[i].indexOf("=") > -1) {
if (this.fieldPropertyIsPrefixMatch(field, "htmlID", names[i], "id")) {
return i;
2017-11-06 20:16:06 +01:00
}
if (this.fieldPropertyIsPrefixMatch(field, "htmlName", names[i], "name")) {
return i;
2017-11-06 20:16:06 +01:00
}
if (this.fieldPropertyIsPrefixMatch(field, "label-tag", names[i], "label")) {
return i;
2017-11-06 20:16:06 +01:00
}
if (this.fieldPropertyIsPrefixMatch(field, "label-aria", names[i], "label")) {
return i;
}
if (this.fieldPropertyIsPrefixMatch(field, "placeholder", names[i], "placeholder")) {
return i;
}
}
if (this.fieldPropertyIsMatch(field, "htmlID", names[i])) {
return i;
}
if (this.fieldPropertyIsMatch(field, "htmlName", names[i])) {
return i;
}
if (this.fieldPropertyIsMatch(field, "label-tag", names[i])) {
return i;
}
if (this.fieldPropertyIsMatch(field, "label-aria", names[i])) {
return i;
}
if (this.fieldPropertyIsMatch(field, "placeholder", names[i])) {
return i;
}
}
return -1;
}
private fieldPropertyIsPrefixMatch(
field: any,
property: string,
name: string,
prefix: string,
separator = "="
): boolean {
if (name.indexOf(prefix + separator) === 0) {
const sepIndex = name.indexOf(separator);
const val = name.substring(sepIndex + 1);
return val != null && this.fieldPropertyIsMatch(field, property, val);
}
return false;
}
private fieldPropertyIsMatch(field: any, property: string, name: string): boolean {
let fieldVal = field[property] as string;
if (!AutofillService.hasValue(fieldVal)) {
return false;
}
fieldVal = fieldVal.trim().replace(/(?:\r\n|\r|\n)/g, "");
if (name.startsWith("regex=")) {
try {
const regexParts = name.split("=", 2);
if (regexParts.length === 2) {
const regex = new RegExp(regexParts[1], "i");
return regex.test(fieldVal);
2017-11-06 20:16:06 +01:00
}
} catch (e) {
this.logService.error(e);
}
} else if (name.startsWith("csv=")) {
const csvParts = name.split("=", 2);
if (csvParts.length === 2) {
const csvVals = csvParts[1].split(",");
for (let i = 0; i < csvVals.length; i++) {
const val = csvVals[i];
if (val != null && val.trim().toLowerCase() === fieldVal.toLowerCase()) {
2018-11-25 04:54:30 +01:00
return true;
}
2018-11-25 04:54:30 +01:00
}
2017-11-06 20:16:06 +01:00
return false;
}
2017-11-06 20:16:06 +01:00
}
return fieldVal.toLowerCase() === name;
}
2017-11-06 20:16:06 +01:00
static fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean {
if (AutofillService.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) {
return true;
}
if (AutofillService.hasValue(field.htmlName) && this.fuzzyMatch(names, field.htmlName)) {
return true;
}
if (
AutofillService.hasValue(field["label-tag"]) &&
this.fuzzyMatch(names, field["label-tag"])
) {
return true;
}
if (AutofillService.hasValue(field.placeholder) && this.fuzzyMatch(names, field.placeholder)) {
return true;
}
if (
AutofillService.hasValue(field["label-left"]) &&
this.fuzzyMatch(names, field["label-left"])
) {
return true;
}
if (
AutofillService.hasValue(field["label-top"]) &&
this.fuzzyMatch(names, field["label-top"])
) {
return true;
}
if (
AutofillService.hasValue(field["label-aria"]) &&
this.fuzzyMatch(names, field["label-aria"])
) {
return true;
}
return false;
}
2017-11-06 20:16:06 +01:00
private static fuzzyMatch(options: string[], value: string): boolean {
if (options == null || options.length === 0 || value == null || value === "") {
return false;
2017-11-06 20:16:06 +01:00
}
value = value
.replace(/(?:\r\n|\r|\n)/g, "")
.trim()
.toLowerCase();
for (let i = 0; i < options.length; i++) {
if (value.indexOf(options[i]) > -1) {
return true;
}
}
return false;
}
2017-11-06 20:16:06 +01:00
static hasValue(str: string): boolean {
return str && str !== "";
}
2017-11-06 20:16:06 +01:00
static setFillScriptForFocus(
filledFields: { [id: string]: AutofillField },
fillScript: AutofillScript
): AutofillScript {
let lastField: AutofillField = null;
let lastPasswordField: AutofillField = null;
2017-11-06 20:16:06 +01:00
for (const opid in filledFields) {
2022-02-24 18:14:04 +01:00
// eslint-disable-next-line
if (filledFields.hasOwnProperty(opid) && filledFields[opid].viewable) {
lastField = filledFields[opid];
2017-11-06 20:16:06 +01:00
if (filledFields[opid].type === "password") {
lastPasswordField = filledFields[opid];
}
}
2017-11-06 20:16:06 +01:00
}
2019-04-21 03:29:13 +02:00
// Prioritize password field over others.
if (lastPasswordField) {
fillScript.script.push(["focus_by_opid", lastPasswordField.opid]);
} else if (lastField) {
fillScript.script.push(["focus_by_opid", lastField.opid]);
2019-04-21 03:29:13 +02:00
}
return fillScript;
}
static fillByOpid(fillScript: AutofillScript, field: AutofillField, value: string): void {
if (field.maxLength && value && value.length > field.maxLength) {
value = value.substr(0, value.length);
}
if (field.tagName !== "span") {
fillScript.script.push(["click_on_opid", field.opid]);
fillScript.script.push(["focus_by_opid", field.opid]);
}
fillScript.script.push(["fill_by_opid", field.opid, value]);
}
static forCustomFieldsOnly(field: AutofillField): boolean {
return field.tagName === "span";
}
2017-11-06 20:16:06 +01:00
}