1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-23 16:13:21 +00:00

merged with master and fixed conflicts

This commit is contained in:
gbubemismith
2023-09-08 17:37:32 -04:00
413 changed files with 16939 additions and 6582 deletions

View File

@@ -15,7 +15,7 @@
"euDomain" | i18n
}}</label>
<label *ngSwitchCase="ServerEnvironmentType.SelfHosted" class="text-primary">{{
"selfHosted" | i18n
"selfHostedServer" | i18n
}}</label>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</a>
@@ -44,7 +44,6 @@
selectedEnvironment === ServerEnvironmentType.US ? 'visible' : 'hidden'
"
></i>
<img class="img-us" alt="" />
<span>{{ "usDomain" | i18n }}</span>
</button>
<br />
@@ -62,7 +61,6 @@
selectedEnvironment === ServerEnvironmentType.EU ? 'visible' : 'hidden'
"
></i>
<img class="img-eu" alt="" />
<span>{{ "euDomain" | i18n }}</span>
</button>
<br *ngIf="euServerFlagEnabled" />
@@ -79,12 +77,7 @@
selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'visible' : 'hidden'
"
></i>
<i
class="bwi bwi-fw bwi-md bwi-pencil-square"
style="padding-bottom: 1px"
aria-hidden="true"
></i>
<span>{{ "selfHosted" | i18n }}</span>
<span>{{ "selfHostedServer" | i18n }}</span>
</button>
</div>
</div>

View File

@@ -89,7 +89,7 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
async updateEnvironmentInfo() {
this.selectedEnvironment = this.environmentService.selectedRegion;
this.euServerFlagEnabled = await this.configService.getFeatureFlagBool(
this.euServerFlagEnabled = await this.configService.getFeatureFlag<boolean>(
FeatureFlag.DisplayEuEnvironmentFlag
);
}

View File

@@ -42,7 +42,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]],
masterPassword: ["", [Validators.required, Validators.minLength(8)]],
masterPassword: [
"",
[Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)],
],
rememberEmail: [false],
});
@@ -278,6 +281,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
switch (error.errorName) {
case "email":
return this.i18nService.t("invalidEmail");
case "minlength":
return this.i18nService.t("masterPasswordMinlength", Utils.originalMinimumPasswordLength);
default:
return this.i18nService.t(this.errorTag(error));
}

View File

