[PS-816] Add Autofill Shortcut to MV3 Extension (#3131)

* Work on background service worker.

* Work on shortcuts

* Work on supporting service worker

* Put new background behind version check

* Fix build

* Use new storage service

* create commands from crypto service (#2995)

* Work on service worker autofill

* Got basic autofill working

* Final touches

* Work on tests

* Revert some changes

* Add modifications

* Remove unused ciphers for now

* Cleanup

* Address PR feedback

* Update lock file

* Update noop service

* Add chrome type

* Handle "/" in branch names

Updates web workflow to handle the `/` in branch names when it's a PR.

* Remove any

Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Micaiah Martin <77340197+mimartin12@users.noreply.github.com>
This commit is contained in:
Justin Baur 2022-08-09 21:30:26 -04:00 committed by GitHub
parent cfc8858ef9
commit 43d428b3df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 488 additions and 183 deletions

View File

@ -1,7 +1,7 @@
{ {
"extends": "../tsconfig", "extends": "../tsconfig",
"compilerOptions": { "compilerOptions": {
"types": ["node", "jest"], "types": ["node", "jest", "chrome"],
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"], "exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"],

View File

@ -1,6 +1,13 @@
import MainBackground from "./background/main.background"; import MainBackground from "./background/main.background";
import { onCommandListener } from "./listeners/onCommandListener";
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground()); const manifest = chrome.runtime.getManifest();
bitwardenMain.bootstrap().then(() => {
// Finished bootstrapping if (manifest.manifest_version === 3) {
}); chrome.commands.onCommand.addListener(onCommandListener);
} else {
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
bitwardenMain.bootstrap().then(() => {
// Finished bootstrapping
});
}

View File

@ -81,6 +81,10 @@ export default class ContextMenusBackground {
} }
private async cipherAction(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) { private async cipherAction(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
if (typeof info.menuItemId !== "string") {
return;
}
const id = info.menuItemId.split("_")[1]; const id = info.menuItemId.split("_")[1];
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {

View File

@ -14,7 +14,10 @@ export default class WebRequestBackground {
private cipherService: CipherService, private cipherService: CipherService,
private authService: AuthService private authService: AuthService
) { ) {
this.webRequest = (window as any).chrome.webRequest; const manifest = chrome.runtime.getManifest();
if (manifest.manifest_version === 2) {
this.webRequest = (window as any).chrome.webRequest;
}
this.isFirefox = platformUtilsService.isFirefox(); this.isFirefox = platformUtilsService.isFirefox();
} }

View File

@ -0,0 +1,44 @@
import AutofillPageDetails from "../models/autofillPageDetails";
import { AutofillService } from "../services/abstractions/autofill.service";
export class AutoFillActiveTabCommand {
constructor(private autofillService: AutofillService) {}
async doAutoFillActiveTabCommand(tab: chrome.tabs.Tab) {
if (!tab.id) {
throw new Error("Tab does not have an id, cannot complete autofill.");
}
const details = await this.collectPageDetails(tab.id);
await this.autofillService.doAutoFillOnTab(
[
{
frameId: 0,
tab: tab,
details: details,
},
],
tab,
true
);
}
private async collectPageDetails(tabId: number): Promise<AutofillPageDetails> {
return new Promise((resolve, reject) => {
chrome.tabs.sendMessage(
tabId,
{
command: "collectPageDetailsImmediately",
},
(response: AutofillPageDetails) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve(response);
}
);
});
}
}

View File

@ -39,6 +39,7 @@
6. Rename com.agilebits.* stuff to com.bitwarden.* 6. Rename com.agilebits.* stuff to com.bitwarden.*
7. Remove "some useful globals" on window 7. Remove "some useful globals" on window
8. Add ability to autofill span[data-bwautofill] elements 8. Add ability to autofill span[data-bwautofill] elements
9. Add new handler, for new command that responds with page details in response callback
*/ */
function collect(document, undefined) { function collect(document, undefined) {
@ -1037,6 +1038,11 @@
fill(document, msg.fillScript); fill(document, msg.fillScript);
sendResponse(); sendResponse();
return true; return true;
} else if (msg.command === 'collectPageDetailsImmediately') {
var pageDetails = collect(document);
var pageDetailsObj = JSON.parse(pageDetails);
sendResponse(pageDetailsObj);
return true;
} }
}); });
})(); })();

View File

@ -0,0 +1,140 @@
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
import { AuthService } from "@bitwarden/common/services/auth.service";
import { CipherService } from "@bitwarden/common/services/cipher.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
import NoOpEventService from "@bitwarden/common/services/noOpEvent.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFunction.service";
import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand";
import { Account } from "../models/account";
import { StateService as AbstractStateService } from "../services/abstractions/state.service";
import AutofillService from "../services/autofill.service";
import { BrowserCryptoService } from "../services/browserCrypto.service";
import BrowserLocalStorageService from "../services/browserLocalStorage.service";
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import I18nService from "../services/i18n.service";
import { KeyGenerationService } from "../services/keyGeneration.service";
import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service";
import { StateService } from "../services/state.service";
export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) => {
switch (command) {
case "autofill_login":
await doAutoFillLogin(tab);
break;
}
};
const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise<void> => {
const logService = new ConsoleLogService(false);
const cryptoFunctionService = new WebCryptoFunctionService(self);
const storageService = new BrowserLocalStorageService();
const secureStorageService = new BrowserLocalStorageService();
const memoryStorageService = new LocalBackedSessionStorageService(
new EncryptService(cryptoFunctionService, logService, false),
new KeyGenerationService(cryptoFunctionService)
);
const stateFactory = new StateFactory(GlobalState, Account);
const stateMigrationService = new StateMigrationService(
storageService,
secureStorageService,
stateFactory
);
const stateService: AbstractStateService = new StateService(
storageService,
secureStorageService,
memoryStorageService, // AbstractStorageService
logService,
stateMigrationService,
stateFactory
);
await stateService.init();
const platformUtils = new BrowserPlatformUtilsService(
null, // MessagingService
stateService,
null, // clipboardWriteCallback
null // biometricCallback
);
const cryptoService = new BrowserCryptoService(
cryptoFunctionService,
null, // AbstractEncryptService
platformUtils,
logService,
stateService
);
const settingsService = new SettingsService(stateService);
const i18nService = new I18nService(chrome.i18n.getUILanguage());
await i18nService.init();
// Don't love this pt.1
let searchService: SearchService = null;
const cipherService = new CipherService(
cryptoService,
settingsService,
null, // ApiService
null, // FileUploadService,
i18nService,
() => searchService, // Don't love this pt.2
logService,
stateService
);
// Don't love this pt.3
searchService = new SearchService(cipherService, logService, i18nService);
// TODO: Remove this before we encourage anyone to start using this
const eventService = new NoOpEventService();
const autofillService = new AutofillService(
cipherService,
stateService,
null, // TotpService
eventService,
logService
);
const authService = new AuthService(
cryptoService, // CryptoService
null, // ApiService
null, // TokenService
null, // AppIdService
platformUtils,
null, // MessagingService
logService,
null, // KeyConnectorService
null, // EnvironmentService
stateService,
null, // TwoFactorService
i18nService
);
const authStatus = await authService.getAuthStatus();
if (authStatus < AuthenticationStatus.Unlocked) {
// TODO: Add back in unlock on autofill
logService.info("Currently not unlocked, MV3 does not support unlock on autofill currently.");
return;
}
const command = new AutoFillActiveTabCommand(autofillService);
await command.doAutoFillActiveTabCommand(tab);
};

