Merge branch 'master' into reconnect_timeout

This commit is contained in:
Ankit Kumar 2025-11-19 10:36:32 +05:30 committed by GitHub
commit 5d7e66bd87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1715 additions and 581 deletions

View File

@ -9,6 +9,9 @@ on:
jobs: jobs:
npm: npm:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: | - run: |
@ -18,6 +21,9 @@ jobs:
if: github.event_name != 'release' if: github.event_name != 'release'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
# Node 24 is needed to get npm > 11.5.1, which is a requirement for
# OIDC auth.
node-version: 24
# Needs to be explicitly specified for auth to work # Needs to be explicitly specified for auth to work
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: npm install - run: npm install
@ -26,22 +32,16 @@ jobs:
name: npm name: npm
path: lib path: lib
- run: npm publish --access public - run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: | if: |
github.repository == 'novnc/noVNC' && github.repository == 'novnc/noVNC' &&
github.event_name == 'release' && github.event_name == 'release' &&
!github.event.release.prerelease !github.event.release.prerelease
- run: npm publish --access public --tag beta - run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: | if: |
github.repository == 'novnc/noVNC' && github.repository == 'novnc/noVNC' &&
github.event_name == 'release' && github.event_name == 'release' &&
github.event.release.prerelease github.event.release.prerelease
- run: npm publish --access public --tag dev - run: npm publish --access public --tag dev
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: | if: |
github.repository == 'novnc/noVNC' && github.repository == 'novnc/noVNC' &&
github.event_name == 'push' && github.event_name == 'push' &&

80
app/locale/hu.json Normal file
View File

@ -0,0 +1,80 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS nélkül futtatni nem ajánlott, összeomlások vagy más problémák várhatók.",
"Connecting...": "Kapcsolódás...",
"Disconnecting...": "Kapcsolat bontása...",
"Reconnecting...": "Újrakapcsolódás...",
"Internal error": "Belső hiba",
"Failed to connect to server: ": "Nem sikerült csatlakozni a szerverhez: ",
"Connected (encrypted) to ": "Kapcsolódva (titkosítva) ehhez: ",
"Connected (unencrypted) to ": "Kapcsolódva (titkosítatlanul) ehhez: ",
"Something went wrong, connection is closed": "Valami hiba történt, a kapcsolat lezárult",
"Failed to connect to server": "Nem sikerült csatlakozni a szerverhez",
"Disconnected": "Kapcsolat bontva",
"New connection has been rejected with reason: ": "Az új kapcsolat elutasítva, indok: ",
"New connection has been rejected": "Az új kapcsolat elutasítva",
"Credentials are required": "Hitelesítő adatok szükségesek",
"noVNC encountered an error:": "A noVNC hibát észlelt:",
"Hide/Show the control bar": "Vezérlősáv elrejtése/megjelenítése",
"Drag": "Húzás",
"Move/Drag viewport": "Nézet mozgatása/húzása",
"Keyboard": "Billentyűzet",
"Show keyboard": "Billentyűzet megjelenítése",
"Extra keys": "Extra billentyűk",
"Show extra keys": "Extra billentyűk megjelenítése",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl lenyomása/felengedése",
"Alt": "Alt",
"Toggle Alt": "Alt lenyomása/felengedése",
"Toggle Windows": "Windows lenyomása/felengedése",
"Windows": "Windows",
"Send Tab": "Tab küldése",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape küldése",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del küldése",
"Shutdown/Reboot": "Leállítás/Újraindítás",
"Shutdown/Reboot...": "Leállítás/Újraindítás...",
"Power": "Bekapcsolás",
"Shutdown": "Leállítás",
"Reboot": "Újraindítás",
"Reset": "Reset",
"Clipboard": "Vágólap",
"Edit clipboard content in the textarea below.": "Itt tudod módosítani a vágólap tartalmát.",
"Full screen": "Teljes képernyő",
"Settings": "Beállítások",
"Shared mode": "Megosztott mód",
"View only": "Csak megtekintés",
"Clip to window": "Ablakhoz igazítás",
"Scaling mode:": "Méretezési mód:",
"None": "Nincs",
"Local scaling": "Helyi méretezés",
"Remote resizing": "Távoli átméretezés",
"Advanced": "Speciális",
"Quality:": "Minőség:",
"Compression level:": "Tömörítési szint:",
"Repeater ID:": "Ismétlő azonosító:",
"WebSocket": "WebSocket",
"Encrypt": "Titkosítás",
"Host:": "Hoszt:",
"Port:": "Port:",
"Path:": "Útvonal:",
"Automatic reconnect": "Automatikus újracsatlakozás",
"Reconnect delay (ms):": "Újracsatlakozás késleltetése (ms):",
"Show dot when no cursor": "Kurzor hiányában pont mutatása",
"Logging:": "Naplózás:",
"Version:": "Verzió:",
"Disconnect": "Kapcsolat bontása",
"Connect": "Csatlakozás",
"Server identity": "Szerver azonosító",
"The server has provided the following identifying information:": "A szerver a következő azonosító információt adta meg:",
"Fingerprint:": "Ujjlenyomat:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Ellenőrizze, hogy az információ helyes-e és nyomja meg a \"Jóváhagyás\" gombot. Ellenkező esetben nyomja meg az \"Elutasítás\" gombot.",
"Approve": "Jóváhagyás",
"Reject": "Elutasítás",
"Credentials": "Hitelesítő adatok",
"Username:": "Felhasználónév:",
"Password:": "Jelszó:",
"Send credentials": "Hitelesítő adatok küldése",
"Cancel": "Mégse"
}

View File