@@ -331,7 +331,7 @@ describe("SsoComponent", () => {
describe("Trusted Device Encryption scenarios", () => {
beforeEach(() => {
mockConfigService.getFeatureFlagBool.mockResolvedValue(true); // TDE enabled
mockConfigService.getFeatureFlag.mockResolvedValue(true); // TDE enabled
});
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {

View File

@@ -242,7 +242,7 @@ export class SsoComponent {
private async isTrustedDeviceEncEnabled(
trustedDeviceOption: TrustedDeviceUserDecryptionOption
): Promise<boolean> {
const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool(
const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlag<boolean>(
FeatureFlag.TrustedDeviceEncryption
);

View File

@@ -376,7 +376,7 @@ describe("TwoFactorComponent", () => {
describe("Trusted Device Encryption scenarios", () => {
beforeEach(() => {
mockConfigService.getFeatureFlagBool.mockResolvedValue(true);
mockConfigService.getFeatureFlag.mockResolvedValue(true);
});
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {

View File

@@ -257,7 +257,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
trustedDeviceOption: TrustedDeviceUserDecryptionOption
): Promise<boolean> {
const ssoTo2faFlowActive = this.route.snapshot.queryParamMap.get("sso") === "true";
const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool(
const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlag<boolean>(
FeatureFlag.TrustedDeviceEncryption
);

View File

@@ -15,6 +15,14 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
interface VaultTimeoutFormValue {
vaultTimeout: number | null;
custom: {
hours: number | null;
minutes: number | null;
};
}
@Directive()
export class VaultTimeoutInputComponent
implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges
@@ -70,26 +78,38 @@ export class VaultTimeoutInputComponent
this.applyVaultTimeoutPolicy();
});
this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
if (this.onChange) {
this.onChange(this.getVaultTimeout(value));
}
});
this.form.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((value: VaultTimeoutFormValue) => {
if (this.onChange) {
this.onChange(this.getVaultTimeout(value));
}
});
// Assign the previous value to the custom fields
// Assign the current value to the custom fields
// so that if the user goes from a numeric value to custom
// we can initialize the custom fields with the current value
// ex: user picks 5 min, goes to custom, we want to show 0 hr, 5 min in the custom fields
this.form.controls.vaultTimeout.valueChanges
.pipe(
filter((value) => value !== VaultTimeoutInputComponent.CUSTOM_VALUE),
takeUntil(this.destroy$)
)
.subscribe((_) => {
const current = Math.max(this.form.value.vaultTimeout, 0);
this.form.patchValue({
custom: {
hours: Math.floor(current / 60),
minutes: current % 60,
.subscribe((value) => {
const current = Math.max(value, 0);
// This cannot emit an event b/c it would cause form.valueChanges to fire again
// and we are already handling that above so just silently update
// custom fields when vaultTimeout changes to a non-custom value
this.form.patchValue(
{
custom: {
hours: Math.floor(current / 60),
minutes: current % 60,
},
},
});
{ emitEvent: false }
);
});
this.canLockVault$ = this.vaultTimeoutSettingsService
@@ -113,7 +133,7 @@ export class VaultTimeoutInputComponent
}
}
getVaultTimeout(value: any) {
getVaultTimeout(value: VaultTimeoutFormValue) {
if (value.vaultTimeout !== VaultTimeoutInputComponent.CUSTOM_VALUE) {
return value.vaultTimeout;
}

View File

@@ -1,5 +1,6 @@
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
import { ClientType } from "@bitwarden/common/enums";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@Directive({
@@ -15,6 +16,9 @@ export class CopyTextDirective {
return;
}
this.platformUtilsService.copyToClipboard(this.copyText, { window: window });
const timeout = this.platformUtilsService.getClientType() === ClientType.Desktop ? 100 : 0;
setTimeout(() => {
this.platformUtilsService.copyToClipboard(this.copyText, { window: window });
}, timeout);
}
}

View File

@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { mock, MockProxy } from "jest-mock-extended";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -41,21 +41,12 @@ describe("IfFeatureDirective", () => {
let content: HTMLElement;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: any) => {
if (typeof flagValue === "boolean") {
mockConfigService.getFeatureFlagBool.mockImplementation((f, defaultValue = false) =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
} else if (typeof flagValue === "string") {
mockConfigService.getFeatureFlagString.mockImplementation((f, defaultValue = "") =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
} else if (typeof flagValue === "number") {
mockConfigService.getFeatureFlagNumber.mockImplementation((f, defaultValue = 0) =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
}
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => {
mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
};
const queryContent = (testId: string) =>
fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement;
@@ -126,7 +117,7 @@ describe("IfFeatureDirective", () => {
});
it("hides content when the directive throws an unexpected exception", async () => {
mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error"));
mockConfigService.getFeatureFlag.mockImplementation(() => Promise.reject("Some error"));
fixture.detectChanges();
await fixture.whenStable();

View File

@@ -1,12 +1,9 @@
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
// Replace this with a type safe lookup of the feature flag values in PM-2282
type FlagValue = boolean | number | string;
/**
* Directive that conditionally renders the element when the feature flag is enabled and/or
* matches the value specified by {@link appIfFeatureValue}.
@@ -26,7 +23,7 @@ export class IfFeatureDirective implements OnInit {
* Optional value to compare against the value of the feature flag in the config service.
* @default true
*/
@Input() appIfFeatureValue: FlagValue = true;
@Input() appIfFeatureValue: FeatureFlagValue = true;
private hasView = false;
@@ -39,15 +36,7 @@ export class IfFeatureDirective implements OnInit {
async ngOnInit() {
try {
let flagValue: FlagValue;
if (typeof this.appIfFeatureValue === "boolean") {
flagValue = await this.configService.getFeatureFlagBool(this.appIfFeature);
} else if (typeof this.appIfFeatureValue === "number") {
flagValue = await this.configService.getFeatureFlagNumber(this.appIfFeature);
} else if (typeof this.appIfFeatureValue === "string") {
flagValue = await this.configService.getFeatureFlagString(this.appIfFeature);
}
const flagValue = await this.configService.getFeatureFlag(this.appIfFeature);
if (this.appIfFeatureValue === flagValue) {
if (!this.hasView) {

View File

@@ -30,15 +30,15 @@ describe("canAccessFeature", () => {
// Mock the correct getter based on the type of flagValue; also mock default values if one is not provided
if (typeof flagValue === "boolean") {
mockConfigService.getFeatureFlagBool.mockImplementation((flag, defaultValue = false) =>
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = false) =>
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
} else if (typeof flagValue === "string") {
mockConfigService.getFeatureFlagString.mockImplementation((flag, defaultValue = "") =>
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = "") =>
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
} else if (typeof flagValue === "number") {
mockConfigService.getFeatureFlagNumber.mockImplementation((flag, defaultValue = 0) =>
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = 0) =>
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
}
@@ -143,7 +143,7 @@ describe("canAccessFeature", () => {
it("fails to navigate when the config service throws an unexpected exception", async () => {
const { router } = setup(canAccessFeature(testFlag), true);
mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error"));
mockConfigService.getFeatureFlag.mockImplementation(() => Promise.reject("Some error"));
await router.navigate([featureRoute]);

View File

@@ -29,16 +29,8 @@ export const canAccessFeature = (
const i18nService = inject(I18nService);
const logService = inject(LogService);
let flagValue: FlagValue;
try {
if (typeof requiredFlagValue === "boolean") {
flagValue = await configService.getFeatureFlagBool(featureFlag);
} else if (typeof requiredFlagValue === "number") {
flagValue = await configService.getFeatureFlagNumber(featureFlag);
} else if (typeof requiredFlagValue === "string") {
flagValue = await configService.getFeatureFlagString(featureFlag);
}
const flagValue = await configService.getFeatureFlag(featureFlag);
if (flagValue === requiredFlagValue) {
return true;

View File

@@ -77,7 +77,6 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/platform/abstractions/state-migration.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -94,7 +93,6 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
@@ -480,16 +478,10 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
SECURE_STORAGE,
MEMORY_STORAGE,
LogService,
StateMigrationServiceAbstraction,
STATE_FACTORY,
STATE_SERVICE_USE_CACHE,
],
},
{
provide: StateMigrationServiceAbstraction,
useClass: StateMigrationService,
deps: [AbstractStorageService, SECURE_STORAGE, STATE_FACTORY],
},
{
provide: VaultExportServiceAbstraction,
useClass: VaultExportService,
@@ -648,7 +640,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
useClass: SyncNotifierService,
},
{
provide: ConfigServiceAbstraction,
provide: ConfigService,
useClass: ConfigService,
deps: [
StateServiceAbstraction,
@@ -657,6 +649,10 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
EnvironmentServiceAbstraction,
],
},
{
provide: ConfigServiceAbstraction,
useExisting: ConfigService,
},
{
provide: ConfigApiServiceAbstraction,
useClass: ConfigApiService,

View File

@@ -8,8 +8,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
import { GeneratorOptions } from "@bitwarden/common/tools/generator/generator-options";
import {
PasswordGenerationServiceAbstraction,
PasswordGeneratorOptions,
} from "@bitwarden/common/tools/generator/password";
import {
UsernameGenerationServiceAbstraction,
UsernameGeneratorOptions,
} from "@bitwarden/common/tools/generator/username";
@Directive()
export class GeneratorComponent implements OnInit {
@@ -24,8 +31,8 @@ export class GeneratorComponent implements OnInit {
subaddressOptions: any[];
catchallOptions: any[];
forwardOptions: EmailForwarderOptions[];
usernameOptions: any = {};
passwordOptions: any = {};
usernameOptions: UsernameGeneratorOptions = {};
passwordOptions: PasswordGeneratorOptions = {};
username = "-";
password = "-";
showOptions = false;
@@ -118,7 +125,7 @@ export class GeneratorComponent implements OnInit {
}
async typeChanged() {
await this.stateService.setGeneratorOptions({ type: this.type });
await this.stateService.setGeneratorOptions({ type: this.type } as GeneratorOptions);
if (this.regenerateWithoutButtonPress()) {
await this.regenerate();
}
@@ -237,7 +244,7 @@ export class GeneratorComponent implements OnInit {
private async initForwardOptions() {
this.forwardOptions = [
{ name: "AnonAddy", value: "anonaddy", validForSelfHosted: true },
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },

View File

@@ -1,5 +1,6 @@
import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -20,6 +21,23 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { DialogService } from "@bitwarden/components";
// Value = hours
enum DatePreset {
OneHour = 1,
OneDay = 24,
TwoDays = 48,
ThreeDays = 72,
SevenDays = 168,
ThirtyDays = 720,
Custom = 0,
Never = null,
}
interface DatePresetSelectOption {
name: string;
value: DatePreset;
}
@Directive()
export class AddEditComponent implements OnInit, OnDestroy {
@Input() sendId: string;
@@ -29,12 +47,25 @@ export class AddEditComponent implements OnInit, OnDestroy {
@Output() onDeletedSend = new EventEmitter<SendView>();
@Output() onCancelled = new EventEmitter<SendView>();
deletionDatePresets: DatePresetSelectOption[] = [
{ name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
{ name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
{ name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
{ name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
{ name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
{ name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
{ name: this.i18nService.t("custom"), value: DatePreset.Custom },
];
expirationDatePresets: DatePresetSelectOption[] = [
{ name: this.i18nService.t("never"), value: DatePreset.Never },
...this.deletionDatePresets,
];
copyLink = false;
disableSend = false;
disableHideEmail = false;
send: SendView;
deletionDate: string;
expirationDate: string;
hasPassword: boolean;
password: string;
showPassword = false;
@@ -51,6 +82,27 @@ export class AddEditComponent implements OnInit, OnDestroy {
private sendLinkBaseUrl: string;
private destroy$ = new Subject<void>();
protected formGroup = this.formBuilder.group({
name: ["", Validators.required],
text: [],
textHidden: [false],
fileContents: [],
file: [null, Validators.required],
link: [],
copyLink: false,
maxAccessCount: [],
accessCount: [],
password: [],
notes: [],
hideEmail: false,
disabled: false,
type: [],
defaultExpirationDateTime: [],
defaultDeletionDateTime: ["", Validators.required],
selectedDeletionDatePreset: [DatePreset.SevenDays, Validators.required],
selectedExpirationDatePreset: [],
});
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
@@ -59,10 +111,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected sendService: SendService,
protected messagingService: MessagingService,
protected policyService: PolicyService,
private logService: LogService,
protected logService: LogService,
protected stateService: StateService,
protected sendApiService: SendApiService,
protected dialogService: DialogService
protected dialogService: DialogService,
protected formBuilder: FormBuilder
) {
this.typeOptions = [
{ name: i18nService.t("sendTypeFile"), value: SendType.File },
@@ -72,7 +125,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
get link(): string {
if (this.send.id != null && this.send.accessId != null) {
if (this.send != null && this.send.id != null && this.send.accessId != null) {
return this.sendLinkBaseUrl + this.send.accessId + "/" + this.send.urlB64Key;
}
return null;
@@ -92,13 +145,39 @@ export class AddEditComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this.disableSend = policyAppliesToActiveUser;
if (this.disableSend) {
this.formGroup.disable();
}
});
this.policyService
.policyAppliesToActiveUser$(PolicyType.SendOptions, (p) => p.data.disableHideEmail)
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this.disableHideEmail = policyAppliesToActiveUser;
if ((this.disableHideEmail = policyAppliesToActiveUser)) {
this.formGroup.controls.hideEmail.disable();
}
});
this.formGroup.controls.type.valueChanges.subscribe((val) => {
this.type = val;
this.typeChanged();
});
this.formGroup.controls.selectedDeletionDatePreset.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((datePreset) => {
datePreset === DatePreset.Custom
? this.formGroup.controls.defaultDeletionDateTime.enable()
: this.formGroup.controls.defaultDeletionDateTime.disable();
});
this.formGroup.controls.hideEmail.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((val) => {
if (!val && this.disableHideEmail) {
this.formGroup.controls.hideEmail.disable();
}
});
await this.load();
@@ -117,29 +196,33 @@ export class AddEditComponent implements OnInit, OnDestroy {
return this.i18nService.t(this.editMode ? "editSend" : "createSend");
}
setDates(event: { deletionDate: string; expirationDate: string }) {
this.deletionDate = event.deletionDate;
this.expirationDate = event.expirationDate;
}
async load() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
this.emailVerified = await this.stateService.getEmailVerified();
if (!this.canAccessPremium || !this.emailVerified) {
this.type = SendType.Text;
}
this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File;
if (this.send == null) {
if (this.editMode) {
const send = this.loadSend();
this.send = await send.decrypt();
this.type = this.send.type;
this.updateFormValues();
if (this.send.hideEmail) {
this.formGroup.controls.hideEmail.enable();
}
} else {
this.send = new SendView();
this.send.type = this.type == null ? SendType.File : this.type;
this.send.type = this.type;
this.send.file = new SendFileView();
this.send.text = new SendTextView();
this.send.deletionDate = new Date();
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
this.formGroup.controls.type.patchValue(this.send.type);
this.formGroup.patchValue({
selectedDeletionDatePreset: DatePreset.SevenDays,
selectedExpirationDatePreset: DatePreset.Never,
});
}
}
@@ -147,6 +230,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
async submit(): Promise<boolean> {
this.formGroup.markAllAsTouched();
if (this.disableSend) {
this.platformUtilsService.showToast(
"error",
@@ -156,6 +241,17 @@ export class AddEditComponent implements OnInit, OnDestroy {
return false;
}
this.send.name = this.formGroup.controls.name.value;
this.send.text.text = this.formGroup.controls.text.value;
this.send.text.hidden = this.formGroup.controls.textHidden.value;
this.send.maxAccessCount = this.formGroup.controls.maxAccessCount.value;
this.send.accessCount = this.formGroup.controls.accessCount.value;
this.send.password = this.formGroup.controls.password.value;
this.send.notes = this.formGroup.controls.notes.value;
this.send.hideEmail = this.formGroup.controls.hideEmail.value;
this.send.disabled = this.formGroup.controls.disabled.value;
this.send.type = this.type;
if (this.send.name == null || this.send.name === "") {
this.platformUtilsService.showToast(
"error",
@@ -166,7 +262,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
let file: File = null;
if (this.send.type === SendType.File && !this.editMode) {
if (this.type === SendType.File && !this.editMode) {
const fileEl = document.getElementById("file") as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
@@ -190,7 +286,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
}
if (this.password != null && this.password.trim() === "") {
if (
this.formGroup.controls.password.value != null &&
this.formGroup.controls.password.value.trim() === ""
) {
this.password = null;
}
@@ -204,7 +303,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.send.accessId = encSend[0].accessId;
}
this.onSavedSend.emit(this.send);
if (this.copyLink && this.link != null) {
if (this.formGroup.controls.copyLink.value && this.link != null) {
await this.handleCopyLinkToClipboard();
return;
}
@@ -227,7 +326,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
return Promise.resolve(this.platformUtilsService.copyToClipboard(link));
}
async delete(): Promise<boolean> {
protected async delete(): Promise<boolean> {
if (this.deletePromise != null) {
return false;
}
@@ -257,7 +356,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
typeChanged() {
if (this.send.type === SendType.File && !this.alertShown) {
if (this.type === SendType.File && !this.alertShown) {
if (!this.canAccessPremium) {
this.alertShown = true;
this.messagingService.send("premiumRequired");
@@ -266,6 +365,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.messagingService.send("emailVerificationRequired");
}
}
this.type === SendType.Text || this.editMode
? this.formGroup.controls.file.disable()
: this.formGroup.controls.file.enable();
}
toggleOptions() {
@@ -277,17 +379,23 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
protected async encryptSend(file: File): Promise<[Send, EncArrayBuffer]> {
const sendData = await this.sendService.encrypt(this.send, file, this.password, null);
const sendData = await this.sendService.encrypt(
this.send,
file,
this.formGroup.controls.password.value,
null
);
// Parse dates
try {
sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
sendData[0].deletionDate =
this.formattedDeletionDate == null ? null : new Date(this.formattedDeletionDate);
} catch {
sendData[0].deletionDate = null;
}
try {
sendData[0].expirationDate =
this.expirationDate == null ? null : new Date(this.expirationDate);
this.formattedExpirationDate == null ? null : new Date(this.formattedExpirationDate);
} catch {
sendData[0].expirationDate = null;
}
@@ -299,6 +407,34 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.showPassword = !this.showPassword;
document.getElementById("password").focus();
}
updateFormValues() {
this.formGroup.patchValue({
name: this.send?.name ?? "",
text: this.send?.text?.text ?? "",
textHidden: this.send?.text?.hidden ?? false,
link: this.link ?? "",
maxAccessCount: this.send?.maxAccessCount,
accessCount: this.send?.accessCount ?? 0,
notes: this.send?.notes ?? "",
hideEmail: this.send?.hideEmail ?? false,
disabled: this.send?.disabled ?? false,
type: this.send.type ?? this.type,
password: "",
selectedDeletionDatePreset: this.editMode ? DatePreset.Custom : DatePreset.SevenDays,
selectedExpirationDatePreset: this.editMode ? DatePreset.Custom : DatePreset.Never,
defaultExpirationDateTime:
this.send.expirationDate != null
? this.datePipe.transform(new Date(this.send.expirationDate), "yyyy-MM-ddTHH:mm")
: null,
defaultDeletionDateTime: this.datePipe.transform(
new Date(this.send.deletionDate),
"yyyy-MM-ddTHH:mm"
),
});
}
private async handleCopyLinkToClipboard() {
const copySuccess = await this.copyLinkToClipboard(this.link);
if (copySuccess ?? true) {
@@ -319,4 +455,46 @@ export class AddEditComponent implements OnInit, OnDestroy {
await this.copyLinkToClipboard(this.link);
}
}
clearExpiration() {
this.formGroup.controls.defaultExpirationDateTime.patchValue(null);
}
get formattedExpirationDate(): string {
switch (this.formGroup.controls.selectedExpirationDatePreset.value as DatePreset) {
case DatePreset.Never:
return null;
case DatePreset.Custom:
if (!this.formGroup.controls.defaultExpirationDateTime.value) {
return null;
}
return this.formGroup.controls.defaultExpirationDateTime.value;
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() +
(this.formGroup.controls.selectedExpirationDatePreset.value as number) * 60 * 60 * 1000
);
return new Date(milliseconds).toString();
}
}
}
get formattedDeletionDate(): string {
switch (this.formGroup.controls.selectedDeletionDatePreset.value as DatePreset) {
case DatePreset.Never:
this.formGroup.controls.selectedDeletionDatePreset.patchValue(DatePreset.SevenDays);
return this.formattedDeletionDate;
case DatePreset.Custom:
return this.formGroup.controls.defaultDeletionDateTime.value;
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() +
(this.formGroup.controls.selectedDeletionDatePreset.value as number) * 60 * 60 * 1000
);
return new Date(milliseconds).toString();
}
}
}
}

View File

@@ -1,356 +0,0 @@
import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
// Different BrowserPath = different controls.
enum BrowserPath {
// Native datetime-locale.
// We are happy.
Default = "default",
// Native date and time inputs, but no datetime-locale.
// We use individual date and time inputs and create a datetime programatically on submit.
Firefox = "firefox",
// No native date, time, or datetime-locale inputs.
// We use a polyfill for dates and a dropdown for times.
Safari = "safari",
}
enum DateField {
DeletionDate = "deletion",
ExpirationDate = "expiration",
}
// Value = hours
enum DatePreset {
OneHour = 1,
OneDay = 24,
TwoDays = 48,
ThreeDays = 72,
SevenDays = 168,
ThirtyDays = 720,
Custom = 0,
Never = null,
}
// TimeOption is used for the dropdown implementation of custom times
// twelveHour = displayed time; twentyFourHour = time used in logic
interface TimeOption {
twelveHour: string;
twentyFourHour: string;
}
@Directive()
export class EffluxDatesComponent implements OnInit {
@Input() readonly initialDeletionDate: Date;
@Input() readonly initialExpirationDate: Date;
@Input() readonly editMode: boolean;
@Input() readonly disabled: boolean;
@Output() datesChanged = new EventEmitter<{ deletionDate: string; expirationDate: string }>();
get browserPath(): BrowserPath {
if (this.platformUtilsService.isFirefox()) {
return BrowserPath.Firefox;
} else if (this.platformUtilsService.isSafari()) {
return BrowserPath.Safari;
}
return BrowserPath.Default;
}
datesForm = new UntypedFormGroup({
selectedDeletionDatePreset: new UntypedFormControl(),
selectedExpirationDatePreset: new UntypedFormControl(),
defaultDeletionDateTime: new UntypedFormControl(),
defaultExpirationDateTime: new UntypedFormControl(),
fallbackDeletionDate: new UntypedFormControl(),
fallbackDeletionTime: new UntypedFormControl(),
fallbackExpirationDate: new UntypedFormControl(),
fallbackExpirationTime: new UntypedFormControl(),
});
deletionDatePresets: any[] = [
{ name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
{ name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
{ name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
{ name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
{ name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
{ name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
{ name: this.i18nService.t("custom"), value: DatePreset.Custom },
];
expirationDatePresets: any[] = [
{ name: this.i18nService.t("never"), value: DatePreset.Never },
].concat([...this.deletionDatePresets]);
get selectedDeletionDatePreset(): UntypedFormControl {
return this.datesForm.get("selectedDeletionDatePreset") as UntypedFormControl;
}
get selectedExpirationDatePreset(): UntypedFormControl {
return this.datesForm.get("selectedExpirationDatePreset") as UntypedFormControl;
}
get defaultDeletionDateTime(): UntypedFormControl {
return this.datesForm.get("defaultDeletionDateTime") as UntypedFormControl;
}
get defaultExpirationDateTime(): UntypedFormControl {
return this.datesForm.get("defaultExpirationDateTime") as UntypedFormControl;
}
get fallbackDeletionDate(): UntypedFormControl {
return this.datesForm.get("fallbackDeletionDate") as UntypedFormControl;
}
get fallbackDeletionTime(): UntypedFormControl {
return this.datesForm.get("fallbackDeletionTime") as UntypedFormControl;
}
get fallbackExpirationDate(): UntypedFormControl {
return this.datesForm.get("fallbackExpirationDate") as UntypedFormControl;
}
get fallbackExpirationTime(): UntypedFormControl {
return this.datesForm.get("fallbackExpirationTime") as UntypedFormControl;
}
// Should be able to call these at any time and compute a submitable value
get formattedDeletionDate(): string {
switch (this.selectedDeletionDatePreset.value as DatePreset) {
case DatePreset.Never:
this.selectedDeletionDatePreset.setValue(DatePreset.SevenDays);
return this.formattedDeletionDate;
case DatePreset.Custom:
switch (this.browserPath) {
case BrowserPath.Safari:
case BrowserPath.Firefox:
return this.fallbackDeletionDate.value + "T" + this.fallbackDeletionTime.value;
default:
return this.defaultDeletionDateTime.value;
}
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() + (this.selectedDeletionDatePreset.value as number) * 60 * 60 * 1000
);
return new Date(milliseconds).toString();
}
}
}
get formattedExpirationDate(): string {
switch (this.selectedExpirationDatePreset.value as DatePreset) {
case DatePreset.Never:
return null;
case DatePreset.Custom:
switch (this.browserPath) {
case BrowserPath.Safari:
case BrowserPath.Firefox:
if (
(!this.fallbackExpirationDate.value || !this.fallbackExpirationTime.value) &&
this.editMode
) {
return null;
}
return this.fallbackExpirationDate.value + "T" + this.fallbackExpirationTime.value;
default:
if (!this.defaultExpirationDateTime.value) {
return null;
}
return this.defaultExpirationDateTime.value;
}
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() + (this.selectedExpirationDatePreset.value as number) * 60 * 60 * 1000
);
return new Date(milliseconds).toString();
}
}
}
//
get safariDeletionTimePresetOptions() {
return this.safariTimePresetOptions(DateField.DeletionDate);
}
get safariExpirationTimePresetOptions() {
return this.safariTimePresetOptions(DateField.ExpirationDate);
}
private get nextWeek(): Date {
const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);
return nextWeek;
}
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected datePipe: DatePipe
) {}
ngOnInit(): void {
this.setInitialFormValues();
this.emitDates();
this.datesForm.valueChanges.subscribe(() => {
this.emitDates();
});
}
onDeletionDatePresetSelect(value: DatePreset) {
this.selectedDeletionDatePreset.setValue(value);
}
clearExpiration() {
switch (this.browserPath) {
case BrowserPath.Safari:
case BrowserPath.Firefox:
this.fallbackExpirationDate.setValue(null);
this.fallbackExpirationTime.setValue(null);
break;
case BrowserPath.Default:
this.defaultExpirationDateTime.setValue(null);
break;
}
}
protected emitDates() {
this.datesChanged.emit({
deletionDate: this.formattedDeletionDate,
expirationDate: this.formattedExpirationDate,
});
}
protected setInitialFormValues() {
if (this.editMode) {
this.selectedDeletionDatePreset.setValue(DatePreset.Custom);
this.selectedExpirationDatePreset.setValue(DatePreset.Custom);
switch (this.browserPath) {
case BrowserPath.Safari:
case BrowserPath.Firefox:
this.fallbackDeletionDate.setValue(this.initialDeletionDate.toISOString().slice(0, 10));
this.fallbackDeletionTime.setValue(this.initialDeletionDate.toTimeString().slice(0, 5));
if (this.initialExpirationDate != null) {
this.fallbackExpirationDate.setValue(
this.initialExpirationDate.toISOString().slice(0, 10)
);
this.fallbackExpirationTime.setValue(
this.initialExpirationDate.toTimeString().slice(0, 5)
);
}
break;
case BrowserPath.Default:
if (this.initialExpirationDate) {
this.defaultExpirationDateTime.setValue(
this.datePipe.transform(new Date(this.initialExpirationDate), "yyyy-MM-ddTHH:mm")
);
}
this.defaultDeletionDateTime.setValue(
this.datePipe.transform(new Date(this.initialDeletionDate), "yyyy-MM-ddTHH:mm")
);
break;
}
} else {
this.selectedDeletionDatePreset.setValue(DatePreset.SevenDays);
this.selectedExpirationDatePreset.setValue(DatePreset.Never);
switch (this.browserPath) {
case BrowserPath.Safari:
this.fallbackDeletionDate.setValue(this.nextWeek.toISOString().slice(0, 10));
this.fallbackDeletionTime.setValue(
this.safariTimePresetOptions(DateField.DeletionDate)[1].twentyFourHour
);
break;
default:
break;
}
}
}
protected safariTimePresetOptions(field: DateField): TimeOption[] {
// init individual arrays for major sort groups
const noon: TimeOption[] = [];
const midnight: TimeOption[] = [];
const ams: TimeOption[] = [];
const pms: TimeOption[] = [];
// determine minute skip (5 min, 10 min, 15 min, etc.)
const minuteIncrementer = 15;
// loop through each hour on a 12 hour system
for (let h = 1; h <= 12; h++) {
// loop through each minute in the hour using the skip to increment
for (let m = 0; m < 60; m += minuteIncrementer) {
// init the final strings that will be added to the lists
let hour = h.toString();
let minutes = m.toString();
// add prepending 0s to single digit hours/minutes
if (h < 10) {
hour = "0" + hour;
}
if (m < 10) {
minutes = "0" + minutes;
}
// build time strings and push to relevant sort groups
if (h === 12) {
const midnightOption: TimeOption = {
twelveHour: `${hour}:${minutes} AM`,
twentyFourHour: `00:${minutes}`,
};
midnight.push(midnightOption);
const noonOption: TimeOption = {
twelveHour: `${hour}:${minutes} PM`,
twentyFourHour: `${hour}:${minutes}`,
};
noon.push(noonOption);
} else {
const amOption: TimeOption = {
twelveHour: `${hour}:${minutes} AM`,
twentyFourHour: `${hour}:${minutes}`,
};
ams.push(amOption);
const pmOption: TimeOption = {
twelveHour: `${hour}:${minutes} PM`,
twentyFourHour: `${h + 12}:${minutes}`,
};
pms.push(pmOption);
}
}
}
// bring all the arrays together in the right order
const validTimes = [...midnight, ...ams, ...noon, ...pms];
// determine if an unsupported value already exists on the send & add that to the top of the option list
// example: if the Send was created with a different client
if (field === DateField.ExpirationDate && this.initialExpirationDate != null && this.editMode) {
const previousValue: TimeOption = {
twelveHour: this.datePipe.transform(this.initialExpirationDate, "hh:mm a"),
twentyFourHour: this.datePipe.transform(this.initialExpirationDate, "HH:mm"),
};
return [previousValue, { twelveHour: null, twentyFourHour: null }, ...validTimes];
} else if (
field === DateField.DeletionDate &&
this.initialDeletionDate != null &&
this.editMode
) {
const previousValue: TimeOption = {
twelveHour: this.datePipe.transform(this.initialDeletionDate, "hh:mm a"),
twentyFourHour: this.datePipe.transform(this.initialDeletionDate, "HH:mm"),
};
return [previousValue, ...validTimes];
} else {
return [{ twelveHour: null, twentyFourHour: null }, ...validTimes];
}
}
}