mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 04:43:36 +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 { SimpleLoader } from "@/lib/SimpleLoader.tsx";
|
||||||
import { getBasePath } from "@/lib/base-path.ts";
|
import { getBasePath } from "@/lib/base-path.ts";
|
||||||
import { buildGuacamoleWebSocketBaseUrl } from "./guacamole-websocket-url.ts";
|
import { buildGuacamoleWebSocketBaseUrl } from "./guacamole-websocket-url.ts";
|
||||||
|
import {
|
||||||
|
isFirefoxBrowser,
|
||||||
|
isPasteShortcut,
|
||||||
|
pasteTextToRemote,
|
||||||
|
} from "./guacamole-clipboard.ts";
|
||||||
|
|
||||||
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
||||||
|
|
||||||
@@ -338,6 +343,32 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
displayElement.setAttribute("tabindex", "0");
|
displayElement.setAttribute("tabindex", "0");
|
||||||
displayElement.style.outline = "none";
|
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 = () => {
|
display.onresize = () => {
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
rescaleDisplay(true);
|
rescaleDisplay(true);
|
||||||
@@ -576,7 +607,7 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
|
|
||||||
const syncClipboard = useCallback(() => {
|
const syncClipboard = useCallback(() => {
|
||||||
const client = clientRef.current;
|
const client = clientRef.current;
|
||||||
if (!client || !navigator.clipboard?.readText) return;
|
if (!client || isFirefoxBrowser() || !navigator.clipboard?.readText) return;
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.readText()
|
.readText()
|
||||||
.then((text) => {
|
.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