ui.js 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795
  1. /*
  2. * noVNC: HTML5 VNC client
  3. * Copyright (C) 2019 The noVNC Authors
  4. * Licensed under MPL 2.0 (see LICENSE.txt)
  5. *
  6. * See README.md for usage and integration instructions.
  7. */
  8. import * as Log from '../core/util/logging.js';
  9. import _, { l10n } from './localization.js';
  10. import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
  11. hasScrollbarGutter, dragThreshold }
  12. from '../core/util/browser.js';
  13. import { setCapture, getPointerEvent } from '../core/util/events.js';
  14. import KeyTable from "../core/input/keysym.js";
  15. import keysyms from "../core/input/keysymdef.js";
  16. import Keyboard from "../core/input/keyboard.js";
  17. import RFB from "../core/rfb.js";
  18. import * as WebUtil from "./webutil.js";
  19. const PAGE_TITLE = "noVNC";
  20. // String validation
  21. function isAlphaNumeric(str) { return (str.match(/^[A-Za-z0-9]+$/) != null); };
  22. function isSafeString(str) { return ((typeof str == 'string') && (str.indexOf('<') == -1) && (str.indexOf('>') == -1) && (str.indexOf('&') == -1) && (str.indexOf('"') == -1) && (str.indexOf('\'') == -1) && (str.indexOf('+') == -1) && (str.indexOf('(') == -1) && (str.indexOf(')') == -1) && (str.indexOf('#') == -1) && (str.indexOf('%') == -1)) };
  23. // Parse URL arguments, only keep safe values
  24. function parseUriArgs() {
  25. var href = window.document.location.href;
  26. if (href.endsWith('#')) { href = href.substring(0, href.length - 1); }
  27. var name, r = {}, parsedUri = href.split(/[\?&|\=]/);
  28. parsedUri.splice(0, 1);
  29. for (x in parsedUri) {
  30. switch (x % 2) {
  31. case 0: { name = decodeURIComponent(parsedUri[x]); break; }
  32. case 1: {
  33. r[name] = decodeURIComponent(parsedUri[x]);
  34. if (!isSafeString(r[name])) { delete r[name]; } else { var x = parseInt(r[name]); if (x == r[name]) { r[name] = x; } }
  35. break;
  36. } default: { break; }
  37. }
  38. }
  39. return r;
  40. }
  41. var urlargs = parseUriArgs();
  42. const UI = {
  43. connected: false,
  44. desktopName: "",
  45. statusTimeout: null,
  46. hideKeyboardTimeout: null,
  47. idleControlbarTimeout: null,
  48. closeControlbarTimeout: null,
  49. controlbarGrabbed: false,
  50. controlbarDrag: false,
  51. controlbarMouseDownClientY: 0,
  52. controlbarMouseDownOffsetY: 0,
  53. lastKeyboardinput: null,
  54. defaultKeyboardinputLen: 100,
  55. inhibitReconnect: true,
  56. reconnectCallback: null,
  57. reconnectPassword: null,
  58. prime() {
  59. return WebUtil.initSettings().then(() => {
  60. if (document.readyState === "interactive" || document.readyState === "complete") {
  61. return UI.start();
  62. }
  63. return new Promise((resolve, reject) => {
  64. document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject));
  65. });
  66. });
  67. },
  68. // Render default UI and initialize settings menu
  69. start() {
  70. UI.initSettings();
  71. // Translate the DOM
  72. l10n.translateDOM();
  73. // We rely on modern APIs which might not be available in an
  74. // insecure context
  75. if (!window.isSecureContext) {
  76. // FIXME: This gets hidden when connecting
  77. UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error');
  78. }
  79. // Try to fetch version number
  80. fetch('./package.json')
  81. .then((response) => {
  82. if (!response.ok) {
  83. throw Error("" + response.status + " " + response.statusText);
  84. }
  85. return response.json();
  86. })
  87. .then((packageInfo) => {
  88. Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
  89. })
  90. .catch((err) => {
  91. Log.Error("Couldn't fetch package.json: " + err);
  92. Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
  93. .concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
  94. .forEach(el => el.style.display = 'none');
  95. });
  96. // Adapt the interface for touch screen devices
  97. if (isTouchDevice) {
  98. // Remove the address bar
  99. setTimeout(() => window.scrollTo(0, 1), 100);
  100. }
  101. // Restore control bar position
  102. if (WebUtil.readSetting('controlbar_pos') === 'right') {
  103. UI.toggleControlbarSide();
  104. }
  105. UI.initFullscreen();
  106. // Setup event handlers
  107. UI.addControlbarHandlers();
  108. UI.addTouchSpecificHandlers();
  109. UI.addExtraKeysHandlers();
  110. UI.addMachineHandlers();
  111. UI.addConnectionControlHandlers();
  112. UI.addClipboardHandlers();
  113. UI.addSettingsHandlers();
  114. document.getElementById("noVNC_status")
  115. .addEventListener('click', UI.hideStatus);
  116. // Bootstrap fallback input handler
  117. UI.keyboardinputReset();
  118. UI.openControlbar();
  119. UI.updateVisualState('init');
  120. document.documentElement.classList.remove("noVNC_loading");
  121. let autoconnect = WebUtil.getConfigVar('autoconnect', false);
  122. if (autoconnect === 'true' || autoconnect == '1') {
  123. autoconnect = true;
  124. UI.connect();
  125. } else {
  126. autoconnect = false;
  127. // Show the connect panel on first load unless autoconnecting
  128. UI.openConnectPanel();
  129. }
  130. return Promise.resolve(UI.rfb);
  131. },
  132. initFullscreen() {
  133. // Only show the button if fullscreen is properly supported
  134. // * Safari doesn't support alphanumerical input while in fullscreen
  135. if (!isSafari() &&
  136. (document.documentElement.requestFullscreen ||
  137. document.documentElement.mozRequestFullScreen ||
  138. document.documentElement.webkitRequestFullscreen ||
  139. document.body.msRequestFullscreen)) {
  140. document.getElementById('noVNC_fullscreen_button')
  141. .classList.remove("noVNC_hidden");
  142. UI.addFullscreenHandlers();
  143. }
  144. },
  145. initSettings() {
  146. // Logging selection dropdown
  147. const llevels = ['error', 'warn', 'info', 'debug'];
  148. for (let i = 0; i < llevels.length; i += 1) {
  149. UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]);
  150. }
  151. // Settings with immediate effects
  152. UI.initSetting('logging', 'warn');
  153. UI.updateLogging();
  154. // if port == 80 (or 443) then it won't be present and should be
  155. // set manually
  156. let port = window.location.port;
  157. if (!port) {
  158. if (window.location.protocol.substring(0, 5) == 'https') {
  159. port = 443;
  160. } else if (window.location.protocol.substring(0, 4) == 'http') {
  161. port = 80;
  162. }
  163. }
  164. /* Populate the controls if defaults are provided in the URL */
  165. UI.initSetting('host', window.location.hostname);
  166. UI.initSetting('port', port);
  167. UI.initSetting('encrypt', (window.location.protocol === "https:"));
  168. UI.initSetting('view_clip', false);
  169. UI.initSetting('resize', 'off');
  170. UI.initSetting('quality', 6);
  171. UI.initSetting('compression', 2);
  172. UI.initSetting('shared', true);
  173. UI.initSetting('view_only', false);
  174. UI.initSetting('show_dot', false);
  175. UI.initSetting('path', 'websockify');
  176. UI.initSetting('repeaterID', '');
  177. UI.initSetting('reconnect', false);
  178. UI.initSetting('reconnect_delay', 5000);
  179. UI.setupSettingLabels();
  180. },
  181. // Adds a link to the label elements on the corresponding input elements
  182. setupSettingLabels() {
  183. const labels = document.getElementsByTagName('LABEL');
  184. for (let i = 0; i < labels.length; i++) {
  185. const htmlFor = labels[i].htmlFor;
  186. if (htmlFor != '') {
  187. const elem = document.getElementById(htmlFor);
  188. if (elem) elem.label = labels[i];
  189. } else {
  190. // If 'for' isn't set, use the first input element child
  191. const children = labels[i].children;
  192. for (let j = 0; j < children.length; j++) {
  193. if (children[j].form !== undefined) {
  194. children[j].label = labels[i];
  195. break;
  196. }
  197. }
  198. }
  199. }
  200. },
  201. /* ------^-------
  202. * /INIT
  203. * ==============
  204. * EVENT HANDLERS
  205. * ------v------*/
  206. addControlbarHandlers() {
  207. document.getElementById("noVNC_control_bar")
  208. .addEventListener('mousemove', UI.activateControlbar);
  209. document.getElementById("noVNC_control_bar")
  210. .addEventListener('mouseup', UI.activateControlbar);
  211. document.getElementById("noVNC_control_bar")
  212. .addEventListener('mousedown', UI.activateControlbar);
  213. document.getElementById("noVNC_control_bar")
  214. .addEventListener('keydown', UI.activateControlbar);
  215. document.getElementById("noVNC_control_bar")
  216. .addEventListener('mousedown', UI.keepControlbar);
  217. document.getElementById("noVNC_control_bar")
  218. .addEventListener('keydown', UI.keepControlbar);
  219. document.getElementById("noVNC_view_drag_button")
  220. .addEventListener('click', UI.toggleViewDrag);
  221. document.getElementById("noVNC_control_bar_handle")
  222. .addEventListener('mousedown', UI.controlbarHandleMouseDown);
  223. document.getElementById("noVNC_control_bar_handle")
  224. .addEventListener('mouseup', UI.controlbarHandleMouseUp);
  225. document.getElementById("noVNC_control_bar_handle")
  226. .addEventListener('mousemove', UI.dragControlbarHandle);
  227. // resize events aren't available for elements
  228. window.addEventListener('resize', UI.updateControlbarHandle);
  229. const exps = document.getElementsByClassName("noVNC_expander");
  230. for (let i = 0;i < exps.length;i++) {
  231. exps[i].addEventListener('click', UI.toggleExpander);
  232. }
  233. },
  234. addTouchSpecificHandlers() {
  235. document.getElementById("noVNC_keyboard_button")
  236. .addEventListener('click', UI.toggleVirtualKeyboard);
  237. UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));
  238. UI.touchKeyboard.onkeyevent = UI.keyEvent;
  239. UI.touchKeyboard.grab();
  240. document.getElementById("noVNC_keyboardinput")
  241. .addEventListener('input', UI.keyInput);
  242. document.getElementById("noVNC_keyboardinput")
  243. .addEventListener('focus', UI.onfocusVirtualKeyboard);
  244. document.getElementById("noVNC_keyboardinput")
  245. .addEventListener('blur', UI.onblurVirtualKeyboard);
  246. document.getElementById("noVNC_keyboardinput")
  247. .addEventListener('submit', () => false);
  248. document.documentElement
  249. .addEventListener('mousedown', UI.keepVirtualKeyboard, true);
  250. document.getElementById("noVNC_control_bar")
  251. .addEventListener('touchstart', UI.activateControlbar);
  252. document.getElementById("noVNC_control_bar")
  253. .addEventListener('touchmove', UI.activateControlbar);
  254. document.getElementById("noVNC_control_bar")
  255. .addEventListener('touchend', UI.activateControlbar);
  256. document.getElementById("noVNC_control_bar")
  257. .addEventListener('input', UI.activateControlbar);
  258. document.getElementById("noVNC_control_bar")
  259. .addEventListener('touchstart', UI.keepControlbar);
  260. document.getElementById("noVNC_control_bar")
  261. .addEventListener('input', UI.keepControlbar);
  262. document.getElementById("noVNC_control_bar_handle")
  263. .addEventListener('touchstart', UI.controlbarHandleMouseDown);
  264. document.getElementById("noVNC_control_bar_handle")
  265. .addEventListener('touchend', UI.controlbarHandleMouseUp);
  266. document.getElementById("noVNC_control_bar_handle")
  267. .addEventListener('touchmove', UI.dragControlbarHandle);
  268. },
  269. addExtraKeysHandlers() {
  270. document.getElementById("noVNC_toggle_extra_keys_button")
  271. .addEventListener('click', UI.toggleExtraKeys);
  272. document.getElementById("noVNC_toggle_ctrl_button")
  273. .addEventListener('click', UI.toggleCtrl);
  274. document.getElementById("noVNC_toggle_windows_button")
  275. .addEventListener('click', UI.toggleWindows);
  276. document.getElementById("noVNC_toggle_alt_button")
  277. .addEventListener('click', UI.toggleAlt);
  278. document.getElementById("noVNC_send_tab_button")
  279. .addEventListener('click', UI.sendTab);
  280. document.getElementById("noVNC_send_esc_button")
  281. .addEventListener('click', UI.sendEsc);
  282. document.getElementById("noVNC_send_ctrl_alt_del_button")
  283. .addEventListener('click', UI.sendCtrlAltDel);
  284. },
  285. addMachineHandlers() {
  286. document.getElementById("noVNC_shutdown_button")
  287. .addEventListener('click', () => UI.rfb.machineShutdown());
  288. document.getElementById("noVNC_reboot_button")
  289. .addEventListener('click', () => UI.rfb.machineReboot());
  290. document.getElementById("noVNC_reset_button")
  291. .addEventListener('click', () => UI.rfb.machineReset());
  292. document.getElementById("noVNC_power_button")
  293. .addEventListener('click', UI.togglePowerPanel);
  294. },
  295. addConnectionControlHandlers() {
  296. document.getElementById("noVNC_disconnect_button")
  297. .addEventListener('click', UI.disconnect);
  298. document.getElementById("noVNC_connect_button")
  299. .addEventListener('click', UI.connect);
  300. document.getElementById("noVNC_cancel_reconnect_button")
  301. .addEventListener('click', UI.cancelReconnect);
  302. document.getElementById("noVNC_approve_server_button")
  303. .addEventListener('click', UI.approveServer);
  304. document.getElementById("noVNC_reject_server_button")
  305. .addEventListener('click', UI.rejectServer);
  306. document.getElementById("noVNC_credentials_button")
  307. .addEventListener('click', UI.setCredentials);
  308. },
  309. addClipboardHandlers() {
  310. document.getElementById("noVNC_clipboard_button")
  311. .addEventListener('click', UI.toggleClipboardPanel);
  312. document.getElementById("noVNC_clipboard_text")
  313. .addEventListener('change', UI.clipboardSend);
  314. },
  315. // Add a call to save settings when the element changes,
  316. // unless the optional parameter changeFunc is used instead.
  317. addSettingChangeHandler(name, changeFunc) {
  318. const settingElem = document.getElementById("noVNC_setting_" + name);
  319. if (changeFunc === undefined) {
  320. changeFunc = () => UI.saveSetting(name);
  321. }
  322. settingElem.addEventListener('change', changeFunc);
  323. },
  324. addSettingsHandlers() {
  325. document.getElementById("noVNC_settings_button")
  326. .addEventListener('click', UI.toggleSettingsPanel);
  327. UI.addSettingChangeHandler('encrypt');
  328. UI.addSettingChangeHandler('resize');
  329. UI.addSettingChangeHandler('resize', UI.applyResizeMode);
  330. UI.addSettingChangeHandler('resize', UI.updateViewClip);
  331. UI.addSettingChangeHandler('quality');
  332. UI.addSettingChangeHandler('quality', UI.updateQuality);
  333. UI.addSettingChangeHandler('compression');
  334. UI.addSettingChangeHandler('compression', UI.updateCompression);
  335. UI.addSettingChangeHandler('view_clip');
  336. UI.addSettingChangeHandler('view_clip', UI.updateViewClip);
  337. UI.addSettingChangeHandler('shared');
  338. UI.addSettingChangeHandler('view_only');
  339. UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
  340. UI.addSettingChangeHandler('show_dot');
  341. UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
  342. UI.addSettingChangeHandler('host');
  343. UI.addSettingChangeHandler('port');
  344. UI.addSettingChangeHandler('path');
  345. UI.addSettingChangeHandler('repeaterID');
  346. UI.addSettingChangeHandler('logging');
  347. UI.addSettingChangeHandler('logging', UI.updateLogging);
  348. UI.addSettingChangeHandler('reconnect');
  349. UI.addSettingChangeHandler('reconnect_delay');
  350. },
  351. addFullscreenHandlers() {
  352. document.getElementById("noVNC_fullscreen_button")
  353. .addEventListener('click', UI.toggleFullscreen);
  354. window.addEventListener('fullscreenchange', UI.updateFullscreenButton);
  355. window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton);
  356. window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton);
  357. window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
  358. },
  359. /* ------^-------
  360. * /EVENT HANDLERS
  361. * ==============
  362. * VISUAL
  363. * ------v------*/
  364. // Disable/enable controls depending on connection state
  365. updateVisualState(state) {
  366. document.documentElement.classList.remove("noVNC_connecting");
  367. document.documentElement.classList.remove("noVNC_connected");
  368. document.documentElement.classList.remove("noVNC_disconnecting");
  369. document.documentElement.classList.remove("noVNC_reconnecting");
  370. const transitionElem = document.getElementById("noVNC_transition_text");
  371. switch (state) {
  372. case 'init':
  373. break;
  374. case 'connecting':
  375. transitionElem.textContent = _("Connecting...");
  376. document.documentElement.classList.add("noVNC_connecting");
  377. break;
  378. case 'connected':
  379. document.documentElement.classList.add("noVNC_connected");
  380. break;
  381. case 'disconnecting':
  382. transitionElem.textContent = _("Disconnecting...");
  383. document.documentElement.classList.add("noVNC_disconnecting");
  384. break;
  385. case 'disconnected':
  386. break;
  387. case 'reconnecting':
  388. transitionElem.textContent = _("Reconnecting...");
  389. document.documentElement.classList.add("noVNC_reconnecting");
  390. break;
  391. default:
  392. Log.Error("Invalid visual state: " + state);
  393. UI.showStatus(_("Internal error"), 'error');
  394. return;
  395. }
  396. if (UI.connected) {
  397. UI.updateViewClip();
  398. UI.disableSetting('encrypt');
  399. UI.disableSetting('shared');
  400. UI.disableSetting('host');
  401. UI.disableSetting('port');
  402. UI.disableSetting('path');
  403. UI.disableSetting('repeaterID');
  404. // Hide the controlbar after 2 seconds
  405. UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
  406. } else {
  407. UI.enableSetting('encrypt');
  408. UI.enableSetting('shared');
  409. UI.enableSetting('host');
  410. UI.enableSetting('port');
  411. UI.enableSetting('path');
  412. UI.enableSetting('repeaterID');
  413. UI.updatePowerButton();
  414. UI.keepControlbar();
  415. }
  416. // State change closes dialogs as they may not be relevant
  417. // anymore
  418. UI.closeAllPanels();
  419. document.getElementById('noVNC_verify_server_dlg')
  420. .classList.remove('noVNC_open');
  421. document.getElementById('noVNC_credentials_dlg')
  422. .classList.remove('noVNC_open');
  423. },
  424. showStatus(text, statusType, time) {
  425. const statusElem = document.getElementById('noVNC_status');
  426. if (typeof statusType === 'undefined') {
  427. statusType = 'normal';
  428. }
  429. // Don't overwrite more severe visible statuses and never
  430. // errors. Only shows the first error.
  431. if (statusElem.classList.contains("noVNC_open")) {
  432. if (statusElem.classList.contains("noVNC_status_error")) {
  433. return;
  434. }
  435. if (statusElem.classList.contains("noVNC_status_warn") &&
  436. statusType === 'normal') {
  437. return;
  438. }
  439. }
  440. clearTimeout(UI.statusTimeout);
  441. switch (statusType) {
  442. case 'error':
  443. statusElem.classList.remove("noVNC_status_warn");
  444. statusElem.classList.remove("noVNC_status_normal");
  445. statusElem.classList.add("noVNC_status_error");
  446. break;
  447. case 'warning':
  448. case 'warn':
  449. statusElem.classList.remove("noVNC_status_error");
  450. statusElem.classList.remove("noVNC_status_normal");
  451. statusElem.classList.add("noVNC_status_warn");
  452. break;
  453. case 'normal':
  454. case 'info':
  455. default:
  456. statusElem.classList.remove("noVNC_status_error");
  457. statusElem.classList.remove("noVNC_status_warn");
  458. statusElem.classList.add("noVNC_status_normal");
  459. break;
  460. }
  461. statusElem.textContent = text;
  462. statusElem.classList.add("noVNC_open");
  463. // If no time was specified, show the status for 1.5 seconds
  464. if (typeof time === 'undefined') {
  465. time = 1500;
  466. }
  467. // Error messages do not timeout
  468. if (statusType !== 'error') {
  469. UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
  470. }
  471. },
  472. hideStatus() {
  473. clearTimeout(UI.statusTimeout);
  474. document.getElementById('noVNC_status').classList.remove("noVNC_open");
  475. },
  476. activateControlbar(event) {
  477. clearTimeout(UI.idleControlbarTimeout);
  478. // We manipulate the anchor instead of the actual control
  479. // bar in order to avoid creating new a stacking group
  480. document.getElementById('noVNC_control_bar_anchor')
  481. .classList.remove("noVNC_idle");
  482. UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000);
  483. },
  484. idleControlbar() {
  485. // Don't fade if a child of the control bar has focus
  486. if (document.getElementById('noVNC_control_bar')
  487. .contains(document.activeElement) && document.hasFocus()) {
  488. UI.activateControlbar();
  489. return;
  490. }
  491. document.getElementById('noVNC_control_bar_anchor')
  492. .classList.add("noVNC_idle");
  493. },
  494. keepControlbar() {
  495. clearTimeout(UI.closeControlbarTimeout);
  496. },
  497. openControlbar() {
  498. document.getElementById('noVNC_control_bar')
  499. .classList.add("noVNC_open");
  500. },
  501. closeControlbar() {
  502. UI.closeAllPanels();
  503. document.getElementById('noVNC_control_bar')
  504. .classList.remove("noVNC_open");
  505. UI.rfb.focus();
  506. },
  507. toggleControlbar() {
  508. if (document.getElementById('noVNC_control_bar')
  509. .classList.contains("noVNC_open")) {
  510. UI.closeControlbar();
  511. } else {
  512. UI.openControlbar();
  513. }
  514. },
  515. toggleControlbarSide() {
  516. // Temporarily disable animation, if bar is displayed, to avoid weird
  517. // movement. The transitionend-event will not fire when display=none.
  518. const bar = document.getElementById('noVNC_control_bar');
  519. const barDisplayStyle = window.getComputedStyle(bar).display;
  520. if (barDisplayStyle !== 'none') {
  521. bar.style.transitionDuration = '0s';
  522. bar.addEventListener('transitionend', () => bar.style.transitionDuration = '');
  523. }
  524. const anchor = document.getElementById('noVNC_control_bar_anchor');
  525. if (anchor.classList.contains("noVNC_right")) {
  526. WebUtil.writeSetting('controlbar_pos', 'left');
  527. anchor.classList.remove("noVNC_right");
  528. } else {
  529. WebUtil.writeSetting('controlbar_pos', 'right');
  530. anchor.classList.add("noVNC_right");
  531. }
  532. // Consider this a movement of the handle
  533. UI.controlbarDrag = true;
  534. // The user has "followed" hint, let's hide it until the next drag
  535. UI.showControlbarHint(false, false);
  536. },
  537. showControlbarHint(show, animate=true) {
  538. const hint = document.getElementById('noVNC_control_bar_hint');
  539. if (animate) {
  540. hint.classList.remove("noVNC_notransition");
  541. } else {
  542. hint.classList.add("noVNC_notransition");
  543. }
  544. if (show) {
  545. hint.classList.add("noVNC_active");
  546. } else {
  547. hint.classList.remove("noVNC_active");
  548. }
  549. },
  550. dragControlbarHandle(e) {
  551. if (!UI.controlbarGrabbed) return;
  552. const ptr = getPointerEvent(e);
  553. const anchor = document.getElementById('noVNC_control_bar_anchor');
  554. if (ptr.clientX < (window.innerWidth * 0.1)) {
  555. if (anchor.classList.contains("noVNC_right")) {
  556. UI.toggleControlbarSide();
  557. }
  558. } else if (ptr.clientX > (window.innerWidth * 0.9)) {
  559. if (!anchor.classList.contains("noVNC_right")) {
  560. UI.toggleControlbarSide();
  561. }
  562. }
  563. if (!UI.controlbarDrag) {
  564. const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
  565. if (dragDistance < dragThreshold) return;
  566. UI.controlbarDrag = true;
  567. }
  568. const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
  569. UI.moveControlbarHandle(eventY);
  570. e.preventDefault();
  571. e.stopPropagation();
  572. UI.keepControlbar();
  573. UI.activateControlbar();
  574. },
  575. // Move the handle but don't allow any position outside the bounds
  576. moveControlbarHandle(viewportRelativeY) {
  577. const handle = document.getElementById("noVNC_control_bar_handle");
  578. const handleHeight = handle.getBoundingClientRect().height;
  579. const controlbarBounds = document.getElementById("noVNC_control_bar")
  580. .getBoundingClientRect();
  581. const margin = 10;
  582. // These heights need to be non-zero for the below logic to work
  583. if (handleHeight === 0 || controlbarBounds.height === 0) {
  584. return;
  585. }
  586. let newY = viewportRelativeY;
  587. // Check if the coordinates are outside the control bar
  588. if (newY < controlbarBounds.top + margin) {
  589. // Force coordinates to be below the top of the control bar
  590. newY = controlbarBounds.top + margin;
  591. } else if (newY > controlbarBounds.top +
  592. controlbarBounds.height - handleHeight - margin) {
  593. // Force coordinates to be above the bottom of the control bar
  594. newY = controlbarBounds.top +
  595. controlbarBounds.height - handleHeight - margin;
  596. }
  597. // Corner case: control bar too small for stable position
  598. if (controlbarBounds.height < (handleHeight + margin * 2)) {
  599. newY = controlbarBounds.top +
  600. (controlbarBounds.height - handleHeight) / 2;
  601. }
  602. // The transform needs coordinates that are relative to the parent
  603. const parentRelativeY = newY - controlbarBounds.top;
  604. handle.style.transform = "translateY(" + parentRelativeY + "px)";
  605. },
  606. updateControlbarHandle() {
  607. // Since the control bar is fixed on the viewport and not the page,
  608. // the move function expects coordinates relative the the viewport.
  609. const handle = document.getElementById("noVNC_control_bar_handle");
  610. const handleBounds = handle.getBoundingClientRect();
  611. UI.moveControlbarHandle(handleBounds.top);
  612. },
  613. controlbarHandleMouseUp(e) {
  614. if ((e.type == "mouseup") && (e.button != 0)) return;
  615. // mouseup and mousedown on the same place toggles the controlbar
  616. if (UI.controlbarGrabbed && !UI.controlbarDrag) {
  617. UI.toggleControlbar();
  618. e.preventDefault();
  619. e.stopPropagation();
  620. UI.keepControlbar();
  621. UI.activateControlbar();
  622. }
  623. UI.controlbarGrabbed = false;
  624. UI.showControlbarHint(false);
  625. },
  626. controlbarHandleMouseDown(e) {
  627. if ((e.type == "mousedown") && (e.button != 0)) return;
  628. const ptr = getPointerEvent(e);
  629. const handle = document.getElementById("noVNC_control_bar_handle");
  630. const bounds = handle.getBoundingClientRect();
  631. // Touch events have implicit capture
  632. if (e.type === "mousedown") {
  633. setCapture(handle);
  634. }
  635. UI.controlbarGrabbed = true;
  636. UI.controlbarDrag = false;
  637. UI.showControlbarHint(true);
  638. UI.controlbarMouseDownClientY = ptr.clientY;
  639. UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
  640. e.preventDefault();
  641. e.stopPropagation();
  642. UI.keepControlbar();
  643. UI.activateControlbar();
  644. },
  645. toggleExpander(e) {
  646. if (this.classList.contains("noVNC_open")) {
  647. this.classList.remove("noVNC_open");
  648. } else {
  649. this.classList.add("noVNC_open");
  650. }
  651. },
  652. /* ------^-------
  653. * /VISUAL
  654. * ==============
  655. * SETTINGS
  656. * ------v------*/
  657. // Initial page load read/initialization of settings
  658. initSetting(name, defVal) {
  659. // Check Query string followed by cookie
  660. let val = WebUtil.getConfigVar(name);
  661. if (val === null) {
  662. val = WebUtil.readSetting(name, defVal);
  663. }
  664. WebUtil.setSetting(name, val);
  665. UI.updateSetting(name);
  666. return val;
  667. },
  668. // Set the new value, update and disable form control setting
  669. forceSetting(name, val) {
  670. WebUtil.setSetting(name, val);
  671. UI.updateSetting(name);
  672. UI.disableSetting(name);
  673. },
  674. // Update cookie and form control setting. If value is not set, then
  675. // updates from control to current cookie setting.
  676. updateSetting(name) {
  677. // Update the settings control
  678. let value = UI.getSetting(name);
  679. const ctrl = document.getElementById('noVNC_setting_' + name);
  680. if (ctrl.type === 'checkbox') {
  681. ctrl.checked = value;
  682. } else if (typeof ctrl.options !== 'undefined') {
  683. for (let i = 0; i < ctrl.options.length; i += 1) {
  684. if (ctrl.options[i].value === value) {
  685. ctrl.selectedIndex = i;
  686. break;
  687. }
  688. }
  689. } else {
  690. ctrl.value = value;
  691. }
  692. },
  693. // Save control setting to cookie
  694. saveSetting(name) {
  695. const ctrl = document.getElementById('noVNC_setting_' + name);
  696. let val;
  697. if (ctrl.type === 'checkbox') {
  698. val = ctrl.checked;
  699. } else if (typeof ctrl.options !== 'undefined') {
  700. val = ctrl.options[ctrl.selectedIndex].value;
  701. } else {
  702. val = ctrl.value;
  703. }
  704. WebUtil.writeSetting(name, val);
  705. //Log.Debug("Setting saved '" + name + "=" + val + "'");
  706. return val;
  707. },
  708. // Read form control compatible setting from cookie
  709. getSetting(name) {
  710. const ctrl = document.getElementById('noVNC_setting_' + name);
  711. let val = WebUtil.readSetting(name);
  712. if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') {
  713. if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {
  714. val = false;
  715. } else {
  716. val = true;
  717. }
  718. }
  719. return val;
  720. },
  721. // These helpers compensate for the lack of parent-selectors and
  722. // previous-sibling-selectors in CSS which are needed when we want to
  723. // disable the labels that belong to disabled input elements.
  724. disableSetting(name) {
  725. const ctrl = document.getElementById('noVNC_setting_' + name);
  726. ctrl.disabled = true;
  727. ctrl.label.classList.add('noVNC_disabled');
  728. },
  729. enableSetting(name) {
  730. const ctrl = document.getElementById('noVNC_setting_' + name);
  731. ctrl.disabled = false;
  732. ctrl.label.classList.remove('noVNC_disabled');
  733. },
  734. /* ------^-------
  735. * /SETTINGS
  736. * ==============
  737. * PANELS
  738. * ------v------*/
  739. closeAllPanels() {
  740. UI.closeSettingsPanel();
  741. UI.closePowerPanel();
  742. UI.closeClipboardPanel();
  743. UI.closeExtraKeys();
  744. },
  745. /* ------^-------
  746. * /PANELS
  747. * ==============
  748. * SETTINGS (panel)
  749. * ------v------*/
  750. openSettingsPanel() {
  751. UI.closeAllPanels();
  752. UI.openControlbar();
  753. // Refresh UI elements from saved cookies
  754. UI.updateSetting('encrypt');
  755. UI.updateSetting('view_clip');
  756. UI.updateSetting('resize');
  757. UI.updateSetting('quality');
  758. UI.updateSetting('compression');
  759. UI.updateSetting('shared');
  760. UI.updateSetting('view_only');
  761. UI.updateSetting('path');
  762. UI.updateSetting('repeaterID');
  763. UI.updateSetting('logging');
  764. UI.updateSetting('reconnect');
  765. UI.updateSetting('reconnect_delay');
  766. document.getElementById('noVNC_settings')
  767. .classList.add("noVNC_open");
  768. document.getElementById('noVNC_settings_button')
  769. .classList.add("noVNC_selected");
  770. },
  771. closeSettingsPanel() {
  772. document.getElementById('noVNC_settings')
  773. .classList.remove("noVNC_open");
  774. document.getElementById('noVNC_settings_button')
  775. .classList.remove("noVNC_selected");
  776. },
  777. toggleSettingsPanel() {
  778. if (document.getElementById('noVNC_settings')
  779. .classList.contains("noVNC_open")) {
  780. UI.closeSettingsPanel();
  781. } else {
  782. UI.openSettingsPanel();
  783. }
  784. },
  785. /* ------^-------
  786. * /SETTINGS
  787. * ==============
  788. * POWER
  789. * ------v------*/
  790. openPowerPanel() {
  791. UI.closeAllPanels();
  792. UI.openControlbar();
  793. document.getElementById('noVNC_power')
  794. .classList.add("noVNC_open");
  795. document.getElementById('noVNC_power_button')
  796. .classList.add("noVNC_selected");
  797. },
  798. closePowerPanel() {
  799. document.getElementById('noVNC_power')
  800. .classList.remove("noVNC_open");
  801. document.getElementById('noVNC_power_button')
  802. .classList.remove("noVNC_selected");
  803. },
  804. togglePowerPanel() {
  805. if (document.getElementById('noVNC_power')
  806. .classList.contains("noVNC_open")) {
  807. UI.closePowerPanel();
  808. } else {
  809. UI.openPowerPanel();
  810. }
  811. },
  812. // Disable/enable power button
  813. updatePowerButton() {
  814. if (UI.connected &&
  815. UI.rfb.capabilities.power &&
  816. !UI.rfb.viewOnly) {
  817. document.getElementById('noVNC_power_button')
  818. .classList.remove("noVNC_hidden");
  819. } else {
  820. document.getElementById('noVNC_power_button')
  821. .classList.add("noVNC_hidden");
  822. // Close power panel if open
  823. UI.closePowerPanel();
  824. }
  825. },
  826. /* ------^-------
  827. * /POWER
  828. * ==============
  829. * CLIPBOARD
  830. * ------v------*/
  831. openClipboardPanel() {
  832. UI.closeAllPanels();
  833. UI.openControlbar();
  834. document.getElementById('noVNC_clipboard')
  835. .classList.add("noVNC_open");
  836. document.getElementById('noVNC_clipboard_button')
  837. .classList.add("noVNC_selected");
  838. },
  839. closeClipboardPanel() {
  840. document.getElementById('noVNC_clipboard')
  841. .classList.remove("noVNC_open");
  842. document.getElementById('noVNC_clipboard_button')
  843. .classList.remove("noVNC_selected");
  844. },
  845. toggleClipboardPanel() {
  846. if (document.getElementById('noVNC_clipboard')
  847. .classList.contains("noVNC_open")) {
  848. UI.closeClipboardPanel();
  849. } else {
  850. UI.openClipboardPanel();
  851. }
  852. },
  853. clipboardReceive(e) {
  854. Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "...");
  855. document.getElementById('noVNC_clipboard_text').value = e.detail.text;
  856. Log.Debug("<< UI.clipboardReceive");
  857. },
  858. clipboardSend() {
  859. const text = document.getElementById('noVNC_clipboard_text').value;
  860. Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "...");
  861. UI.rfb.clipboardPasteFrom(text);
  862. Log.Debug("<< UI.clipboardSend");
  863. },
  864. /* ------^-------
  865. * /CLIPBOARD
  866. * ==============
  867. * CONNECTION
  868. * ------v------*/
  869. openConnectPanel() {
  870. document.getElementById('noVNC_connect_dlg')
  871. .classList.add("noVNC_open");
  872. },
  873. closeConnectPanel() {
  874. document.getElementById('noVNC_connect_dlg')
  875. .classList.remove("noVNC_open");
  876. },
  877. connect(event, password) {
  878. // Ignore when rfb already exists
  879. if (typeof UI.rfb !== 'undefined') {
  880. return;
  881. }
  882. const host = UI.getSetting('host');
  883. const port = UI.getSetting('port');
  884. const path = UI.getSetting('path');
  885. if (typeof password === 'undefined') {
  886. password = WebUtil.getConfigVar('password');
  887. UI.reconnectPassword = password;
  888. }
  889. if (password === null) {
  890. password = undefined;
  891. }
  892. UI.hideStatus();
  893. if (!host) {
  894. Log.Error("Can't connect when host is: " + host);
  895. UI.showStatus(_("Must set host"), 'error');
  896. return;
  897. }
  898. UI.closeConnectPanel();
  899. UI.updateVisualState('connecting');
  900. try {
  901. UI.rfb = new RFB(document.getElementById('noVNC_container'), urlargs.ws,
  902. { shared: UI.getSetting('shared'),
  903. repeaterID: UI.getSetting('repeaterID'),
  904. credentials: { password: password } });
  905. } catch (exc) {
  906. Log.Error("Failed to connect to server: " + exc);
  907. UI.updateVisualState('disconnected');
  908. UI.showStatus(_("Failed to connect to server: ") + exc, 'error');
  909. return;
  910. }
  911. UI.rfb.addEventListener("connect", UI.connectFinished);
  912. UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
  913. UI.rfb.addEventListener("serververification", UI.serverVerify);
  914. UI.rfb.addEventListener("credentialsrequired", UI.credentials);
  915. UI.rfb.addEventListener("securityfailure", UI.securityFailed);
  916. UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag);
  917. UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
  918. UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
  919. UI.rfb.addEventListener("bell", UI.bell);
  920. UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
  921. UI.rfb.clipViewport = UI.getSetting('view_clip');
  922. UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
  923. UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
  924. UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
  925. UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
  926. UI.rfb.showDotCursor = UI.getSetting('show_dot');
  927. UI.updateViewOnly(); // requires UI.rfb
  928. },
  929. disconnect() {
  930. UI.rfb.disconnect();
  931. UI.connected = false;
  932. // Disable automatic reconnecting
  933. UI.inhibitReconnect = true;
  934. UI.updateVisualState('disconnecting');
  935. // Don't display the connection settings until we're actually disconnected
  936. },
  937. reconnect() {
  938. UI.reconnectCallback = null;
  939. // if reconnect has been disabled in the meantime, do nothing.
  940. if (UI.inhibitReconnect) {
  941. return;
  942. }
  943. UI.connect(null, UI.reconnectPassword);
  944. },
  945. cancelReconnect() {
  946. if (UI.reconnectCallback !== null) {
  947. clearTimeout(UI.reconnectCallback);
  948. UI.reconnectCallback = null;
  949. }
  950. UI.updateVisualState('disconnected');
  951. UI.openControlbar();
  952. UI.openConnectPanel();
  953. },
  954. connectFinished(e) {
  955. UI.connected = true;
  956. UI.inhibitReconnect = false;
  957. let msg;
  958. if (UI.getSetting('encrypt')) {
  959. msg = _("Connected (encrypted) to ") + UI.desktopName;
  960. } else {
  961. msg = _("Connected (unencrypted) to ") + UI.desktopName;
  962. }
  963. UI.showStatus(msg);
  964. UI.updateVisualState('connected');
  965. // Do this last because it can only be used on rendered elements
  966. UI.rfb.focus();
  967. },
  968. disconnectFinished(e) {
  969. const wasConnected = UI.connected;
  970. // This variable is ideally set when disconnection starts, but
  971. // when the disconnection isn't clean or if it is initiated by
  972. // the server, we need to do it here as well since
  973. // UI.disconnect() won't be used in those cases.
  974. UI.connected = false;
  975. UI.rfb = undefined;
  976. if (!e.detail.clean) {
  977. UI.updateVisualState('disconnected');
  978. if (wasConnected) {
  979. UI.showStatus(_("Something went wrong, connection is closed"),
  980. 'error');
  981. } else {
  982. UI.showStatus(_("Failed to connect to server"), 'error');
  983. }
  984. }
  985. // If reconnecting is allowed process it now
  986. if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
  987. UI.updateVisualState('reconnecting');
  988. const delay = parseInt(UI.getSetting('reconnect_delay'));
  989. UI.reconnectCallback = setTimeout(UI.reconnect, delay);
  990. return;
  991. } else {
  992. UI.updateVisualState('disconnected');
  993. UI.showStatus(_("Disconnected"), 'normal');
  994. }
  995. document.title = PAGE_TITLE;
  996. UI.openControlbar();
  997. UI.openConnectPanel();
  998. },
  999. securityFailed(e) {
  1000. let msg = "";
  1001. // On security failures we might get a string with a reason
  1002. // directly from the server. Note that we can't control if
  1003. // this string is translated or not.
  1004. if ('reason' in e.detail) {
  1005. msg = _("New connection has been rejected with reason: ") +
  1006. e.detail.reason;
  1007. } else {
  1008. msg = _("New connection has been rejected");
  1009. }
  1010. UI.showStatus(msg, 'error');
  1011. },
  1012. /* ------^-------
  1013. * /CONNECTION
  1014. * ==============
  1015. * SERVER VERIFY
  1016. * ------v------*/
  1017. async serverVerify(e) {
  1018. const type = e.detail.type;
  1019. if (type === 'RSA') {
  1020. const publickey = e.detail.publickey;
  1021. let fingerprint = await window.crypto.subtle.digest("SHA-1", publickey);
  1022. // The same fingerprint format as RealVNC
  1023. fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map(
  1024. x => x.toString(16).padStart(2, '0')).join('-');
  1025. document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open');
  1026. document.getElementById('noVNC_fingerprint').innerHTML = fingerprint;
  1027. }
  1028. },
  1029. approveServer(e) {
  1030. e.preventDefault();
  1031. document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
  1032. UI.rfb.approveServer();
  1033. },
  1034. rejectServer(e) {
  1035. e.preventDefault();
  1036. document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
  1037. UI.disconnect();
  1038. },
  1039. /* ------^-------
  1040. * /SERVER VERIFY
  1041. * ==============
  1042. * PASSWORD
  1043. * ------v------*/
  1044. credentials(e) {
  1045. // FIXME: handle more types
  1046. document.getElementById("noVNC_username_block").classList.remove("noVNC_hidden");
  1047. document.getElementById("noVNC_password_block").classList.remove("noVNC_hidden");
  1048. let inputFocus = "none";
  1049. if (e.detail.types.indexOf("username") === -1) {
  1050. document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
  1051. } else {
  1052. inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
  1053. }
  1054. if (e.detail.types.indexOf("password") === -1) {
  1055. document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
  1056. } else {
  1057. inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
  1058. }
  1059. document.getElementById('noVNC_credentials_dlg')
  1060. .classList.add('noVNC_open');
  1061. setTimeout(() => document
  1062. .getElementById(inputFocus).focus(), 100);
  1063. Log.Warn("Server asked for credentials");
  1064. UI.showStatus(_("Credentials are required"), "warning");
  1065. },
  1066. setCredentials(e) {
  1067. // Prevent actually submitting the form
  1068. e.preventDefault();
  1069. let inputElemUsername = document.getElementById('noVNC_username_input');
  1070. const username = inputElemUsername.value;
  1071. let inputElemPassword = document.getElementById('noVNC_password_input');
  1072. const password = inputElemPassword.value;
  1073. // Clear the input after reading the password
  1074. inputElemPassword.value = "";
  1075. UI.rfb.sendCredentials({ username: username, password: password });
  1076. UI.reconnectPassword = password;
  1077. document.getElementById('noVNC_credentials_dlg')
  1078. .classList.remove('noVNC_open');
  1079. },
  1080. /* ------^-------
  1081. * /PASSWORD
  1082. * ==============
  1083. * FULLSCREEN
  1084. * ------v------*/
  1085. toggleFullscreen() {
  1086. if (document.fullscreenElement || // alternative standard method
  1087. document.mozFullScreenElement || // currently working methods
  1088. document.webkitFullscreenElement ||
  1089. document.msFullscreenElement) {
  1090. if (document.exitFullscreen) {
  1091. document.exitFullscreen();
  1092. } else if (document.mozCancelFullScreen) {
  1093. document.mozCancelFullScreen();
  1094. } else if (document.webkitExitFullscreen) {
  1095. document.webkitExitFullscreen();
  1096. } else if (document.msExitFullscreen) {
  1097. document.msExitFullscreen();
  1098. }
  1099. } else {
  1100. if (document.documentElement.requestFullscreen) {
  1101. document.documentElement.requestFullscreen();
  1102. } else if (document.documentElement.mozRequestFullScreen) {
  1103. document.documentElement.mozRequestFullScreen();
  1104. } else if (document.documentElement.webkitRequestFullscreen) {
  1105. document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
  1106. } else if (document.body.msRequestFullscreen) {
  1107. document.body.msRequestFullscreen();
  1108. }
  1109. }
  1110. UI.updateFullscreenButton();
  1111. },
  1112. updateFullscreenButton() {
  1113. if (document.fullscreenElement || // alternative standard method
  1114. document.mozFullScreenElement || // currently working methods
  1115. document.webkitFullscreenElement ||
  1116. document.msFullscreenElement ) {
  1117. document.getElementById('noVNC_fullscreen_button')
  1118. .classList.add("noVNC_selected");
  1119. } else {
  1120. document.getElementById('noVNC_fullscreen_button')
  1121. .classList.remove("noVNC_selected");
  1122. }
  1123. },
  1124. /* ------^-------
  1125. * /FULLSCREEN
  1126. * ==============
  1127. * RESIZE
  1128. * ------v------*/
  1129. // Apply remote resizing or local scaling
  1130. applyResizeMode() {
  1131. if (!UI.rfb) return;
  1132. UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
  1133. UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
  1134. },
  1135. /* ------^-------
  1136. * /RESIZE
  1137. * ==============
  1138. * VIEW CLIPPING
  1139. * ------v------*/
  1140. // Update viewport clipping property for the connection. The normal
  1141. // case is to get the value from the setting. There are special cases
  1142. // for when the viewport is scaled or when a touch device is used.
  1143. updateViewClip() {
  1144. if (!UI.rfb) return;
  1145. const scaling = UI.getSetting('resize') === 'scale';
  1146. // Some platforms have overlay scrollbars that are difficult
  1147. // to use in our case, which means we have to force panning
  1148. // FIXME: Working scrollbars can still be annoying to use with
  1149. // touch, so we should ideally be able to have both
  1150. // panning and scrollbars at the same time
  1151. let brokenScrollbars = false;
  1152. if (!hasScrollbarGutter) {
  1153. if (isIOS() || isAndroid() || isMac() || isChromeOS()) {
  1154. brokenScrollbars = true;
  1155. }
  1156. }
  1157. if (scaling) {
  1158. // Can't be clipping if viewport is scaled to fit
  1159. UI.forceSetting('view_clip', false);
  1160. UI.rfb.clipViewport = false;
  1161. } else if (brokenScrollbars) {
  1162. UI.forceSetting('view_clip', true);
  1163. UI.rfb.clipViewport = true;
  1164. } else {
  1165. UI.enableSetting('view_clip');
  1166. UI.rfb.clipViewport = UI.getSetting('view_clip');
  1167. }
  1168. // Changing the viewport may change the state of
  1169. // the dragging button
  1170. UI.updateViewDrag();
  1171. },
  1172. /* ------^-------
  1173. * /VIEW CLIPPING
  1174. * ==============
  1175. * VIEWDRAG
  1176. * ------v------*/
  1177. toggleViewDrag() {
  1178. if (!UI.rfb) return;
  1179. UI.rfb.dragViewport = !UI.rfb.dragViewport;
  1180. UI.updateViewDrag();
  1181. },
  1182. updateViewDrag() {
  1183. if (!UI.connected) return;
  1184. const viewDragButton = document.getElementById('noVNC_view_drag_button');
  1185. if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) &&
  1186. UI.rfb.dragViewport) {
  1187. // We are no longer clipping the viewport. Make sure
  1188. // viewport drag isn't active when it can't be used.
  1189. UI.rfb.dragViewport = false;
  1190. }
  1191. if (UI.rfb.dragViewport) {
  1192. viewDragButton.classList.add("noVNC_selected");
  1193. } else {
  1194. viewDragButton.classList.remove("noVNC_selected");
  1195. }
  1196. if (UI.rfb.clipViewport) {
  1197. viewDragButton.classList.remove("noVNC_hidden");
  1198. } else {
  1199. viewDragButton.classList.add("noVNC_hidden");
  1200. }
  1201. viewDragButton.disabled = !UI.rfb.clippingViewport;
  1202. },
  1203. /* ------^-------
  1204. * /VIEWDRAG
  1205. * ==============
  1206. * QUALITY
  1207. * ------v------*/
  1208. updateQuality() {
  1209. if (!UI.rfb) return;
  1210. UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
  1211. },
  1212. /* ------^-------
  1213. * /QUALITY
  1214. * ==============
  1215. * COMPRESSION
  1216. * ------v------*/
  1217. updateCompression() {
  1218. if (!UI.rfb) return;
  1219. UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
  1220. },
  1221. /* ------^-------
  1222. * /COMPRESSION
  1223. * ==============
  1224. * KEYBOARD
  1225. * ------v------*/
  1226. showVirtualKeyboard() {
  1227. if (!isTouchDevice) return;
  1228. const input = document.getElementById('noVNC_keyboardinput');
  1229. if (document.activeElement == input) return;
  1230. input.focus();
  1231. try {
  1232. const l = input.value.length;
  1233. // Move the caret to the end
  1234. input.setSelectionRange(l, l);
  1235. } catch (err) {
  1236. // setSelectionRange is undefined in Google Chrome
  1237. }
  1238. },
  1239. hideVirtualKeyboard() {
  1240. if (!isTouchDevice) return;
  1241. const input = document.getElementById('noVNC_keyboardinput');
  1242. if (document.activeElement != input) return;
  1243. input.blur();
  1244. },
  1245. toggleVirtualKeyboard() {
  1246. if (document.getElementById('noVNC_keyboard_button')
  1247. .classList.contains("noVNC_selected")) {
  1248. UI.hideVirtualKeyboard();
  1249. } else {
  1250. UI.showVirtualKeyboard();
  1251. }
  1252. },
  1253. onfocusVirtualKeyboard(event) {
  1254. document.getElementById('noVNC_keyboard_button')
  1255. .classList.add("noVNC_selected");
  1256. if (UI.rfb) {
  1257. UI.rfb.focusOnClick = false;
  1258. }
  1259. },
  1260. onblurVirtualKeyboard(event) {
  1261. document.getElementById('noVNC_keyboard_button')
  1262. .classList.remove("noVNC_selected");
  1263. if (UI.rfb) {
  1264. UI.rfb.focusOnClick = true;
  1265. }
  1266. },
  1267. keepVirtualKeyboard(event) {
  1268. const input = document.getElementById('noVNC_keyboardinput');
  1269. // Only prevent focus change if the virtual keyboard is active
  1270. if (document.activeElement != input) {
  1271. return;
  1272. }
  1273. // Only allow focus to move to other elements that need
  1274. // focus to function properly
  1275. if (event.target.form !== undefined) {
  1276. switch (event.target.type) {
  1277. case 'text':
  1278. case 'email':
  1279. case 'search':
  1280. case 'password':
  1281. case 'tel':
  1282. case 'url':
  1283. case 'textarea':
  1284. case 'select-one':
  1285. case 'select-multiple':
  1286. return;
  1287. }
  1288. }
  1289. event.preventDefault();
  1290. },
  1291. keyboardinputReset() {
  1292. const kbi = document.getElementById('noVNC_keyboardinput');
  1293. kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
  1294. UI.lastKeyboardinput = kbi.value;
  1295. },
  1296. keyEvent(keysym, code, down) {
  1297. if (!UI.rfb) return;
  1298. UI.rfb.sendKey(keysym, code, down);
  1299. },
  1300. // When normal keyboard events are left uncought, use the input events from
  1301. // the keyboardinput element instead and generate the corresponding key events.
  1302. // This code is required since some browsers on Android are inconsistent in
  1303. // sending keyCodes in the normal keyboard events when using on screen keyboards.
  1304. keyInput(event) {
  1305. if (!UI.rfb) return;
  1306. const newValue = event.target.value;
  1307. if (!UI.lastKeyboardinput) {
  1308. UI.keyboardinputReset();
  1309. }
  1310. const oldValue = UI.lastKeyboardinput;
  1311. let newLen;
  1312. try {
  1313. // Try to check caret position since whitespace at the end
  1314. // will not be considered by value.length in some browsers
  1315. newLen = Math.max(event.target.selectionStart, newValue.length);
  1316. } catch (err) {
  1317. // selectionStart is undefined in Google Chrome
  1318. newLen = newValue.length;
  1319. }
  1320. const oldLen = oldValue.length;
  1321. let inputs = newLen - oldLen;
  1322. let backspaces = inputs < 0 ? -inputs : 0;
  1323. // Compare the old string with the new to account for
  1324. // text-corrections or other input that modify existing text
  1325. for (let i = 0; i < Math.min(oldLen, newLen); i++) {
  1326. if (newValue.charAt(i) != oldValue.charAt(i)) {
  1327. inputs = newLen - i;
  1328. backspaces = oldLen - i;
  1329. break;
  1330. }
  1331. }
  1332. // Send the key events
  1333. for (let i = 0; i < backspaces; i++) {
  1334. UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
  1335. }
  1336. for (let i = newLen - inputs; i < newLen; i++) {
  1337. UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
  1338. }
  1339. // Control the text content length in the keyboardinput element
  1340. if (newLen > 2 * UI.defaultKeyboardinputLen) {
  1341. UI.keyboardinputReset();
  1342. } else if (newLen < 1) {
  1343. // There always have to be some text in the keyboardinput
  1344. // element with which backspace can interact.
  1345. UI.keyboardinputReset();
  1346. // This sometimes causes the keyboard to disappear for a second
  1347. // but it is required for the android keyboard to recognize that
  1348. // text has been added to the field
  1349. event.target.blur();
  1350. // This has to be ran outside of the input handler in order to work
  1351. setTimeout(event.target.focus.bind(event.target), 0);
  1352. } else {
  1353. UI.lastKeyboardinput = newValue;
  1354. }
  1355. },
  1356. /* ------^-------
  1357. * /KEYBOARD
  1358. * ==============
  1359. * EXTRA KEYS
  1360. * ------v------*/
  1361. openExtraKeys() {
  1362. UI.closeAllPanels();
  1363. UI.openControlbar();
  1364. document.getElementById('noVNC_modifiers')
  1365. .classList.add("noVNC_open");
  1366. document.getElementById('noVNC_toggle_extra_keys_button')
  1367. .classList.add("noVNC_selected");
  1368. },
  1369. closeExtraKeys() {
  1370. document.getElementById('noVNC_modifiers')
  1371. .classList.remove("noVNC_open");
  1372. document.getElementById('noVNC_toggle_extra_keys_button')
  1373. .classList.remove("noVNC_selected");
  1374. },
  1375. toggleExtraKeys() {
  1376. if (document.getElementById('noVNC_modifiers')
  1377. .classList.contains("noVNC_open")) {
  1378. UI.closeExtraKeys();
  1379. } else {
  1380. UI.openExtraKeys();
  1381. }
  1382. },
  1383. sendEsc() {
  1384. UI.sendKey(KeyTable.XK_Escape, "Escape");
  1385. },
  1386. sendTab() {
  1387. UI.sendKey(KeyTable.XK_Tab, "Tab");
  1388. },
  1389. toggleCtrl() {
  1390. const btn = document.getElementById('noVNC_toggle_ctrl_button');
  1391. if (btn.classList.contains("noVNC_selected")) {
  1392. UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
  1393. btn.classList.remove("noVNC_selected");
  1394. } else {
  1395. UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
  1396. btn.classList.add("noVNC_selected");
  1397. }
  1398. },
  1399. toggleWindows() {
  1400. const btn = document.getElementById('noVNC_toggle_windows_button');
  1401. if (btn.classList.contains("noVNC_selected")) {
  1402. UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
  1403. btn.classList.remove("noVNC_selected");
  1404. } else {
  1405. UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
  1406. btn.classList.add("noVNC_selected");
  1407. }
  1408. },
  1409. toggleAlt() {
  1410. const btn = document.getElementById('noVNC_toggle_alt_button');
  1411. if (btn.classList.contains("noVNC_selected")) {
  1412. UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
  1413. btn.classList.remove("noVNC_selected");
  1414. } else {
  1415. UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
  1416. btn.classList.add("noVNC_selected");
  1417. }
  1418. },
  1419. sendCtrlAltDel() {
  1420. UI.rfb.sendCtrlAltDel();
  1421. // See below
  1422. UI.rfb.focus();
  1423. UI.idleControlbar();
  1424. },
  1425. sendKey(keysym, code, down) {
  1426. UI.rfb.sendKey(keysym, code, down);
  1427. // Move focus to the screen in order to be able to use the
  1428. // keyboard right after these extra keys.
  1429. // The exception is when a virtual keyboard is used, because
  1430. // if we focus the screen the virtual keyboard would be closed.
  1431. // In this case we focus our special virtual keyboard input
  1432. // element instead.
  1433. if (document.getElementById('noVNC_keyboard_button')
  1434. .classList.contains("noVNC_selected")) {
  1435. document.getElementById('noVNC_keyboardinput').focus();
  1436. } else {
  1437. UI.rfb.focus();
  1438. }
  1439. // fade out the controlbar to highlight that
  1440. // the focus has been moved to the screen
  1441. UI.idleControlbar();
  1442. },
  1443. /* ------^-------
  1444. * /EXTRA KEYS
  1445. * ==============
  1446. * MISC
  1447. * ------v------*/
  1448. updateViewOnly() {
  1449. if (!UI.rfb) return;
  1450. UI.rfb.viewOnly = UI.getSetting('view_only');
  1451. // Hide input related buttons in view only mode
  1452. if (UI.rfb.viewOnly) {
  1453. document.getElementById('noVNC_keyboard_button')
  1454. .classList.add('noVNC_hidden');
  1455. document.getElementById('noVNC_toggle_extra_keys_button')
  1456. .classList.add('noVNC_hidden');
  1457. document.getElementById('noVNC_clipboard_button')
  1458. .classList.add('noVNC_hidden');
  1459. } else {
  1460. document.getElementById('noVNC_keyboard_button')
  1461. .classList.remove('noVNC_hidden');
  1462. document.getElementById('noVNC_toggle_extra_keys_button')
  1463. .classList.remove('noVNC_hidden');
  1464. document.getElementById('noVNC_clipboard_button')
  1465. .classList.remove('noVNC_hidden');
  1466. }
  1467. },
  1468. updateShowDotCursor() {
  1469. if (!UI.rfb) return;
  1470. UI.rfb.showDotCursor = UI.getSetting('show_dot');
  1471. },
  1472. updateLogging() {
  1473. WebUtil.initLogging(UI.getSetting('logging'));
  1474. },
  1475. updateDesktopName(e) {
  1476. UI.desktopName = e.detail.name;
  1477. // Display the desktop name in the document title
  1478. document.title = e.detail.name + " - " + PAGE_TITLE;
  1479. },
  1480. bell(e) {
  1481. if (WebUtil.getConfigVar('bell', 'on') === 'on') {
  1482. const promise = document.getElementById('noVNC_bell').play();
  1483. // The standards disagree on the return value here
  1484. if (promise) {
  1485. promise.catch((e) => {
  1486. if (e.name === "NotAllowedError") {
  1487. // Ignore when the browser doesn't let us play audio.
  1488. // It is common that the browsers require audio to be
  1489. // initiated from a user action.
  1490. } else {
  1491. Log.Error("Unable to play bell: " + e);
  1492. }
  1493. });
  1494. }
  1495. }
  1496. },
  1497. //Helper to add options to dropdown.
  1498. addOption(selectbox, text, value) {
  1499. const optn = document.createElement("OPTION");
  1500. optn.text = text;
  1501. optn.value = value;
  1502. selectbox.options.add(optn);
  1503. },
  1504. /* ------^-------
  1505. * /MISC
  1506. * ==============
  1507. */
  1508. };
  1509. // Set up translations
  1510. const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
  1511. l10n.setup(LINGUAS, "app/locale/")
  1512. .catch(err => Log.Error("Failed to load translations: " + err))
  1513. .then(UI.prime);
  1514. export default UI;