@ -9,7 +9,7 @@
import * as Log from '../core/util/logging.js'; import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js'; import _, { l10n } from './localization.js';
import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari, import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
hasScrollbarGutter, dragThreshold } hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }
from '../core/util/browser.js'; from '../core/util/browser.js';
import { setCapture, getPointerEvent } from '../core/util/events.js'; import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js"; import KeyTable from "../core/input/keysym.js";
@ -20,7 +20,7 @@ import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC"; const PAGE_TITLE = "noVNC";
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "hu", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
const UI = { const UI = {
@ -1107,6 +1107,7 @@ const UI = {
UI.rfb.showDotCursor = UI.getSetting('show_dot'); UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb UI.updateViewOnly(); // requires UI.rfb
UI.updateClipboard();
}, },
disconnect() { disconnect() {
@ -1175,6 +1176,9 @@ const UI = {
UI.updateVisualState('connected'); UI.updateVisualState('connected');
// Here we can reset the retry count // Here we can reset the retry count
UI.resetFirstReconnection(); UI.resetFirstReconnection();
UI.updateBeforeUnload();
// Do this last because it can only be used on rendered elements // Do this last because it can only be used on rendered elements
UI.rfb.focus(); UI.rfb.focus();
}, },
@ -1211,6 +1215,8 @@ const UI = {
UI.showStatus(_("Disconnected"), 'normal'); UI.showStatus(_("Disconnected"), 'normal');
} }
UI.updateBeforeUnload();
document.title = PAGE_TITLE; document.title = PAGE_TITLE;
UI.openControlbar(); UI.openControlbar();
@ -1231,6 +1237,24 @@ const UI = {
UI.showStatus(msg, 'error'); UI.showStatus(msg, 'error');
}, },
handleBeforeUnload(e) {
// Trigger a "Leave site?" warning prompt before closing the
// page. Modern browsers (Oct 2025) accept either (or both)
// preventDefault() or a nonempty returnValue, though the latter is
// considered legacy. The custom string is ignored by modern browsers,
// which display a native message, but older browsers will show it.
e.preventDefault();
e.returnValue = _("Are you sure you want to disconnect the session?");
},
updateBeforeUnload() {
// Remove first to avoid adding duplicates
window.removeEventListener("beforeunload", UI.handleBeforeUnload);
if (!UI.rfb?.viewOnly && UI.connected) {
window.addEventListener("beforeunload", UI.handleBeforeUnload);
}
},
/* ------^------- /* ------^-------
* /CONNECTION * /CONNECTION
* ============== * ==============
@ -1757,6 +1781,8 @@ const UI = {
if (!UI.rfb) return; if (!UI.rfb) return;
UI.rfb.viewOnly = UI.getSetting('view_only'); UI.rfb.viewOnly = UI.getSetting('view_only');
UI.updateBeforeUnload();
// Hide input related buttons in view only mode // Hide input related buttons in view only mode
if (UI.rfb.viewOnly) { if (UI.rfb.viewOnly) {
document.getElementById('noVNC_keyboard_button') document.getElementById('noVNC_keyboard_button')
@ -1775,6 +1801,31 @@ const UI = {
} }
}, },
updateClipboard() {
browserAsyncClipboardSupport()
.then((support) => {
if (support === 'unsupported') {
// Use fallback clipboard panel
return;
}
if (support === 'denied' || support === 'available') {
UI.closeClipboardPanel();
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.removeEventListener('click', UI.toggleClipboardPanel);
document.getElementById('noVNC_clipboard_text')
.removeEventListener('change', UI.clipboardSend);
if (UI.rfb) {
UI.rfb.removeEventListener('clipboard', UI.clipboardReceive);
}
}
})
.catch(() => {
// Treat as unsupported
});
},
updateShowDotCursor() { updateShowDotCursor() {
if (!UI.rfb) return; if (!UI.rfb) return;
UI.rfb.showDotCursor = UI.getSetting('show_dot'); UI.rfb.showDotCursor = UI.getSetting('show_dot');

72
core/clipboard.js Normal file
View File

@ -0,0 +1,72 @@
/*
* noVNC: HTML5 VNC client
* Copyright (c) 2025 The noVNC authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
import * as Log from './util/logging.js';
import { browserAsyncClipboardSupport } from './util/browser.js';
export default class AsyncClipboard {
constructor(target) {
this._target = target || null;
this._isAvailable = null;
this._eventHandlers = {
'focus': this._handleFocus.bind(this),
};
// ===== EVENT HANDLERS =====
this.onpaste = () => {};
}
// ===== PRIVATE METHODS =====
async _ensureAvailable() {
if (this._isAvailable !== null) return this._isAvailable;
try {
const status = await browserAsyncClipboardSupport();
this._isAvailable = (status === 'available');
} catch {
this._isAvailable = false;
}
return this._isAvailable;
}
async _handleFocus(event) {
if (!(await this._ensureAvailable())) return;
try {
const text = await navigator.clipboard.readText();
this.onpaste(text);
} catch (error) {
Log.Error("Clipboard read failed: ", error);
}
}
// ===== PUBLIC METHODS =====
writeClipboard(text) {
// Can lazily check cached availability
if (!this._isAvailable) return false;
navigator.clipboard.writeText(text)
.catch(error => Log.Error("Clipboard write failed: ", error));
return true;
}
grab() {
if (!this._target) return;
this._ensureAvailable()
.then((isAvailable) => {
if (isAvailable) {
this._target.addEventListener('focus', this._eventHandlers.focus);
}
});
}
ungrab() {
if (!this._target) return;
this._target.removeEventListener('focus', this._eventHandlers.focus);
}
}

View File

@ -119,18 +119,33 @@ export default class JPEGDecoder {
let extra = 0; let extra = 0;
if (type === 0xDA) { if (type === 0xDA) {
// start of scan // start of scan
extra += 2; if (sock.rQwait("JPEG", length-2 + 2, 4)) {
return null;
}
let len = sock.rQlen();
let data = sock.rQpeekBytes(len, false);
while (true) { while (true) {
if (sock.rQwait("JPEG", length-2+extra, 4)) { let idx = data.indexOf(0xFF, length-2+extra);
if (idx === -1) {
sock.rQwait("JPEG", Infinity, 4);
return null; return null;
} }
let data = sock.rQpeekBytes(length-2+extra, false);
if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 && if (idx === len-1) {
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) { sock.rQwait("JPEG", Infinity, 4);
extra -= 2; return null;
break;
} }
extra++;
if (data.at(idx+1) === 0x00 ||
(data.at(idx+1) >= 0xD0 && data.at(idx+1) <= 0xD7)) {
extra = idx+2 - (length-2);
continue;
}
extra = idx - (length-2);
break;
} }
} }

View File

@ -521,6 +521,9 @@ export default class Display {
return; return;
} }
this.drawImage(a.img, a.x, a.y); this.drawImage(a.img, a.x, a.y);
// This helps the browser free the memory right
// away, rather than ballooning
a.img.src = "";
} else { } else {
a.img._noVNCDisplay = this; a.img._noVNCDisplay = this;
a.img.addEventListener('load', this._resumeRenderQ); a.img.addEventListener('load', this._resumeRenderQ);

View File

@ -15,6 +15,7 @@ import { clientToElement } from './util/element.js';
import { setCapture } from './util/events.js'; import { setCapture } from './util/events.js';
import EventTargetMixin from './util/eventtarget.js'; import EventTargetMixin from './util/eventtarget.js';
import Display from "./display.js"; import Display from "./display.js";
import AsyncClipboard from "./clipboard.js";
import Inflator from "./inflator.js"; import Inflator from "./inflator.js";
import Deflator from "./deflator.js"; import Deflator from "./deflator.js";
import Keyboard from "./input/keyboard.js"; import Keyboard from "./input/keyboard.js";
@ -164,6 +165,7 @@ export default class RFB extends EventTargetMixin {
this._sock = null; // Websock object this._sock = null; // Websock object
this._display = null; // Display object this._display = null; // Display object
this._flushing = false; // Display flushing state this._flushing = false; // Display flushing state
this._asyncClipboard = null; // Async clipboard object
this._keyboard = null; // Keyboard input handler object this._keyboard = null; // Keyboard input handler object
this._gestures = null; // Gesture input handler object this._gestures = null; // Gesture input handler object
this._resizeObserver = null; // Resize observer object this._resizeObserver = null; // Resize observer object
@ -266,6 +268,9 @@ export default class RFB extends EventTargetMixin {
throw exc; throw exc;
} }
this._asyncClipboard = new AsyncClipboard(this._canvas);
this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
this._keyboard = new Keyboard(this._canvas); this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._remoteCapsLock = null; // Null indicates unknown or irrelevant this._remoteCapsLock = null; // Null indicates unknown or irrelevant
@ -315,8 +320,10 @@ export default class RFB extends EventTargetMixin {
this._rfbConnectionState === "connected") { this._rfbConnectionState === "connected") {
if (viewOnly) { if (viewOnly) {
this._keyboard.ungrab(); this._keyboard.ungrab();
this._asyncClipboard.ungrab();
} else { } else {
this._keyboard.grab(); this._keyboard.grab();
this._asyncClipboard.grab();
} }
} }
} }
@ -2208,7 +2215,10 @@ export default class RFB extends EventTargetMixin {
this._setDesktopName(name); this._setDesktopName(name);
this._resize(width, height); this._resize(width, height);
if (!this._viewOnly) { this._keyboard.grab(); } if (!this._viewOnly) {
this._keyboard.grab();
this._asyncClipboard.grab();
}
this._fbDepth = 24; this._fbDepth = 24;
@ -2323,6 +2333,15 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unexpected SetColorMapEntries message"); return this._fail("Unexpected SetColorMapEntries message");
} }
_writeClipboard(text) {
if (this._viewOnly) return;
if (this._asyncClipboard.writeClipboard(text)) return;
// Fallback clipboard
this.dispatchEvent(
new CustomEvent("clipboard", {detail: {text: text}})
);
}
_handleServerCutText() { _handleServerCutText() {
Log.Debug("ServerCutText"); Log.Debug("ServerCutText");
@ -2342,9 +2361,7 @@ export default class RFB extends EventTargetMixin {
return true; return true;
} }
this.dispatchEvent(new CustomEvent( this._writeClipboard(text);
"clipboard",
{ detail: { text: text } }));
} else { } else {
//Extended msg. //Extended msg.
@ -2480,9 +2497,7 @@ export default class RFB extends EventTargetMixin {
textData = textData.replaceAll("\r\n", "\n"); textData = textData.replaceAll("\r\n", "\n");
this.dispatchEvent(new CustomEvent( this._writeClipboard(textData);
"clipboard",
{ detail: { text: textData } }));
} }
} else { } else {
return this._fail("Unexpected action in extended clipboard message: " + actions); return this._fail("Unexpected action in extended clipboard message: " + actions);

View File

@ -11,6 +11,39 @@
import * as Log from './logging.js'; import * as Log from './logging.js';
import Base64 from '../base64.js'; import Base64 from '../base64.js';
// Async clipboard detection
/* Evaluates if there is browser support for the async clipboard API and
* relevant clipboard permissions. Returns 'unsupported' if permission states
* cannot be resolved. On the other hand, detecting 'granted' or 'prompt'
* permission states for both read and write indicates full API support with no
* imposed native browser paste prompt. Conversely, detecting 'denied' indicates
* the user elected to disable clipboard.
*/
export async function browserAsyncClipboardSupport() {
if (!(navigator?.permissions?.query &&
navigator?.clipboard?.writeText &&
navigator?.clipboard?.readText)) {
return 'unsupported';
}
try {
const writePerm = await navigator.permissions.query(
{name: "clipboard-write", allowWithoutGesture: true});
const readPerm = await navigator.permissions.query(
{name: "clipboard-read", allowWithoutGesture: false});
if (writePerm.state === "denied" || readPerm.state === "denied") {
return 'denied';
}
if ((writePerm.state === "granted" || writePerm.state === "prompt") &&
(readPerm.state === "granted" || readPerm.state === "prompt")) {
return 'available';
}
} catch {
return 'unsupported';
}
return 'unsupported';
}
// Touch detection // Touch detection
export let isTouchDevice = ('ontouchstart' in document.documentElement) || export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
// required for Chrome debugger // required for Chrome debugger

View File

