From f172633715adc9721e552f58ea2e117ac1ade3ea Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 10:08:10 +0200 Subject: [PATCH 01/11] Sort API alphabetically So it is easier to find things as the API grows. --- docs/API.md | 512 ++++++++++++++++++++++++++-------------------------- 1 file changed, 256 insertions(+), 256 deletions(-) diff --git a/docs/API.md b/docs/API.md index 77e90567..34a77f97 100644 --- a/docs/API.md +++ b/docs/API.md @@ -16,61 +16,12 @@ protocol stream. ### Properties -`viewOnly` - - Is a `boolean` indicating if any events (e.g. key presses or mouse - movement) should be prevented from being sent to the server. - Disabled by default. - -`focusOnClick` - - Is a `boolean` indicating if keyboard focus should automatically be - moved to the remote session when a `mousedown` or `touchstart` - event is received. Enabled by default. - -`clipViewport` - - Is a `boolean` indicating if the remote session should be clipped - to its container. When disabled scrollbars will be shown to handle - the resulting overflow. Disabled by default. - -`dragViewport` - - Is a `boolean` indicating if mouse events should control the - relative position of a clipped remote session. Only relevant if - `clipViewport` is enabled. Disabled by default. - -`scaleViewport` - - Is a `boolean` indicating if the remote session should be scaled - locally so it fits its container. When disabled it will be centered - if the remote session is smaller than its container, or handled - according to `clipViewport` if it is larger. Disabled by default. - -`resizeSession` - - Is a `boolean` indicating if a request to resize the remote session - should be sent whenever the container changes dimensions. 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. - `background` - Is a valid CSS [background](https://developer.mozilla.org/en-US/docs/Web/CSS/background) style value indicating which background style should be applied to the element containing the remote session screen. The default value is `rgb(40, 40, 40)` (solid gray color). -`qualityLevel` - - Is an `int` in range `[0-9]` controlling the desired JPEG quality. - Value `0` implies low quality and `9` implies high quality. - Default value is `6`. - -`compressionLevel` - - Is an `int` in range `[0-9]` controlling the desired compression - level. Value `0` means no compression. Level 1 uses a minimum of CPU - resources and achieves weak compression ratios, while level 9 offers - best compression but is slow in terms of CPU consumption on the server - side. Use high levels with very slow network connections. - Default value is `2`. - `capabilities` *Read only* - Is an `Object` indicating which optional extensions are available on the server. Some methods may only be called if the corresponding @@ -80,71 +31,113 @@ protocol stream. | -------- | --------- | ----------- | `power` | `boolean` | Machine power control is available +`clipViewport` + - Is a `boolean` indicating if the remote session should be clipped + to its container. When disabled scrollbars will be shown to handle + the resulting overflow. Disabled by default. + +`compressionLevel` + - Is an `int` in range `[0-9]` controlling the desired compression + level. Value `0` means no compression. Level 1 uses a minimum of CPU + resources and achieves weak compression ratios, while level 9 offers + best compression but is slow in terms of CPU consumption on the server + side. Use high levels with very slow network connections. + Default value is `2`. + +`dragViewport` + - Is a `boolean` indicating if mouse events should control the + relative position of a clipped remote session. Only relevant if + `clipViewport` is enabled. Disabled by default. + +`focusOnClick` + - Is a `boolean` indicating if keyboard focus should automatically be + moved to the remote session when a `mousedown` or `touchstart` + event is received. Enabled by default. + +`qualityLevel` + - Is an `int` in range `[0-9]` controlling the desired JPEG quality. + Value `0` implies low quality and `9` implies high quality. + Default value is `6`. + +`resizeSession` + - Is a `boolean` indicating if a request to resize the remote session + should be sent whenever the container changes dimensions. Disabled + by default. + +`scaleViewport` + - Is a `boolean` indicating if the remote session should be scaled + locally so it fits its container. When disabled it will be centered + if the remote session is smaller than its container, or handled + 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` + - Is a `boolean` indicating if any events (e.g. key presses or mouse + movement) should be prevented from being sent to the server. + Disabled by default. + ### Events -[`connect`](#connect) - - The `connect` event is fired when the `RFB` object has completed - the connection and handshaking with the server. - -[`disconnect`](#disconnect) - - The `disconnect` event is fired when the `RFB` object disconnects. - -[`serververification`](#serververification) - - The `serververification` event is fired when the server identity - must be confirmed by the user. - -[`credentialsrequired`](#credentialsrequired) - - The `credentialsrequired` event is fired when more credentials must - be given to continue. - -[`securityfailure`](#securityfailure) - - The `securityfailure` event is fired when the security negotiation - with the server fails. - -[`clipboard`](#clipboard) - - The `clipboard` event is fired when clipboard data is received from - the server. - [`bell`](#bell) - The `bell` event is fired when a audible bell request is received from the server. -[`desktopname`](#desktopname) - - The `desktopname` event is fired when the remote desktop name - changes. - [`capabilities`](#capabilities) - The `capabilities` event is fired when `RFB.capabilities` is updated. -### Methods +[`clipboard`](#clipboard) + - The `clipboard` event is fired when clipboard data is received from + the server. -[`RFB.disconnect()`](#rfbdisconnect) - - Disconnect from the server. +[`connect`](#connect) + - The `connect` event is fired when the `RFB` object has completed + the connection and handshaking with the server. + +[`credentialsrequired`](#credentialsrequired) + - The `credentialsrequired` event is fired when more credentials must + be given to continue. + +[`desktopname`](#desktopname) + - The `desktopname` event is fired when the remote desktop name + changes. + +[`disconnect`](#disconnect) + - The `disconnect` event is fired when the `RFB` object disconnects. + +[`securityfailure`](#securityfailure) + - The `securityfailure` event is fired when the security negotiation + with the server fails. + +[`serververification`](#serververification) + - The `serververification` event is fired when the server identity + must be confirmed by the user. + +### Methods [`RFB.approveServer()`](#rfbapproveserver) - Proceed connecting to the server. Should be called after the [`serververification`](#serververification) event has fired and the user has verified the identity of the server. -[`RFB.sendCredentials()`](#rfbsendcredentials) - - Send credentials to server. Should be called after the - [`credentialsrequired`](#credentialsrequired) event has fired. +[`RFB.blur()`](#rfbblur) + - Remove keyboard focus from the remote session. -[`RFB.sendKey()`](#rfbsendkey) - - Send a key event. +[`RFB.clipboardPasteFrom()`](#rfbclipboardpastefrom) + - Send clipboard contents to server. -[`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel) - - Send Ctrl-Alt-Del key sequence. +[`RFB.disconnect()`](#rfbdisconnect) + - Disconnect from the server. [`RFB.focus()`](#rfbfocus) - Move keyboard focus to the remote session. -[`RFB.blur()`](#rfbblur) - - Remove keyboard focus from the remote session. - -[`RFB.machineShutdown()`](#rfbmachineshutdown) - - Request a shutdown of the remote machine. +[`RFB.getImageData()`](#rfbgetimagedata) + - Return the current content of the screen as an ImageData array. [`RFB.machineReboot()`](#rfbmachinereboot) - Request a reboot of the remote machine. @@ -152,18 +145,25 @@ protocol stream. [`RFB.machineReset()`](#rfbmachinereset) - Request a reset of the remote machine. -[`RFB.clipboardPasteFrom()`](#rfbclipboardpastefrom) - - Send clipboard contents to server. +[`RFB.machineShutdown()`](#rfbmachineshutdown) + - Request a shutdown of the remote machine. -[`RFB.getImageData()`](#rfbgetimagedata) - - Return the current content of the screen as an ImageData array. +[`RFB.sendCredentials()`](#rfbsendcredentials) + - Send credentials to server. Should be called after the + [`credentialsrequired`](#credentialsrequired) event has fired. -[`RFB.toDataURL()`](#rfbtodataurl) - - Return the current content of the screen as data-url encoded image file. +[`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel) + - Send Ctrl-Alt-Del key sequence. + +[`RFB.sendKey()`](#rfbsendkey) + - Send a key event. [`RFB.toBlob()`](#rfbtoblob) - Return the current content of the screen as Blob encoded image file. +[`RFB.toDataURL()`](#rfbtodataurl) + - Return the current content of the screen as data-url encoded image file. + ### Details #### RFB() @@ -216,12 +216,42 @@ connection to a specified VNC server. - An `Array` of `DOMString`s specifying the sub-protocols to use in the WebSocket connection. Empty by default. +#### bell + +The `bell` event is fired when the server has requested an audible +bell. + +#### capabilities + +The `capabilities` event is fired whenever an entry is added or removed +from `RFB.capabilities`. The `detail` property is an `Object` with the +property `capabilities` containing the new value of `RFB.capabilities`. + +#### clipboard + +The `clipboard` event is fired when the server has sent clipboard data. +The `detail` property is an `Object` containing the property `text` +which is a `DOMString` with the clipboard data. + +#### credentialsrequired + +The `credentialsrequired` event is fired when the server requests more +credentials than were specified to [`RFB()`](#rfb-1). The `detail` +property is an `Object` containing the property `types` which is an +`Array` of `DOMString` listing the credentials that are required. + #### connect The `connect` event is fired after all the handshaking with the server is completed and the connection is fully established. After this event the `RFB` object is ready to recieve graphics updates and to send input. +#### desktopname + +The `desktopname` event is fired when the name of the remote desktop +changes. The `detail` property is an `Object` with the property `name` +which is a `DOMString` specifying the new name. + #### disconnect The `disconnect` event is fired when the connection has been @@ -230,27 +260,6 @@ property `clean`. `clean` is a `boolean` indicating if the termination was clean or not. In the event of an unexpected termination or an error `clean` will be set to false. -#### serververification - -The `serververification` event is fired when the server provides -information that allows the user to verify that it is the correct server -and protect against a man-in-the-middle attack. The `detail` property is -an `Object` containing the property `type` which is a `DOMString` -specifying which type of information the server has provided. Other -properties are also available, depending on the value of `type`: - -`"RSA"` - - The server identity is verified using just a RSA key. The property - `publickey` is a `Uint8Array` containing the public key in a unsigned - big endian representation. - -#### credentialsrequired - -The `credentialsrequired` event is fired when the server requests more -credentials than were specified to [`RFB()`](#rfb-1). The `detail` -property is an `Object` containing the property `types` which is an -`Array` of `DOMString` listing the credentials that are required. - #### securityfailure The `securityfailure` event is fired when the handshaking process with @@ -271,37 +280,19 @@ thus the language of the string is not known. However most servers will probably send English strings. The server can choose to not send a reason and in these cases the `reason` property will be omitted. -#### clipboard +#### serververification -The `clipboard` event is fired when the server has sent clipboard data. -The `detail` property is an `Object` containing the property `text` -which is a `DOMString` with the clipboard data. +The `serververification` event is fired when the server provides +information that allows the user to verify that it is the correct server +and protect against a man-in-the-middle attack. The `detail` property is +an `Object` containing the property `type` which is a `DOMString` +specifying which type of information the server has provided. Other +properties are also available, depending on the value of `type`: -#### bell - -The `bell` event is fired when the server has requested an audible -bell. - -#### desktopname - -The `desktopname` event is fired when the name of the remote desktop -changes. The `detail` property is an `Object` with the property `name` -which is a `DOMString` specifying the new name. - -#### capabilities - -The `capabilities` event is fired whenever an entry is added or removed -from `RFB.capabilities`. The `detail` property is an `Object` with the -property `capabilities` containing the new value of `RFB.capabilities`. - -#### RFB.disconnect() - -The `RFB.disconnect()` method is used to disconnect from the currently -connected server. - -##### Syntax - - RFB.disconnect( ); +`"RSA"` + - The server identity is verified using just a RSA key. The property + `publickey` is a `Uint8Array` containing the public key in a unsigned + big endian representation. #### RFB.approveServer() @@ -313,6 +304,94 @@ and that the connection can continue. RFB.approveServer( ); +#### RFB.blur() + +The `RFB.blur()` method remove keyboard focus on the remote session. +Keyboard events will no longer be sent to the remote server after this +point. + +##### Syntax + + RFB.blur( ); + +#### RFB.clipboardPasteFrom() + +The `RFB.clipboardPasteFrom()` method is used to send clipboard data +to the remote server. + +##### Syntax + + RFB.clipboardPasteFrom( text ); + +###### Parameters + +**`text`** + - A `DOMString` specifying the clipboard data to send. + +#### RFB.disconnect() + +The `RFB.disconnect()` method is used to disconnect from the currently +connected server. + +##### Syntax + + RFB.disconnect( ); + +#### RFB.focus() + +The `RFB.focus()` method sets the keyboard focus on the remote session. +Keyboard events will be sent to the remote server after this point. + +##### Syntax + + RFB.focus( [options] ); + +###### Parameters + +**`options`** *Optional* + - A `object` providing options to control how the focus will be + performed. Please see [`HTMLElement.focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) + for available options. + +#### RFB.getImageData() + +The `RFB.getImageData()` method is used to return the current content of the +screen encoded as [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData). + +##### Syntax + + RFB.getImageData(); + +#### RFB.machineReboot() + +The `RFB.machineReboot()` method is used to request a clean reboot of +the remote machine. The capability `power` must be set for this method +to have any effect. + +##### Syntax + + RFB.machineReboot( ); + +#### RFB.machineReset() + +The `RFB.machineReset()` method is used to request a forced reset of +the remote machine. The capability `power` must be set for this method +to have any effect. + +##### Syntax + + RFB.machineReset( ); + +#### RFB.machineShutdown() + +The `RFB.machineShutdown()` method is used to request to shut down the +remote machine. The capability `power` must be set for this method to +have any effect. + +##### Syntax + + RFB.machineShutdown( ); + #### RFB.sendCredentials() The `RFB.sendCredentials()` method is used to provide the missing @@ -328,6 +407,16 @@ credentials after a `credentialsrequired` event has been fired. - An `Object` specifying the credentials to provide to the server when authenticating. See [`RFB()`](#rfb-1) for details. +#### RFB.sendCtrlAltDel() + +The `RFB.sendCtrlAltDel()` method is used to send the key sequence +*left Control*, *left Alt*, *Delete*. This is a convenience wrapper +around [`RFB.sendKey()`](#rfbsendkey). + +##### Syntax + + RFB.sendCtrlAltDel( ); + #### RFB.sendKey() The `RFB.sendKey()` method is used to send a key event to the server. @@ -353,115 +442,6 @@ The `RFB.sendKey()` method is used to send a key event to the server. - A `boolean` specifying if a press or a release event should be sent. If omitted then both a press and release event are sent. -#### RFB.sendCtrlAltDel() - -The `RFB.sendCtrlAltDel()` method is used to send the key sequence -*left Control*, *left Alt*, *Delete*. This is a convenience wrapper -around [`RFB.sendKey()`](#rfbsendkey). - -##### Syntax - - RFB.sendCtrlAltDel( ); - -#### RFB.focus() - -The `RFB.focus()` method sets the keyboard focus on the remote session. -Keyboard events will be sent to the remote server after this point. - -##### Syntax - - RFB.focus( [options] ); - -###### Parameters - -**`options`** *Optional* - - A `object` providing options to control how the focus will be - performed. Please see [`HTMLElement.focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) - for available options. - -#### RFB.blur() - -The `RFB.blur()` method remove keyboard focus on the remote session. -Keyboard events will no longer be sent to the remote server after this -point. - -##### Syntax - - RFB.blur( ); - -#### RFB.machineShutdown() - -The `RFB.machineShutdown()` method is used to request to shut down the -remote machine. The capability `power` must be set for this method to -have any effect. - -##### Syntax - - RFB.machineShutdown( ); - -#### RFB.machineReboot() - -The `RFB.machineReboot()` method is used to request a clean reboot of -the remote machine. The capability `power` must be set for this method -to have any effect. - -##### Syntax - - RFB.machineReboot( ); - -#### RFB.machineReset() - -The `RFB.machineReset()` method is used to request a forced reset of -the remote machine. The capability `power` must be set for this method -to have any effect. - -##### Syntax - - RFB.machineReset( ); - -#### RFB.clipboardPasteFrom() - -The `RFB.clipboardPasteFrom()` method is used to send clipboard data -to the remote server. - -##### Syntax - - RFB.clipboardPasteFrom( text ); - -###### Parameters - -**`text`** - - A `DOMString` specifying the clipboard data to send. - -#### RFB.getImageData() - -The `RFB.getImageData()` method is used to return the current content of the -screen encoded as [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData). - -##### Syntax - - RFB.getImageData(); - -#### RFB.toDataURL() - -The `RFB.toDataURL()` method is used to return the current content of the -screen encoded as a data URL that could for example be put in the `src` attribute -of an `img` tag. - -##### Syntax - - RFB.toDataURL(); - RFB.toDataURL(type); - RFB.toDataURL(type, encoderOptions); - -###### Parameters - -**`type`** *Optional* - - A string indicating the requested MIME type of the image - -**`encoderOptions`** *Optional* - - A number between 0 and 1 indicating the image quality. - #### RFB.toBlob() The `RFB.toBlob()` method is used to return the current content of the @@ -484,3 +464,23 @@ screen encoded as [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob **`encoderOptions`** *Optional* - A number between 0 and 1 indicating the image quality. + +#### RFB.toDataURL() + +The `RFB.toDataURL()` method is used to return the current content of the +screen encoded as a data URL that could for example be put in the `src` attribute +of an `img` tag. + +##### Syntax + + RFB.toDataURL(); + RFB.toDataURL(type); + RFB.toDataURL(type, encoderOptions); + +###### Parameters + +**`type`** *Optional* + - A string indicating the requested MIME type of the image + +**`encoderOptions`** *Optional* + - A number between 0 and 1 indicating the image quality. From 7f4a9eebc865a627407e69f22a5de18454cde6df Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 10:34:19 +0200 Subject: [PATCH 02/11] Export clipping state externally So that UI can reflect if it is currently possible to drag the viewport or not. --- app/ui.js | 6 +++++- core/rfb.js | 15 +++++++++++++++ docs/API.md | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index ff0f1760..c277eeaf 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1049,6 +1049,7 @@ const UI = { UI.rfb.addEventListener("serververification", UI.serverVerify); UI.rfb.addEventListener("credentialsrequired", UI.credentials); UI.rfb.addEventListener("securityfailure", UI.securityFailed); + UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag); UI.rfb.addEventListener("capabilities", UI.updatePowerButton); UI.rfb.addEventListener("clipboard", UI.clipboardReceive); UI.rfb.addEventListener("bell", UI.bell); @@ -1362,7 +1363,8 @@ const UI = { const viewDragButton = document.getElementById('noVNC_view_drag_button'); - if (!UI.rfb.clipViewport && UI.rfb.dragViewport) { + if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) && + UI.rfb.dragViewport) { // We are no longer clipping the viewport. Make sure // viewport drag isn't active when it can't be used. UI.rfb.dragViewport = false; @@ -1379,6 +1381,8 @@ const UI = { } else { viewDragButton.classList.add("noVNC_hidden"); } + + viewDragButton.disabled = !UI.rfb.clippingViewport; }, /* ------^------- diff --git a/core/rfb.js b/core/rfb.js index 2ccd61a1..6afd7c65 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -287,6 +287,7 @@ export default class RFB extends EventTargetMixin { this._viewOnly = false; this._clipViewport = false; + this._clippingViewport = false; this._scaleViewport = false; this._resizeSession = false; @@ -318,6 +319,16 @@ export default class RFB extends EventTargetMixin { get capabilities() { return this._capabilities; } + get clippingViewport() { return this._clippingViewport; } + _setClippingViewport(on) { + if (on === this._clippingViewport) { + return; + } + this._clippingViewport = on; + this.dispatchEvent(new CustomEvent("clippingviewport", + { detail: this._clippingViewport })); + } + get touchButton() { return 0; } set touchButton(button) { Log.Warn("Using old API!"); } @@ -749,6 +760,10 @@ export default class RFB extends EventTargetMixin { const size = this._screenSize(); this._display.viewportChangeSize(size.w, size.h); this._fixScrollbars(); + this._setClippingViewport(size.w < this._display.width || + size.h < this._display.height); + } else { + this._setClippingViewport(false); } // When changing clipping we might show or hide scrollbars. diff --git a/docs/API.md b/docs/API.md index 34a77f97..a16799bc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -31,6 +31,11 @@ protocol stream. | -------- | --------- | ----------- | `power` | `boolean` | Machine power control is available +`clippingViewport` *Read only* + - Is a `boolean` indicating if the remote session is currently being + clipped to its container. Only relevant if `clipViewport` is + enabled. + `clipViewport` - Is a `boolean` indicating if the remote session should be clipped to its container. When disabled scrollbars will be shown to handle @@ -94,6 +99,10 @@ protocol stream. - The `clipboard` event is fired when clipboard data is received from the server. +[`clippingviewport`](#clippingviewport) + - The `clippingviewport` event is fired when `RFB.clippingViewport` is + updated. + [`connect`](#connect) - The `connect` event is fired when the `RFB` object has completed the connection and handshaking with the server. @@ -227,6 +236,12 @@ The `capabilities` event is fired whenever an entry is added or removed from `RFB.capabilities`. The `detail` property is an `Object` with the property `capabilities` containing the new value of `RFB.capabilities`. +#### clippingviewport + +The `clippingviewport` event is fired whenever `RFB.clippingViewport` +changes between `true` and `false`. The `detail` property is a `boolean` +with the new value of `RFB.clippingViewport`. + #### clipboard The `clipboard` event is fired when the server has sent clipboard data. From 262a90b0e03da1ddf0b6ac5acd55a3167c4b558d Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 27 Dec 2022 12:40:49 +0100 Subject: [PATCH 03/11] Consistently use "first" indentation We already enforced this for most things, so let's fix up the last few variants as well. --- .eslintrc | 3 +++ app/webutil.js | 4 ++-- core/des.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index 7797028e..10e15cec 100644 --- a/.eslintrc +++ b/.eslintrc @@ -26,10 +26,13 @@ "brace-style": ["error", "1tbs", { "allowSingleLine": true }], "indent": ["error", 4, { "SwitchCase": 1, + "VariableDeclarator": "first", "FunctionDeclaration": { "parameters": "first" }, + "FunctionExpression": { "parameters": "first" }, "CallExpression": { "arguments": "first" }, "ArrayExpression": "first", "ObjectExpression": "first", + "ImportDeclaration": "first", "ignoreComments": true }], "comma-spacing": ["error"], "comma-style": ["error"], diff --git a/app/webutil.js b/app/webutil.js index d42b7f25..084c69f6 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -32,7 +32,7 @@ export function initLogging(level) { export function getQueryVar(name, defVal) { "use strict"; const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), - match = ''.concat(document.location.href, window.location.hash).match(re); + match = ''.concat(document.location.href, window.location.hash).match(re); if (typeof defVal === 'undefined') { defVal = null; } if (match) { @@ -46,7 +46,7 @@ export function getQueryVar(name, defVal) { export function getHashVar(name, defVal) { "use strict"; const re = new RegExp('.*[&#]' + name + '=([^&]*)'), - match = document.location.hash.match(re); + match = document.location.hash.match(re); if (typeof defVal === 'undefined') { defVal = null; } if (match) { diff --git a/core/des.js b/core/des.js index d2f807b8..ba1ebde0 100644 --- a/core/des.js +++ b/core/des.js @@ -81,7 +81,7 @@ const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], - totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28]; + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28]; const z = 0x0; let a,b,c,d,e,f; From 28c9670427ee987e14975fe75159e4bc5d7ee869 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 08:35:30 +0200 Subject: [PATCH 04/11] Remove test code for old Chrome We don't care about ancient versions of Chrome anyway, so let's keep things simple. --- tests/test.helper.js | 20 ++-------------- tests/test.keyboard.js | 49 ++++---------------------------------- tests/test.localization.js | 10 +------- tests/test.webutil.js | 9 +------ 4 files changed, 9 insertions(+), 79 deletions(-) diff --git a/tests/test.helper.js b/tests/test.helper.js index ed65770e..ff83c539 100644 --- a/tests/test.helper.js +++ b/tests/test.helper.js @@ -71,18 +71,10 @@ describe('Helpers', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Mac x86_64"; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); it('should respect ContextMenu on modern browser', function () { @@ -196,19 +188,11 @@ describe('Helpers', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Windows"; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a, diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 6b59cde5..0d8cac60 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -144,18 +144,10 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Mac x86_64"; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); it('should change Alt to AltGraph', function () { @@ -267,17 +259,10 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); it('should toggle caps lock on key press on iOS', function () { @@ -334,19 +319,11 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Windows"; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a, @@ -375,20 +352,12 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Windows x86_64"; this.clock = sinon.useFakeTimers(); }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); if (this.clock !== undefined) { this.clock.restore(); } @@ -520,20 +489,12 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Windows x86_64"; this.clock = sinon.useFakeTimers(); }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); if (this.clock !== undefined) { this.clock.restore(); } diff --git a/tests/test.localization.js b/tests/test.localization.js index 311353a1..7e8e6c13 100644 --- a/tests/test.localization.js +++ b/tests/test.localization.js @@ -13,18 +13,10 @@ describe('Localization', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.languages !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.languages = []; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); it('should use English by default', function () { diff --git a/tests/test.webutil.js b/tests/test.webutil.js index 6681b3c7..76aa763a 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -66,11 +66,6 @@ describe('WebUtil', function () { origLocalStorage = Object.getOwnPropertyDescriptor(window, "localStorage"); Object.defineProperty(window, "localStorage", {value: {}}); - if (window.localStorage.setItem !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } window.localStorage.setItem = sinon.stub(); window.localStorage.getItem = sinon.stub(); @@ -79,9 +74,7 @@ describe('WebUtil', function () { return WebUtil.initSettings(); }); afterEach(function () { - if (origLocalStorage !== undefined) { - Object.defineProperty(window, "localStorage", origLocalStorage); - } + Object.defineProperty(window, "localStorage", origLocalStorage); }); describe('writeSetting', function () { From 88a36370a983c86026fdd0a8ac47ce820034c6a0 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 08:56:47 +0200 Subject: [PATCH 05/11] Add unit tests for browser detection --- tests/test.browser.js | 55 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/test.browser.js diff --git a/tests/test.browser.js b/tests/test.browser.js new file mode 100644 index 00000000..ae446ccf --- /dev/null +++ b/tests/test.browser.js @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ +const expect = chai.expect; + +import { isSafari, isFirefox } from '../core/util/browser.js'; + +describe('Browser detection', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + + Object.defineProperty(window, "navigator", {value: {}}); + }); + + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should handle Chrome', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + }); + + it('should handle Chromium', function () { + navigator.userAgent = "Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Raspbian Chromium/74.0.3729.157 Chrome/74.0.3729.157 Safari/537.36"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + }); + + it('should handle Firefox', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.true; + }); + + it('should handle Edge', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.34"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + }); + + it('should handle Opera', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 OPR/91.0.4516.20"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + }); +}); From 4a34ee4b1e1da67111d50eb2cd059e38aae4349a Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 08:59:57 +0200 Subject: [PATCH 06/11] Remove navigator check from browser tests This is a fundamental object that should always be present. --- core/util/browser.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/core/util/browser.js b/core/util/browser.js index 24b5e960..fb0d3443 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -78,26 +78,25 @@ export const hasScrollbarGutter = _hasScrollbarGutter; */ export function isMac() { - return navigator && !!(/mac/i).exec(navigator.platform); + return !!(/mac/i).exec(navigator.platform); } export function isWindows() { - return navigator && !!(/win/i).exec(navigator.platform); + return !!(/win/i).exec(navigator.platform); } export function isIOS() { - return navigator && - (!!(/ipad/i).exec(navigator.platform) || + return (!!(/ipad/i).exec(navigator.platform) || !!(/iphone/i).exec(navigator.platform) || !!(/ipod/i).exec(navigator.platform)); } export function isSafari() { - return navigator && (navigator.userAgent.indexOf('Safari') !== -1 && - navigator.userAgent.indexOf('Chrome') === -1); + return (navigator.userAgent.indexOf('Safari') !== -1 && + navigator.userAgent.indexOf('Chrome') === -1); } export function isFirefox() { - return navigator && !!(/firefox/i).exec(navigator.userAgent); + return !!(/firefox/i).exec(navigator.userAgent); } From ee5e3c5fa3f247d032d48554cb41a63315522870 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 09:14:49 +0200 Subject: [PATCH 07/11] Refine browser detection Try to follow the principle outlined by Mozilla when detecting browsers and engines. --- core/util/browser.js | 46 ++++++++++++++++++++-- tests/test.browser.js | 88 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 4 deletions(-) diff --git a/core/util/browser.js b/core/util/browser.js index fb0d3443..7d2bfd8d 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -77,6 +77,8 @@ export const hasScrollbarGutter = _hasScrollbarGutter; * It's better to use feature detection than platform detection. */ +/* OS */ + export function isMac() { return !!(/mac/i).exec(navigator.platform); } @@ -91,12 +93,50 @@ export function isIOS() { !!(/ipod/i).exec(navigator.platform)); } +/* Browser */ + export function isSafari() { - return (navigator.userAgent.indexOf('Safari') !== -1 && - navigator.userAgent.indexOf('Chrome') === -1); + return !!navigator.userAgent.match('Safari/...') && + !navigator.userAgent.match('Chrome/...') && + !navigator.userAgent.match('Chromium/...') && + !navigator.userAgent.match('Epiphany/...'); } export function isFirefox() { - return !!(/firefox/i).exec(navigator.userAgent); + return !!navigator.userAgent.match('Firefox/...') && + !navigator.userAgent.match('Seamonkey/...'); } +export function isChrome() { + return !!navigator.userAgent.match('Chrome/...') && + !navigator.userAgent.match('Chromium/...') && + !navigator.userAgent.match('Edg/...') && + !navigator.userAgent.match('OPR/...'); +} + +export function isChromium() { + return !!navigator.userAgent.match('Chromium/...'); +} + +export function isOpera() { + return !!navigator.userAgent.match('OPR/...'); +} + +export function isEdge() { + return !!navigator.userAgent.match('Edg/...'); +} + +/* Engine */ + +export function isGecko() { + return !!navigator.userAgent.match('Gecko/...'); +} + +export function isWebKit() { + return !!navigator.userAgent.match('AppleWebKit/...') && + !navigator.userAgent.match('Chrome/...'); +} + +export function isBlink() { + return !!navigator.userAgent.match('Chrome/...'); +} diff --git a/tests/test.browser.js b/tests/test.browser.js index ae446ccf..f80b12e6 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -1,7 +1,8 @@ /* eslint-disable no-console */ const expect = chai.expect; -import { isSafari, isFirefox } from '../core/util/browser.js'; +import { isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, + isGecko, isWebKit, isBlink } from '../core/util/browser.js'; describe('Browser detection', function () { let origNavigator; @@ -23,6 +24,14 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.true; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.true; }); it('should handle Chromium', function () { @@ -30,6 +39,14 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.true; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.true; }); it('should handle Firefox', function () { @@ -37,6 +54,44 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.true; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.true; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.false; + }); + + it('should handle Seamonkey', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 6.1; rv:36.0) Gecko/20100101 Firefox/36.0 Seamonkey/2.33.1"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.true; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.false; + }); + + it('should handle Safari', function () { + navigator.userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"; + + expect(isSafari()).to.be.true; + expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.true; + expect(isBlink()).to.be.false; }); it('should handle Edge', function () { @@ -44,6 +99,14 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.true; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.true; }); it('should handle Opera', function () { @@ -51,5 +114,28 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.true; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.true; + }); + + it('should handle Epiphany', function () { + navigator.userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Safari/605.1.15 Epiphany/605.1.15"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.true; + expect(isBlink()).to.be.false; }); }); From 8fb30fb9dc6771ca0e0c2ca8a37d13ee37a503da Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 23 Dec 2022 16:26:00 +0100 Subject: [PATCH 08/11] Add unit tests for OS detection --- tests/test.browser.js | 62 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/tests/test.browser.js b/tests/test.browser.js index f80b12e6..4fdc084e 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -1,9 +1,69 @@ /* eslint-disable no-console */ const expect = chai.expect; -import { isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, +import { isMac, isWindows, isIOS, + isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, isGecko, isWebKit, isBlink } from '../core/util/browser.js'; +describe('OS detection', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + + Object.defineProperty(window, "navigator", {value: {}}); + }); + + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should handle macOS', function () { + const platforms = [ + "MacIntel", + "MacPPC", + ]; + + platforms.forEach((platform) => { + navigator.platform = platform; + expect(isMac()).to.be.true; + expect(isWindows()).to.be.false; + expect(isIOS()).to.be.false; + }); + }); + + it('should handle Windows', function () { + const platforms = [ + "Win32", + "Win64", + ]; + + platforms.forEach((platform) => { + navigator.platform = platform; + expect(isMac()).to.be.false; + expect(isWindows()).to.be.true; + expect(isIOS()).to.be.false; + }); + }); + + it('should handle iOS', function () { + const platforms = [ + "iPhone", + "iPod", + "iPad", + ]; + + platforms.forEach((platform) => { + navigator.platform = platform; + expect(isMac()).to.be.false; + expect(isWindows()).to.be.false; + expect(isIOS()).to.be.true; + }); + }); +}); + describe('Browser detection', function () { let origNavigator; beforeEach(function () { From a187821e4f576c0569e7ca9a71ebb525734dda00 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 23 Dec 2022 16:36:49 +0100 Subject: [PATCH 09/11] Add OS checks for Android and ChromeOS --- core/util/browser.js | 9 +++++++++ tests/test.browser.js | 44 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/core/util/browser.js b/core/util/browser.js index 7d2bfd8d..81954383 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -93,6 +93,15 @@ export function isIOS() { !!(/ipod/i).exec(navigator.platform)); } +export function isAndroid() { + return !!(/android/i).exec(navigator.platform); +} + +export function isChromeOS() { + /* ChromeOS sets navigator.platform to Linux :/ */ + return !!navigator.userAgent.match(' CrOS '); +} + /* Browser */ export function isSafari() { diff --git a/tests/test.browser.js b/tests/test.browser.js index 4fdc084e..4b6c1273 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ const expect = chai.expect; -import { isMac, isWindows, isIOS, +import { isMac, isWindows, isIOS, isAndroid, isChromeOS, isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, isGecko, isWebKit, isBlink } from '../core/util/browser.js'; @@ -26,11 +26,14 @@ describe('OS detection', function () { "MacPPC", ]; + navigator.userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"; platforms.forEach((platform) => { navigator.platform = platform; expect(isMac()).to.be.true; expect(isWindows()).to.be.false; expect(isIOS()).to.be.false; + expect(isAndroid()).to.be.false; + expect(isChromeOS()).to.be.false; }); }); @@ -40,11 +43,14 @@ describe('OS detection', function () { "Win64", ]; + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"; platforms.forEach((platform) => { navigator.platform = platform; expect(isMac()).to.be.false; expect(isWindows()).to.be.true; expect(isIOS()).to.be.false; + expect(isAndroid()).to.be.false; + expect(isChromeOS()).to.be.false; }); }); @@ -55,11 +61,47 @@ describe('OS detection', function () { "iPad", ]; + navigator.userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1"; platforms.forEach((platform) => { navigator.platform = platform; expect(isMac()).to.be.false; expect(isWindows()).to.be.false; expect(isIOS()).to.be.true; + expect(isAndroid()).to.be.false; + expect(isChromeOS()).to.be.false; + }); + }); + + it('should handle Android', function () { + const platforms = [ + "Android", + ]; + + navigator.userAgent = "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0"; + platforms.forEach((platform) => { + navigator.platform = platform; + expect(isMac()).to.be.false; + expect(isWindows()).to.be.false; + expect(isIOS()).to.be.false; + expect(isAndroid()).to.be.true; + expect(isChromeOS()).to.be.false; + }); + }); + + it('should handle ChromeOS', function () { + let userAgents = [ + "Mozilla/5.0 (X11; CrOS x86_64 15183.59.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.75 Safari/537.36", + "Mozilla/5.0 (X11; CrOS aarch64 15183.59.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.75 Safari/537.36", + ]; + + navigator.platform = "Linux x86_64"; + userAgents.forEach((ua) => { + navigator.userAgent = ua; + expect(isMac()).to.be.false; + expect(isWindows()).to.be.false; + expect(isIOS()).to.be.false; + expect(isAndroid()).to.be.false; + expect(isChromeOS()).to.be.true; }); }); }); From 12a7c6f0de0ca51126c9a7292669c03f483049e0 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 23 Dec 2022 16:58:45 +0100 Subject: [PATCH 10/11] Check for Android using userAgent Modern Android systems seem to report "Linux" for navigator.platform, so we can no longer rely on that. --- core/util/browser.js | 3 ++- tests/test.browser.js | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/util/browser.js b/core/util/browser.js index 81954383..bbc9f5c1 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -94,7 +94,8 @@ export function isIOS() { } export function isAndroid() { - return !!(/android/i).exec(navigator.platform); + /* Android sets navigator.platform to Linux :/ */ + return !!navigator.userAgent.match('Android '); } export function isChromeOS() { diff --git a/tests/test.browser.js b/tests/test.browser.js index 4b6c1273..3b2299f6 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -73,13 +73,14 @@ describe('OS detection', function () { }); it('should handle Android', function () { - const platforms = [ - "Android", + let userAgents = [ + "Mozilla/5.0 (Linux; Android 13; SM-G960U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.128 Mobile Safari/537.36", + "Mozilla/5.0 (Android 13; Mobile; LG-M255; rv:108.0) Gecko/108.0 Firefox/108.0", ]; - navigator.userAgent = "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0"; - platforms.forEach((platform) => { - navigator.platform = platform; + navigator.platform = "Linux x86_64"; + userAgents.forEach((ua) => { + navigator.userAgent = ua; expect(isMac()).to.be.false; expect(isWindows()).to.be.false; expect(isIOS()).to.be.false; From 5de478d6e7b70c080a4a64ec6deb590c773ca416 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 23 Dec 2022 16:59:32 +0100 Subject: [PATCH 11/11] Restrict forced panning to known bad platforms Let's not punish systems that implement overlay scrollbars in a functional way. The only current example is Firefox on Windows 11 and on Linux. --- app/ui.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/ui.js b/app/ui.js index c277eeaf..c1f6776e 100644 --- a/app/ui.js +++ b/app/ui.js @@ -8,7 +8,8 @@ import * as Log from '../core/util/logging.js'; import _, { l10n } from './localization.js'; -import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold } +import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari, + hasScrollbarGutter, dragThreshold } from '../core/util/browser.js'; import { setCapture, getPointerEvent } from '../core/util/events.js'; import KeyTable from "../core/input/keysym.js"; @@ -1326,13 +1327,25 @@ const UI = { const scaling = UI.getSetting('resize') === 'scale'; + // Some platforms have overlay scrollbars that are difficult + // to use in our case, which means we have to force panning + // FIXME: Working scrollbars can still be annoying to use with + // touch, so we should ideally be able to have both + // panning and scrollbars at the same time + + let brokenScrollbars = false; + + if (!hasScrollbarGutter) { + if (isIOS() || isAndroid() || isMac() || isChromeOS()) { + brokenScrollbars = true; + } + } + if (scaling) { // Can't be clipping if viewport is scaled to fit UI.forceSetting('view_clip', false); UI.rfb.clipViewport = false; - } else if (!hasScrollbarGutter) { - // Some platforms have scrollbars that are difficult - // to use in our case, so we always use our own panning + } else if (brokenScrollbars) { UI.forceSetting('view_clip', true); UI.rfb.clipViewport = true; } else {