mirror of
https://github.com/bitwarden/browser
synced 2026-02-17 18:09:17 +00:00
Merge branch 'main' into desktop/pm-18769/migrate-vault-filters
This commit is contained in:
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@@ -329,6 +329,7 @@ name = "autotype"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"mockall",
|
||||
"serial_test",
|
||||
"tracing",
|
||||
@@ -1026,6 +1027,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
@@ -1617,6 +1624,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
|
||||
@@ -39,6 +39,7 @@ futures = "=0.3.31"
|
||||
hex = "=0.4.3"
|
||||
homedir = "=0.3.6"
|
||||
interprocess = "=2.2.1"
|
||||
itertools = "=0.14.0"
|
||||
libc = "=0.2.178"
|
||||
linux-keyutils = "=0.2.4"
|
||||
memsec = "=0.7.0"
|
||||
|
||||
@@ -9,6 +9,7 @@ publish.workspace = true
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
itertools.workspace = true
|
||||
mockall = "=0.14.0"
|
||||
serial_test = "=3.2.0"
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -28,6 +28,6 @@ pub fn get_foreground_window_title() -> Result<String> {
|
||||
/// This function returns an `anyhow::Error` if there is any
|
||||
/// issue in typing the input. Detailed reasons will
|
||||
/// vary based on platform implementation.
|
||||
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
pub fn type_input(input: &[u16], keyboard_shortcut: &[String]) -> Result<()> {
|
||||
windowing::type_input(input, keyboard_shortcut)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ pub fn get_foreground_window_title() -> anyhow::Result<String> {
|
||||
todo!("Bitwarden does not yet support Linux autotype");
|
||||
}
|
||||
|
||||
pub fn type_input(_input: Vec<u16>, _keyboard_shortcut: Vec<String>) -> anyhow::Result<()> {
|
||||
pub fn type_input(_input: &[u16], _keyboard_shortcut: &[String]) -> anyhow::Result<()> {
|
||||
todo!("Bitwarden does not yet support Linux autotype");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ pub fn get_foreground_window_title() -> anyhow::Result<String> {
|
||||
todo!("Bitwarden does not yet support macOS autotype");
|
||||
}
|
||||
|
||||
pub fn type_input(_input: Vec<u16>, _keyboard_shortcut: Vec<String>) -> anyhow::Result<()> {
|
||||
pub fn type_input(_input: &[u16], _keyboard_shortcut: &[String]) -> anyhow::Result<()> {
|
||||
todo!("Bitwarden does not yet support macOS autotype");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use tracing::debug;
|
||||
use windows::Win32::Foundation::{GetLastError, SetLastError, WIN32_ERROR};
|
||||
use windows::Win32::{
|
||||
Foundation::{GetLastError, SetLastError, WIN32_ERROR},
|
||||
UI::Input::KeyboardAndMouse::INPUT,
|
||||
};
|
||||
|
||||
mod type_input;
|
||||
mod window_title;
|
||||
@@ -12,7 +16,7 @@ const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0);
|
||||
/// win32 errors.
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
trait ErrorOperations {
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror>
|
||||
fn set_last_error(err: u32) {
|
||||
debug!(err, "Calling SetLastError");
|
||||
unsafe {
|
||||
@@ -20,7 +24,7 @@ trait ErrorOperations {
|
||||
}
|
||||
}
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror>
|
||||
fn get_last_error() -> WIN32_ERROR {
|
||||
let last_err = unsafe { GetLastError() };
|
||||
debug!("GetLastError(): {}", last_err.to_hresult().message());
|
||||
@@ -36,6 +40,23 @@ pub fn get_foreground_window_title() -> Result<String> {
|
||||
window_title::get_foreground_window_title()
|
||||
}
|
||||
|
||||
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
type_input::type_input(input, keyboard_shortcut)
|
||||
/// `KeyboardShortcutInput` is an `INPUT` of one of the valid shortcut keys:
|
||||
/// - Control
|
||||
/// - Alt
|
||||
/// - Super
|
||||
/// - Shift
|
||||
/// - \[a-z\]\[A-Z\]
|
||||
struct KeyboardShortcutInput(INPUT);
|
||||
|
||||
pub fn type_input(input: &[u16], keyboard_shortcut: &[String]) -> Result<()> {
|
||||
debug!(?keyboard_shortcut, "type_input() called.");
|
||||
|
||||
// convert the raw string input to Windows input and error
|
||||
// if any key is not a valid keyboard shortcut input
|
||||
let keyboard_shortcut: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(s.as_str()))
|
||||
.try_collect()?;
|
||||
|
||||
type_input::type_input(input, &keyboard_shortcut)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,15 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||
VIRTUAL_KEY,
|
||||
};
|
||||
|
||||
use super::{ErrorOperations, Win32ErrorOperations};
|
||||
use super::{ErrorOperations, KeyboardShortcutInput, Win32ErrorOperations};
|
||||
|
||||
const SHIFT_KEY_STR: &str = "Shift";
|
||||
const CONTROL_KEY_STR: &str = "Control";
|
||||
const ALT_KEY_STR: &str = "Alt";
|
||||
const LEFT_WINDOWS_KEY_STR: &str = "Super";
|
||||
|
||||
const IS_VIRTUAL_KEY: bool = true;
|
||||
const IS_REAL_KEY: bool = false;
|
||||
|
||||
/// `InputOperations` provides an interface to Window32 API for
|
||||
/// working with inputs.
|
||||
@@ -13,7 +21,7 @@ use super::{ErrorOperations, Win32ErrorOperations};
|
||||
trait InputOperations {
|
||||
/// Attempts to type the provided input wherever the user's cursor is.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
fn send_input(inputs: &[INPUT]) -> u32;
|
||||
}
|
||||
|
||||
@@ -21,8 +29,11 @@ struct Win32InputOperations;
|
||||
|
||||
impl InputOperations for Win32InputOperations {
|
||||
fn send_input(inputs: &[INPUT]) -> u32 {
|
||||
const INPUT_STRUCT_SIZE: i32 = std::mem::size_of::<INPUT>() as i32;
|
||||
let insert_count = unsafe { SendInput(inputs, INPUT_STRUCT_SIZE) };
|
||||
const INPUT_STRUCT_SIZE: usize = std::mem::size_of::<INPUT>();
|
||||
|
||||
let size = i32::try_from(INPUT_STRUCT_SIZE).expect("INPUT size to fit in i32");
|
||||
|
||||
let insert_count = unsafe { SendInput(inputs, size) };
|
||||
|
||||
debug!(insert_count, "SendInput() called.");
|
||||
|
||||
@@ -33,40 +44,37 @@ impl InputOperations for Win32InputOperations {
|
||||
/// Attempts to type the input text wherever the user's cursor is.
|
||||
///
|
||||
/// `input` must be a vector of utf-16 encoded characters to insert.
|
||||
/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super,
|
||||
/// Shift, letters a - Z
|
||||
/// `keyboard_shortcut` is a vector of valid shortcut keys.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
pub(super) fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
pub(super) fn type_input(input: &[u16], keyboard_shortcut: &[KeyboardShortcutInput]) -> Result<()> {
|
||||
// the length of this vec is always shortcut keys to release + (2x length of input chars)
|
||||
let mut keyboard_inputs: Vec<INPUT> =
|
||||
Vec::with_capacity(keyboard_shortcut.len() + (input.len() * 2));
|
||||
|
||||
debug!(?keyboard_shortcut, "Converting keyboard shortcut to input.");
|
||||
|
||||
// Add key "up" inputs for the shortcut
|
||||
for key in keyboard_shortcut {
|
||||
keyboard_inputs.push(convert_shortcut_key_to_up_input(key)?);
|
||||
// insert the keyboard shortcut
|
||||
for shortcut in keyboard_shortcut {
|
||||
keyboard_inputs.push(shortcut.0);
|
||||
}
|
||||
|
||||
add_input(&input, &mut keyboard_inputs);
|
||||
add_input(input, &mut keyboard_inputs);
|
||||
|
||||
send_input::<Win32InputOperations, Win32ErrorOperations>(keyboard_inputs)
|
||||
send_input::<Win32InputOperations, Win32ErrorOperations>(&keyboard_inputs)
|
||||
}
|
||||
|
||||
// Add key "down" and "up" inputs for the input
|
||||
// (currently in this form: {username}/t{password})
|
||||
fn add_input(input: &[u16], keyboard_inputs: &mut Vec<INPUT>) {
|
||||
const TAB_KEY: u8 = 9;
|
||||
const TAB_KEY: u16 = 9;
|
||||
|
||||
for i in input {
|
||||
let next_down_input = if *i == TAB_KEY.into() {
|
||||
build_virtual_key_input(InputKeyPress::Down, *i as u8)
|
||||
let next_down_input = if *i == TAB_KEY {
|
||||
build_virtual_key_input(InputKeyPress::Down, *i)
|
||||
} else {
|
||||
build_unicode_input(InputKeyPress::Down, *i)
|
||||
};
|
||||
let next_up_input = if *i == TAB_KEY.into() {
|
||||
build_virtual_key_input(InputKeyPress::Up, *i as u8)
|
||||
let next_up_input = if *i == TAB_KEY {
|
||||
build_virtual_key_input(InputKeyPress::Up, *i)
|
||||
} else {
|
||||
build_unicode_input(InputKeyPress::Up, *i)
|
||||
};
|
||||
@@ -76,26 +84,27 @@ fn add_input(input: &[u16], keyboard_inputs: &mut Vec<INPUT>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a valid shortcut key to an "up" keyboard input.
|
||||
///
|
||||
/// `input` must be a valid shortcut key: Control, Alt, Super, Shift, letters [a-z][A-Z]
|
||||
fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT> {
|
||||
const SHIFT_KEY: u8 = 0x10;
|
||||
const SHIFT_KEY_STR: &str = "Shift";
|
||||
const CONTROL_KEY: u8 = 0x11;
|
||||
const CONTROL_KEY_STR: &str = "Control";
|
||||
const ALT_KEY: u8 = 0x12;
|
||||
const ALT_KEY_STR: &str = "Alt";
|
||||
const LEFT_WINDOWS_KEY: u8 = 0x5B;
|
||||
const LEFT_WINDOWS_KEY_STR: &str = "Super";
|
||||
impl TryFrom<&str> for KeyboardShortcutInput {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
Ok(match key.as_str() {
|
||||
SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY),
|
||||
CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY),
|
||||
ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY),
|
||||
LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY),
|
||||
_ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?),
|
||||
})
|
||||
fn try_from(key: &str) -> std::result::Result<Self, Self::Error> {
|
||||
const SHIFT_KEY: u16 = 0x10;
|
||||
const CONTROL_KEY: u16 = 0x11;
|
||||
const ALT_KEY: u16 = 0x12;
|
||||
const LEFT_WINDOWS_KEY: u16 = 0x5B;
|
||||
|
||||
// the modifier keys are using the Up keypress variant because the user has already
|
||||
// pressed those keys in order to trigger the feature.
|
||||
let input = match key {
|
||||
SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY),
|
||||
CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY),
|
||||
ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY),
|
||||
LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY),
|
||||
_ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?),
|
||||
};
|
||||
|
||||
Ok(KeyboardShortcutInput(input))
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a letter that is a String, get the utf16 encoded
|
||||
@@ -105,7 +114,7 @@ fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT> {
|
||||
/// Because we only accept [a-z][A-Z], the decimal u16
|
||||
/// cast of the letter is safe because the unicode code point
|
||||
/// of these characters fits in a u16.
|
||||
fn get_alphabetic_hotkey(letter: String) -> Result<u16> {
|
||||
fn get_alphabetic_hotkey(letter: &str) -> Result<u16> {
|
||||
if letter.len() != 1 {
|
||||
error!(
|
||||
len = letter.len(),
|
||||
@@ -135,23 +144,28 @@ fn get_alphabetic_hotkey(letter: String) -> Result<u16> {
|
||||
}
|
||||
|
||||
/// An input key can be either pressed (down), or released (up).
|
||||
#[derive(Copy, Clone)]
|
||||
enum InputKeyPress {
|
||||
Down,
|
||||
Up,
|
||||
}
|
||||
|
||||
/// A function for easily building keyboard unicode INPUT structs used in SendInput().
|
||||
///
|
||||
/// Before modifying this function, make sure you read the SendInput() documentation:
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
/// Before modifying this function, make sure you read the `SendInput()` documentation:
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes>
|
||||
fn build_input(key_press: InputKeyPress, character: u16, is_virtual: bool) -> INPUT {
|
||||
let (w_vk, w_scan) = if is_virtual {
|
||||
(VIRTUAL_KEY(character), 0)
|
||||
} else {
|
||||
(VIRTUAL_KEY::default(), character)
|
||||
};
|
||||
match key_press {
|
||||
InputKeyPress::Down => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: Default::default(),
|
||||
wScan: character,
|
||||
wVk: w_vk,
|
||||
wScan: w_scan,
|
||||
dwFlags: KEYEVENTF_UNICODE,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
@@ -162,8 +176,8 @@ fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: Default::default(),
|
||||
wScan: character,
|
||||
wVk: w_vk,
|
||||
wScan: w_scan,
|
||||
dwFlags: KEYEVENTF_KEYUP | KEYEVENTF_UNICODE,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
@@ -173,53 +187,29 @@ fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
}
|
||||
}
|
||||
|
||||
/// A function for easily building keyboard virtual-key INPUT structs used in SendInput().
|
||||
///
|
||||
/// Before modifying this function, make sure you read the SendInput() documentation:
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT {
|
||||
match key_press {
|
||||
InputKeyPress::Down => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: VIRTUAL_KEY(virtual_key as u16),
|
||||
wScan: Default::default(),
|
||||
dwFlags: Default::default(),
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
InputKeyPress::Up => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: VIRTUAL_KEY(virtual_key as u16),
|
||||
wScan: Default::default(),
|
||||
dwFlags: KEYEVENTF_KEYUP,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
/// A function for easily building keyboard unicode `INPUT` structs used in `SendInput()`.
|
||||
fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
build_input(key_press, character, IS_REAL_KEY)
|
||||
}
|
||||
|
||||
fn send_input<I, E>(inputs: Vec<INPUT>) -> Result<()>
|
||||
/// A function for easily building keyboard virtual-key `INPUT` structs used in `SendInput()`.
|
||||
fn build_virtual_key_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
build_input(key_press, character, IS_VIRTUAL_KEY)
|
||||
}
|
||||
|
||||
fn send_input<I, E>(inputs: &[INPUT]) -> Result<()>
|
||||
where
|
||||
I: InputOperations,
|
||||
E: ErrorOperations,
|
||||
{
|
||||
let insert_count = I::send_input(&inputs);
|
||||
let insert_count = I::send_input(inputs);
|
||||
|
||||
if insert_count == 0 {
|
||||
let last_err = E::get_last_error().to_hresult().message();
|
||||
error!(GetLastError = %last_err, "SendInput sent 0 inputs. Input was blocked by another thread.");
|
||||
|
||||
return Err(anyhow!("SendInput sent 0 inputs. Input was blocked by another thread. GetLastError: {last_err}"));
|
||||
} else if insert_count != inputs.len() as u32 {
|
||||
} else if insert_count != u32::try_from(inputs.len()).expect("to convert inputs len to u32") {
|
||||
let last_err = E::get_last_error().to_hresult().message();
|
||||
error!(sent = %insert_count, expected = inputs.len(), GetLastError = %last_err,
|
||||
"SendInput sent does not match expected."
|
||||
@@ -237,8 +227,9 @@ where
|
||||
mod tests {
|
||||
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
|
||||
//! crate in order to mock those, since the mock expectations set have to be global in
|
||||
//! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
|
||||
//! absence of a `self`. More info: <https://docs.rs/mockall/latest/mockall/#static-methods>
|
||||
|
||||
use itertools::Itertools;
|
||||
use serial_test::serial;
|
||||
use windows::Win32::Foundation::WIN32_ERROR;
|
||||
|
||||
@@ -249,7 +240,7 @@ mod tests {
|
||||
fn get_alphabetic_hot_key_succeeds() {
|
||||
for c in ('a'..='z').chain('A'..='Z') {
|
||||
let letter = c.to_string();
|
||||
let converted = get_alphabetic_hotkey(letter).unwrap();
|
||||
let converted = get_alphabetic_hotkey(&letter).unwrap();
|
||||
assert_eq!(converted, c as u16);
|
||||
}
|
||||
}
|
||||
@@ -258,14 +249,14 @@ mod tests {
|
||||
#[should_panic = "Final keyboard shortcut key should be a single character: foo"]
|
||||
fn get_alphabetic_hot_key_fail_not_single_char() {
|
||||
let letter = String::from("foo");
|
||||
get_alphabetic_hotkey(letter).unwrap();
|
||||
get_alphabetic_hotkey(&letter).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '}'"]
|
||||
fn get_alphabetic_hot_key_fail_not_alphabetic() {
|
||||
let letter = String::from("}");
|
||||
get_alphabetic_hotkey(letter).unwrap();
|
||||
get_alphabetic_hotkey(&letter).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,7 +266,7 @@ mod tests {
|
||||
ctxi.checkpoint();
|
||||
ctxi.expect().returning(|_| 1);
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
@@ -284,6 +275,29 @@ mod tests {
|
||||
drop(ctxi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn keyboard_shortcut_conversion_succeeds() {
|
||||
let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "B"];
|
||||
let _: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(*s))
|
||||
.try_collect()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '1'"]
|
||||
fn keyboard_shortcut_conversion_fails_invalid_key() {
|
||||
let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "1"];
|
||||
let _: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(*s))
|
||||
.try_collect()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic(
|
||||
@@ -298,7 +312,7 @@ mod tests {
|
||||
ctxge.checkpoint();
|
||||
ctxge.expect().returning(|| WIN32_ERROR(1));
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
@@ -320,7 +334,7 @@ mod tests {
|
||||
ctxge.checkpoint();
|
||||
ctxge.expect().returning(|| WIN32_ERROR(1));
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
|
||||
@@ -11,10 +11,10 @@ use super::{ErrorOperations, Win32ErrorOperations, WIN32_SUCCESS};
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
trait WindowHandleOperations {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw>
|
||||
fn get_window_text_length_w(&self) -> Result<i32>;
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw>
|
||||
fn get_window_text_w(&self, buffer: &mut Vec<u16>) -> Result<i32>;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ pub(super) fn get_foreground_window_title() -> Result<String> {
|
||||
|
||||
/// Retrieves the foreground window handle and validates it.
|
||||
fn get_foreground_window_handle() -> Result<WindowHandle> {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow>
|
||||
let handle = unsafe { GetForegroundWindow() };
|
||||
|
||||
debug!("GetForegroundWindow() called.");
|
||||
@@ -87,7 +87,7 @@ fn get_foreground_window_handle() -> Result<WindowHandle> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - If the length zero and GetLastError() != 0, return the GetLastError() message.
|
||||
/// - If the length zero and `GetLastError()` != 0, return the `GetLastError()` message.
|
||||
fn get_window_title_length<H, E>(window_handle: &H) -> Result<usize>
|
||||
where
|
||||
H: WindowHandleOperations,
|
||||
@@ -128,7 +128,7 @@ where
|
||||
/// # Errors
|
||||
///
|
||||
/// - If the actual window title length (what the win32 API declares was written into the buffer),
|
||||
/// is length zero and GetLastError() != 0 , return the GetLastError() message.
|
||||
/// is length zero and `GetLastError()` != 0 , return the `GetLastError()` message.
|
||||
fn get_window_title<H, E>(window_handle: &H, expected_title_length: usize) -> Result<String>
|
||||
where
|
||||
H: WindowHandleOperations,
|
||||
@@ -140,7 +140,7 @@ where
|
||||
// The upstream will make a contains comparison on what we return, so an empty string
|
||||
// will not result on a match.
|
||||
warn!("Window title length is zero.");
|
||||
return Ok(String::from(""));
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let mut buffer: Vec<u16> = vec![0; expected_title_length + 1]; // add extra space for the null character
|
||||
@@ -171,7 +171,7 @@ where
|
||||
mod tests {
|
||||
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
|
||||
//! crate in order to mock those, since the mock expectations set have to be global in
|
||||
//! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
|
||||
//! absence of a `self`. More info: <https://docs.rs/mockall/latest/mockall/#static-methods>
|
||||
|
||||
use mockall::predicate;
|
||||
use serial_test::serial;
|
||||
|
||||
@@ -226,7 +226,7 @@ impl BitwardenDesktopAgent {
|
||||
keystore.0.write().expect("RwLock is not poisoned").clear();
|
||||
|
||||
self.needs_unlock
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
for (key, name, cipher_id) in new_keys.iter() {
|
||||
match parse_key_safe(key) {
|
||||
@@ -307,87 +307,3 @@ fn parse_key_safe(pem: &str) -> Result<ssh_key::private::PrivateKey, anyhow::Err
|
||||
Err(e) => Err(anyhow::Error::msg(format!("Failed to parse key: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_agent() -> (
|
||||
BitwardenDesktopAgent,
|
||||
tokio::sync::mpsc::Receiver<SshAgentUIRequest>,
|
||||
tokio::sync::broadcast::Sender<(u32, bool)>,
|
||||
) {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(10);
|
||||
let (response_tx, response_rx) = tokio::sync::broadcast::channel(10);
|
||||
let agent = BitwardenDesktopAgent::new(tx, Arc::new(Mutex::new(response_rx)));
|
||||
(agent, rx, response_tx)
|
||||
}
|
||||
|
||||
const TEST_ED25519_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACCWETEIh/JX+ZaK0Xlg5xZ9QIfjiKD2Qs57PjhRY45trwAAAIhqmvSbapr0
|
||||
mwAAAAtzc2gtZWQyNTUxOQAAACCWETEIh/JX+ZaK0Xlg5xZ9QIfjiKD2Qs57PjhRY45trw
|
||||
AAAEAHVflTgR/OEl8mg9UEKcO7SeB0FH4AiaUurhVfBWT4eZYRMQiH8lf5lorReWDnFn1A
|
||||
h+OIoPZCzns+OFFjjm2vAAAAAAECAwQF
|
||||
-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_needs_unlock_initial_state() {
|
||||
let (agent, _rx, _response_tx) = create_test_agent();
|
||||
|
||||
// Initially, needs_unlock should be true
|
||||
assert!(agent
|
||||
.needs_unlock
|
||||
.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_needs_unlock_after_set_keys() {
|
||||
let (mut agent, _rx, _response_tx) = create_test_agent();
|
||||
agent
|
||||
.is_running
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Set keys should set needs_unlock to false
|
||||
let keys = vec![(
|
||||
TEST_ED25519_KEY.to_string(),
|
||||
"test_key".to_string(),
|
||||
"cipher_id".to_string(),
|
||||
)];
|
||||
|
||||
agent.set_keys(keys).unwrap();
|
||||
|
||||
assert!(!agent
|
||||
.needs_unlock
|
||||
.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_needs_unlock_after_clear_keys() {
|
||||
let (mut agent, _rx, _response_tx) = create_test_agent();
|
||||
agent
|
||||
.is_running
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Set keys first
|
||||
let keys = vec![(
|
||||
TEST_ED25519_KEY.to_string(),
|
||||
"test_key".to_string(),
|
||||
"cipher_id".to_string(),
|
||||
)];
|
||||
agent.set_keys(keys).unwrap();
|
||||
|
||||
// Verify needs_unlock is false
|
||||
assert!(!agent
|
||||
.needs_unlock
|
||||
.load(std::sync::atomic::Ordering::Relaxed));
|
||||
|
||||
// Clear keys should set needs_unlock back to true
|
||||
agent.clear_keys().unwrap();
|
||||
|
||||
// Verify needs_unlock is true
|
||||
assert!(agent
|
||||
.needs_unlock
|
||||
.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1241,8 +1241,7 @@ pub mod autotype {
|
||||
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())
|
||||
})
|
||||
autotype::type_input(&input, &keyboard_shortcut)
|
||||
.map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"productName": "Bitwarden Beta",
|
||||
"appId": "com.bitwarden.desktop.beta",
|
||||
"buildDependenciesFromSource": true,
|
||||
"copyright": "Copyright © 2015-2025 Bitwarden Inc.",
|
||||
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"productName": "Bitwarden",
|
||||
"appId": "com.bitwarden.desktop",
|
||||
"buildDependenciesFromSource": true,
|
||||
"copyright": "Copyright © 2015-2025 Bitwarden Inc.",
|
||||
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
|
||||
@@ -70,6 +70,7 @@ import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
@@ -198,6 +199,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private readonly tokenService: TokenService,
|
||||
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
|
||||
private readonly lockService: LockService,
|
||||
private premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
|
||||
@@ -305,7 +307,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
|
||||
break;
|
||||
case "openPremium":
|
||||
this.dialogService.open(PremiumComponent);
|
||||
await this.premiumUpgradePromptService.promptForPremium();
|
||||
break;
|
||||
case "showFingerprintPhrase": {
|
||||
const activeUserId = await firstValueFrom(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CalloutModule, DialogModule } from "@bitwarden/components";
|
||||
import { AssignCollectionsComponent } from "@bitwarden/vault";
|
||||
|
||||
@@ -15,6 +16,7 @@ import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { DesktopPremiumUpgradePromptService } from "../services/desktop-premium-upgrade-prompt.service";
|
||||
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
|
||||
@@ -51,7 +53,13 @@ import { SharedModule } from "./shared/shared.module";
|
||||
PremiumComponent,
|
||||
SearchComponent,
|
||||
],
|
||||
providers: [SshAgentService],
|
||||
providers: [
|
||||
SshAgentService,
|
||||
{
|
||||
provide: PremiumUpgradePromptService,
|
||||
useClass: DesktopPremiumUpgradePromptService,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { mock } from "jest-mock-extended";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { DialogService, NavigationModule } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
import {
|
||||
RoutedVaultFilterService,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
@@ -45,6 +47,8 @@ describe("DesktopLayoutComponent", () => {
|
||||
let component: DesktopLayoutComponent;
|
||||
let fixture: ComponentFixture<DesktopLayoutComponent>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DesktopLayoutComponent, RouterModule.forRoot([]), NavigationModule],
|
||||
@@ -85,6 +89,10 @@ describe("DesktopLayoutComponent", () => {
|
||||
provide: DialogService,
|
||||
useValue: mock<DialogService>(),
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(DesktopLayoutComponent, {
|
||||
|
||||
@@ -2,7 +2,9 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
|
||||
@@ -24,6 +26,8 @@ describe("DesktopSideNavComponent", () => {
|
||||
let component: DesktopSideNavComponent;
|
||||
let fixture: ComponentFixture<DesktopSideNavComponent>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DesktopSideNavComponent, NavigationModule],
|
||||
@@ -32,6 +36,10 @@ describe("DesktopSideNavComponent", () => {
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import { RouterTestingHarness } from "@angular/router/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { SendFiltersNavComponent } from "./send-filters-nav.component";
|
||||
|
||||
@@ -35,6 +37,8 @@ describe("SendFiltersNavComponent", () => {
|
||||
let filterFormValueSubject: BehaviorSubject<{ sendType: SendType | null }>;
|
||||
let mockSendListFiltersService: Partial<SendListFiltersService>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
filterFormValueSubject = new BehaviorSubject<{ sendType: SendType | null }>({
|
||||
sendType: null,
|
||||
@@ -72,6 +76,10 @@ describe("SendFiltersNavComponent", () => {
|
||||
t: jest.fn((key) => key),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -1776,19 +1776,19 @@
|
||||
"message": "Buradan xaricə köçür"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "Xaricə köçürmə",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "Xaricə köçür",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "Daxilə köçürmə",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "Daxilə köçür",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
|
||||
@@ -4388,6 +4388,9 @@
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
|
||||
@@ -1414,7 +1414,7 @@
|
||||
"message": "Zobraziť Bitwarden v Docku aj keď je minimalizovaný na panel úloh."
|
||||
},
|
||||
"confirmTrayTitle": {
|
||||
"message": "Potvrdiť vypnutie systémovej lišty"
|
||||
"message": "Potvrdiť skrývanie systémovej lišty"
|
||||
},
|
||||
"confirmTrayDesc": {
|
||||
"message": "Vypnutím tohto nastavenia vypnete aj ostatné nastavenia súvisiace so systémovou lištou."
|
||||
@@ -2849,10 +2849,10 @@
|
||||
"message": "Použiť možnosti subadresovania svojho poskytovateľa e-mailu."
|
||||
},
|
||||
"catchallEmail": {
|
||||
"message": "E-mail Catch-all"
|
||||
"message": "Doménový kôš"
|
||||
},
|
||||
"catchallEmailDesc": {
|
||||
"message": "Použiť doručenú poštu typu catch-all nastavenú na doméne."
|
||||
"message": "Použiť nastavený doménový kôš."
|
||||
},
|
||||
"useThisEmail": {
|
||||
"message": "Použiť tento e-mail"
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
"message": "添加附件"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "项目已传输"
|
||||
"message": "项目已转移"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "修复加密"
|
||||
@@ -1776,19 +1776,19 @@
|
||||
"message": "导出自"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "导出",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "导出",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "导入",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "导入",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
@@ -4454,7 +4454,7 @@
|
||||
"message": "我该如何管理我的密码库?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "传输项目到 $ORGANIZATION$",
|
||||
"message": "转移项目到 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4463,7 +4463,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4472,7 +4472,7 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "接受传输"
|
||||
"message": "接受转移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "拒绝并退出"
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
"message": "新增附件"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
"message": "項目已轉移"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "修正加密"
|
||||
@@ -1199,7 +1199,7 @@
|
||||
"message": "關注我們"
|
||||
},
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
"message": "立即同步"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "變更主密碼"
|
||||
@@ -1776,19 +1776,19 @@
|
||||
"message": "匯出自"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "匯出",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "匯出",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "匯入",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "匯入",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
@@ -4344,43 +4344,43 @@
|
||||
"message": "升級到 Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
"message": "您的組織已不再使用主密碼登入 Bitwarden。若要繼續,請驗證組織與網域。"
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
"message": "繼續登入"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
"message": "不要繼續"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
"message": "網域"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
"message": "此網域將儲存您帳號的加密金鑰,請確認您信任它。若不確定,請洽詢您的管理員。"
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
"message": "驗證您的組織以登入"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
"message": "組織已驗證"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
"message": "已驗證網域"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
"message": "若您未驗證組織,將會被撤銷對該組織的存取權限。"
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
"message": "立即離開"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
"message": "驗證您的網域以登入"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
"message": "若要繼續登入,請驗證此網域。"
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
"message": "若要繼續登入,請驗證組織與網域。"
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "逾時後動作"
|
||||
@@ -4430,19 +4430,19 @@
|
||||
"message": "設定一個解鎖方式來變更您的密碼庫逾時動作。"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
"message": "升級"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
"message": "確定要離開嗎?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
"message": "若選擇拒絕,您的個人項目將保留在帳號中,但您將失去對共用項目與組織功能的存取權。"
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
"message": "請聯絡您的管理員以重新取得存取權限。"
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"message": "離開 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4451,10 +4451,10 @@
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
"message": "我要如何管理我的密碼庫?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"message": "將項目轉移至 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4463,7 +4463,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"message": "$ORGANIZATION$ 為了安全性與合規性,要求所有項目皆由組織擁有。點擊接受即可轉移您項目的擁有權。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4472,12 +4472,12 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
"message": "同意轉移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
"message": "拒絕並離開"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
"message": "為什麼我會看到此訊息?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,24 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
|
||||
import { DesktopPremiumUpgradePromptService } from "./desktop-premium-upgrade-prompt.service";
|
||||
|
||||
describe("DesktopPremiumUpgradePromptService", () => {
|
||||
let service: DesktopPremiumUpgradePromptService;
|
||||
let messager: MockProxy<MessagingService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
messager = mock<MessagingService>();
|
||||
configService = mock<ConfigService>();
|
||||
dialogService = mock<DialogService>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DesktopPremiumUpgradePromptService,
|
||||
{ provide: MessagingService, useValue: messager },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
],
|
||||
@@ -52,10 +50,10 @@ describe("DesktopPremiumUpgradePromptService", () => {
|
||||
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
|
||||
);
|
||||
expect(openSpy).toHaveBeenCalledWith(dialogService);
|
||||
expect(messager.send).not.toHaveBeenCalled();
|
||||
expect(dialogService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends openPremium message when feature flag is disabled", async () => {
|
||||
it("opens the PremiumComponent when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
await service.promptForPremium();
|
||||
@@ -63,7 +61,7 @@ describe("DesktopPremiumUpgradePromptService", () => {
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog,
|
||||
);
|
||||
expect(messager.send).toHaveBeenCalledWith("openPremium");
|
||||
expect(dialogService.open).toHaveBeenCalledWith(PremiumComponent);
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,15 +3,15 @@ import { inject } from "@angular/core";
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
|
||||
/**
|
||||
* This class handles the premium upgrade process for the desktop.
|
||||
*/
|
||||
export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService {
|
||||
private messagingService = inject(MessagingService);
|
||||
private configService = inject(ConfigService);
|
||||
private dialogService = inject(DialogService);
|
||||
|
||||
@@ -23,7 +23,7 @@ export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptS
|
||||
if (showNewDialog) {
|
||||
PremiumUpgradeDialogComponent.open(this.dialogService);
|
||||
} else {
|
||||
this.messagingService.send("openPremium");
|
||||
this.dialogService.open(PremiumComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +565,7 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
}
|
||||
}
|
||||
|
||||
if (!cipher.organizationId && !cipher.isDeleted && !cipher.isArchived) {
|
||||
if (userCanArchive && !cipher.isDeleted && !cipher.isArchived) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("archiveVerb"),
|
||||
click: async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<authors>Bitwarden Inc.</authors>
|
||||
<projectUrl>https://bitwarden.com/</projectUrl>
|
||||
<iconUrl>https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png</iconUrl>
|
||||
<copyright>Copyright © 2015-2025 Bitwarden Inc.</copyright>
|
||||
<copyright>Copyright © 2015-2026 Bitwarden Inc.</copyright>
|
||||
<projectSourceUrl>https://github.com/bitwarden/clients/</projectSourceUrl>
|
||||
<docsUrl>https://bitwarden.com/help/</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/bitwarden/clients/issues</bugTrackerUrl>
|
||||
|
||||
Reference in New Issue
Block a user