@ -124,6 +124,10 @@ export default class Websock {
return res >>> 0; return res >>> 0;
} }
rQlen() {
return this._rQlen - this._rQi;
}
rQshiftStr(len) { rQshiftStr(len) {
let str = ""; let str = "";
// Handle large arrays in steps to avoid long strings on the stack // Handle large arrays in steps to avoid long strings on the stack

View File

@ -18,6 +18,8 @@ keysym values.
* __Display__ (core/display.js): Efficient 2D rendering abstraction * __Display__ (core/display.js): Efficient 2D rendering abstraction
layered on the HTML5 canvas element. layered on the HTML5 canvas element.
* __Clipboard__ (core/clipboard.js): Clipboard event handler.
* __Websock__ (core/websock.js): Websock client from websockify * __Websock__ (core/websock.js): Websock client from websockify
with transparent binary data support. with transparent binary data support.
[Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page. [Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page.
@ -25,10 +27,10 @@ with transparent binary data support.
## 1.2 Callbacks ## 1.2 Callbacks
For the Mouse, Keyboard and Display objects the callback functions are For the Mouse, Keyboard, Display, and Clipboard objects, the callback
assigned to configuration attributes, just as for the RFB object. The functions are assigned to configuration attributes, just as for the RFB
WebSock module has a method named 'on' that takes two parameters: the object. The WebSock module has a method named 'on' that takes two
callback event name, and the callback function. parameters: the callback event name, and the callback function.
## 2. Modules ## 2. Modules
@ -81,3 +83,23 @@ None
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display | blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
| drawImage | (img, x, y) | Draw image and track damage | drawImage | (img, x, y) | Draw image and track damage
| autoscale | (containerWidth, containerHeight) | Scale the display | autoscale | (containerWidth, containerHeight) | Scale the display
## 2.3 Clipboard module
### 2.3.1 Configuration attributes
None
### 2.3.2 Methods
| name | parameters | description
| ------------------ | ----------------- | ------------
| writeClipboard | (text) | An async write text to clipboard
| grab | () | Begin capturing clipboard events
| ungrab | () | Stop capturing clipboard events
### 2.3.3 Callbacks
| name | parameters | description
| ------- | ---------- | ------------
| onpaste | (text) | Called following a target focus event and an async clipboard read

View File

@ -77,11 +77,6 @@ protocol stream.
if the remote session is smaller than its container, or handled if the remote session is smaller than its container, or handled
according to `clipViewport` if it is larger. Disabled by default. according to `clipViewport` if it is larger. Disabled by default.
`showDotCursor`
- Is a `boolean` indicating whether a dot cursor should be shown
instead of a zero-sized or fully-transparent cursor if the server
sets such invisible cursor. Disabled by default.
`viewOnly` `viewOnly`
- Is a `boolean` indicating if any events (e.g. key presses or mouse - Is a `boolean` indicating if any events (e.g. key presses or mouse
movement) should be prevented from being sent to the server. movement) should be prevented from being sent to the server.

View File

@ -89,9 +89,6 @@ Currently, the following options are available:
* `compression` - The session compression level. Can be `0` to `9`. * `compression` - The session compression level. Can be `0` to `9`.
* `show_dot` - If a dot cursor should be shown when the remote server provides
no local cursor, or provides a fully-transparent (invisible) cursor.
* `logging` - The console log level. Can be one of `error`, `warn`, `info` or * `logging` - The console log level. Can be one of `error`, `warn`, `info` or
`debug`. `debug`.

View File

@ -2,24 +2,20 @@
"name": "@novnc/novnc", "name": "@novnc/novnc",
"version": "1.6.0", "version": "1.6.0",
"description": "An HTML5 VNC client", "description": "An HTML5 VNC client",
"browser": "lib/rfb", "type": "module",
"directories": {
"lib": "lib",
"doc": "docs",
"test": "tests"
},
"files": [ "files": [
"lib", "core",
"vendor",
"AUTHORS", "AUTHORS",
"VERSION", "VERSION",
"docs/API.md", "docs/API.md",
"docs/LIBRARY.md", "docs/LIBRARY.md",
"docs/LICENSE*" "docs/LICENSE*"
], ],
"exports": "./core/rfb.js",
"scripts": { "scripts": {
"lint": "eslint app core po/po2js po/xgettext-html tests utils", "lint": "eslint app core po/po2js po/xgettext-html tests utils",
"test": "karma start karma.conf.js", "test": "karma start karma.conf.cjs"
"prepublish": "node ./utils/convert.js --clean"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -2,7 +2,7 @@ all:
.PHONY: update-po update-js update-pot .PHONY: update-po update-js update-pot
.PHONY: FORCE .PHONY: FORCE
LINGUAS := cs de el es fr it ja ko nl pl pt_BR ru sv tr zh_CN zh_TW LINGUAS := cs de el es fr hr hu it ja ko nl pl pt_BR ru sv tr zh_CN zh_TW
VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4) VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4)

338
po/hr.po Normal file
View File

@ -0,0 +1,338 @@
# Croatian translations for noVNC package
# Hrvatski prijevod za noVNC paket
# Copyright (C) 2025 The noVNC authors
# This file is distributed under the same license as the noVNC package.
# Milo Ivir <mail@milotype.de>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
"PO-Revision-Date: 2025-08-25 18:24+0200\n"
"Last-Translator: Milo Ivir <mail@mivirtype.de>\n"
"Language-Team: \n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.7\n"
#: ../app/ui.js:84
msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr ""
"Pokretanje bez HTTPS-a se ne preporučuje, vjerojatno će se dogoditi prekidi "
"rada ili drugi problemi."
#: ../app/ui.js:413
msgid "Connecting..."
msgstr "Povezivanje …"
#: ../app/ui.js:420
msgid "Disconnecting..."
msgstr "Odspajanje …"
#: ../app/ui.js:426
msgid "Reconnecting..."
msgstr "Ponovno povezivanje …"
#: ../app/ui.js:431
msgid "Internal error"
msgstr "Interna greška"
#: ../app/ui.js:1079
msgid "Failed to connect to server: "
msgstr "Povezivanje sa serverom nije uspjelo: "
#: ../app/ui.js:1145
msgid "Connected (encrypted) to "
msgstr "Povezano (šifrirano) na "
#: ../app/ui.js:1147
msgid "Connected (unencrypted) to "
msgstr "Povezano (nešifrirano) na "
#: ../app/ui.js:1170
msgid "Something went wrong, connection is closed"
msgstr "Nešto nije u redu, veza je zatvorena"
#: ../app/ui.js:1173
msgid "Failed to connect to server"
msgstr "Povezivanje sa serverom nije uspjelo"
#: ../app/ui.js:1185
msgid "Disconnected"
msgstr "Odspojeno"
#: ../app/ui.js:1200
msgid "New connection has been rejected with reason: "
msgstr "Nova veza je odbijena s razlogom: "
#: ../app/ui.js:1203
msgid "New connection has been rejected"
msgstr "Nova veza je odbijena"
#: ../app/ui.js:1269
msgid "Credentials are required"
msgstr "Podaci za prijavu su obavezni"
#: ../vnc.html:106
msgid "noVNC encountered an error:"
msgstr "noVNC je naišao na grešku:"
#: ../vnc.html:116
msgid "Hide/Show the control bar"
msgstr "Sakrij/Prikaži traku kontrola"
#: ../vnc.html:125
msgid "Drag"
msgstr "Povuci"
#: ../vnc.html:125
msgid "Move/Drag viewport"
msgstr "Pomakni/Povuci vidljivo područje"
#: ../vnc.html:131
msgid "Keyboard"
msgstr "Tipkovnica"
#: ../vnc.html:131
msgid "Show keyboard"
msgstr "Prikaži tipkovnicu"
#: ../vnc.html:136
msgid "Extra keys"
msgstr "Dodatne tipke"
#: ../vnc.html:136
msgid "Show extra keys"
msgstr "Prikaži dodatne tipke"
#: ../vnc.html:141
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:141
msgid "Toggle Ctrl"
msgstr "Uključi/Isključi Ctrl"
#: ../vnc.html:144
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:144
msgid "Toggle Alt"
msgstr "Uključi/Isključi Alt"
#: ../vnc.html:147
msgid "Toggle Windows"
msgstr "Uključi/Isključi Windows"
#: ../vnc.html:147
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:150
msgid "Send Tab"
msgstr "Pošalji tabulator"
#: ../vnc.html:150
msgid "Tab"
msgstr "Tabulator"
#: ../vnc.html:153
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:153
msgid "Send Escape"
msgstr "Pošalji Escape"
#: ../vnc.html:156
msgid "Ctrl+Alt+Del"
msgstr "Ctrl + Alt + Del"
#: ../vnc.html:156
msgid "Send Ctrl-Alt-Del"
msgstr "Pošalji Ctrl+Alt+Del"
#: ../vnc.html:163
msgid "Shutdown/Reboot"
msgstr "Isključi/Ponovo pokreni"
#: ../vnc.html:163
msgid "Shutdown/Reboot..."
msgstr "Isključi/Ponovo pokreni …"
#: ../vnc.html:169
msgid "Power"
msgstr "Napajanje"
#: ../vnc.html:171
msgid "Shutdown"
msgstr "Isključi"
#: ../vnc.html:172
msgid "Reboot"
msgstr "Ponovo pokreni"
#: ../vnc.html:173
msgid "Reset"
msgstr "Resetiraj"
#: ../vnc.html:178 ../vnc.html:184
msgid "Clipboard"
msgstr "Međuspremnik"
#: ../vnc.html:186
msgid "Edit clipboard content in the textarea below."
msgstr "Uredi sadržaj međuspremnika u donjem području teksta."
#: ../vnc.html:194
msgid "Full screen"
msgstr "Cjeloekranski prikaz"
#: ../vnc.html:199 ../vnc.html:205
msgid "Settings"
msgstr "Postavke"
#: ../vnc.html:211
msgid "Shared mode"
msgstr "Dijeljeni modus"
#: ../vnc.html:218
msgid "View only"
msgstr "Samo prikaz"
#: ../vnc.html:226
msgid "Clip to window"
msgstr "Isijeci na veličinu prozora"
#: ../vnc.html:231
msgid "Scaling mode:"
msgstr "Modus skaliranja:"
#: ../vnc.html:233
msgid "None"
msgstr "Bez"
#: ../vnc.html:234
msgid "Local scaling"
msgstr "Lokalno skaliranje"
#: ../vnc.html:235
msgid "Remote resizing"
msgstr "Daljinsko mijenjanje veličine"
#: ../vnc.html:240
msgid "Advanced"
msgstr "Napredno"
#: ../vnc.html:243
msgid "Quality:"
msgstr "Kvaliteta:"
#: ../vnc.html:247
msgid "Compression level:"
msgstr "Razina kompresije:"
#: ../vnc.html:252
msgid "Repeater ID:"
msgstr "ID repetitora:"
#: ../vnc.html:256
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:261
msgid "Encrypt"
msgstr "Šifriraj"
#: ../vnc.html:266
msgid "Host:"
msgstr "Host:"
#: ../vnc.html:270
msgid "Port:"
msgstr "Priključak:"
#: ../vnc.html:274
msgid "Path:"
msgstr "Putanja:"
#: ../vnc.html:283
msgid "Automatic reconnect"
msgstr "Automatsko ponovno povezivanje"
#: ../vnc.html:288
msgid "Reconnect delay (ms):"
msgstr "Kašnjenje ponovnog povezivanja (ms):"
#: ../vnc.html:295
msgid "Show dot when no cursor"
msgstr "Prikaži točku kada nema pokazivača"
#: ../vnc.html:302
msgid "Logging:"
msgstr "Zapisivanje:"
#: ../vnc.html:311
msgid "Version:"
msgstr "Verzija:"
#: ../vnc.html:319
msgid "Disconnect"
msgstr "Odspoji"
#: ../vnc.html:342
msgid "Connect"
msgstr "Poveži"
#: ../vnc.html:351
msgid "Server identity"
msgstr "Identitet servera"
#: ../vnc.html:354
msgid "The server has provided the following identifying information:"
msgstr "Server je pružio sljedeće identifikacijske podatke:"
#: ../vnc.html:357
msgid "Fingerprint:"
msgstr "Otisak:"
#: ../vnc.html:361
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
"Provjeri jesu li podaci točni i pritisni „Odobri“. U suprotnom pritisni "
"„Odbaci“."
#: ../vnc.html:366
msgid "Approve"
msgstr "Odobri"
#: ../vnc.html:367
msgid "Reject"
msgstr "Odbij"
#: ../vnc.html:375
msgid "Credentials"
msgstr "Podaci za prijavu"
#: ../vnc.html:379
msgid "Username:"
msgstr "Korisničko ime:"
#: ../vnc.html:383
msgid "Password:"
msgstr "Lozinka:"
#: ../vnc.html:387
msgid "Send credentials"
msgstr "Pošalji podatke za prijavu"
#: ../vnc.html:396
msgid "Cancel"
msgstr "Odustani"

332
po/hu.po Normal file
View File

@ -0,0 +1,332 @@
# Hungarian translations for noVNC package.
# Copyright (C) 2025 The noVNC authors
# This file is distributed under the same license as the noVNC package.
# Daniel Felso <danielfelso@protonmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
"PO-Revision-Date: 2025-10-06 14:38+0200\n"
"Last-Translator: Daniel Felso <danielfelso@protonmail.com>\n"
"Language-Team: \n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../app/ui.js:84
msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr "HTTPS nélkül futtatni nem ajánlott, összeomlások vagy más problémák várhatók."
#: ../app/ui.js:413
msgid "Connecting..."
msgstr "Kapcsolódás..."
#: ../app/ui.js:420
msgid "Disconnecting..."
msgstr "Kapcsolat bontása..."
#: ../app/ui.js:426
msgid "Reconnecting..."
msgstr "Újrakapcsolódás..."
#: ../app/ui.js:431
msgid "Internal error"
msgstr "Belső hiba"
#: ../app/ui.js:1079
msgid "Failed to connect to server: "
msgstr "Nem sikerült csatlakozni a szerverhez: "
#: ../app/ui.js:1145
msgid "Connected (encrypted) to "
msgstr "Kapcsolódva (titkosítva) ehhez: "
#: ../app/ui.js:1147
msgid "Connected (unencrypted) to "
msgstr "Kapcsolódva (titkosítatlanul) ehhez: "
#: ../app/ui.js:1170
msgid "Something went wrong, connection is closed"
msgstr "Valami hiba történt, a kapcsolat lezárult"
#: ../app/ui.js:1173
msgid "Failed to connect to server"
msgstr "Nem sikerült csatlakozni a szerverhez"
#: ../app/ui.js:1185
msgid "Disconnected"
msgstr "Kapcsolat bontva"
#: ../app/ui.js:1200
msgid "New connection has been rejected with reason: "
msgstr "Az új kapcsolat elutasítva, indok: "
#: ../app/ui.js:1203
msgid "New connection has been rejected"
msgstr "Az új kapcsolat elutasítva"
#: ../app/ui.js:1269
msgid "Credentials are required"
msgstr "Hitelesítő adatok szükségesek"
#: ../vnc.html:106
msgid "noVNC encountered an error:"
msgstr "A noVNC hibát észlelt:"
#: ../vnc.html:116
msgid "Hide/Show the control bar"
msgstr "Vezérlősáv elrejtése/megjelenítése"
#: ../vnc.html:125
msgid "Drag"
msgstr "Húzás"
#: ../vnc.html:125
msgid "Move/Drag viewport"
msgstr "Nézet mozgatása/húzása"
#: ../vnc.html:131
msgid "Keyboard"
msgstr "Billentyűzet"
#: ../vnc.html:131
msgid "Show keyboard"
msgstr "Billentyűzet megjelenítése"
#: ../vnc.html:136
msgid "Extra keys"
msgstr "Extra billentyűk"
#: ../vnc.html:136
msgid "Show extra keys"
msgstr "Extra billentyűk megjelenítése"
#: ../vnc.html:141
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:141
msgid "Toggle Ctrl"
msgstr "Ctrl lenyomása/felengedése"
#: ../vnc.html:144
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:144
msgid "Toggle Alt"
msgstr "Alt lenyomása/felengedése"
#: ../vnc.html:147
msgid "Toggle Windows"
msgstr "Windows lenyomása/felengedése"
#: ../vnc.html:147
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:150
msgid "Send Tab"
msgstr "Tab küldése"
#: ../vnc.html:150
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:153
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:153
msgid "Send Escape"
msgstr "Escape küldése"
#: ../vnc.html:156
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:156
msgid "Send Ctrl-Alt-Del"
msgstr "Ctrl-Alt-Del küldése"
#: ../vnc.html:163
msgid "Shutdown/Reboot"
msgstr "Leállítás/Újraindítás"
#: ../vnc.html:163
msgid "Shutdown/Reboot..."
msgstr "Leállítás/Újraindítás..."
#: ../vnc.html:169
msgid "Power"
msgstr "Bekapcsolás"
#: ../vnc.html:171
msgid "Shutdown"
msgstr "Leállítás"
#: ../vnc.html:172
msgid "Reboot"
msgstr "Újraindítás"
#: ../vnc.html:173
msgid "Reset"
msgstr "Reset"
#: ../vnc.html:178 ../vnc.html:184
msgid "Clipboard"
msgstr "Vágólap"
#: ../vnc.html:186
msgid "Edit clipboard content in the textarea below."
msgstr "Itt tudod módosítani a vágólap tartalmát."
#: ../vnc.html:194
msgid "Full screen"
msgstr "Teljes képernyő"
#: ../vnc.html:199 ../vnc.html:205
msgid "Settings"
msgstr "Beállítások"
#: ../vnc.html:211
msgid "Shared mode"
msgstr "Megosztott mód"
#: ../vnc.html:218
msgid "View only"
msgstr "Csak megtekintés"
#: ../vnc.html:226
msgid "Clip to window"
msgstr "Ablakhoz igazítás"
#: ../vnc.html:231
msgid "Scaling mode:"
msgstr "Méretezési mód:"
#: ../vnc.html:233
msgid "None"
msgstr "Nincs"
#: ../vnc.html:234
msgid "Local scaling"
msgstr "Helyi méretezés"
#: ../vnc.html:235
msgid "Remote resizing"
msgstr "Távoli átméretezés"
#: ../vnc.html:240
msgid "Advanced"
msgstr "Speciális"
#: ../vnc.html:243
msgid "Quality:"
msgstr "Minőség:"
#: ../vnc.html:247
msgid "Compression level:"
msgstr "Tömörítési szint:"
#: ../vnc.html:252
msgid "Repeater ID:"
msgstr "Ismétlő azonosító:"
#: ../vnc.html:256
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:261
msgid "Encrypt"
msgstr "Titkosítás"
#: ../vnc.html:266
msgid "Host:"
msgstr "Hoszt:"
#: ../vnc.html:270
msgid "Port:"
msgstr "Port:"
#: ../vnc.html:274
msgid "Path:"
msgstr "Útvonal:"
#: ../vnc.html:283
msgid "Automatic reconnect"
msgstr "Automatikus újracsatlakozás"
#: ../vnc.html:288
msgid "Reconnect delay (ms):"
msgstr "Újracsatlakozás késleltetése (ms):"
#: ../vnc.html:295
msgid "Show dot when no cursor"
msgstr "Kurzor hiányában pont mutatása"
#: ../vnc.html:302
msgid "Logging:"
msgstr "Naplózás:"
#: ../vnc.html:311
msgid "Version:"
msgstr "Verzió:"
#: ../vnc.html:319
msgid "Disconnect"
msgstr "Kapcsolat bontása"
#: ../vnc.html:342
msgid "Connect"
msgstr "Csatlakozás"
#: ../vnc.html:351
msgid "Server identity"
msgstr "Szerver azonosító"
#: ../vnc.html:354
msgid "The server has provided the following identifying information:"
msgstr "A szerver a következő azonosító információt adta meg:"
#: ../vnc.html:357
msgid "Fingerprint:"
msgstr "Ujjlenyomat:"
#: ../vnc.html:361
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr "Ellenőrizze, hogy az információ helyes-e és nyomja meg a \"Jóváhagyás\" gombot. Ellenkező esetben nyomja meg az \"Elutasítás\" gombot."
#: ../vnc.html:366
msgid "Approve"
msgstr "Jóváhagyás"
#: ../vnc.html:367
msgid "Reject"
msgstr "Elutasítás"
#: ../vnc.html:375
msgid "Credentials"
msgstr "Hitelesítő adatok"
#: ../vnc.html:379
msgid "Username:"
msgstr "Felhasználónév:"
#: ../vnc.html:383
msgid "Password:"
msgstr "Jelszó:"
#: ../vnc.html:387
msgid "Send credentials"
msgstr "Hitelesítő adatok küldése"
#: ../vnc.html:396
msgid "Cancel"
msgstr "Mégse"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: noVNC 1.6.0\n" "Project-Id-Version: noVNC 1.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-02-14 10:14+0100\n" "POT-Creation-Date: 2025-10-31 09:17+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -38,39 +38,43 @@ msgstr ""
msgid "Internal error" msgid "Internal error"
msgstr "" msgstr ""
#: ../app/ui.js:1079 #: ../app/ui.js:1084
msgid "Failed to connect to server: " msgid "Failed to connect to server: "
msgstr "" msgstr ""
#: ../app/ui.js:1145 #: ../app/ui.js:1151
msgid "Connected (encrypted) to " msgid "Connected (encrypted) to "
msgstr "" msgstr ""
#: ../app/ui.js:1147 #: ../app/ui.js:1153
msgid "Connected (unencrypted) to " msgid "Connected (unencrypted) to "
msgstr "" msgstr ""
#: ../app/ui.js:1170 #: ../app/ui.js:1178
msgid "Something went wrong, connection is closed" msgid "Something went wrong, connection is closed"
msgstr "" msgstr ""
#: ../app/ui.js:1173 #: ../app/ui.js:1181
msgid "Failed to connect to server" msgid "Failed to connect to server"
msgstr "" msgstr ""
#: ../app/ui.js:1185 #: ../app/ui.js:1193
msgid "Disconnected" msgid "Disconnected"
msgstr "" msgstr ""
#: ../app/ui.js:1200 #: ../app/ui.js:1210
msgid "New connection has been rejected with reason: " msgid "New connection has been rejected with reason: "
msgstr "" msgstr ""
#: ../app/ui.js:1203 #: ../app/ui.js:1213
msgid "New connection has been rejected" msgid "New connection has been rejected"
msgstr "" msgstr ""
#: ../app/ui.js:1269 #: ../app/ui.js:1225
msgid "Are you sure you want to disconnect the session?"
msgstr ""
#: ../app/ui.js:1297
msgid "Credentials are required" msgid "Credentials are required"
msgstr "" msgstr ""

View File

@ -17,9 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
const { program } = require('commander'); import { program } from 'commander';
const fs = require('fs'); import fs from 'fs';
const pofile = require("pofile"); import pofile from "pofile";
program program
.argument('<input>') .argument('<input>')

700
po/sv.po
View File

@ -1,348 +1,352 @@
# Swedish translations for noVNC package # Swedish translations for noVNC package
# Svenska översättningar för paketet noVNC. # Svenska översättningar för paketet noVNC.
# Copyright (C) 2025 The noVNC authors # Copyright (C) 2025 The noVNC authors
# This file is distributed under the same license as the noVNC package. # This file is distributed under the same license as the noVNC package.
# Samuel Mannehed <samuel@cendio.se>, 2020. # Samuel Mannehed <samuel@cendio.se>, 2020.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: noVNC 1.6.0\n" "Project-Id-Version: noVNC 1.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-02-14 10:14+0100\n" "POT-Creation-Date: 2025-10-31 09:17+0100\n"
"PO-Revision-Date: 2025-02-14 10:29+0100\n" "PO-Revision-Date: 2025-10-31 10:48+0100\n"
"Last-Translator: Alexander Zeijlon <aleze@cendio.com>\n" "Last-Translator: Alexander Zeijlon <aleze@cendio.com>\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: sv\n" "Language: sv\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.5\n" "X-Generator: Poedit 3.7\n"
#: ../app/ui.js:84 #: ../app/ui.js:84
msgid "" msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely." "Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr "" msgstr ""
"Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är " "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är "
"troliga." "troliga."
#: ../app/ui.js:413 #: ../app/ui.js:413
msgid "Connecting..." msgid "Connecting..."
msgstr "Ansluter..." msgstr "Ansluter..."
#: ../app/ui.js:420 #: ../app/ui.js:420
msgid "Disconnecting..." msgid "Disconnecting..."
msgstr "Kopplar ner..." msgstr "Kopplar ifrån..."
#: ../app/ui.js:426 #: ../app/ui.js:426
msgid "Reconnecting..." msgid "Reconnecting..."
msgstr "Återansluter..." msgstr "Återansluter..."
#: ../app/ui.js:431 #: ../app/ui.js:431
msgid "Internal error" msgid "Internal error"
msgstr "Internt fel" msgstr "Internt fel"
#: ../app/ui.js:1079 #: ../app/ui.js:1084
msgid "Failed to connect to server: " msgid "Failed to connect to server: "
msgstr "Misslyckades att ansluta till servern: " msgstr "Misslyckades att ansluta till servern: "
#: ../app/ui.js:1145 #: ../app/ui.js:1151
msgid "Connected (encrypted) to " msgid "Connected (encrypted) to "
msgstr "Ansluten (krypterat) till " msgstr "Ansluten (krypterat) till "
#: ../app/ui.js:1147 #: ../app/ui.js:1153
msgid "Connected (unencrypted) to " msgid "Connected (unencrypted) to "
msgstr "Ansluten (okrypterat) till " msgstr "Ansluten (okrypterat) till "
#: ../app/ui.js:1170 #: ../app/ui.js:1178
msgid "Something went wrong, connection is closed" msgid "Something went wrong, connection is closed"
msgstr "Något gick fel, anslutningen avslutades" msgstr "Något gick fel, anslutningen avslutades"
#: ../app/ui.js:1173 #: ../app/ui.js:1181
msgid "Failed to connect to server" msgid "Failed to connect to server"
msgstr "Misslyckades att ansluta till servern" msgstr "Misslyckades att ansluta till servern"
#: ../app/ui.js:1185 #: ../app/ui.js:1193
msgid "Disconnected" msgid "Disconnected"
msgstr "Frånkopplad" msgstr "Frånkopplad"
#: ../app/ui.js:1200 #: ../app/ui.js:1210
msgid "New connection has been rejected with reason: " msgid "New connection has been rejected with reason: "
msgstr "Ny anslutning har blivit nekad med följande skäl: " msgstr "Ny anslutning har blivit nekad med följande skäl: "
#: ../app/ui.js:1203 #: ../app/ui.js:1213
msgid "New connection has been rejected" msgid "New connection has been rejected"
msgstr "Ny anslutning har blivit nekad" msgstr "Ny anslutning har blivit nekad"
#: ../app/ui.js:1269 #: ../app/ui.js:1225
msgid "Credentials are required" msgid "Are you sure you want to disconnect the session?"
msgstr "Användaruppgifter krävs" msgstr "Är du säker på att du vill koppla ifrån sessionen?"
#: ../vnc.html:106 #: ../app/ui.js:1297
msgid "noVNC encountered an error:" msgid "Credentials are required"
msgstr "noVNC stötte på ett problem:" msgstr "Användaruppgifter krävs"
#: ../vnc.html:116 #: ../vnc.html:106
msgid "Hide/Show the control bar" msgid "noVNC encountered an error:"
msgstr "Göm/Visa kontrollbaren" msgstr "noVNC stötte på ett problem:"
#: ../vnc.html:125 #: ../vnc.html:116
msgid "Drag" msgid "Hide/Show the control bar"
msgstr "Dra" msgstr "Göm/Visa kontrollbaren"
#: ../vnc.html:125 #: ../vnc.html:125
msgid "Move/Drag viewport" msgid "Drag"
msgstr "Flytta/Dra vyn" msgstr "Dra"
#: ../vnc.html:131 #: ../vnc.html:125
msgid "Keyboard" msgid "Move/Drag viewport"
msgstr "Tangentbord" msgstr "Flytta/Dra vyn"
#: ../vnc.html:131 #: ../vnc.html:131
msgid "Show keyboard" msgid "Keyboard"
msgstr "Visa tangentbord" msgstr "Tangentbord"
#: ../vnc.html:136 #: ../vnc.html:131
msgid "Extra keys" msgid "Show keyboard"
msgstr "Extraknappar" msgstr "Visa tangentbord"
#: ../vnc.html:136 #: ../vnc.html:136
msgid "Show extra keys" msgid "Extra keys"
msgstr "Visa extraknappar" msgstr "Extraknappar"
#: ../vnc.html:141 #: ../vnc.html:136
msgid "Ctrl" msgid "Show extra keys"
msgstr "Ctrl" msgstr "Visa extraknappar"
#: ../vnc.html:141 #: ../vnc.html:141
msgid "Toggle Ctrl" msgid "Ctrl"
msgstr "Växla Ctrl" msgstr "Ctrl"
#: ../vnc.html:144 #: ../vnc.html:141
msgid "Alt" msgid "Toggle Ctrl"
msgstr "Alt" msgstr "Växla Ctrl"
#: ../vnc.html:144 #: ../vnc.html:144
msgid "Toggle Alt" msgid "Alt"
msgstr "Växla Alt" msgstr "Alt"
#: ../vnc.html:147 #: ../vnc.html:144
msgid "Toggle Windows" msgid "Toggle Alt"
msgstr "Växla Windows" msgstr "Växla Alt"
#: ../vnc.html:147 #: ../vnc.html:147
msgid "Windows" msgid "Toggle Windows"
msgstr "Windows" msgstr "Växla Windows"
#: ../vnc.html:150 #: ../vnc.html:147
msgid "Send Tab" msgid "Windows"
msgstr "Skicka Tab" msgstr "Windows"
#: ../vnc.html:150 #: ../vnc.html:150
msgid "Tab" msgid "Send Tab"
msgstr "Tab" msgstr "Skicka Tab"
#: ../vnc.html:153 #: ../vnc.html:150
msgid "Esc" msgid "Tab"
msgstr "Esc" msgstr "Tab"
#: ../vnc.html:153 #: ../vnc.html:153
msgid "Send Escape" msgid "Esc"
msgstr "Skicka Escape" msgstr "Esc"
#: ../vnc.html:156 #: ../vnc.html:153
msgid "Ctrl+Alt+Del" msgid "Send Escape"
msgstr "Ctrl+Alt+Del" msgstr "Skicka Escape"
#: ../vnc.html:156 #: ../vnc.html:156
msgid "Send Ctrl-Alt-Del" msgid "Ctrl+Alt+Del"
msgstr "Skicka Ctrl-Alt-Del" msgstr "Ctrl+Alt+Del"
#: ../vnc.html:163 #: ../vnc.html:156
msgid "Shutdown/Reboot" msgid "Send Ctrl-Alt-Del"
msgstr "Stäng av/Boota om" msgstr "Skicka Ctrl-Alt-Del"
#: ../vnc.html:163 #: ../vnc.html:163
msgid "Shutdown/Reboot..." msgid "Shutdown/Reboot"
msgstr "Stäng av/Boota om..." msgstr "Stäng av/Starta om"
#: ../vnc.html:169 #: ../vnc.html:163
msgid "Power" msgid "Shutdown/Reboot..."
msgstr "Ström" msgstr "Stäng av/Starta om..."
#: ../vnc.html:171 #: ../vnc.html:169
msgid "Shutdown" msgid "Power"
msgstr "Stäng av" msgstr "Ström"
#: ../vnc.html:172 #: ../vnc.html:171
msgid "Reboot" msgid "Shutdown"
msgstr "Boota om" msgstr "Stäng av"
#: ../vnc.html:173 #: ../vnc.html:172
msgid "Reset" msgid "Reboot"
msgstr "Återställ" msgstr "Starta om"
#: ../vnc.html:178 ../vnc.html:184 #: ../vnc.html:173
msgid "Clipboard" msgid "Reset"
msgstr "Urklipp" msgstr "Återställ"
#: ../vnc.html:186 #: ../vnc.html:178 ../vnc.html:184
msgid "Edit clipboard content in the textarea below." msgid "Clipboard"
msgstr "Redigera urklippets innehåll i fältet nedan." msgstr "Urklipp"
#: ../vnc.html:194 #: ../vnc.html:186
msgid "Full screen" msgid "Edit clipboard content in the textarea below."
msgstr "Fullskärm" msgstr "Redigera urklippets innehåll i fältet nedan."
#: ../vnc.html:199 ../vnc.html:205 #: ../vnc.html:194
msgid "Settings" msgid "Full screen"
msgstr "Inställningar" msgstr "Fullskärm"
#: ../vnc.html:211 #: ../vnc.html:199 ../vnc.html:205
msgid "Shared mode" msgid "Settings"
msgstr "Delat läge" msgstr "Inställningar"
#: ../vnc.html:218 #: ../vnc.html:211
msgid "View only" msgid "Shared mode"
msgstr "Endast visning" msgstr "Delat läge"
#: ../vnc.html:226 #: ../vnc.html:218
msgid "Clip to window" msgid "View only"
msgstr "Begränsa till fönster" msgstr "Endast visning"
#: ../vnc.html:231 #: ../vnc.html:226
msgid "Scaling mode:" msgid "Clip to window"
msgstr "Skalningsläge:" msgstr "Begränsa till fönster"
#: ../vnc.html:233 #: ../vnc.html:231
msgid "None" msgid "Scaling mode:"
msgstr "Ingen" msgstr "Skalningsläge:"
#: ../vnc.html:234 #: ../vnc.html:233
msgid "Local scaling" msgid "None"
msgstr "Lokal skalning" msgstr "Ingen"
#: ../vnc.html:235 #: ../vnc.html:234
msgid "Remote resizing" msgid "Local scaling"
msgstr "Ändra storlek" msgstr "Lokal skalning"
#: ../vnc.html:240 #: ../vnc.html:235
msgid "Advanced" msgid "Remote resizing"
msgstr "Avancerat" msgstr "Ändra storlek"
#: ../vnc.html:243 #: ../vnc.html:240
msgid "Quality:" msgid "Advanced"
msgstr "Kvalitet:" msgstr "Avancerat"
#: ../vnc.html:247 #: ../vnc.html:243
msgid "Compression level:" msgid "Quality:"
msgstr "Kompressionsnivå:" msgstr "Kvalitet:"
#: ../vnc.html:252 #: ../vnc.html:247
msgid "Repeater ID:" msgid "Compression level:"
msgstr "Repeater-ID:" msgstr "Kompressionsnivå:"
#: ../vnc.html:256 #: ../vnc.html:252
msgid "WebSocket" msgid "Repeater ID:"
msgstr "WebSocket" msgstr "Repeater-ID:"
#: ../vnc.html:261 #: ../vnc.html:256
msgid "Encrypt" msgid "WebSocket"
msgstr "Kryptera" msgstr "WebSocket"
#: ../vnc.html:266 #: ../vnc.html:261
msgid "Host:" msgid "Encrypt"
msgstr "Värd:" msgstr "Kryptera"
#: ../vnc.html:270 #: ../vnc.html:266
msgid "Port:" msgid "Host:"
msgstr "Port:" msgstr "Värd:"
#: ../vnc.html:274 #: ../vnc.html:270
msgid "Path:" msgid "Port:"
msgstr "Sökväg:" msgstr "Port:"
#: ../vnc.html:283 #: ../vnc.html:274
msgid "Automatic reconnect" msgid "Path:"
msgstr "Automatisk återanslutning" msgstr "Sökväg:"
#: ../vnc.html:288 #: ../vnc.html:283
msgid "Reconnect delay (ms):" msgid "Automatic reconnect"
msgstr "Fördröjning (ms):" msgstr "Automatisk återanslutning"
#: ../vnc.html:295 #: ../vnc.html:288
msgid "Show dot when no cursor" msgid "Reconnect delay (ms):"
msgstr "Visa prick när ingen muspekare finns" msgstr "Fördröjning (ms):"
#: ../vnc.html:302 #: ../vnc.html:295
msgid "Logging:" msgid "Show dot when no cursor"
msgstr "Loggning:" msgstr "Visa prick när ingen muspekare finns"
#: ../vnc.html:311 #: ../vnc.html:302
msgid "Version:" msgid "Logging:"
msgstr "Version:" msgstr "Loggning:"
#: ../vnc.html:319 #: ../vnc.html:311
msgid "Disconnect" msgid "Version:"
msgstr "Koppla från" msgstr "Version:"
#: ../vnc.html:342 #: ../vnc.html:319
msgid "Connect" msgid "Disconnect"
msgstr "Anslut" msgstr "Koppla ifrån"
#: ../vnc.html:351 #: ../vnc.html:342
msgid "Server identity" msgid "Connect"
msgstr "Server-identitet" msgstr "Anslut"
#: ../vnc.html:354 #: ../vnc.html:351
msgid "The server has provided the following identifying information:" msgid "Server identity"
msgstr "Servern har gett följande identifierande information:" msgstr "Serveridentitet"
#: ../vnc.html:357 #: ../vnc.html:354
msgid "Fingerprint:" msgid "The server has provided the following identifying information:"
msgstr "Fingeravtryck:" msgstr "Servern har gett följande identifierande information:"
#: ../vnc.html:361 #: ../vnc.html:357
msgid "" msgid "Fingerprint:"
"Please verify that the information is correct and press \"Approve\". " msgstr "Fingeravtryck:"
"Otherwise press \"Reject\"."
msgstr "" #: ../vnc.html:361
"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck " msgid ""
"annars \"Neka\"." "Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
#: ../vnc.html:366 msgstr ""
msgid "Approve" "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck "
msgstr "Godkänn" "annars \"Avvisa\"."
#: ../vnc.html:367 #: ../vnc.html:366
msgid "Reject" msgid "Approve"
msgstr "Neka" msgstr "Godkänn"
#: ../vnc.html:375 #: ../vnc.html:367
msgid "Credentials" msgid "Reject"
msgstr "Användaruppgifter" msgstr "Avvisa"
#: ../vnc.html:379 #: ../vnc.html:375
msgid "Username:" msgid "Credentials"
msgstr "Användarnamn:" msgstr "Användaruppgifter"
#: ../vnc.html:383 #: ../vnc.html:379
msgid "Password:" msgid "Username:"
msgstr "Lösenord:" msgstr "Användarnamn:"
#: ../vnc.html:387 #: ../vnc.html:383
msgid "Send credentials" msgid "Password:"
msgstr "Skicka användaruppgifter" msgstr "Lösenord:"
#: ../vnc.html:396 #: ../vnc.html:387
msgid "Cancel" msgid "Send credentials"
msgstr "Avbryt" msgstr "Skicka användaruppgifter"
#~ msgid "Must set host" #: ../vnc.html:396
#~ msgstr "Du måste specifiera en värd" msgid "Cancel"
msgstr "Avbryt"
#~ msgid "HTTPS is required for full functionality"
#~ msgstr "HTTPS krävs för full funktionalitet" #~ msgid "Must set host"
#~ msgstr "Du måste specifiera en värd"
#~ msgid "Clear"
#~ msgstr "Rensa" #~ msgid "HTTPS is required for full functionality"
#~ msgstr "HTTPS krävs för full funktionalitet"
#~ msgid "Clear"
#~ msgstr "Rensa"

View File

@ -5,9 +5,9 @@
* Licensed under MPL 2.0 (see LICENSE.txt) * Licensed under MPL 2.0 (see LICENSE.txt)
*/ */
const { program } = require('commander'); import { program } from 'commander';
const jsdom = require("jsdom"); import jsdom from 'jsdom';
const fs = require("fs"); import fs from 'fs';
program program
.argument('<INPUT...>') .argument('<INPUT...>')
@ -106,7 +106,7 @@ let output = "";
for (let str in strings) { for (let str in strings) {
output += "#:"; output += "#:";
for (location in strings[str]) { for (let location in strings[str]) {
output += " " + location; output += " " + location;
} }
output += "\n"; output += "\n";

View File

@ -1,4 +1,4 @@
import * as chai from '../node_modules/chai/chai.js'; import * as chai from '../node_modules/chai/index.js';
import sinon from '../node_modules/sinon/pkg/sinon-esm.js'; import sinon from '../node_modules/sinon/pkg/sinon-esm.js';
import sinonChai from '../node_modules/sinon-chai/lib/sinon-chai.js'; import sinonChai from '../node_modules/sinon-chai/lib/sinon-chai.js';

View File

@ -1,6 +1,74 @@
import { isMac, isWindows, isIOS, isAndroid, isChromeOS, import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,
isGecko, isWebKit, isBlink } from '../core/util/browser.js'; isGecko, isWebKit, isBlink,
browserAsyncClipboardSupport } from '../core/util/browser.js';
describe('Async clipboard', function () {
"use strict";
beforeEach(function () {
sinon.stub(navigator, "clipboard").value({
writeText: sinon.stub(),
readText: sinon.stub(),
});
sinon.stub(navigator, "permissions").value({
query: sinon.stub().resolves({ state: "granted" })
});
});
afterEach(function () {
sinon.restore();
});
it("queries permissions with correct parameters", async function () {
const queryStub = navigator.permissions.query;
await browserAsyncClipboardSupport();
expect(queryStub.firstCall).to.have.been.calledWithExactly({
name: "clipboard-write",
allowWithoutGesture: true
});
expect(queryStub.secondCall).to.have.been.calledWithExactly({
name: "clipboard-read",
allowWithoutGesture: false
});
});
it("is available when API present and permissions granted", async function () {
navigator.permissions.query.resolves({ state: "granted" });
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('available');
});
it("is available when API present and permissions yield 'prompt'", async function () {
navigator.permissions.query.resolves({ state: "prompt" });
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('available');
});
it("is unavailable when permissions denied", async function () {
navigator.permissions.query.resolves({ state: "denied" });
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('denied');
});
it("is unavailable when permissions API fails", async function () {
navigator.permissions.query.rejects(new Error("fail"));
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('unsupported');
});
it("is unavailable when write text API missing", async function () {
navigator.clipboard.writeText = undefined;
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('unsupported');
});
it("is unavailable when read text API missing", async function () {
navigator.clipboard.readText = undefined;
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('unsupported');
});
});
describe('OS detection', function () { describe('OS detection', function () {
let origNavigator; let origNavigator;

121
tests/test.clipboard.js Normal file
View File

@ -0,0 +1,121 @@
import AsyncClipboard from '../core/clipboard.js';
describe('Async Clipboard', function () {
"use strict";
let targetMock;
let clipboard;
beforeEach(function () {
sinon.stub(navigator, "clipboard").value({
writeText: sinon.stub().resolves(),
readText: sinon.stub().resolves(),
});
sinon.stub(navigator, "permissions").value({
query: sinon.stub(),
});
targetMock = document.createElement("canvas");
clipboard = new AsyncClipboard(targetMock);
});
afterEach(function () {
sinon.restore();
targetMock = null;
clipboard = null;
});
function stubClipboardPermissions(state) {
navigator.permissions.query
.withArgs({ name: 'clipboard-write', allowWithoutGesture: true })
.resolves({ state: state });
navigator.permissions.query
.withArgs({ name: 'clipboard-read', allowWithoutGesture: false })
.resolves({ state: state });
}
function nextTick() {
return new Promise(resolve => setTimeout(resolve, 0));
}
it('grab() adds listener if permissions granted', async function () {
stubClipboardPermissions('granted');
const addListenerSpy = sinon.spy(targetMock, 'addEventListener');
clipboard.grab();
await nextTick();
expect(addListenerSpy.calledWith('focus')).to.be.true;
});
it('grab() does not add listener if permissions denied', async function () {
stubClipboardPermissions('denied');
const addListenerSpy = sinon.spy(targetMock, 'addEventListener');
clipboard.grab();
await nextTick();
expect(addListenerSpy.calledWith('focus')).to.be.false;
});
it('focus event triggers onpaste() if permissions granted', async function () {
stubClipboardPermissions('granted');
const text = 'hello clipboard world';
navigator.clipboard.readText.resolves(text);
const spyPromise = new Promise(resolve => clipboard.onpaste = resolve);
clipboard.grab();
await nextTick();
targetMock.dispatchEvent(new Event('focus'));
const res = await spyPromise;
expect(res).to.equal(text);
});
it('focus event does not trigger onpaste() if permissions denied', async function () {
stubClipboardPermissions('denied');
const text = 'should not read';
navigator.clipboard.readText.resolves(text);
clipboard.onpaste = sinon.spy();
clipboard.grab();
await nextTick();
targetMock.dispatchEvent(new Event('focus'));
expect(clipboard.onpaste.called).to.be.false;
});
it('writeClipboard() calls navigator.clipboard.writeText() if permissions granted', async function () {
stubClipboardPermissions('granted');
clipboard._isAvailable = true;
const text = 'writing to clipboard';
const result = clipboard.writeClipboard(text);
expect(navigator.clipboard.writeText.calledWith(text)).to.be.true;
expect(result).to.be.true;
});
it('writeClipboard() does not call navigator.clipboard.writeText() if permissions denied', async function () {
stubClipboardPermissions('denied');
clipboard._isAvailable = false;
const text = 'should not write';
const result = clipboard.writeClipboard(text);
expect(navigator.clipboard.writeText.called).to.be.false;
expect(result).to.be.false;
});
});

View File

@ -384,10 +384,11 @@ describe('Display/Canvas helper', function () {
}); });
it('should draw an image from an image object on type "img" (if complete)', function () { it('should draw an image from an image object on type "img" (if complete)', function () {
const img = { complete: true };
display.drawImage = sinon.spy(); display.drawImage = sinon.spy();
display._renderQPush({ type: 'img', x: 3, y: 4, img: { complete: true } }); display._renderQPush({ type: 'img', x: 3, y: 4, img: img });
expect(display.drawImage).to.have.been.calledOnce; expect(display.drawImage).to.have.been.calledOnce;
expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4); expect(display.drawImage).to.have.been.calledWith(img, 3, 4);
}); });
}); });
}); });