View File

@ -47,7 +47,8 @@
} }
], ],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js",
"type": "module"
}, },
"action": { "action": {
"default_icon": { "default_icon": {

View File

@ -22,4 +22,5 @@ export default class AutofillField {
selectInfo: any; selectInfo: any;
maxLength: number; maxLength: number;
tagName: string; tagName: string;
[key: string]: any;
} }

View File

@ -1,7 +1,7 @@
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
export default abstract class AbstractChromeStorageService implements AbstractStorageService { export default abstract class AbstractChromeStorageService implements AbstractStorageService {
protected abstract chromeStorageApi: any; protected abstract chromeStorageApi: chrome.storage.StorageArea;
async get<T>(key: string): Promise<T> { async get<T>(key: string): Promise<T> {
return new Promise((resolve) => { return new Promise((resolve) => {

View File

@ -1,7 +1,41 @@
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import AutofillField from "../../models/autofillField";
import AutofillForm from "../../models/autofillForm";
import AutofillPageDetails from "../../models/autofillPageDetails"; import AutofillPageDetails from "../../models/autofillPageDetails";
export abstract class AutofillService { export interface PageDetail {
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => any[]; frameId: number;
doAutoFill: (options: any) => Promise<string>; tab: chrome.tabs.Tab;
doAutoFillActiveTab: (pageDetails: any, fromCommand: boolean) => Promise<string>; details: AutofillPageDetails;
}
export interface AutoFillOptions {
cipher: CipherView;
pageDetails: PageDetail[];
doc?: typeof window.document;
tab: chrome.tabs.Tab;
skipUsernameOnlyFill?: boolean;
onlyEmptyFields?: boolean;
onlyVisibleFields?: boolean;
fillNewPassword?: boolean;
skipLastUsed?: boolean;
}
export interface FormData {
form: AutofillForm;
password: AutofillField;
username: AutofillField;
passwords: AutofillField[];
}
export abstract class AutofillService {
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
doAutoFill: (options: AutoFillOptions) => Promise<string>;
doAutoFillOnTab: (
pageDetails: PageDetail[],
tab: chrome.tabs.Tab,
fromCommand: boolean
) => Promise<string>;
doAutoFillActiveTab: (pageDetails: PageDetail[], fromCommand: boolean) => Promise<string>;
} }

View File

@ -15,13 +15,26 @@ import AutofillPageDetails from "../models/autofillPageDetails";
import AutofillScript from "../models/autofillScript"; import AutofillScript from "../models/autofillScript";
import { StateService } from "../services/abstractions/state.service"; import { StateService } from "../services/abstractions/state.service";
import { AutofillService as AutofillServiceInterface } from "./abstractions/autofill.service"; import {
AutoFillOptions,
AutofillService as AutofillServiceInterface,
PageDetail,
FormData,
} from "./abstractions/autofill.service";
import { import {
AutoFillConstants, AutoFillConstants,
CreditCardAutoFillConstants, CreditCardAutoFillConstants,
IdentityAutoFillConstants, IdentityAutoFillConstants,
} from "./autofillConstants"; } from "./autofillConstants";
export interface GenerateFillScriptOptions {
skipUsernameOnlyFill: boolean;
onlyEmptyFields: boolean;
onlyVisibleFields: boolean;
fillNewPassword: boolean;
cipher: CipherView;
}
export default class AutofillService implements AutofillServiceInterface { export default class AutofillService implements AutofillServiceInterface {
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
@ -31,10 +44,16 @@ export default class AutofillService implements AutofillServiceInterface {
private logService: LogService private logService: LogService
) {} ) {}
getFormsWithPasswordFields(pageDetails: AutofillPageDetails): any[] { getFormsWithPasswordFields(pageDetails: AutofillPageDetails): FormData[] {
const formData: any[] = []; const formData: FormData[] = [];
const passwordFields = this.loadPasswordFields(pageDetails, true, true, false, false); const passwordFields = AutofillService.loadPasswordFields(
pageDetails,
true,
true,
false,
false
);
if (passwordFields.length === 0) { if (passwordFields.length === 0) {
return formData; return formData;
} }
@ -64,16 +83,17 @@ export default class AutofillService implements AutofillServiceInterface {
return formData; return formData;
} }
async doAutoFill(options: any) { async doAutoFill(options: AutoFillOptions) {
let totpPromise: Promise<string> = null;
const tab = options.tab; const tab = options.tab;
if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) { if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) {
throw new Error("Nothing to auto-fill."); throw new Error("Nothing to auto-fill.");
} }
let totpPromise: Promise<string> = null;
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await this.stateService.getCanAccessPremium();
let didAutofill = false; let didAutofill = false;
options.pageDetails.forEach((pd: any) => { options.pageDetails.forEach((pd) => {
// make sure we're still on correct tab // make sure we're still on correct tab
if (pd.tab.id !== tab.id || pd.tab.url !== tab.url) { if (pd.tab.id !== tab.id || pd.tab.url !== tab.url) {
return; return;
@ -138,12 +158,7 @@ export default class AutofillService implements AutofillServiceInterface {
} }
} }
async doAutoFillActiveTab(pageDetails: any, fromCommand: boolean) { async doAutoFillOnTab(pageDetails: PageDetail[], tab: chrome.tabs.Tab, fromCommand: boolean) {
const tab = await this.getActiveTab();
if (!tab || !tab.url) {
return;
}
let cipher: CipherView; let cipher: CipherView;
if (fromCommand) { if (fromCommand) {
cipher = await this.cipherService.getNextCipherForUrl(tab.url); cipher = await this.cipherService.getNextCipherForUrl(tab.url);
@ -186,9 +201,18 @@ export default class AutofillService implements AutofillServiceInterface {
return totpCode; return totpCode;
} }
async doAutoFillActiveTab(pageDetails: PageDetail[], fromCommand: boolean) {
const tab = await this.getActiveTab();
if (!tab || !tab.url) {
return;
}
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
}
// Helpers // Helpers
private async getActiveTab(): Promise<any> { private async getActiveTab(): Promise<chrome.tabs.Tab> {
const tab = await BrowserApi.getTabFromCurrentWindow(); const tab = await BrowserApi.getTabFromCurrentWindow();
if (!tab) { if (!tab) {
throw new Error("No tab found."); throw new Error("No tab found.");
@ -197,7 +221,10 @@ export default class AutofillService implements AutofillServiceInterface {
return tab; return tab;
} }
private generateFillScript(pageDetails: AutofillPageDetails, options: any): AutofillScript { private generateFillScript(
pageDetails: AutofillPageDetails,
options: GenerateFillScriptOptions
): AutofillScript {
if (!pageDetails || !options.cipher) { if (!pageDetails || !options.cipher) {
return null; return null;
} }
@ -209,13 +236,13 @@ export default class AutofillService implements AutofillServiceInterface {
if (fields && fields.length) { if (fields && fields.length) {
const fieldNames: string[] = []; const fieldNames: string[] = [];
fields.forEach((f: any) => { fields.forEach((f) => {
if (this.hasValue(f.name)) { if (AutofillService.hasValue(f.name)) {
fieldNames.push(f.name.toLowerCase()); fieldNames.push(f.name.toLowerCase());
} }
}); });
pageDetails.fields.forEach((field: any) => { pageDetails.fields.forEach((field) => {
// eslint-disable-next-line // eslint-disable-next-line
if (filledFields.hasOwnProperty(field.opid)) { if (filledFields.hasOwnProperty(field.opid)) {
return; return;
@ -228,10 +255,10 @@ export default class AutofillService implements AutofillServiceInterface {
const matchingIndex = this.findMatchingFieldIndex(field, fieldNames); const matchingIndex = this.findMatchingFieldIndex(field, fieldNames);
if (matchingIndex > -1) { if (matchingIndex > -1) {
const matchingField: FieldView = fields[matchingIndex]; const matchingField: FieldView = fields[matchingIndex];
let val; let val: string;
if (matchingField.type === FieldType.Linked) { if (matchingField.type === FieldType.Linked) {
// Assumption: Linked Field is not being used to autofill a boolean value // Assumption: Linked Field is not being used to autofill a boolean value
val = options.cipher.linkedFieldValue(matchingField.linkedId); val = options.cipher.linkedFieldValue(matchingField.linkedId) as string;
} else { } else {
val = matchingField.value; val = matchingField.value;
if (val == null && matchingField.type === FieldType.Boolean) { if (val == null && matchingField.type === FieldType.Boolean) {
@ -240,7 +267,7 @@ export default class AutofillService implements AutofillServiceInterface {
} }
filledFields[field.opid] = field; filledFields[field.opid] = field;
this.fillByOpid(fillScript, field, val); AutofillService.fillByOpid(fillScript, field, val);
} }
}); });
} }
@ -269,9 +296,9 @@ export default class AutofillService implements AutofillServiceInterface {
private generateLoginFillScript( private generateLoginFillScript(
fillScript: AutofillScript, fillScript: AutofillScript,
pageDetails: any, pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField }, filledFields: { [id: string]: AutofillField },
options: any options: GenerateFillScriptOptions
): AutofillScript { ): AutofillScript {
if (!options.cipher.login) { if (!options.cipher.login) {
return null; return null;
@ -285,11 +312,11 @@ export default class AutofillService implements AutofillServiceInterface {
if (!login.password || login.password === "") { if (!login.password || login.password === "") {
// No password for this login. Maybe they just wanted to auto-fill some custom fields? // No password for this login. Maybe they just wanted to auto-fill some custom fields?
fillScript = this.setFillScriptForFocus(filledFields, fillScript); fillScript = AutofillService.setFillScriptForFocus(filledFields, fillScript);
return fillScript; return fillScript;
} }
let passwordFields = this.loadPasswordFields( let passwordFields = AutofillService.loadPasswordFields(
pageDetails, pageDetails,
false, false,
false, false,
@ -298,7 +325,7 @@ export default class AutofillService implements AutofillServiceInterface {
); );
if (!passwordFields.length && !options.onlyVisibleFields) { if (!passwordFields.length && !options.onlyVisibleFields) {
// not able to find any viewable password fields. maybe there are some "hidden" ones? // not able to find any viewable password fields. maybe there are some "hidden" ones?
passwordFields = this.loadPasswordFields( passwordFields = AutofillService.loadPasswordFields(
pageDetails, pageDetails,
true, true,
true, true,
@ -362,11 +389,11 @@ export default class AutofillService implements AutofillServiceInterface {
if (!passwordFields.length && !options.skipUsernameOnlyFill) { if (!passwordFields.length && !options.skipUsernameOnlyFill) {
// No password fields on this page. Let's try to just fuzzy fill the username. // No password fields on this page. Let's try to just fuzzy fill the username.
pageDetails.fields.forEach((f: any) => { pageDetails.fields.forEach((f) => {
if ( if (
f.viewable && f.viewable &&
(f.type === "text" || f.type === "email" || f.type === "tel") && (f.type === "text" || f.type === "email" || f.type === "tel") &&
this.fieldIsFuzzyMatch(f, AutoFillConstants.UsernameFieldNames) AutofillService.fieldIsFuzzyMatch(f, AutoFillConstants.UsernameFieldNames)
) { ) {
usernames.push(f); usernames.push(f);
} }
@ -380,7 +407,7 @@ export default class AutofillService implements AutofillServiceInterface {
} }
filledFields[u.opid] = u; filledFields[u.opid] = u;
this.fillByOpid(fillScript, u, login.username); AutofillService.fillByOpid(fillScript, u, login.username);
}); });
passwords.forEach((p) => { passwords.forEach((p) => {
@ -390,18 +417,18 @@ export default class AutofillService implements AutofillServiceInterface {
} }
filledFields[p.opid] = p; filledFields[p.opid] = p;
this.fillByOpid(fillScript, p, login.password); AutofillService.fillByOpid(fillScript, p, login.password);
}); });
fillScript = this.setFillScriptForFocus(filledFields, fillScript); fillScript = AutofillService.setFillScriptForFocus(filledFields, fillScript);
return fillScript; return fillScript;
} }
private generateCardFillScript( private generateCardFillScript(
fillScript: AutofillScript, fillScript: AutofillScript,
pageDetails: any, pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField }, filledFields: { [id: string]: AutofillField },
options: any options: GenerateFillScriptOptions
): AutofillScript { ): AutofillScript {
if (!options.cipher.card) { if (!options.cipher.card) {
return null; return null;
@ -409,8 +436,8 @@ export default class AutofillService implements AutofillServiceInterface {
const fillFields: { [id: string]: AutofillField } = {}; const fillFields: { [id: string]: AutofillField } = {};
pageDetails.fields.forEach((f: any) => { pageDetails.fields.forEach((f) => {
if (this.forCustomFieldsOnly(f)) { if (AutofillService.forCustomFieldsOnly(f)) {
return; return;
} }
@ -429,7 +456,7 @@ export default class AutofillService implements AutofillServiceInterface {
// ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/ // ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/
if ( if (
!fillFields.cardholderName && !fillFields.cardholderName &&
this.isFieldMatch( AutofillService.isFieldMatch(
f[attr], f[attr],
CreditCardAutoFillConstants.CardHolderFieldNames, CreditCardAutoFillConstants.CardHolderFieldNames,
CreditCardAutoFillConstants.CardHolderFieldNameValues CreditCardAutoFillConstants.CardHolderFieldNameValues
@ -439,7 +466,7 @@ export default class AutofillService implements AutofillServiceInterface {
break; break;
} else if ( } else if (
!fillFields.number && !fillFields.number &&
this.isFieldMatch( AutofillService.isFieldMatch(
f[attr], f[attr],
CreditCardAutoFillConstants.CardNumberFieldNames, CreditCardAutoFillConstants.CardNumberFieldNames,
CreditCardAutoFillConstants.CardNumberFieldNameValues CreditCardAutoFillConstants.CardNumberFieldNameValues
@ -449,7 +476,7 @@ export default class AutofillService implements AutofillServiceInterface {
break; break;
} else if ( } else if (
!fillFields.exp && !fillFields.exp &&
this.isFieldMatch( AutofillService.isFieldMatch(
f[attr], f[attr],
CreditCardAutoFillConstants.CardExpiryFieldNames, CreditCardAutoFillConstants.CardExpiryFieldNames,
CreditCardAutoFillConstants.CardExpiryFieldNameValues CreditCardAutoFillConstants.CardExpiryFieldNameValues
@ -459,25 +486,25 @@ export default class AutofillService implements AutofillServiceInterface {
break; break;
} else if ( } else if (
!fillFields.expMonth && !fillFields.expMonth &&
this.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryMonthFieldNames) AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryMonthFieldNames)
) { ) {
fillFields.expMonth = f; fillFields.expMonth = f;
break; break;
} else if ( } else if (
!fillFields.expYear && !fillFields.expYear &&
this.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryYearFieldNames) AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryYearFieldNames)
) { ) {
fillFields.expYear = f; fillFields.expYear = f;
break; break;
} else if ( } else if (
!fillFields.code && !fillFields.code &&
this.isFieldMatch(f[attr], CreditCardAutoFillConstants.CVVFieldNames) AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.CVVFieldNames)
) { ) {
fillFields.code = f; fillFields.code = f;
break; break;
} else if ( } else if (
!fillFields.brand && !fillFields.brand &&
this.isFieldMatch(f[attr], CreditCardAutoFillConstants.CardBrandFieldNames) AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.CardBrandFieldNames)
) { ) {
fillFields.brand = f; fillFields.brand = f;
break; break;
@ -491,7 +518,7 @@ export default class AutofillService implements AutofillServiceInterface {
this.makeScriptAction(fillScript, card, fillFields, filledFields, "code"); this.makeScriptAction(fillScript, card, fillFields, filledFields, "code");
this.makeScriptAction(fillScript, card, fillFields, filledFields, "brand"); this.makeScriptAction(fillScript, card, fillFields, filledFields, "brand");
if (fillFields.expMonth && this.hasValue(card.expMonth)) { if (fillFields.expMonth && AutofillService.hasValue(card.expMonth)) {
let expMonth: string = card.expMonth; let expMonth: string = card.expMonth;
if (fillFields.expMonth.selectInfo && fillFields.expMonth.selectInfo.options) { if (fillFields.expMonth.selectInfo && fillFields.expMonth.selectInfo.options) {
@ -526,10 +553,10 @@ export default class AutofillService implements AutofillServiceInterface {
} }
filledFields[fillFields.expMonth.opid] = fillFields.expMonth; filledFields[fillFields.expMonth.opid] = fillFields.expMonth;
this.fillByOpid(fillScript, fillFields.expMonth, expMonth); AutofillService.fillByOpid(fillScript, fillFields.expMonth, expMonth);
} }
if (fillFields.expYear && this.hasValue(card.expYear)) { if (fillFields.expYear && AutofillService.hasValue(card.expYear)) {
let expYear: string = card.expYear; let expYear: string = card.expYear;
if (fillFields.expYear.selectInfo && fillFields.expYear.selectInfo.options) { if (fillFields.expYear.selectInfo && fillFields.expYear.selectInfo.options) {
for (let i = 0; i < fillFields.expYear.selectInfo.options.length; i++) { for (let i = 0; i < fillFields.expYear.selectInfo.options.length; i++) {
@ -572,10 +599,14 @@ export default class AutofillService implements AutofillServiceInterface {
} }
filledFields[fillFields.expYear.opid] = fillFields.expYear; filledFields[fillFields.expYear.opid] = fillFields.expYear;
this.fillByOpid(fillScript, fillFields.expYear, expYear); AutofillService.fillByOpid(fillScript, fillFields.expYear, expYear);
} }
if (fillFields.exp && this.hasValue(card.expMonth) && this.hasValue(card.expYear)) { if (
fillFields.exp &&
AutofillService.hasValue(card.expMonth) &&
AutofillService.hasValue(card.expYear)
) {
const fullMonth = ("0" + card.expMonth).slice(-2); const fullMonth = ("0" + card.expMonth).slice(-2);
let fullYear: string = card.expYear; let fullYear: string = card.expYear;
@ -712,7 +743,7 @@ export default class AutofillService implements AutofillServiceInterface {
return fillScript; return fillScript;
} }
private fieldAttrsContain(field: any, containsVal: string) { private fieldAttrsContain(field: AutofillField, containsVal: string) {
if (!field) { if (!field) {
return false; return false;
} }
@ -734,9 +765,9 @@ export default class AutofillService implements AutofillServiceInterface {
private generateIdentityFillScript( private generateIdentityFillScript(
fillScript: AutofillScript, fillScript: AutofillScript,
pageDetails: any, pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField }, filledFields: { [id: string]: AutofillField },
options: any options: GenerateFillScriptOptions
): AutofillScript { ): AutofillScript {
if (!options.cipher.identity) { if (!options.cipher.identity) {
return null; return null;
@ -744,8 +775,8 @@ export default class AutofillService implements AutofillServiceInterface {
const fillFields: { [id: string]: AutofillField } = {}; const fillFields: { [id: string]: AutofillField } = {};
pageDetails.fields.forEach((f: any) => { pageDetails.fields.forEach((f) => {
if (this.forCustomFieldsOnly(f)) { if (AutofillService.forCustomFieldsOnly(f)) {
return; return;
} }
@ -764,7 +795,7 @@ export default class AutofillService implements AutofillServiceInterface {
// ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/ // ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/
if ( if (
!fillFields.name && !fillFields.name &&
this.isFieldMatch( AutofillService.isFieldMatch(
f[attr], f[attr],
IdentityAutoFillConstants.FullNameFieldNames, IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues IdentityAutoFillConstants.FullNameFieldNameValues
@ -774,37 +805,37 @@ export default class AutofillService implements AutofillServiceInterface {
break; break;
} else if ( } else if (
!fillFields.firstName && !fillFields.firstName &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.FirstnameFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.FirstnameFieldNames)
) { ) {
fillFields.firstName = f; fillFields.firstName = f;
break; break;
} else if ( } else if (
!fillFields.middleName && !fillFields.middleName &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.MiddlenameFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.MiddlenameFieldNames)
) { ) {
fillFields.middleName = f; fillFields.middleName = f;
break; break;
} else if ( } else if (
!fillFields.lastName && !fillFields.lastName &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.LastnameFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.LastnameFieldNames)
) { ) {
fillFields.lastName = f; fillFields.lastName = f;
break; break;
} else if ( } else if (
!fillFields.title && !fillFields.title &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.TitleFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.TitleFieldNames)
) { ) {
fillFields.title = f; fillFields.title = f;
break; break;
} else if ( } else if (
!fillFields.email && !fillFields.email &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.EmailFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.EmailFieldNames)
) { ) {
fillFields.email = f; fillFields.email = f;
break; break;
} else if ( } else if (
!fillFields.address && !fillFields.address &&
this.isFieldMatch( AutofillService.isFieldMatch(
f[attr], f[attr],
IdentityAutoFillConstants.AddressFieldNames, IdentityAutoFillConstants.AddressFieldNames,
IdentityAutoFillConstants.AddressFieldNameValues IdentityAutoFillConstants.AddressFieldNameValues
@ -814,61 +845,61 @@ export default class AutofillService implements AutofillServiceInterface {
break; break;
} else if ( } else if (
!fillFields.address1 && !fillFields.address1 &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.Address1FieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address1FieldNames)
) { ) {
fillFields.address1 = f; fillFields.address1 = f;
break; break;
} else if ( } else if (
!fillFields.address2 && !fillFields.address2 &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.Address2FieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address2FieldNames)
) { ) {
fillFields.address2 = f; fillFields.address2 = f;
break; break;
} else if ( } else if (
!fillFields.address3 && !fillFields.address3 &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.Address3FieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address3FieldNames)
) { ) {
fillFields.address3 = f; fillFields.address3 = f;
break; break;
} else if ( } else if (
!fillFields.postalCode && !fillFields.postalCode &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.PostalCodeFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PostalCodeFieldNames)
) { ) {
fillFields.postalCode = f; fillFields.postalCode = f;
break; break;
} else if ( } else if (
!fillFields.city && !fillFields.city &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.CityFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CityFieldNames)
) { ) {
fillFields.city = f; fillFields.city = f;
break; break;
} else if ( } else if (
!fillFields.state && !fillFields.state &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.StateFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.StateFieldNames)
) { ) {
fillFields.state = f; fillFields.state = f;
break; break;
} else if ( } else if (
!fillFields.country && !fillFields.country &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.CountryFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CountryFieldNames)
) { ) {
fillFields.country = f; fillFields.country = f;
break; break;
} else if ( } else if (
!fillFields.phone && !fillFields.phone &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.PhoneFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PhoneFieldNames)
) { ) {
fillFields.phone = f; fillFields.phone = f;
break; break;
} else if ( } else if (
!fillFields.username && !fillFields.username &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.UserNameFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.UserNameFieldNames)
) { ) {
fillFields.username = f; fillFields.username = f;
break; break;
} else if ( } else if (
!fillFields.company && !fillFields.company &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.CompanyFieldNames) AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CompanyFieldNames)
) { ) {
fillFields.company = f; fillFields.company = f;
break; break;
@ -923,16 +954,16 @@ export default class AutofillService implements AutofillServiceInterface {
if (fillFields.name && (identity.firstName || identity.lastName)) { if (fillFields.name && (identity.firstName || identity.lastName)) {
let fullName = ""; let fullName = "";
if (this.hasValue(identity.firstName)) { if (AutofillService.hasValue(identity.firstName)) {
fullName = identity.firstName; fullName = identity.firstName;
} }
if (this.hasValue(identity.middleName)) { if (AutofillService.hasValue(identity.middleName)) {
if (fullName !== "") { if (fullName !== "") {
fullName += " "; fullName += " ";
} }
fullName += identity.middleName; fullName += identity.middleName;
} }
if (this.hasValue(identity.lastName)) { if (AutofillService.hasValue(identity.lastName)) {
if (fullName !== "") { if (fullName !== "") {
fullName += " "; fullName += " ";
} }
@ -942,18 +973,18 @@ export default class AutofillService implements AutofillServiceInterface {
this.makeScriptActionWithValue(fillScript, fullName, fillFields.name, filledFields); this.makeScriptActionWithValue(fillScript, fullName, fillFields.name, filledFields);
} }
if (fillFields.address && this.hasValue(identity.address1)) { if (fillFields.address && AutofillService.hasValue(identity.address1)) {
let address = ""; let address = "";
if (this.hasValue(identity.address1)) { if (AutofillService.hasValue(identity.address1)) {
address = identity.address1; address = identity.address1;
} }
if (this.hasValue(identity.address2)) { if (AutofillService.hasValue(identity.address2)) {
if (address !== "") { if (address !== "") {
address += ", "; address += ", ";
} }
address += identity.address2; address += identity.address2;
} }
if (this.hasValue(identity.address3)) { if (AutofillService.hasValue(identity.address3)) {
if (address !== "") { if (address !== "") {
address += ", "; address += ", ";
} }
@ -970,7 +1001,11 @@ export default class AutofillService implements AutofillServiceInterface {
return excludedTypes.indexOf(type) > -1; return excludedTypes.indexOf(type) > -1;
} }
private isFieldMatch(value: string, options: string[], containsOptions?: string[]): boolean { private static isFieldMatch(
value: string,
options: string[],
containsOptions?: string[]
): boolean {
value = value value = value
.trim() .trim()
.toLowerCase() .toLowerCase()
@ -1011,12 +1046,15 @@ export default class AutofillService implements AutofillServiceInterface {
filledFields: { [id: string]: AutofillField } filledFields: { [id: string]: AutofillField }
) { ) {
let doFill = false; let doFill = false;
if (this.hasValue(dataValue) && field) { if (AutofillService.hasValue(dataValue) && field) {
if (field.type === "select-one" && field.selectInfo && field.selectInfo.options) { if (field.type === "select-one" && field.selectInfo && field.selectInfo.options) {
for (let i = 0; i < field.selectInfo.options.length; i++) { for (let i = 0; i < field.selectInfo.options.length; i++) {
const option = field.selectInfo.options[i]; const option = field.selectInfo.options[i];
for (let j = 0; j < option.length; j++) { for (let j = 0; j < option.length; j++) {
if (this.hasValue(option[j]) && option[j].toLowerCase() === dataValue.toLowerCase()) { if (
AutofillService.hasValue(option[j]) &&
option[j].toLowerCase() === dataValue.toLowerCase()
) {
doFill = true; doFill = true;
if (option.length > 1) { if (option.length > 1) {
dataValue = option[1]; dataValue = option[1];
@ -1036,11 +1074,11 @@ export default class AutofillService implements AutofillServiceInterface {
if (doFill) { if (doFill) {
filledFields[field.opid] = field; filledFields[field.opid] = field;
this.fillByOpid(fillScript, field, dataValue); AutofillService.fillByOpid(fillScript, field, dataValue);
} }
} }
private loadPasswordFields( static loadPasswordFields(
pageDetails: AutofillPageDetails, pageDetails: AutofillPageDetails,
canBeHidden: boolean, canBeHidden: boolean,
canBeReadOnly: boolean, canBeReadOnly: boolean,
@ -1049,7 +1087,7 @@ export default class AutofillService implements AutofillServiceInterface {
) { ) {
const arr: AutofillField[] = []; const arr: AutofillField[] = [];
pageDetails.fields.forEach((f) => { pageDetails.fields.forEach((f) => {
if (this.forCustomFieldsOnly(f)) { if (AutofillService.forCustomFieldsOnly(f)) {
return; return;
} }
@ -1111,7 +1149,7 @@ export default class AutofillService implements AutofillServiceInterface {
let usernameField: AutofillField = null; let usernameField: AutofillField = null;
for (let i = 0; i < pageDetails.fields.length; i++) { for (let i = 0; i < pageDetails.fields.length; i++) {
const f = pageDetails.fields[i]; const f = pageDetails.fields[i];
if (this.forCustomFieldsOnly(f)) { if (AutofillService.forCustomFieldsOnly(f)) {
continue; continue;
} }
@ -1195,7 +1233,7 @@ export default class AutofillService implements AutofillServiceInterface {
private fieldPropertyIsMatch(field: any, property: string, name: string): boolean { private fieldPropertyIsMatch(field: any, property: string, name: string): boolean {
let fieldVal = field[property] as string; let fieldVal = field[property] as string;
if (!this.hasValue(fieldVal)) { if (!AutofillService.hasValue(fieldVal)) {
return false; return false;
} }
@ -1227,33 +1265,45 @@ export default class AutofillService implements AutofillServiceInterface {
return fieldVal.toLowerCase() === name; return fieldVal.toLowerCase() === name;
} }
private fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean { static fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean {
if (this.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) { if (AutofillService.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) {
return true; return true;
} }
if (this.hasValue(field.htmlName) && this.fuzzyMatch(names, field.htmlName)) { if (AutofillService.hasValue(field.htmlName) && this.fuzzyMatch(names, field.htmlName)) {
return true; return true;
} }
if (this.hasValue(field["label-tag"]) && this.fuzzyMatch(names, field["label-tag"])) { if (
AutofillService.hasValue(field["label-tag"]) &&
this.fuzzyMatch(names, field["label-tag"])
) {
return true; return true;
} }
if (this.hasValue(field.placeholder) && this.fuzzyMatch(names, field.placeholder)) { if (AutofillService.hasValue(field.placeholder) && this.fuzzyMatch(names, field.placeholder)) {
return true; return true;
} }
if (this.hasValue(field["label-left"]) && this.fuzzyMatch(names, field["label-left"])) { if (
AutofillService.hasValue(field["label-left"]) &&
this.fuzzyMatch(names, field["label-left"])
) {
return true; return true;
} }
if (this.hasValue(field["label-top"]) && this.fuzzyMatch(names, field["label-top"])) { if (
AutofillService.hasValue(field["label-top"]) &&
this.fuzzyMatch(names, field["label-top"])
) {
return true; return true;
} }
if (this.hasValue(field["label-aria"]) && this.fuzzyMatch(names, field["label-aria"])) { if (
AutofillService.hasValue(field["label-aria"]) &&
this.fuzzyMatch(names, field["label-aria"])
) {
return true; return true;
} }
return false; return false;
} }
private fuzzyMatch(options: string[], value: string): boolean { private static fuzzyMatch(options: string[], value: string): boolean {
if (options == null || options.length === 0 || value == null || value === "") { if (options == null || options.length === 0 || value == null || value === "") {
return false; return false;
} }
@ -1272,11 +1322,11 @@ export default class AutofillService implements AutofillServiceInterface {
return false; return false;
} }
private hasValue(str: string): boolean { static hasValue(str: string): boolean {
return str && str !== ""; return str && str !== "";
} }
private setFillScriptForFocus( static setFillScriptForFocus(
filledFields: { [id: string]: AutofillField }, filledFields: { [id: string]: AutofillField },
fillScript: AutofillScript fillScript: AutofillScript
): AutofillScript { ): AutofillScript {
@ -1304,7 +1354,7 @@ export default class AutofillService implements AutofillServiceInterface {
return fillScript; return fillScript;
} }
private fillByOpid(fillScript: AutofillScript, field: AutofillField, value: string): void { static fillByOpid(fillScript: AutofillScript, field: AutofillField, value: string): void {
if (field.maxLength && value && value.length > field.maxLength) { if (field.maxLength && value && value.length > field.maxLength) {
value = value.substr(0, value.length); value = value.substr(0, value.length);
} }
@ -1315,7 +1365,7 @@ export default class AutofillService implements AutofillServiceInterface {
fillScript.script.push(["fill_by_opid", field.opid, value]); fillScript.script.push(["fill_by_opid", field.opid, value]);
} }
private forCustomFieldsOnly(field: AutofillField): boolean { static forCustomFieldsOnly(field: AutofillField): boolean {
return field.tagName === "span"; return field.tagName === "span";
} }
} }

View File

@ -1,5 +1,5 @@
import AbstractChromeStorageService from "./abstractChromeStorageApi.service"; import AbstractChromeStorageService from "./abstractChromeStorageApi.service";
export default class BrowserLocalStorageService extends AbstractChromeStorageService { export default class BrowserLocalStorageService extends AbstractChromeStorageService {
protected chromeStorageApi: any = chrome.storage.local; protected chromeStorageApi = chrome.storage.local;
} }

View File

@ -1,5 +1,5 @@
import AbstractChromeStorageService from "./abstractChromeStorageApi.service"; import AbstractChromeStorageService from "./abstractChromeStorageApi.service";
export default class BrowserMemoryStorageService extends AbstractChromeStorageService { export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
protected chromeStorageApi: any = (chrome.storage as any).session; protected chromeStorageApi = chrome.storage.session;
} }

View File

@ -176,13 +176,6 @@ const config = {
return chunk.name === "popup/main"; return chunk.name === "popup/main";
}, },
}, },
commons2: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: (chunk) => {
return chunk.name === "background";
},
},
}, },
}, },
}, },
@ -209,4 +202,16 @@ const config = {
plugins: plugins, plugins: plugins,
}; };
if (manifestVersion == 2) {
// We can't use this in manifest v3
// Ideally we understand why this breaks it and we don't have to do this
config.optimization.splitChunks.cacheGroups.commons2 = {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: (chunk) => {
return chunk.name === "background";
},
};
}
module.exports = config; module.exports = config;

View File

@ -10,7 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
templateUrl: "verify-email.component.html", templateUrl: "verify-email.component.html",
}) })
export class VerifyEmailComponent { export class VerifyEmailComponent {
actionPromise: Promise<any>; actionPromise: Promise<unknown>;
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,

View File

@ -78,25 +78,4 @@ describe("ConsoleLogService", () => {
error: { 0: "this is an error message" }, error: { 0: "this is an error message" },
}); });
}); });
it("times with output to info", async () => {
logService.time();
await new Promise((r) => setTimeout(r, 250));
const duration = logService.timeEnd();
expect(duration[0]).toBe(0);
expect(duration[1]).toBeGreaterThan(0);
expect(duration[1]).toBeLessThan(500 * 10e6);
expect(caughtMessage).toEqual(expect.arrayContaining([]));
expect(caughtMessage.log.length).toBe(1);
expect(caughtMessage.log[0]).toEqual(expect.stringMatching(/^default: \d+\.?\d*ms$/));
});
it("filters time output", async () => {
logService = new ConsoleLogService(true, () => true);
logService.time();
logService.timeEnd();
expect(caughtMessage).toEqual({});
});
}); });

View File

@ -6,6 +6,4 @@ export abstract class LogService {
warning: (message: string) => void; warning: (message: string) => void;
error: (message: string) => void; error: (message: string) => void;
write: (level: LogLevelType, message: string) => void; write: (level: LogLevelType, message: string) => void;
time: (label: string) => void;
timeEnd: (label: string) => [number, number];
} }

View File

@ -1,17 +1,28 @@
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
import * as tldjs from "tldjs"; import * as tldjs from "tldjs";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service"; import { I18nService } from "../abstractions/i18n.service";
const nodeURL = typeof window === "undefined" ? require("url") : null; const nodeURL = typeof window === "undefined" ? require("url") : null;
declare global {
/* eslint-disable-next-line no-var */
var bitwardenContainerService: BitwardenContainerService;
}
interface BitwardenContainerService {
getCryptoService: () => CryptoService;
}
export class Utils { export class Utils {
static inited = false; static inited = false;
static isNode = false; static isNode = false;
static isBrowser = true; static isBrowser = true;
static isMobileBrowser = false; static isMobileBrowser = false;
static isAppleMobileBrowser = false; static isAppleMobileBrowser = false;
static global: any = null; static global: typeof global = null;
static tldEndingRegex = static tldEndingRegex =
/.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/; /.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/;
// Transpiled version of /\p{Emoji_Presentation}/gu using https://mothereff.in/regexpu. Used for compatability in older browsers. // Transpiled version of /\p{Emoji_Presentation}/gu using https://mothereff.in/regexpu. Used for compatability in older browsers.
@ -29,16 +40,25 @@ export class Utils {
(process as any).release != null && (process as any).release != null &&
(process as any).release.name === "node"; (process as any).release.name === "node";
Utils.isBrowser = typeof window !== "undefined"; Utils.isBrowser = typeof window !== "undefined";
Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window); Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window);
Utils.isAppleMobileBrowser = Utils.isBrowser && this.isAppleMobile(window); Utils.isAppleMobileBrowser = Utils.isBrowser && this.isAppleMobile(window);
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
if (Utils.isNode) {
Utils.global = global;
} else if (Utils.isBrowser) {
Utils.global = window;
} else {
// If it's not browser or node then it must be a service worker
Utils.global = self;
}
} }
static fromB64ToArray(str: string): Uint8Array { static fromB64ToArray(str: string): Uint8Array {
if (Utils.isNode) { if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "base64")); return new Uint8Array(Buffer.from(str, "base64"));
} else { } else {
const binaryString = window.atob(str); const binaryString = Utils.global.atob(str);
const bytes = new Uint8Array(binaryString.length); const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) { for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i); bytes[i] = binaryString.charCodeAt(i);
@ -93,7 +113,7 @@ export class Utils {
for (let i = 0; i < bytes.byteLength; i++) { for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]); binary += String.fromCharCode(bytes[i]);
} }
return window.btoa(binary); return Utils.global.btoa(binary);
} }
} }
@ -157,7 +177,7 @@ export class Utils {
if (Utils.isNode) { if (Utils.isNode) {
return Buffer.from(utfStr, "utf8").toString("base64"); return Buffer.from(utfStr, "utf8").toString("base64");
} else { } else {
return decodeURIComponent(escape(window.btoa(utfStr))); return decodeURIComponent(escape(Utils.global.btoa(utfStr)));
} }
} }
@ -169,7 +189,7 @@ export class Utils {
if (Utils.isNode) { if (Utils.isNode) {
return Buffer.from(b64Str, "base64").toString("utf8"); return Buffer.from(b64Str, "base64").toString("utf8");
} else { } else {
return decodeURIComponent(escape(window.atob(b64Str))); return decodeURIComponent(escape(Utils.global.atob(b64Str)));
} }
} }
@ -384,7 +404,7 @@ export class Utils {
return new nodeURL.URL(uriString); return new nodeURL.URL(uriString);
} else if (typeof URL === "function") { } else if (typeof URL === "function") {
return new URL(uriString); return new URL(uriString);
} else if (window != null) { } else if (typeof window !== "undefined") {
const hasProtocol = uriString.indexOf("://") > -1; const hasProtocol = uriString.indexOf("://") > -1;
if (!hasProtocol && uriString.indexOf(".") > -1) { if (!hasProtocol && uriString.indexOf(".") > -1) {
uriString = "http://" + uriString; uriString = "http://" + uriString;

View File

@ -48,7 +48,7 @@ export class Attachment extends Domain {
if (this.key != null) { if (this.key != null) {
let cryptoService: CryptoService; let cryptoService: CryptoService;
const containerService = (Utils.global as any).bitwardenContainerService; const containerService = Utils.global.bitwardenContainerService;
if (containerService) { if (containerService) {
cryptoService = containerService.getCryptoService(); cryptoService = containerService.getCryptoService();
} else { } else {

View File

@ -104,7 +104,7 @@ export class EncString implements IEncrypted {
} }
let cryptoService: CryptoService; let cryptoService: CryptoService;
const containerService = (Utils.global as any).bitwardenContainerService; const containerService = Utils.global.bitwardenContainerService;
if (containerService) { if (containerService) {
cryptoService = containerService.getCryptoService(); cryptoService = containerService.getCryptoService();
} else { } else {

View File

@ -341,7 +341,7 @@ export class CipherService implements CipherServiceAbstraction {
throw new Error("No key."); throw new Error("No key.");
} }
const promises: any[] = []; const promises: Promise<number>[] = [];
const ciphers = await this.getAll(); const ciphers = await this.getAll();
ciphers.forEach(async (cipher) => { ciphers.forEach(async (cipher) => {
promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));

View File

@ -1,5 +1,3 @@
import * as hrtime from "browser-hrtime";
import { LogService as LogServiceAbstraction } from "../abstractions/log.service"; import { LogService as LogServiceAbstraction } from "../abstractions/log.service";
import { LogLevelType } from "../enums/logLevelType"; import { LogLevelType } from "../enums/logLevelType";
@ -56,17 +54,4 @@ export class ConsoleLogService implements LogServiceAbstraction {
break; break;
} }
} }
time(label = "default") {
if (!this.timersMap.has(label)) {
this.timersMap.set(label, hrtime());
}
}
timeEnd(label = "default"): [number, number] {
const elapsed = hrtime(this.timersMap.get(label));
this.timersMap.delete(label);
this.write(LogLevelType.Info, `${label}: ${elapsed[0] * 1000 + elapsed[1] / 10e6}ms`);
return elapsed;
}
} }

View File

@ -0,0 +1,24 @@
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { EventType } from "@bitwarden/common/enums/eventType";
/**
* If you want to use this, don't.
* If you think you should use that after the warning, don't.
*/
export default class NoOpEventService implements EventService {
constructor() {
if (chrome.runtime.getManifest().manifest_version !== 3) {
throw new Error("You are not allowed to use this when not in manifest_version 3");
}
}
collect(eventType: EventType, cipherId?: string, uploadImmediately?: boolean) {
return Promise.resolve();
}
uploadEvents(userId?: string) {
return Promise.resolve();
}
clearEvents(userId?: string) {
return Promise.resolve();
}
}

View File

@ -11,6 +11,8 @@ import { CipherView } from "../models/view/cipherView";
import { SendView } from "../models/view/sendView"; import { SendView } from "../models/view/sendView";
export class SearchService implements SearchServiceAbstraction { export class SearchService implements SearchServiceAbstraction {
private static registeredPipeline = false;
indexedEntityId?: string = null; indexedEntityId?: string = null;
private indexing = false; private indexing = false;
private index: lunr.Index = null; private index: lunr.Index = null;
@ -31,8 +33,13 @@ export class SearchService implements SearchServiceAbstraction {
} }
}); });
//register lunr pipeline function // Currently have to ensure this is only done a single time. Lunr allows you to register a function
lunr.Pipeline.registerFunction(this.normalizeAccentsPipelineFunction, "normalizeAccents"); // multiple times but they will add a warning message to the console. The way they do that breaks when ran on a service worker.
if (!SearchService.registeredPipeline) {
SearchService.registeredPipeline = true;
//register lunr pipeline function
lunr.Pipeline.registerFunction(this.normalizeAccentsPipelineFunction, "normalizeAccents");
}
} }
clearIndex(): void { clearIndex(): void {
@ -54,7 +61,6 @@ export class SearchService implements SearchServiceAbstraction {
return; return;
} }
this.logService.time("search indexing");
this.indexing = true; this.indexing = true;
this.indexedEntityId = indexedEntityId; this.indexedEntityId = indexedEntityId;
this.index = null; this.index = null;
@ -95,7 +101,7 @@ export class SearchService implements SearchServiceAbstraction {
this.indexing = false; this.indexing = false;
this.logService.timeEnd("search indexing"); this.logService.info("Finished search indexing");
} }
async searchCiphers( async searchCiphers(

View File

@ -610,7 +610,7 @@ export class StateService<
); );
} }
@withPrototypeForArrayMembers(CipherView) @withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> { async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))

View File

@ -9,7 +9,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
private crypto: Crypto; private crypto: Crypto;
private subtle: SubtleCrypto; private subtle: SubtleCrypto;
constructor(win: Window) { constructor(win: Window | typeof global) {
this.crypto = typeof win.crypto !== "undefined" ? win.crypto : null; this.crypto = typeof win.crypto !== "undefined" ? win.crypto : null;
this.subtle = this.subtle =
!!this.crypto && typeof win.crypto.subtle !== "undefined" ? win.crypto.subtle : null; !!this.crypto && typeof win.crypto.subtle !== "undefined" ? win.crypto.subtle : null;

15
package-lock.json generated
View File

@ -30,7 +30,6 @@
"big-integer": "^1.6.51", "big-integer": "^1.6.51",
"bootstrap": "4.6.0", "bootstrap": "4.6.0",
"braintree-web-drop-in": "^1.33.1", "braintree-web-drop-in": "^1.33.1",
"browser-hrtime": "^1.1.8",
"bufferutil": "^4.0.6", "bufferutil": "^4.0.6",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"commander": "^7.2.0", "commander": "^7.2.0",
@ -83,7 +82,7 @@
"@storybook/angular": "^6.5.7", "@storybook/angular": "^6.5.7",
"@storybook/builder-webpack5": "^6.5.7", "@storybook/builder-webpack5": "^6.5.7",
"@storybook/manager-webpack5": "^6.5.7", "@storybook/manager-webpack5": "^6.5.7",
"@types/chrome": "^0.0.139", "@types/chrome": "^0.0.190",
"@types/duo_web_sdk": "^2.7.1", "@types/duo_web_sdk": "^2.7.1",
"@types/firefox-webext-browser": "^82.0.0", "@types/firefox-webext-browser": "^82.0.0",
"@types/inquirer": "^8.2.1", "@types/inquirer": "^8.2.1",
@ -12504,9 +12503,9 @@
} }
}, },
"node_modules/@types/chrome": { "node_modules/@types/chrome": {
"version": "0.0.139", "version": "0.0.190",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.139.tgz", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.190.tgz",
"integrity": "sha512-YZDKFlSVGFp4zldJlO+PUpxMH8N9vLke0fD6K9PA+TzXxPXu8LBLo5X2dzlOs2N/n+uMdI1lw7OPT1Emop10lQ==", "integrity": "sha512-lCwwIBfaD+PhG62qFB46mIBpD+xBIa+PedNB24KR9YnmJ0Zn9h0OwP1NQBhI8Cbu1rKwTQHTxhs7GhWGyUvinw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/filesystem": "*", "@types/filesystem": "*",
@ -52084,9 +52083,9 @@
} }
}, },
"@types/chrome": { "@types/chrome": {
"version": "0.0.139", "version": "0.0.190",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.139.tgz", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.190.tgz",
"integrity": "sha512-YZDKFlSVGFp4zldJlO+PUpxMH8N9vLke0fD6K9PA+TzXxPXu8LBLo5X2dzlOs2N/n+uMdI1lw7OPT1Emop10lQ==", "integrity": "sha512-lCwwIBfaD+PhG62qFB46mIBpD+xBIa+PedNB24KR9YnmJ0Zn9h0OwP1NQBhI8Cbu1rKwTQHTxhs7GhWGyUvinw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/filesystem": "*", "@types/filesystem": "*",

View File

@ -45,7 +45,7 @@
"@storybook/angular": "^6.5.7", "@storybook/angular": "^6.5.7",
"@storybook/builder-webpack5": "^6.5.7", "@storybook/builder-webpack5": "^6.5.7",
"@storybook/manager-webpack5": "^6.5.7", "@storybook/manager-webpack5": "^6.5.7",
"@types/chrome": "^0.0.139", "@types/chrome": "^0.0.190",
"@types/duo_web_sdk": "^2.7.1", "@types/duo_web_sdk": "^2.7.1",
"@types/firefox-webext-browser": "^82.0.0", "@types/firefox-webext-browser": "^82.0.0",
"@types/inquirer": "^8.2.1", "@types/inquirer": "^8.2.1",
@ -155,7 +155,6 @@
"big-integer": "^1.6.51", "big-integer": "^1.6.51",
"bootstrap": "4.6.0", "bootstrap": "4.6.0",
"braintree-web-drop-in": "^1.33.1", "braintree-web-drop-in": "^1.33.1",
"browser-hrtime": "^1.1.8",
"bufferutil": "^4.0.6", "bufferutil": "^4.0.6",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"commander": "^7.2.0", "commander": "^7.2.0",