mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
Fix Firefox RDP clipboard paste (#1033)
This commit is contained in:
@@ -12,6 +12,11 @@ import { getGuacamoleToken, isElectron, isEmbeddedMode } from "@/main-axios.ts";
|
||||
import { SimpleLoader } from "@/lib/SimpleLoader.tsx";
|
||||
import { getBasePath } from "@/lib/base-path.ts";
|
||||
import { buildGuacamoleWebSocketBaseUrl } from "./guacamole-websocket-url.ts";
|
||||
import {
|
||||
isFirefoxBrowser,
|
||||
isPasteShortcut,
|
||||
pasteTextToRemote,
|
||||
} from "./guacamole-clipboard.ts";
|
||||
|
||||
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
||||
|
||||
@@ -338,6 +343,32 @@ export const GuacamoleDisplay = forwardRef<
|
||||
displayElement.setAttribute("tabindex", "0");
|
||||
displayElement.style.outline = "none";
|
||||
|
||||
const useNativePasteFallback = isFirefoxBrowser();
|
||||
if (useNativePasteFallback) {
|
||||
displayElement.addEventListener(
|
||||
"keydown",
|
||||
(event) => {
|
||||
if (isPasteShortcut(event)) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
displayElement.addEventListener(
|
||||
"paste",
|
||||
(event) => {
|
||||
if (clientRef.current !== client) return;
|
||||
const text = event.clipboardData?.getData("text/plain");
|
||||
if (!text) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
pasteTextToRemote(client, text);
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
display.onresize = () => {
|
||||
if (!isMountedRef.current) return;
|
||||
rescaleDisplay(true);
|
||||
@@ -576,7 +607,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
|
||||
const syncClipboard = useCallback(() => {
|
||||
const client = clientRef.current;
|
||||
if (!client || !navigator.clipboard?.readText) return;
|
||||
if (!client || isFirefoxBrowser() || !navigator.clipboard?.readText) return;
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then((text) => {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
isFirefoxBrowser,
|
||||
isPasteShortcut,
|
||||
pasteTextToRemote,
|
||||
type GuacamoleClipboardClient,
|
||||
} from "./guacamole-clipboard.js";
|
||||
|
||||
describe("Guacamole Firefox clipboard fallback", () => {
|
||||
it("only enables the native paste path for Firefox", () => {
|
||||
expect(
|
||||
isFirefoxBrowser(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isFirefoxBrowser(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/140.0.0.0",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("recognizes Ctrl+V and Command+V without intercepting Alt+V", () => {
|
||||
expect(
|
||||
isPasteShortcut({
|
||||
key: "v",
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
altKey: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPasteShortcut({
|
||||
key: "V",
|
||||
ctrlKey: false,
|
||||
metaKey: true,
|
||||
altKey: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPasteShortcut({
|
||||
key: "v",
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
altKey: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("updates the remote clipboard before sending Ctrl+V", () => {
|
||||
const events: string[] = [];
|
||||
const client: GuacamoleClipboardClient = {
|
||||
createClipboardStream: vi.fn((mimetype: string) => {
|
||||
events.push(`stream:${mimetype}`);
|
||||
return {
|
||||
sendBlob: () => events.push("blob"),
|
||||
sendEnd: () => events.push("end"),
|
||||
};
|
||||
}),
|
||||
sendKeyEvent: vi.fn((pressed: number, keysym: number) => {
|
||||
events.push(`key:${pressed}:${keysym}`);
|
||||
}),
|
||||
};
|
||||
|
||||
pasteTextToRemote(client, "Firefox clipboard");
|
||||
|
||||
expect(events).toEqual([
|
||||
"stream:text/plain",
|
||||
"blob",
|
||||
"end",
|
||||
"key:1:65507",
|
||||
"key:1:118",
|
||||
"key:0:118",
|
||||
"key:0:65507",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import Guacamole from "guacamole-common-js";
|
||||
|
||||
const CONTROL_LEFT_KEYSYM = 0xffe3;
|
||||
const V_KEYSYM = 0x76;
|
||||
|
||||
interface ClipboardOutputStream {
|
||||
sendBlob(data: string): void;
|
||||
sendEnd(): void;
|
||||
}
|
||||
|
||||
export interface GuacamoleClipboardClient {
|
||||
createClipboardStream(mimetype: string): ClipboardOutputStream;
|
||||
sendKeyEvent(pressed: number, keysym: number): void;
|
||||
}
|
||||
|
||||
export function isFirefoxBrowser(userAgent = navigator.userAgent): boolean {
|
||||
return /(?:Firefox|FxiOS)\//.test(userAgent);
|
||||
}
|
||||
|
||||
export function isPasteShortcut(
|
||||
event: Pick<KeyboardEvent, "altKey" | "ctrlKey" | "key" | "metaKey">,
|
||||
): boolean {
|
||||
return (
|
||||
!event.altKey &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key.toLowerCase() === "v"
|
||||
);
|
||||
}
|
||||
|
||||
export function pasteTextToRemote(
|
||||
client: GuacamoleClipboardClient,
|
||||
text: string,
|
||||
): void {
|
||||
const stream = client.createClipboardStream("text/plain");
|
||||
const writer = new Guacamole.StringWriter(stream);
|
||||
writer.sendText(text);
|
||||
writer.sendEnd();
|
||||
|
||||
client.sendKeyEvent(1, CONTROL_LEFT_KEYSYM);
|
||||
client.sendKeyEvent(1, V_KEYSYM);
|
||||
client.sendKeyEvent(0, V_KEYSYM);
|
||||
client.sendKeyEvent(0, CONTROL_LEFT_KEYSYM);
|
||||
}
|
||||
Reference in New Issue
Block a user