View File

@ -3467,17 +3467,48 @@ describe('Remote Frame Buffer protocol client', function () {
}); });
describe('Normal clipboard handling receive', function () { describe('Normal clipboard handling receive', function () {
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () { it('should not dispatch a clipboard event following successful async write clipboard', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(true),
};
const expectedStr = 'cheese!'; const expectedStr = 'cheese!';
const data = [3, 0, 0, 0]; const data = [3, 0, 0, 0];
push32(data, expectedStr.length); push32(data, expectedStr.length);
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); } for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
const spy = sinon.spy();
client.addEventListener("clipboard", spy); const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data)); client._sock._websocket._receiveData(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedStr); expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedStr
)).to.be.true;
expect(dispatchEventSpy.calledWith(
new CustomEvent("clipboard", {detail: {expectedStr: expectedStr}})
)).to.be.false;
});
it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
const expectedStr = 'cheese!';
const data = [3, 0, 0, 0];
push32(data, expectedStr.length);
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedStr
)).to.be.true;
expect(dispatchEventSpy.calledOnceWith(
new CustomEvent("clipboard", {detail: {expectedStr: expectedStr}})
)).to.be.true;
}); });
}); });
@ -3530,8 +3561,71 @@ describe('Remote Frame Buffer protocol client', function () {
client._sock._websocket._receiveData(new Uint8Array(data)); client._sock._websocket._receiveData(new Uint8Array(data));
}); });
it('should not dispatch a clipboard event following successful async write clipboard', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(true),
};
let expectedData = "Schnitzel";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
let text = encodeUTF8("Schnitzel");
let deflatedText = deflateWithSize(text);
// How much data we are sending.
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedData
)).to.be.true;
expect(dispatchEventSpy.calledOnceWith(
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
)).to.be.false;
});
it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
let expectedData = "Potatoes";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
let text = encodeUTF8("Potatoes");
let deflatedText = deflateWithSize(text);
// How much data we are sending.
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedData
)).to.be.true;
expect(dispatchEventSpy.calledOnceWith(
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
)).to.be.true;
});
describe('Handle Provide', function () { describe('Handle Provide', function () {
it('should update clipboard with correct Unicode data from a Provide message', function () { it('should update clipboard with correct Unicode data from a Provide message', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
let expectedData = "Aå漢字!"; let expectedData = "Aå漢字!";
let data = [3, 0, 0, 0]; let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01]; const flags = [0x10, 0x00, 0x00, 0x01];
@ -3545,16 +3639,23 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags); data = data.concat(flags);
data = data.concat(Array.from(deflatedText)); data = data.concat(Array.from(deflatedText));
const spy = sinon.spy(); const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client.addEventListener("clipboard", spy);
client._sock._websocket._receiveData(new Uint8Array(data)); client._sock._websocket._receiveData(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData); expect(client._asyncClipboard.writeClipboard.calledOnceWith(
client.removeEventListener("clipboard", spy); expectedData
)).to.be.true;
expect(dispatchEventSpy.calledOnceWith(
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
)).to.be.true;
}); });
it('should update clipboard with correct escape characters from a Provide message ', function () { it('should update clipboard with correct escape characters from a Provide message ', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
let expectedData = "Oh\nmy\n!"; let expectedData = "Oh\nmy\n!";
let data = [3, 0, 0, 0]; let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01]; const flags = [0x10, 0x00, 0x00, 0x01];
@ -3569,16 +3670,23 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags); data = data.concat(flags);
data = data.concat(Array.from(deflatedText)); data = data.concat(Array.from(deflatedText));
const spy = sinon.spy(); const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client.addEventListener("clipboard", spy);
client._sock._websocket._receiveData(new Uint8Array(data)); client._sock._websocket._receiveData(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData); expect(client._asyncClipboard.writeClipboard.calledOnceWith(
client.removeEventListener("clipboard", spy); expectedData
)).to.be.true;
expect(dispatchEventSpy.calledOnceWith(
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
)).to.be.true;
}); });
it('should be able to handle large Provide messages', function () { it('should be able to handle large Provide messages', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
let expectedData = "hello".repeat(100000); let expectedData = "hello".repeat(100000);
let data = [3, 0, 0, 0]; let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01]; const flags = [0x10, 0x00, 0x00, 0x01];
@ -3593,13 +3701,16 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags); data = data.concat(flags);
data = data.concat(Array.from(deflatedText)); data = data.concat(Array.from(deflatedText));
const spy = sinon.spy(); const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client.addEventListener("clipboard", spy);
client._sock._websocket._receiveData(new Uint8Array(data)); client._sock._websocket._receiveData(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData); expect(client._asyncClipboard.writeClipboard.calledOnceWith(
client.removeEventListener("clipboard", spy); expectedData
)).to.be.true;
expect(dispatchEventSpy.calledOnceWith(
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
)).to.be.true;
}); });
}); });

