diff --git a/crates/iron-remote-desktop/src/clipboard.rs b/crates/iron-remote-desktop/src/clipboard.rs index 08e9a771..fea91575 100644 --- a/crates/iron-remote-desktop/src/clipboard.rs +++ b/crates/iron-remote-desktop/src/clipboard.rs @@ -1,18 +1,15 @@ use wasm_bindgen::JsValue; -use web_sys::js_sys; -pub trait ClipboardTransaction { - type ClipboardContent: ClipboardContent; +pub trait ClipboardData { + type Item: ClipboardItem; fn init() -> Self; - fn add_content(&mut self, content: Self::ClipboardContent); - fn is_empty(&self) -> bool; - fn contents(&self) -> js_sys::Array; + fn add_text(&mut self, mime_type: &str, text: &str); + fn add_binary(&mut self, mime_type: &str, binary: &[u8]); + fn items(&self) -> &[Self::Item]; } -pub trait ClipboardContent { - fn new_text(mime_type: &str, text: &str) -> Self; - fn new_binary(mime_type: &str, binary: &[u8]) -> Self; +pub trait ClipboardItem { fn mime_type(&self) -> &str; - fn value(&self) -> JsValue; + fn value(&self) -> impl Into; } diff --git a/crates/iron-remote-desktop/src/lib.rs b/crates/iron-remote-desktop/src/lib.rs index c067b88b..ad5d44fa 100644 --- a/crates/iron-remote-desktop/src/lib.rs +++ b/crates/iron-remote-desktop/src/lib.rs @@ -6,7 +6,7 @@ mod extension; mod input; mod session; -pub use clipboard::{ClipboardContent, ClipboardTransaction}; +pub use clipboard::{ClipboardData, ClipboardItem}; pub use cursor::CursorStyle; pub use desktop_size::DesktopSize; pub use error::{IronError, IronErrorKind}; @@ -20,8 +20,8 @@ pub trait RemoteDesktopApi { type SessionTerminationInfo: SessionTerminationInfo; type DeviceEvent: DeviceEvent; type InputTransaction: InputTransaction; - type ClipboardTransaction: ClipboardTransaction; - type ClipboardContent: ClipboardContent; + type ClipboardData: ClipboardData; + type ClipboardItem: ClipboardItem; type Error: IronError; /// Called before the logger is set. @@ -38,8 +38,8 @@ macro_rules! export { use wasm_bindgen::prelude::*; use web_sys::{js_sys, HtmlCanvasElement}; use $crate::{ - ClipboardContent as _, ClipboardTransaction as _, DeviceEvent as _, InputTransaction as _, - IronError as _, RemoteDesktopApi, Session as _, SessionBuilder as _, SessionTerminationInfo as _, + ClipboardData as _, ClipboardItem as _, DeviceEvent as _, InputTransaction as _, IronError as _, + RemoteDesktopApi, Session as _, SessionBuilder as _, SessionTerminationInfo as _, }; #[wasm_bindgen] @@ -52,6 +52,12 @@ macro_rules! export { #[wasm_bindgen] pub struct DeviceEvent(<$api as RemoteDesktopApi>::DeviceEvent); + impl From<<$api as RemoteDesktopApi>::DeviceEvent> for DeviceEvent { + fn from(value: <$api as RemoteDesktopApi>::DeviceEvent) -> Self { + Self(value) + } + } + #[wasm_bindgen] impl DeviceEvent { pub fn mouse_button_pressed(button: u8) -> Self { @@ -101,6 +107,12 @@ macro_rules! export { #[wasm_bindgen] pub struct InputTransaction(<$api as RemoteDesktopApi>::InputTransaction); + impl From<<$api as RemoteDesktopApi>::InputTransaction> for InputTransaction { + fn from(value: <$api as RemoteDesktopApi>::InputTransaction) -> Self { + Self(value) + } + } + #[wasm_bindgen] impl InputTransaction { pub fn init() -> Self { @@ -115,6 +127,12 @@ macro_rules! export { #[wasm_bindgen] pub struct IronError(<$api as RemoteDesktopApi>::Error); + impl From<<$api as RemoteDesktopApi>::Error> for IronError { + fn from(value: <$api as RemoteDesktopApi>::Error) -> Self { + Self(value) + } + } + #[wasm_bindgen] impl IronError { pub fn backtrace(&self) -> String { @@ -129,6 +147,12 @@ macro_rules! export { #[wasm_bindgen] pub struct Session(<$api as RemoteDesktopApi>::Session); + impl From<<$api as RemoteDesktopApi>::Session> for Session { + fn from(value: <$api as RemoteDesktopApi>::Session) -> Self { + Self(value) + } + } + #[wasm_bindgen] impl Session { pub async fn run(&self) -> Result { @@ -163,7 +187,7 @@ macro_rules! export { self.0.shutdown().map_err(IronError) } - pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), IronError> { + pub async fn on_clipboard_paste(&self, content: ClipboardData) -> Result<(), IronError> { self.0.on_clipboard_paste(content.0).await.map_err(IronError) } @@ -191,6 +215,12 @@ macro_rules! export { #[wasm_bindgen] pub struct SessionBuilder(<$api as RemoteDesktopApi>::SessionBuilder); + impl From<<$api as RemoteDesktopApi>::SessionBuilder> for SessionBuilder { + fn from(value: <$api as RemoteDesktopApi>::SessionBuilder) -> Self { + Self(value) + } + } + #[wasm_bindgen] impl SessionBuilder { pub fn init() -> Self { @@ -261,6 +291,12 @@ macro_rules! export { #[wasm_bindgen] pub struct SessionTerminationInfo(<$api as RemoteDesktopApi>::SessionTerminationInfo); + impl From<<$api as RemoteDesktopApi>::SessionTerminationInfo> for SessionTerminationInfo { + fn from(value: <$api as RemoteDesktopApi>::SessionTerminationInfo) -> Self { + Self(value) + } + } + #[wasm_bindgen] impl SessionTerminationInfo { pub fn reason(&self) -> String { @@ -269,50 +305,54 @@ macro_rules! export { } #[wasm_bindgen] - pub struct ClipboardTransaction(<$api as RemoteDesktopApi>::ClipboardTransaction); + pub struct ClipboardData(<$api as RemoteDesktopApi>::ClipboardData); + + impl From<<$api as RemoteDesktopApi>::ClipboardData> for ClipboardData { + fn from(value: <$api as RemoteDesktopApi>::ClipboardData) -> Self { + Self(value) + } + } #[wasm_bindgen] - impl ClipboardTransaction { + impl ClipboardData { pub fn init() -> Self { - Self(<<$api as RemoteDesktopApi>::ClipboardTransaction>::init()) + Self(<<$api as RemoteDesktopApi>::ClipboardData>::init()) } - pub fn add_content(&mut self, content: ClipboardContent) { - self.0.add_content(content.0); + pub fn add_text(&mut self, mime_type: &str, text: &str) { + self.0.add_text(mime_type, text); + } + + pub fn add_binary(&mut self, mime_type: &str, binary: &[u8]) { + self.0.add_binary(mime_type, binary); + } + + pub fn items(&self) -> Vec { + self.0.items().into_iter().cloned().map(ClipboardItem).collect() } pub fn is_empty(&self) -> bool { self.0.is_empty() } + } - pub fn content(&self) -> js_sys::Array { - iron_remote_desktop::ClipboardTransaction::contents(&self.0) + #[wasm_bindgen] + pub struct ClipboardItem(<$api as RemoteDesktopApi>::ClipboardItem); + + impl From<<$api as RemoteDesktopApi>::ClipboardItem> for ClipboardItem { + fn from(value: <$api as RemoteDesktopApi>::ClipboardItem) -> Self { + Self(value) } } #[wasm_bindgen] - pub struct ClipboardContent(<$api as RemoteDesktopApi>::ClipboardContent); - - #[wasm_bindgen] - impl ClipboardContent { - pub fn new_text(mime_type: &str, text: &str) -> Self { - Self(<<$api as RemoteDesktopApi>::ClipboardContent>::new_text( - mime_type, text, - )) - } - - pub fn new_binary(mime_type: &str, binary: &[u8]) -> Self { - Self(<<$api as RemoteDesktopApi>::ClipboardContent>::new_binary( - mime_type, binary, - )) - } - + impl ClipboardItem { pub fn mime_type(&self) -> String { self.0.mime_type().to_owned() } pub fn value(&self) -> JsValue { - iron_remote_desktop::ClipboardContent::value(&self.0) + self.0.value().into() } } } diff --git a/crates/iron-remote-desktop/src/session.rs b/crates/iron-remote-desktop/src/session.rs index 705c3246..084fd7b7 100644 --- a/crates/iron-remote-desktop/src/session.rs +++ b/crates/iron-remote-desktop/src/session.rs @@ -1,7 +1,7 @@ use wasm_bindgen::JsValue; use web_sys::{js_sys, HtmlCanvasElement}; -use crate::clipboard::ClipboardTransaction; +use crate::clipboard::ClipboardData; use crate::error::IronError; use crate::input::InputTransaction; use crate::{DesktopSize, Extension}; @@ -46,7 +46,7 @@ pub trait SessionBuilder { pub trait Session { type SessionTerminationInfo: SessionTerminationInfo; type InputTransaction: InputTransaction; - type ClipboardTransaction: ClipboardTransaction; + type ClipboardTransaction: ClipboardData; type Error: IronError; fn run(&self) -> impl core::future::Future>; diff --git a/crates/ironrdp-web/src/clipboard/mod.rs b/crates/ironrdp-web/src/clipboard.rs similarity index 80% rename from crates/ironrdp-web/src/clipboard/mod.rs rename to crates/ironrdp-web/src/clipboard.rs index dbee02e8..b50bcff5 100644 --- a/crates/ironrdp-web/src/clipboard/mod.rs +++ b/crates/ironrdp-web/src/clipboard.rs @@ -11,12 +11,9 @@ //! requested: when pasting into notepad, which does not support "text/html", "text/plain" //! will be requested, and when pasting into WordPad, "text/html" will be requested. -mod transaction; - use std::collections::HashMap; use futures_channel::mpsc; -use iron_remote_desktop::{ClipboardContent as _, ClipboardTransaction as _}; use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackend}; use ironrdp::cliprdr::pdu::{ ClipboardFormat, ClipboardFormatId, ClipboardFormatName, ClipboardGeneralCapabilityFlags, FileContentsRequest, @@ -25,14 +22,10 @@ use ironrdp::cliprdr::pdu::{ use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dibv5}; use ironrdp_cliprdr_format::html::{cf_html_to_plain_html, plain_html_to_cf_html}; use ironrdp_core::{impl_as_any, IntoOwned}; -use transaction::ClipboardContentValue; use wasm_bindgen::prelude::*; use crate::session::RdpInputEvent; -#[rustfmt::skip] -pub(crate) use transaction::{RdpClipboardTransaction, RdpClipboardContent}; - const MIME_TEXT: &str = "text/plain"; const MIME_HTML: &str = "text/html"; const MIME_PNG: &str = "image/png"; @@ -104,7 +97,7 @@ impl WasmClipboardMessageProxy { /// Messages sent by the JS code or CLIPRDR to the backend implementation. #[derive(Debug)] pub(crate) enum WasmClipboardBackendMessage { - LocalClipboardChanged(RdpClipboardTransaction), + LocalClipboardChanged(ClipboardData), RemoteDataRequest(ClipboardFormatId), RemoteClipboardChanged(Vec), @@ -117,8 +110,8 @@ pub(crate) enum WasmClipboardBackendMessage { /// Clipboard backend implementation for web. This object should be created once per session and /// kept alive until session is terminated. pub(crate) struct WasmClipboard { - local_clipboard: Option, - remote_clipboard: RdpClipboardTransaction, + local_clipboard: Option, + remote_clipboard: ClipboardData, remote_mapping: HashMap, remote_formats_to_read: Vec, @@ -138,7 +131,7 @@ impl WasmClipboard { pub(crate) fn new(message_proxy: WasmClipboardMessageProxy, js_callbacks: JsClipboardCallbacks) -> Self { Self { local_clipboard: None, - remote_clipboard: RdpClipboardTransaction::init(), + remote_clipboard: ClipboardData::new(), proxy: message_proxy, js_callbacks, @@ -154,13 +147,10 @@ impl WasmClipboard { } } - fn handle_local_clipboard_changed( - &mut self, - transaction: RdpClipboardTransaction, - ) -> anyhow::Result> { + fn handle_local_clipboard_changed(&mut self, transaction: ClipboardData) -> anyhow::Result> { let mut formats = Vec::new(); - transaction.contents().iter().for_each(|content| { - match content.mime_type() { + transaction.items().iter().for_each(|content| { + match content.mime_type.as_str() { MIME_TEXT => formats.push(ClipboardFormat::new(ClipboardFormatId::CF_UNICODETEXT)), MIME_HTML => { formats.extend([ @@ -204,15 +194,15 @@ impl WasmClipboard { let find_content_by_mime = |mime: &str| { transaction - .contents() + .items() .iter() - .find(|content| content.mime_type() == mime) + .find(|content| content.mime_type.as_str() == mime) }; let find_text_content_by_mime = |mime: &str| { find_content_by_mime(mime) .and_then(|content| { - if let ClipboardContentValue::Text(text) = content.value() { + if let ClipboardItemValue::Text(text) = &content.value { Some(text.as_str()) } else { None @@ -224,7 +214,7 @@ impl WasmClipboard { let find_binary_content_by_mime = |mime: &str| { find_content_by_mime(mime) .and_then(|content| { - if let ClipboardContentValue::Binary(binary) = content.value() { + if let ClipboardItemValue::Binary(binary) = &content.value { Some(binary.as_slice()) } else { None @@ -372,21 +362,21 @@ impl WasmClipboard { let content = match pending_format { ClipboardFormatId::CF_UNICODETEXT => match response.to_unicode_string() { - Ok(text) => Some(RdpClipboardContent::new_text(MIME_TEXT, &text)), + Ok(text) => Some(ClipboardItem::new_text(MIME_TEXT, text)), Err(err) => { error!("CF_UNICODETEXT decode error: {}", err); None } }, ClipboardFormatId::CF_DIB => match dib_to_png(response.data()) { - Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)), + Ok(png) => Some(ClipboardItem::new_binary(MIME_PNG, png)), Err(err) => { warn!("DIB decode error: {}", err); None } }, ClipboardFormatId::CF_DIBV5 => match dibv5_to_png(response.data()) { - Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)), + Ok(png) => Some(ClipboardItem::new_binary(MIME_PNG, png)), Err(err) => { warn!("DIBv5 decode error: {}", err); None @@ -394,23 +384,24 @@ impl WasmClipboard { }, registered => { let format_name = self.remote_mapping.get(®istered).map(|s| s.as_str()); + match format_name { Some(FORMAT_WIN_HTML_NAME) => match cf_html_to_plain_html(response.data()) { - Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, text)), + Ok(text) => Some(ClipboardItem::new_text(MIME_HTML, text.to_owned())), Err(err) => { warn!("CF_HTML decode error: {}", err); None } }, Some(FORMAT_MIME_HTML_NAME) => match response.to_string() { - Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, &text)), + Ok(text) => Some(ClipboardItem::new_text(MIME_HTML, text)), Err(err) => { warn!("text/html decode error: {}", err); None } }, Some(FORMAT_MIME_PNG_NAME) | Some(FORMAT_PNG_NAME) => { - Some(RdpClipboardContent::new_binary(MIME_PNG, response.data())) + Some(ClipboardItem::new_binary(MIME_PNG, response.data().to_owned())) } _ => { // Not supported format @@ -421,7 +412,7 @@ impl WasmClipboard { }; if let Some(content) = content { - self.remote_clipboard.add_content(content); + self.remote_clipboard.add(content); } if let Some(format) = self.remote_formats_to_read.last() { @@ -429,16 +420,20 @@ impl WasmClipboard { self.proxy .send_cliprdr_message(ClipboardMessage::SendInitiatePaste(*format)); } else { - // All formats were read, send clipboard to JS - let transaction = core::mem::take(&mut self.remote_clipboard); - if transaction.is_empty() { + // All formats were read, send clipboard to JS. + let clipboard_data = core::mem::take(&mut self.remote_clipboard); + + if clipboard_data.is_empty() { return Ok(()); } - // Set clipboard when all formats were read + // Set clipboard when all formats were read. self.js_callbacks .on_remote_clipboard_changed - .call1(&JsValue::NULL, &JsValue::from(transaction)) + .call1( + &JsValue::NULL, + &JsValue::from(crate::__wasm_ffi::ClipboardData::from(clipboard_data)), + ) .expect("failed to call JS callback"); } @@ -454,18 +449,18 @@ impl WasmClipboard { self.proxy .send_cliprdr_message(ClipboardMessage::SendInitiateCopy(formats)); } - Err(err) => { - // Not a critical error, we could skip single clipboard update - error!("Failed to handle local clipboard change: {}", err); + Err(e) => { + // Not a critical error, we could skip single clipboard update. + error!(error = format!("{e:#}"), "Failed to handle local clipboard change"); } } } WasmClipboardBackendMessage::RemoteDataRequest(format) => { let message = match self.process_remote_data_request(format) { Ok(message) => message, - Err(err) => { - // Not a critical error, but we should notify remote about error - error!("Failed to process remote data request: {}", err); + Err(e) => { + // Not a critical error, but we should notify remote about it. + error!(error = format!("{e:#}"), "Failed to process remote data request"); FormatDataResponse::new_error() } }; @@ -483,32 +478,31 @@ impl WasmClipboard { Ok(None) => { // No formats to query } - Err(err) => { - error!("Failed to process remote clipboard change: {}", err); + Err(e) => { + error!(error = format!("{e:#}"), "Failed to process remote clipboard change"); } } } WasmClipboardBackendMessage::RemoteDataResponse(formats) => { match self.process_remote_data_response(formats) { Ok(()) => {} - Err(err) => { - error!("Failed to process remote data response: {}", err); + Err(e) => { + error!(error = format!("{e:#}"), "Failed to process remote data response"); } } } WasmClipboardBackendMessage::FormatListReceived => { if let Some(callback) = self.js_callbacks.on_remote_received_format_list.as_mut() { - callback.call0(&JsValue::NULL).expect("Failed to call JS callback"); + callback.call0(&JsValue::NULL).expect("failed to call JS callback"); } } WasmClipboardBackendMessage::ForceClipboardUpdate => { if let Some(callback) = self.js_callbacks.on_force_clipboard_update.as_mut() { - callback.call0(&JsValue::NULL).expect("Failed to call JS callback"); + callback.call0(&JsValue::NULL).expect("failed to call JS callback"); } else { // If no initial clipboard callback was set, send empty format list instead - return self.process_event(WasmClipboardBackendMessage::LocalClipboardChanged( - RdpClipboardTransaction::init(), - )); + return self + .process_event(WasmClipboardBackendMessage::LocalClipboardChanged(ClipboardData::new())); } } }; @@ -587,3 +581,108 @@ impl CliprdrBackend for WasmClipboardBackend { // File transfer not implemented yet } } + +/// Object which represents complete clipboard transaction with multiple MIME types. +#[derive(Debug, Default, Clone)] +pub(crate) struct ClipboardData { + items: Vec, +} + +impl ClipboardData { + pub(crate) fn new() -> Self { + Self { items: Vec::new() } + } + + pub(crate) fn add(&mut self, item: ClipboardItem) { + self.items.push(item); + } + + pub(crate) fn items(&self) -> &[ClipboardItem] { + &self.items + } + + pub(crate) fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub(crate) fn clear(&mut self) { + self.items.clear(); + } +} + +impl iron_remote_desktop::ClipboardData for ClipboardData { + type Item = ClipboardItem; + + fn init() -> Self { + Self::new() + } + + fn add_text(&mut self, mime_type: &str, text: &str) { + self.items.push(ClipboardItem { + mime_type: mime_type.to_owned(), + value: ClipboardItemValue::Text(text.to_owned()), + }) + } + + fn add_binary(&mut self, mime_type: &str, binary: &[u8]) { + self.items.push(ClipboardItem { + mime_type: mime_type.to_owned(), + value: ClipboardItemValue::Binary(binary.to_owned()), + }) + } + + fn items(&self) -> &[Self::Item] { + &self.items + } +} + +impl FromIterator for ClipboardData { + fn from_iter>(iter: T) -> Self { + Self { + items: iter.into_iter().collect(), + } + } +} + +#[derive(Debug, Clone)] +pub(crate) enum ClipboardItemValue { + Text(String), + Binary(Vec), +} + +/// Object which represents single clipboard format represented standard MIME type. +#[derive(Debug, Clone)] +pub(crate) struct ClipboardItem { + pub(crate) mime_type: String, + pub(crate) value: ClipboardItemValue, +} + +impl ClipboardItem { + pub(crate) fn new_text(mime_type: impl Into, text: String) -> Self { + Self { + mime_type: mime_type.into(), + value: ClipboardItemValue::Text(text), + } + } + + pub(crate) fn new_binary(mime_type: impl Into, payload: Vec) -> Self { + Self { + mime_type: mime_type.into(), + value: ClipboardItemValue::Binary(payload), + } + } +} + +impl iron_remote_desktop::ClipboardItem for ClipboardItem { + fn mime_type(&self) -> &str { + &self.mime_type + } + + #[allow(refining_impl_trait)] + fn value(&self) -> JsValue { + match &self.value { + ClipboardItemValue::Text(text) => JsValue::from_str(text), + ClipboardItemValue::Binary(binary) => JsValue::from(js_sys::Uint8Array::from(binary.as_slice())), + } + } +} diff --git a/crates/ironrdp-web/src/clipboard/transaction.rs b/crates/ironrdp-web/src/clipboard/transaction.rs deleted file mode 100644 index 09073d3d..00000000 --- a/crates/ironrdp-web/src/clipboard/transaction.rs +++ /dev/null @@ -1,108 +0,0 @@ -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsValue; - -/// Object which represents complete clipboard transaction with multiple MIME types. -#[wasm_bindgen] -#[derive(Debug, Default, Clone)] -pub(crate) struct RdpClipboardTransaction { - contents: Vec, -} - -impl RdpClipboardTransaction { - pub(crate) fn contents(&self) -> &[RdpClipboardContent] { - &self.contents - } - - pub(crate) fn clear(&mut self) { - self.contents.clear(); - } -} - -impl iron_remote_desktop::ClipboardTransaction for RdpClipboardTransaction { - type ClipboardContent = RdpClipboardContent; - - fn init() -> Self { - Self { contents: Vec::new() } - } - - fn add_content(&mut self, content: Self::ClipboardContent) { - self.contents.push(content); - } - - fn is_empty(&self) -> bool { - self.contents.is_empty() - } - - fn contents(&self) -> js_sys::Array { - js_sys::Array::from_iter( - self.contents - .iter() - .map(|content: &RdpClipboardContent| JsValue::from(content.clone())), - ) - } -} - -impl FromIterator for RdpClipboardTransaction { - fn from_iter>(iter: T) -> Self { - Self { - contents: iter.into_iter().collect(), - } - } -} - -#[derive(Debug, Clone)] -pub(crate) enum ClipboardContentValue { - Text(String), - Binary(Vec), -} - -impl ClipboardContentValue { - pub(crate) fn value(&self) -> JsValue { - match self { - ClipboardContentValue::Text(text) => JsValue::from_str(text), - ClipboardContentValue::Binary(binary) => js_sys::Uint8Array::from(binary.as_slice()).into(), - } - } -} - -/// Object which represents single clipboard format represented standard MIME type. -#[wasm_bindgen] -#[derive(Debug, Clone)] -pub(crate) struct RdpClipboardContent { - mime_type: String, - value: ClipboardContentValue, -} - -impl RdpClipboardContent { - pub(crate) fn mime_type(&self) -> &str { - &self.mime_type - } - - pub(crate) fn value(&self) -> &ClipboardContentValue { - &self.value - } -} - -impl iron_remote_desktop::ClipboardContent for RdpClipboardContent { - fn new_text(mime_type: &str, text: &str) -> Self { - Self { - mime_type: mime_type.into(), - value: ClipboardContentValue::Text(text.to_owned()), - } - } - - fn new_binary(mime_type: &str, binary: &[u8]) -> Self { - Self { - mime_type: mime_type.into(), - value: ClipboardContentValue::Binary(binary.to_vec()), - } - } - - fn mime_type(&self) -> &str { - self.mime_type.as_str() - } - - fn value(&self) -> JsValue { - self.value.value() - } -} diff --git a/crates/ironrdp-web/src/lib.rs b/crates/ironrdp-web/src/lib.rs index df02b2f4..244ed45a 100644 --- a/crates/ironrdp-web/src/lib.rs +++ b/crates/ironrdp-web/src/lib.rs @@ -30,8 +30,8 @@ impl RemoteDesktopApi for Api { type SessionTerminationInfo = session::SessionTerminationInfo; type DeviceEvent = input::DeviceEvent; type InputTransaction = input::InputTransaction; - type ClipboardTransaction = clipboard::RdpClipboardTransaction; - type ClipboardContent = clipboard::RdpClipboardContent; + type ClipboardData = clipboard::ClipboardData; + type ClipboardItem = clipboard::ClipboardItem; type Error = error::IronError; fn post_setup() { diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 4dda9ac4..befa83f8 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -35,7 +35,7 @@ use web_sys::HtmlCanvasElement; use crate::canvas::Canvas; use crate::clipboard; -use crate::clipboard::{RdpClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage}; +use crate::clipboard::{ClipboardData, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage}; use crate::error::IronError; use crate::image::extract_partial_image; use crate::input::InputTransaction; @@ -433,7 +433,7 @@ impl Session { impl iron_remote_desktop::Session for Session { type SessionTerminationInfo = SessionTerminationInfo; type InputTransaction = InputTransaction; - type ClipboardTransaction = RdpClipboardTransaction; + type ClipboardTransaction = ClipboardData; type Error = IronError; async fn run(&self) -> Result { diff --git a/web-client/iron-remote-desktop-rdp/src/main.ts b/web-client/iron-remote-desktop-rdp/src/main.ts index 6601a7af..48ac446b 100644 --- a/web-client/iron-remote-desktop-rdp/src/main.ts +++ b/web-client/iron-remote-desktop-rdp/src/main.ts @@ -7,8 +7,8 @@ import init, { Session, SessionBuilder, SessionTerminationInfo, - ClipboardTransaction, - ClipboardContent, + ClipboardData, + ClipboardItem, Extension, } from '../../../crates/ironrdp-web/pkg/ironrdp_web'; @@ -20,8 +20,8 @@ export default { InputTransaction, IronError, SessionBuilder, - ClipboardTransaction, - ClipboardContent, + ClipboardData, + ClipboardItem, Session, SessionTerminationInfo, Extension, diff --git a/web-client/iron-remote-desktop/src/interfaces/ClipboardContent.ts b/web-client/iron-remote-desktop/src/interfaces/ClipboardContent.ts deleted file mode 100644 index dc9e4bb8..00000000 --- a/web-client/iron-remote-desktop/src/interfaces/ClipboardContent.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ClipboardContent { - new_text(mime_type: string, text: string): ClipboardContent; - new_binary(mime_type: string, binary: Uint8Array): ClipboardContent; - mime_type(): string; - value(): string; -} diff --git a/web-client/iron-remote-desktop/src/interfaces/ClipboardData.ts b/web-client/iron-remote-desktop/src/interfaces/ClipboardData.ts new file mode 100644 index 00000000..2abadf0c --- /dev/null +++ b/web-client/iron-remote-desktop/src/interfaces/ClipboardData.ts @@ -0,0 +1,9 @@ +import type { ClipboardItem } from './ClipboardItem'; + +export interface ClipboardData { + init(): ClipboardData; + add_text(mime_type: string, text: string): void; + add_binary(mime_type: string, binary: Uint8Array): void; + items(): ClipboardItem[]; + is_empty(): boolean; +} diff --git a/web-client/iron-remote-desktop/src/interfaces/ClipboardItem.ts b/web-client/iron-remote-desktop/src/interfaces/ClipboardItem.ts new file mode 100644 index 00000000..6e896a57 --- /dev/null +++ b/web-client/iron-remote-desktop/src/interfaces/ClipboardItem.ts @@ -0,0 +1,4 @@ +export interface ClipboardItem { + mime_type(): string; + value(): string | Uint8Array; +} diff --git a/web-client/iron-remote-desktop/src/interfaces/ClipboardTransaction.ts b/web-client/iron-remote-desktop/src/interfaces/ClipboardTransaction.ts deleted file mode 100644 index 394ac778..00000000 --- a/web-client/iron-remote-desktop/src/interfaces/ClipboardTransaction.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ClipboardContent } from './ClipboardContent'; - -export interface ClipboardTransaction { - init(): ClipboardTransaction; - add_content(content: ClipboardContent): void; - is_empty(): boolean; - content(): Array; -} diff --git a/web-client/iron-remote-desktop/src/interfaces/RemoteDesktopModule.ts b/web-client/iron-remote-desktop/src/interfaces/RemoteDesktopModule.ts index 936590ed..8f058414 100644 --- a/web-client/iron-remote-desktop/src/interfaces/RemoteDesktopModule.ts +++ b/web-client/iron-remote-desktop/src/interfaces/RemoteDesktopModule.ts @@ -5,8 +5,8 @@ import type { IronError } from './session-event'; import type { Session } from './Session'; import type { SessionBuilder } from './SessionBuilder'; import type { SessionTerminationInfo } from './SessionTerminationInfo'; -import type { ClipboardTransaction } from './ClipboardTransaction'; -import type { ClipboardContent } from './ClipboardContent'; +import type { ClipboardData } from './ClipboardData'; +import type { ClipboardItem } from './ClipboardItem'; export interface RemoteDesktopModule { init: () => Promise; @@ -18,6 +18,6 @@ export interface RemoteDesktopModule { Session: Session; SessionBuilder: SessionBuilder; SessionTerminationInfo: SessionTerminationInfo; - ClipboardTransaction: ClipboardTransaction; - ClipboardContent: ClipboardContent; + ClipboardData: ClipboardData; + ClipboardItem: ClipboardItem; } diff --git a/web-client/iron-remote-desktop/src/interfaces/Session.ts b/web-client/iron-remote-desktop/src/interfaces/Session.ts index afba300b..845370cf 100644 --- a/web-client/iron-remote-desktop/src/interfaces/Session.ts +++ b/web-client/iron-remote-desktop/src/interfaces/Session.ts @@ -1,7 +1,7 @@ import type { InputTransaction } from './InputTransaction'; import type { DesktopSize } from './DesktopSize'; import type { SessionTerminationInfo } from './SessionTerminationInfo'; -import type { ClipboardTransaction } from './ClipboardTransaction'; +import type { ClipboardData } from './ClipboardData'; export interface Session { run(): Promise; @@ -11,7 +11,7 @@ export interface Session { synchronize_lock_keys(scroll_lock: boolean, num_lock: boolean, caps_lock: boolean, kana_lock: boolean): void; extension_call(value: unknown): unknown; shutdown(): void; - on_clipboard_paste(content: ClipboardTransaction): Promise; + on_clipboard_paste(data: ClipboardData): Promise; resize( width: number, height: number, diff --git a/web-client/iron-remote-desktop/src/interfaces/SessionBuilder.ts b/web-client/iron-remote-desktop/src/interfaces/SessionBuilder.ts index 1301d93b..da707d89 100644 --- a/web-client/iron-remote-desktop/src/interfaces/SessionBuilder.ts +++ b/web-client/iron-remote-desktop/src/interfaces/SessionBuilder.ts @@ -1,6 +1,6 @@ import type { Session } from './Session'; import type { DesktopSize } from './DesktopSize'; -import type { ClipboardTransaction } from './ClipboardTransaction'; +import type { ClipboardData } from './ClipboardData'; export interface SessionBuilder { init(): SessionBuilder; @@ -76,7 +76,7 @@ interface SetCursorStyleCallback { } interface RemoteClipboardChangedCallback { - (transaction: ClipboardTransaction): void; + (data: ClipboardData): void; } interface RemoteReceiveForwardListCallback { diff --git a/web-client/iron-remote-desktop/src/iron-remote-desktop.svelte b/web-client/iron-remote-desktop/src/iron-remote-desktop.svelte index 846263f9..360df306 100644 --- a/web-client/iron-remote-desktop/src/iron-remote-desktop.svelte +++ b/web-client/iron-remote-desktop/src/iron-remote-desktop.svelte @@ -21,7 +21,7 @@ import type { ResizeEvent } from './interfaces/ResizeEvent'; import { PublicAPI } from './services/PublicAPI'; import { ScreenScale } from './enums/ScreenScale'; - import type { ClipboardTransaction } from './interfaces/ClipboardTransaction'; + import type { ClipboardData } from './interfaces/ClipboardData'; import type { RemoteDesktopModule } from './interfaces/RemoteDesktopModule'; let { @@ -68,14 +68,14 @@ let isClipboardApiSupported = false; let lastClientClipboardItems = new Map(); - let lastClientClipboardTransaction: ClipboardTransaction | null = null; + let lastClientClipboardData: ClipboardData | null = null; let lastClipboardMonitorLoopError: Error | null = null; /* Firefox-specific BEGIN */ - // See `ffRemoteClipboardTransaction` variable docs below - const FF_REMOTE_CLIPBOARD_TRANSACTION_SET_RETRY_INTERVAL = 100; // ms - const FF_REMOTE_CLIPBOARD_TRANSACTION_SET_MAX_RETRIES = 30; // 3 seconds (100ms * 30) + // See `ffRemoteClipboardData` variable docs below + const FF_REMOTE_CLIPBOARD_DATA_SET_RETRY_INTERVAL = 100; // ms + const FF_REMOTE_CLIPBOARD_DATA_SET_MAX_RETRIES = 30; // 3 seconds (100ms * 30) // On Firefox, this interval is used to stop delaying the keyboard events if the paste event has // failed and we haven't received any clipboard data from the remote side. const FF_LOCAL_CLIPBOARD_COPY_TIMEOUT = 1000; // 1s (For text-only data this should be enough) @@ -84,10 +84,10 @@ // called in scope of user-initiated event processing (e.g. keyboard event), but we receive // clipboard data from the remote side asynchronously in wasm service callback. therefore we // set this variable in callback and use its value on the user-initiated copy event. - let ffRemoteClipboardTransaction: ClipboardTransaction | null = null; + let ffRemoteClipboardData: ClipboardData | null = null; // For Firefox we need this variable to perform wait loop for the remote side to finish sending // clipboard content to the client. - let ffRemoteClipboardTransactionRetriesLeft = 0; + let ffRemoteClipboardDataRetriesLeft = 0; let ffPostponeKeyboardEvents = false; let ffDelayedKeyboardEvents: KeyboardEvent[] = []; let ffCnavasFocused = false; @@ -131,12 +131,12 @@ return (evt.ctrlKey && evt.code === 'KeyV') || evt.code == 'Paste'; } - // This function is required to covert `ClipboardTransaction` to a object that can be used + // This function is required to convert `ClipboardData` to a object that can be used // with `ClipboardItem` API. - function clipboardTransactionToRecord(transaction: ClipboardTransaction): Record { + function clipboardDataToRecord(data: ClipboardData): Record { let result = {} as Record; - for (const item of transaction.content()) { + for (const item of data.items()) { let mime = item.mime_type(); let value = new Blob([item.value()], { type: mime }); @@ -149,8 +149,8 @@ // This callback is required to send initial clipboard state if available. function onForceClipboardUpdate() { try { - if (lastClientClipboardTransaction) { - remoteDesktopService.onClipboardChanged(lastClientClipboardTransaction); + if (lastClientClipboardData) { + remoteDesktopService.onClipboardChanged(lastClientClipboardData); } else { remoteDesktopService.onClipboardChangedEmpty(); } @@ -160,9 +160,9 @@ } // This callback is required to update client clipboard state when remote side has changed. - function onRemoteClipboardChanged(transaction: ClipboardTransaction) { + function onRemoteClipboardChanged(data: ClipboardData) { try { - const mime_formats = clipboardTransactionToRecord(transaction); + const mime_formats = clipboardDataToRecord(data); const clipboard_item = new ClipboardItem(mime_formats); navigator.clipboard.write([clipboard_item]); } catch (err) { @@ -236,7 +236,7 @@ if (!sameValue) { lastClientClipboardItems = values; - let transaction = remoteDesktopService.constructClipboardTransaction(); + let data = remoteDesktopService.createClipboardData(); // Iterate over `Record` type values.forEach((value: string | Uint8Array, key: string) => { @@ -246,15 +246,15 @@ } if (key.startsWith('text/') && typeof value === 'string') { - transaction.add_content(remoteDesktopService.constructClipboardContentFromText(key, value)); + data.add_text(key, value); } else if (key.startsWith('image/') && value instanceof Uint8Array) { - transaction.add_content(remoteDesktopService.constructClipboardContentFromBinary(key, value)); + data.add_binary(key, value); } }); - if (!transaction.is_empty()) { - lastClientClipboardTransaction = transaction; - remoteDesktopService.onClipboardChanged(transaction); + if (!data.is_empty()) { + lastClientClipboardData = data; + remoteDesktopService.onClipboardChanged(data); } } } catch (err) { @@ -286,28 +286,35 @@ // Only set variable on callback, the real clipboard update will be performed in keyboard // callback. (User-initiated event is required for Firefox to allow clipboard write) - function ffOnRemoteClipboardChanged(transaction: ClipboardTransaction) { - ffRemoteClipboardTransaction = transaction; + function ffOnRemoteClipboardChanged(data: ClipboardData) { + ffRemoteClipboardData = data; } - function ffWaitForRemoteClipboardTransactionSet() { - if (ffRemoteClipboardTransaction) { + function ffWaitForRemoteClipboardDataSet() { + if (ffRemoteClipboardData) { try { - let transaction = ffRemoteClipboardTransaction; - ffRemoteClipboardTransaction = null; - for (const content of transaction.content()) { + let clipboard_data = ffRemoteClipboardData; + ffRemoteClipboardData = null; + for (const item of clipboard_data.items()) { // Firefox only supports text/plain mime type for clipboard writes :( - if (content.mime_type() === 'text/plain') { - navigator.clipboard.writeText(content.value()); + if (item.mime_type() === 'text/plain') { + const value = item.value(); + + if (typeof value === 'string') { + navigator.clipboard.writeText(value); + } else { + loggingService.error('Unexpected value for text/plain clipboard item'); + } + break; } } } catch (err) { console.error('Failed to set client clipboard: ' + err); } - } else if (ffRemoteClipboardTransactionRetriesLeft > 0) { - ffRemoteClipboardTransactionRetriesLeft--; - setTimeout(ffWaitForRemoteClipboardTransactionSet, FF_REMOTE_CLIPBOARD_TRANSACTION_SET_RETRY_INTERVAL); + } else if (ffRemoteClipboardDataRetriesLeft > 0) { + ffRemoteClipboardDataRetriesLeft--; + setTimeout(ffWaitForRemoteClipboardDataSet, FF_REMOTE_CLIPBOARD_DATA_SET_RETRY_INTERVAL); } } @@ -334,7 +341,7 @@ } try { - let transaction = remoteDesktopService.constructClipboardTransaction(); + let clipboard_data = remoteDesktopService.createClipboardData(); if (evt.clipboardData == null) { return; @@ -345,11 +352,10 @@ if (mime.startsWith('text/')) { clipItem.getAsString((str: string) => { - let content = remoteDesktopService.constructClipboardContentFromText(mime, str); - transaction.add_content(content); + clipboard_data.add_text(mime, str); - if (!transaction.is_empty()) { - remoteDesktopService.onClipboardChanged(transaction as ClipboardTransaction); + if (!clipboard_data.is_empty()) { + remoteDesktopService.onClipboardChanged(clipboard_data); } }); break; @@ -363,11 +369,11 @@ file.arrayBuffer().then((buffer: ArrayBuffer) => { const strict_buffer = new Uint8Array(buffer); - let content = remoteDesktopService.constructClipboardContentFromBinary(mime, strict_buffer); - transaction.add_content(content); - if (!transaction.is_empty()) { - remoteDesktopService.onClipboardChanged(transaction); + clipboard_data.add_binary(mime, strict_buffer); + + if (!clipboard_data.is_empty()) { + remoteDesktopService.onClipboardChanged(clipboard_data); } }); break; @@ -634,8 +640,8 @@ // only after some user-initiated event (e.g. keyboard event). // therefore we need to wait here for the clipboard data to be ready. - ffRemoteClipboardTransactionRetriesLeft = FF_REMOTE_CLIPBOARD_TRANSACTION_SET_MAX_RETRIES; - ffWaitForRemoteClipboardTransactionSet(); + ffRemoteClipboardDataRetriesLeft = FF_REMOTE_CLIPBOARD_DATA_SET_MAX_RETRIES; + ffWaitForRemoteClipboardDataSet(); } remoteDesktopService.sendKeyboardEvent(evt); diff --git a/web-client/iron-remote-desktop/src/main.ts b/web-client/iron-remote-desktop/src/main.ts index dda32551..e14f393e 100644 --- a/web-client/iron-remote-desktop/src/main.ts +++ b/web-client/iron-remote-desktop/src/main.ts @@ -6,8 +6,8 @@ export type { DesktopSize } from './interfaces/DesktopSize'; export type { SessionEvent, IronError, IronErrorKind } from './interfaces/session-event'; export type { SessionEventType } from './enums/SessionEventType'; export type { SessionTerminationInfo } from './interfaces/SessionTerminationInfo'; -export type { ClipboardTransaction } from './interfaces/ClipboardTransaction'; -export type { ClipboardContent } from './interfaces/ClipboardContent'; +export type { ClipboardData } from './interfaces/ClipboardData'; +export type { ClipboardItem } from './interfaces/ClipboardItem'; export type { DeviceEvent } from './interfaces/DeviceEvent'; export type { InputTransaction } from './interfaces/InputTransaction'; export type { Session } from './interfaces/Session'; diff --git a/web-client/iron-remote-desktop/src/services/remote-desktop.service.ts b/web-client/iron-remote-desktop/src/services/remote-desktop.service.ts index ad5f52b1..f5e061f1 100644 --- a/web-client/iron-remote-desktop/src/services/remote-desktop.service.ts +++ b/web-client/iron-remote-desktop/src/services/remote-desktop.service.ts @@ -13,8 +13,7 @@ import type { ResizeEvent } from '../interfaces/ResizeEvent'; import { ScreenScale } from '../enums/ScreenScale'; import type { MousePosition } from '../interfaces/MousePosition'; import type { SessionEvent, IronErrorKind, IronError } from '../interfaces/session-event'; -import type { ClipboardTransaction } from '../interfaces/ClipboardTransaction'; -import type { ClipboardContent } from '../interfaces/ClipboardContent'; +import type { ClipboardData } from '../interfaces/ClipboardData'; import type { Session } from '../interfaces/Session'; import type { DeviceEvent } from '../interfaces/DeviceEvent'; import type { SessionTerminationInfo } from '../interfaces/SessionTerminationInfo'; @@ -22,7 +21,7 @@ import type { RemoteDesktopModule } from '../interfaces/RemoteDesktopModule'; import { ConfigBuilder } from './ConfigBuilder'; import type { Config } from './Config'; -type OnRemoteClipboardChanged = (transaction: ClipboardTransaction) => void; +type OnRemoteClipboardChanged = (data: ClipboardData) => void; type OnRemoteReceivedFormatsList = () => void; type OnForceClipboardUpdate = () => void; @@ -65,16 +64,8 @@ export class RemoteDesktopService { loggingService.info('Web bridge initialized.'); } - constructClipboardTransaction(): ClipboardTransaction { - return this.module.ClipboardTransaction.init(); - } - - constructClipboardContentFromText(mime_type: string, text: string): ClipboardContent { - return this.module.ClipboardContent.new_text(mime_type, text); - } - - constructClipboardContentFromBinary(mime_type: string, binary: Uint8Array): ClipboardContent { - return this.module.ClipboardContent.new_binary(mime_type, binary); + createClipboardData(): ClipboardData { + return this.module.ClipboardData.init(); } async init(debug: LogType) { @@ -273,16 +264,16 @@ export class RemoteDesktopService { /// Triggered by the browser when local clipboard is updated. Clipboard backend should /// cache the content and send it to the server when it is requested. - onClipboardChanged(transaction: ClipboardTransaction): Promise { + onClipboardChanged(data: ClipboardData): Promise { const onClipboardChangedPromise = async () => { - await this.session?.on_clipboard_paste(transaction); + await this.session?.on_clipboard_paste(data); }; return onClipboardChangedPromise(); } onClipboardChangedEmpty(): Promise { const onClipboardChangedPromise = async () => { - await this.session?.on_clipboard_paste(this.module.ClipboardTransaction.init()); + await this.session?.on_clipboard_paste(this.module.ClipboardData.init()); }; return onClipboardChangedPromise(); }