Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/desktop/desktop_native/autotype/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ pub fn get_foreground_window_title() -> std::result::Result<String, ()> {
///
/// TODO: The error handling will be improved in a future PR: PM-23615
#[allow(clippy::result_unit_err)]
pub fn type_input(input: Vec<u16>) -> std::result::Result<(), ()> {
windowing::type_input(input)
pub fn type_input(input: Vec<u16>, keyboardShortcut: Vec<String>) -> std::result::Result<(), ()> {
windowing::type_input(input, keyboardShortcut)
}
4 changes: 3 additions & 1 deletion apps/desktop/desktop_native/autotype/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ pub fn get_foreground_window_title() -> std::result::Result<String, ()> {
/// `input` must be an array of utf-16 encoded characters to insert.
///
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
pub fn type_input(input: Vec<u16>) -> Result<(), ()> {
pub fn type_input(input: Vec<u16>, keyboard_input: Vec<String>) -> Result<(), ()> {
println!("type_input() hit, keyboardInput is: {:?}", keyboard_input);

const TAB_KEY: u16 = 9;
let mut keyboard_inputs: Vec<INPUT> = Vec::new();

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/desktop_native/napi/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,5 +234,5 @@ export declare namespace chromium_importer {
}
export declare namespace autotype {
export function getForegroundWindowTitle(): string
export function typeInput(input: Array<number>): void
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
}
4 changes: 2 additions & 2 deletions apps/desktop/desktop_native/napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,8 +1044,8 @@ pub mod autotype {
}

#[napi]
pub fn type_input(input: Vec<u16>) -> napi::Result<(), napi::Status> {
autotype::type_input(input).map_err(|_| {
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> napi::Result<(), napi::Status> {
autotype::type_input(input, keyboard_shortcut).map_err(|_| {
napi::Error::from_reason("Autotype Error: failed to type input".to_string())
})
}
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/app/accounts/settings.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ <h2>
<small class="help-block" *ngIf="form.value.enableAutotype">
<b>{{ "important" | i18n }}</b>
{{ "enableAutotypeDescriptionTransitionKey" | i18n }}
<b>{{ "editShortcut" | i18n }}</b></small
<span class="settings-link" *ngIf="this.form.value.enableAutotype" (click)="updateAutotypeShortcut()">
{{ "editShortcut" | i18n }}
</span></small
>
</div>
<div class="form-group">
Expand Down
29 changes: 19 additions & 10 deletions apps/desktop/src/app/accounts/settings.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { DeviceType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";

Check failure on line 23 in apps/desktop/src/app/accounts/settings.component.ts

View workflow job for this annotation

GitHub Actions / Lint

'FeatureFlag' is defined but never used
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import {
VaultTimeout,
Expand Down Expand Up @@ -58,6 +58,7 @@
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";

import { SetPinComponent } from "../../auth/components/set-pin.component";
import { SetAutotypeShortcutComponent } from "../../autofill/components/set-autotype-shortcut.component";
import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service";
Expand Down Expand Up @@ -138,6 +139,8 @@

userHasMasterPassword: boolean;
userHasPinSet: boolean;

userHasAutotypeShortcutSet: boolean;

pinEnabled$: Observable<boolean> = of(true);

Expand Down Expand Up @@ -282,15 +285,9 @@
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);

// Autotype is for Windows initially
const isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;

Check failure on line 288 in apps/desktop/src/app/accounts/settings.component.ts

View workflow job for this annotation

GitHub Actions / Lint

'isWindows' is assigned a value but never used
if (isWindows) {
this.configService
.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype)
.pipe(takeUntil(this.destroy$))
.subscribe((enabled) => {
this.showEnableAutotype = enabled;
});
}
const windowsDesktopAutotypeFeatureFlag = true;
this.showEnableAutotype = windowsDesktopAutotypeFeatureFlag;

this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();

Expand Down Expand Up @@ -421,12 +418,12 @@
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
});

if (isWindows) {
if (true) {

Check failure on line 421 in apps/desktop/src/app/accounts/settings.component.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected constant condition
this.billingAccountProfileStateService
.hasPremiumFromAnySource$(activeAccount.id)
.pipe(takeUntil(this.destroy$))
.subscribe((hasPremium) => {
if (hasPremium) {
if (true) {

Check failure on line 426 in apps/desktop/src/app/accounts/settings.component.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected constant condition
this.form.controls.enableAutotype.enable();
}
});
Expand Down Expand Up @@ -899,6 +896,18 @@
await this.desktopAutotypeService.setAutotypeEnabledState(this.form.value.enableAutotype);
}

async updateAutotypeShortcut() {
const dialogRef = SetAutotypeShortcutComponent.open(this.dialogService);

if (dialogRef == null) {
this.form.controls.pin.setValue(false, { emitEvent: false });
return;
}

this.userHasAutotypeShortcutSet = await firstValueFrom(dialogRef.closed);
this.form.controls.pin.setValue(this.userHasAutotypeShortcutSet, { emitEvent: false });
}

private async generateVaultTimeoutOptions(): Promise<VaultTimeoutOption[]> {
let vaultTimeoutOptions: VaultTimeoutOption[] = [
{ name: this.i18nService.t("oneMinute"), value: 1 },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<form [bitSubmit]="submit" [formGroup]="setShortcutForm">
<bit-dialog>
<div class="tw-font-semibold" bitDialogTitle>
{{ "editAutotypeShortcut" | i18n }}
</div>
<div bitDialogContent>
<p>
{{ "editAutotypeShortcutDescription" | i18n }}
</p>
<bit-form-field>
<bit-label>{{ "typeShortcut" | i18n }}</bit-label>
<input
class="tw-font-mono"
bitInput
type="text"
formControlName="shortcut"
(keydown)="onShortcutKeydown($event)"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</bit-form-field>
</div>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary">
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {

Check failure on line 8 in apps/desktop/src/autofill/components/set-autotype-shortcut.component.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups
AsyncActionsModule,
ButtonModule,
DialogModule,
DialogRef,
DialogService,
FormFieldModule,
IconButtonModule,
} from "@bitwarden/components";
import { firstValueFrom } from "rxjs";

Check failure on line 17 in apps/desktop/src/autofill/components/set-autotype-shortcut.component.ts

View workflow job for this annotation

GitHub Actions / Lint

`rxjs` import should occur before import of `@bitwarden/angular/jslib.module`

@Component({
templateUrl: "set-autotype-shortcut.component.html",
imports: [
DialogModule,
CommonModule,
JslibModule,
ButtonModule,
IconButtonModule,
ReactiveFormsModule,
AsyncActionsModule,
FormFieldModule,
],
})
export class SetAutotypeShortcutComponent implements OnInit {

constructor(
private accountService: AccountService,
private dialogRef: DialogRef,
private formBuilder: FormBuilder,
// Autotype service?
) { }

ngOnInit(): void {
// set form value from state
}

setShortcutForm = this.formBuilder.group({
shortcut: [
"",
[Validators.required, this.shortcutCombinationValidator()],
],
requireMasterPasswordOnClientRestart: true,
});

submit = async () => {
const shortcutFormControl = this.setShortcutForm.controls.shortcut;

if (Utils.isNullOrWhitespace(shortcutFormControl.value) || shortcutFormControl.invalid) {
return;
}

const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;

Check failure on line 60 in apps/desktop/src/autofill/components/set-autotype-shortcut.component.ts

View workflow job for this annotation

GitHub Actions / Lint

'userId' is assigned a value but never used

// Save shortcut via autotype service
console.log(shortcutFormControl.value);

Check failure on line 63 in apps/desktop/src/autofill/components/set-autotype-shortcut.component.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement

this.dialogRef.close(true);
};

static open(dialogService: DialogService) {
return dialogService.open<boolean>(SetAutotypeShortcutComponent);
}

onShortcutKeydown(event: KeyboardEvent): void {
event.preventDefault();

const shortcut = this.buildShortcutFromEvent(event);

if (shortcut != null) {
this.setShortcutForm.controls.shortcut.setValue(shortcut);
this.setShortcutForm.controls.shortcut.markAsDirty();
this.setShortcutForm.controls.shortcut.updateValueAndValidity();
}
}

private buildShortcutFromEvent(event: KeyboardEvent): string | null {
const hasCtrl = event.ctrlKey;
const hasAlt = event.altKey;
const hasShift = event.shiftKey;

// Require at least one modifier (Ctrl, Alt, or Shift)
if (!hasCtrl && !hasAlt && !hasShift) {
return null;
}

const key = event.key;

// Ignore pure modifier keys themselves
if (key === "Control" || key === "Alt" || key === "Shift" || key === "Meta") {
return null;
}

// Accept a single alphanumeric letter or number as the base key
const isAlphaNumeric = typeof key === "string" && /^[a-zA-Z0-9]$/.test(key);
if (!isAlphaNumeric) {
return null;
}

const parts: string[] = [];
if (hasCtrl) {
parts.push("Ctrl");
}
if (hasAlt) {
parts.push("Alt");
}
if (hasShift) {
parts.push("Shift");
}
parts.push(key.toUpperCase());

return parts.join("+");
}

private shortcutCombinationValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = (control.value ?? "").toString();
if (value.length === 0) {
return null; // handled by required
}

// Must include at least one modifier and end with a single alphanumeric
// Valid examples: Ctrl+A, Alt+5, Shift+Z, Ctrl+Alt+7, Ctrl+Shift+X, Alt+Shift+Q
const pattern = /^(?=.*\b(Ctrl|Alt|Shift)\b)(?:Ctrl\+)?(?:Alt\+)?(?:Shift\+)?[A-Z0-9]$/i;
return pattern.test(value) ? null : { invalidShortcut: true };
};
}
}
29 changes: 19 additions & 10 deletions apps/desktop/src/autofill/main/main-desktop-autotype.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,31 @@
import { autotype } from "@bitwarden/desktop-napi";
import { LogService } from "@bitwarden/logging";

import { WindowMain } from "../../main/window.main";

Check warning on line 6 in apps/desktop/src/autofill/main/main-desktop-autotype.service.ts

View workflow job for this annotation

GitHub Actions / Lint

'/home/runner/work/clients/clients/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts' imported multiple times

Check failure on line 6 in apps/desktop/src/autofill/main/main-desktop-autotype.service.ts

View workflow job for this annotation

GitHub Actions / Lint

`../models/main-autotype-keyboard-shortcut` import should occur after import of `../../utils`
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut";

Check warning on line 9 in apps/desktop/src/autofill/main/main-desktop-autotype.service.ts

View workflow job for this annotation

GitHub Actions / Lint

'/home/runner/work/clients/clients/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts' imported multiple times
export class MainDesktopAutotypeService {
keySequence: string = "CommandOrControl+Shift+B";
autotypeKeyboardShortcut: AutotypeKeyboardShortcut;

constructor(
private logService: LogService,
private windowMain: WindowMain,
) {}
) {
this.autotypeKeyboardShortcut = new AutotypeKeyboardShortcut();
}

init() {
ipcMain.on("autofill.configureAutotype", (event, data) => {
if (data.enabled === true && !globalShortcut.isRegistered(this.keySequence)) {
const { response } = data;

Check failure on line 23 in apps/desktop/src/autofill/main/main-desktop-autotype.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
let setCorrectly = this.autotypeKeyboardShortcut.set(response.keyboardShortcut);
console.log("Was autotypeKeyboardShortcut set correctly from within the main process? " + setCorrectly);
// TODO: What do we do if it wasn't? The value won't change but we need to send a failure message back

if (response.enabled === true && !globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat())) {
this.enableAutotype();
} else if (data.enabled === false && globalShortcut.isRegistered(this.keySequence)) {
} else if (response.enabled === false && globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat())) {
this.disableAutotype();
}
});
Expand All @@ -30,21 +39,21 @@
stringIsNotUndefinedNullAndEmpty(response.username) &&
stringIsNotUndefinedNullAndEmpty(response.password)
) {
this.doAutotype(response.username, response.password);
this.doAutotype(response.username, response.password, this.autotypeKeyboardShortcut.getArrayFormat());
}
});
}

disableAutotype() {
if (globalShortcut.isRegistered(this.keySequence)) {
globalShortcut.unregister(this.keySequence);
if (globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat())) {
globalShortcut.unregister(this.autotypeKeyboardShortcut.getElectronFormat());
}

this.logService.info("Autotype disabled.");
}

private enableAutotype() {
const result = globalShortcut.register(this.keySequence, () => {
const result = globalShortcut.register(this.autotypeKeyboardShortcut.getElectronFormat(), () => {
const windowTitle = autotype.getForegroundWindowTitle();

this.windowMain.win.webContents.send("autofill.listenAutotypeRequest", {
Expand All @@ -57,14 +66,14 @@
: this.logService.info("Enabling autotype failed.");
}

private doAutotype(username: string, password: string) {
private doAutotype(username: string, password: string, keyboardShortcut: string[]) {
const inputPattern = username + "\t" + password;
const inputArray = new Array<number>(inputPattern.length);

for (let i = 0; i < inputPattern.length; i++) {
inputArray[i] = inputPattern.charCodeAt(i);
}

autotype.typeInput(inputArray);
autotype.typeInput(inputArray, keyboardShortcut);
}
}
Loading
Loading