View File

@ -47,6 +47,20 @@ describe('Websock', function () {
}); });
}); });
describe('rQlen())', function () {
it('should return the number of buffered bytes in the receive queue', function () {
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
expect(sock.rQlen()).to.equal(8);
sock.rQshift8();
expect(sock.rQlen()).to.equal(7);
sock.rQshift16();
expect(sock.rQlen()).to.equal(5);
sock.rQshift32();
expect(sock.rQlen()).to.equal(1);
});
});
describe('rQshiftStr', function () { describe('rQshiftStr', function () {
it('should shift the given number of bytes off of the receive queue and return a string', function () { it('should shift the given number of bytes off of the receive queue and return a string', function () {
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34, websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,

View File

@ -1,140 +0,0 @@
#!/usr/bin/env node
const path = require('path');
const { program } = require('commander');
const fs = require('fs');
const fse = require('fs-extra');
const babel = require('@babel/core');
program
.option('-m, --with-source-maps [type]', 'output source maps when not generating a bundled app (type may be empty for external source maps, inline for inline source maps, or both) ')
.option('--clean', 'clear the lib folder before building')
.parse(process.argv);
// the various important paths
const paths = {
main: path.resolve(__dirname, '..'),
core: path.resolve(__dirname, '..', 'core'),
vendor: path.resolve(__dirname, '..', 'vendor'),
libDirBase: path.resolve(__dirname, '..', 'lib'),
};
// util.promisify requires Node.js 8.x, so we have our own
function promisify(original) {
return function promiseWrap() {
const args = Array.prototype.slice.call(arguments);
return new Promise((resolve, reject) => {
original.apply(this, args.concat((err, value) => {
if (err) return reject(err);
resolve(value);
}));
});
};
}
const writeFile = promisify(fs.writeFile);
const readdir = promisify(fs.readdir);
const lstat = promisify(fs.lstat);
const ensureDir = promisify(fse.ensureDir);
const babelTransformFile = promisify(babel.transformFile);
// walkDir *recursively* walks directories trees,
// calling the callback for all normal files found.
function walkDir(basePath, cb, filter) {
return readdir(basePath)
.then((files) => {
const paths = files.map(filename => path.join(basePath, filename));
return Promise.all(paths.map(filepath => lstat(filepath)
.then((stats) => {
if (filter !== undefined && !filter(filepath, stats)) return;
if (stats.isSymbolicLink()) return;
if (stats.isFile()) return cb(filepath);
if (stats.isDirectory()) return walkDir(filepath, cb, filter);
})));
});
}
function makeLibFiles(sourceMaps) {
// NB: we need to make a copy of babelOpts, since babel sets some defaults on it
const babelOpts = () => ({
plugins: [],
presets: [
[ '@babel/preset-env',
{ modules: 'commonjs' } ]
],
ast: false,
sourceMaps: sourceMaps,
});
fse.ensureDirSync(paths.libDirBase);
const outFiles = [];
const handleDir = (vendorRewrite, inPathBase, filename) => Promise.resolve()
.then(() => {
const outPath = path.join(paths.libDirBase, path.relative(inPathBase, filename));
if (path.extname(filename) !== '.js') {
return; // skip non-javascript files
}
return Promise.resolve()
.then(() => ensureDir(path.dirname(outPath)))
.then(() => {
const opts = babelOpts();
// Adjust for the fact that we move the core files relative
// to the vendor directory
if (vendorRewrite) {
opts.plugins.push(["import-redirect",
{"root": paths.libDirBase,
"redirect": { "vendor/(.+)": "./vendor/$1"}}]);
}
return babelTransformFile(filename, opts)
.then((res) => {
console.log(`Writing ${outPath}`);
const {map} = res;
let {code} = res;
if (sourceMaps === true) {
// append URL for external source map
code += `\n//# sourceMappingURL=${path.basename(outPath)}.map\n`;
}
outFiles.push(`${outPath}`);
return writeFile(outPath, code)
.then(() => {
if (sourceMaps === true || sourceMaps === 'both') {
console.log(` and ${outPath}.map`);
outFiles.push(`${outPath}.map`);
return writeFile(`${outPath}.map`, JSON.stringify(map));
}
});
});
});
});
Promise.resolve()
.then(() => {
const handler = handleDir.bind(null, false, paths.main);
return walkDir(paths.vendor, handler);
})
.then(() => {
const handler = handleDir.bind(null, true, paths.core);
return walkDir(paths.core, handler);
})
.catch((err) => {
console.error(`Failure converting modules: ${err}`);
process.exit(1);
});
}
let options = program.opts();
if (options.clean) {
console.log(`Removing ${paths.libDirBase}`);
fse.removeSync(paths.libDirBase);
}
makeLibFiles(options.withSourceMaps);

View File

@ -7,7 +7,7 @@
"use strict"; "use strict";
const fs = require('fs'); import fs from 'fs';
let showHelp = process.argv.length === 2; let showHelp = process.argv.length === 2;
let filename; let filename;

View File

@ -181,10 +181,8 @@ if [[ -d ${HERE}/websockify ]]; then
echo "Using local websockify at $WEBSOCKIFY" echo "Using local websockify at $WEBSOCKIFY"
else else
WEBSOCKIFY_FROMSYSTEM=$(which websockify 2>/dev/null) WEBSOCKIFY_FROMSYSTEM=$(type -P websockify 2>/dev/null)
WEBSOCKIFY_FROMSNAP=${HERE}/../usr/bin/python2-websockify
[ -f $WEBSOCKIFY_FROMSYSTEM ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSYSTEM [ -f $WEBSOCKIFY_FROMSYSTEM ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSYSTEM
[ -f $WEBSOCKIFY_FROMSNAP ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSNAP
if [ ! -f "$WEBSOCKIFY" ]; then if [ ! -f "$WEBSOCKIFY" ]; then
echo "No installed websockify, attempting to clone websockify..." echo "No installed websockify, attempting to clone websockify..."