mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Compare commits
12 Commits
v16.1.1
...
v17.0.0-ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a6a8f55af | ||
|
|
53bf308497 | ||
|
|
5e1fbc9025 | ||
|
|
d88e71f688 | ||
|
|
919fd0f28d | ||
|
|
da921e680f | ||
|
|
6aece4a83d | ||
|
|
77579614e0 | ||
|
|
e39a1d2ea0 | ||
|
|
68d3659f75 | ||
|
|
bb6dc99d9d | ||
|
|
38b810b2e3 |
1
BUILD.gn
1
BUILD.gn
@@ -379,6 +379,7 @@ source_set("electron_lib") {
|
||||
"//ppapi/shared_impl",
|
||||
"//printing/buildflags",
|
||||
"//services/device/public/cpp/geolocation",
|
||||
"//services/device/public/cpp/hid",
|
||||
"//services/device/public/mojom",
|
||||
"//services/proxy_resolver:lib",
|
||||
"//services/video_capture/public/mojom:constants",
|
||||
|
||||
@@ -1 +1 @@
|
||||
16.0.0-nightly.20210922
|
||||
17.0.0-nightly.20210924
|
||||
@@ -234,6 +234,7 @@ expanding and collapsing the dialog.
|
||||
* `title` String (optional) - Title of the message box, some platforms will not show it.
|
||||
* `detail` String (optional) - Extra information of the message.
|
||||
* `icon` ([NativeImage](native-image.md) | String) (optional)
|
||||
* `textWidth` Integer (optional) _macOS_ - Custom width of the text in the message box.
|
||||
* `cancelId` Integer (optional) - The index of the button to be used to cancel the dialog, via
|
||||
the `Esc` key. By default this is assigned to the first button with "cancel" or "no" as the
|
||||
label. If no such labeled buttons exist and this option is not set, `0` will be used as the
|
||||
@@ -285,6 +286,7 @@ If `browserWindow` is not shown dialog will not be attached to it. In such case
|
||||
* `checkboxChecked` Boolean (optional) - Initial checked state of the
|
||||
checkbox. `false` by default.
|
||||
* `icon` [NativeImage](native-image.md) (optional)
|
||||
* `textWidth` Integer (optional) _macOS_ - Custom width of the text in the message box.
|
||||
* `cancelId` Integer (optional) - The index of the button to be used to cancel the dialog, via
|
||||
the `Esc` key. By default this is assigned to the first button with "cancel" or "no" as the
|
||||
label. If no such labeled buttons exist and this option is not set, `0` will be used as the
|
||||
|
||||
@@ -180,6 +180,96 @@ Emitted when a hunspell dictionary file download fails. For details
|
||||
on the failure you should collect a netlog and inspect the download
|
||||
request.
|
||||
|
||||
#### Event: 'select-hid-device'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `details` Object
|
||||
* `deviceList` [HIDDevice[]](structures/hid-device.md)
|
||||
* `frame` [WebFrameMain](web-frame-main.md)
|
||||
* `callback` Function
|
||||
* `deviceId` String | null (optional)
|
||||
|
||||
Emitted when a HID device needs to be selected when a call to
|
||||
`navigator.hid.requestDevice` is made. `callback` should be called with
|
||||
`deviceId` to be selected; passing no arguments to `callback` will
|
||||
cancel the request. Additionally, permissioning on `navigator.hid` can
|
||||
be further managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler)
|
||||
and [ses.setDevicePermissionHandler(handler)`](#sessetdevicepermissionhandlerhandler).
|
||||
|
||||
```javascript
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
let win = null
|
||||
|
||||
app.whenReady().then(() => {
|
||||
win = new BrowserWindow()
|
||||
|
||||
win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
if (permission === 'hid') {
|
||||
// Add logic here to determine if permission should be given to allow HID selection
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Optionally, retrieve previously persisted devices from a persistent store
|
||||
const grantedDevices = fetchGrantedDevices()
|
||||
|
||||
win.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
|
||||
if (details.device.vendorId === 123 && details.device.productId === 345) {
|
||||
// Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first)
|
||||
return true
|
||||
}
|
||||
|
||||
// Search through the list of devices that have previously been granted permission
|
||||
return grantedDevices.some((grantedDevice) => {
|
||||
return grantedDevice.vendorId === details.device.vendorId &&
|
||||
grantedDevice.productId === details.device.productId &&
|
||||
grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
|
||||
})
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
win.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
event.preventDefault()
|
||||
const selectedDevice = details.deviceList.find((device) => {
|
||||
return device.vendorId === '9025' && device.productId === '67'
|
||||
})
|
||||
callback(selectedPort?.deviceId)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### Event: 'hid-device-added'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `details` Object
|
||||
* `device` [HIDDevice[]](structures/hid-device.md)
|
||||
* `frame` [WebFrameMain](web-frame-main.md)
|
||||
|
||||
Emitted when a new HID device becomes available. For example, when a new USB device is plugged in.
|
||||
|
||||
This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired.
|
||||
|
||||
#### Event: 'hid-device-removed'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `details` Object
|
||||
* `device` [HIDDevice[]](structures/hid-device.md)
|
||||
* `frame` [WebFrameMain](web-frame-main.md)
|
||||
|
||||
Emitted when a HID device has been removed. For example, this event will fire when a USB device is unplugged.
|
||||
|
||||
This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired.
|
||||
|
||||
#### Event: 'select-serial-port'
|
||||
|
||||
Returns:
|
||||
@@ -525,7 +615,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents
|
||||
|
||||
* `handler` Function\<Boolean> | null
|
||||
* `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. All cross origin sub frames making permission checks will pass a `null` webContents to this handler, while certain other permission checks such as `notifications` checks will always pass `null`. You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively.
|
||||
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, or `serial`.
|
||||
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`.
|
||||
* `requestingOrigin` String - The origin URL of the permission check
|
||||
* `details` Object - Some properties are only available on certain permission types.
|
||||
* `embeddingOrigin` String (optional) - The origin of the frame embedding the frame that made the permission check. Only set for cross-origin sub frames making permission checks.
|
||||
@@ -553,6 +643,71 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
|
||||
})
|
||||
```
|
||||
|
||||
#### `ses.setDevicePermissionHandler(handler)`
|
||||
|
||||
* `handler` Function\<Boolean> | null
|
||||
* `details` Object
|
||||
* `deviceType` String - The type of device that permission is being requested on, can be `hid`.
|
||||
* `origin` String - The origin URL of the device permission check.
|
||||
* `device` [HIDDevice](structures/hid-device.md) - the device that permission is being requested for.
|
||||
* `frame` [WebFrameMain](web-frame-main.md) - WebFrameMain checking the device permission.
|
||||
|
||||
Sets the handler which can be used to respond to device permission checks for the `session`.
|
||||
Returning `true` will allow the device to be permitted and `false` will reject it.
|
||||
To clear the handler, call `setDevicePermissionHandler(null)`.
|
||||
This handler can be used to provide default permissioning to devices without first calling for permission
|
||||
to devices (eg via `navigator.hid.requestDevice`). If this handler is not defined, the default device
|
||||
permissions as granted through device selection (eg via `navigator.hid.requestDevice`) will be used.
|
||||
Additionally, the default behavior of Electron is to store granted device permision through the lifetime
|
||||
of the corresponding WebContents. If longer term storage is needed, a developer can store granted device
|
||||
permissions (eg when handling the `select-hid-device` event) and then read from that storage with `setDevicePermissionHandler`.
|
||||
|
||||
```javascript
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
let win = null
|
||||
|
||||
app.whenReady().then(() => {
|
||||
win = new BrowserWindow()
|
||||
|
||||
win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
if (permission === 'hid') {
|
||||
// Add logic here to determine if permission should be given to allow HID selection
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Optionally, retrieve previously persisted devices from a persistent store
|
||||
const grantedDevices = fetchGrantedDevices()
|
||||
|
||||
win.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
|
||||
if (details.device.vendorId === 123 && details.device.productId === 345) {
|
||||
// Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first)
|
||||
return true
|
||||
}
|
||||
|
||||
// Search through the list of devices that have previously been granted permission
|
||||
return grantedDevices.some((grantedDevice) => {
|
||||
return grantedDevice.vendorId === details.device.vendorId &&
|
||||
grantedDevice.productId === details.device.productId &&
|
||||
grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
|
||||
})
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
win.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
event.preventDefault()
|
||||
const selectedDevice = details.deviceList.find((device) => {
|
||||
return device.vendorId === '9025' && device.productId === '67'
|
||||
})
|
||||
callback(selectedPort?.deviceId)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### `ses.clearHostResolverCache()`
|
||||
|
||||
Returns `Promise<void>` - Resolves when the operation is complete.
|
||||
|
||||
8
docs/api/structures/hid-device.md
Normal file
8
docs/api/structures/hid-device.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# HIDDevice Object
|
||||
|
||||
* `deviceId` String - Unique identifier for the device.
|
||||
* `name` String - Name of the device.
|
||||
* `vendorId` Integer - The USB vendor ID.
|
||||
* `productId` Integer - The USB product ID.
|
||||
* `serialNumber` String (optional) - The USB device serial number.
|
||||
* `guid` String (optional) - Unique identifier for the HID interface. A device may have multiple HID interfaces.
|
||||
@@ -1827,7 +1827,8 @@ End subscribing for frame presentation events.
|
||||
#### `contents.startDrag(item)`
|
||||
|
||||
* `item` Object
|
||||
* `file` String[] | String - The path(s) to the file(s) being dragged.
|
||||
* `file` String - The path to the file being dragged.
|
||||
* `files` String[] (optional) - The paths to the files being dragged. (`files` will override `file` field)
|
||||
* `icon` [NativeImage](native-image.md) | String - The image must be
|
||||
non-empty on macOS.
|
||||
|
||||
|
||||
17
docs/fiddles/features/web-bluetooth/index.html
Normal file
17
docs/fiddles/features/web-bluetooth/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
||||
<title>Web Bluetooth API</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Web Bluetooth API</h1>
|
||||
|
||||
<button id="clickme">Test Bluetooth</button>
|
||||
|
||||
<p>Currently selected bluetooth device: <strong id="device-name""></strong></p>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
docs/fiddles/features/web-bluetooth/main.js
Normal file
30
docs/fiddles/features/web-bluetooth/main.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
function createWindow () {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
|
||||
event.preventDefault()
|
||||
if (deviceList && deviceList.length > 0) {
|
||||
callback(deviceList[0].deviceId)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadFile('index.html')
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
8
docs/fiddles/features/web-bluetooth/renderer.js
Normal file
8
docs/fiddles/features/web-bluetooth/renderer.js
Normal file
@@ -0,0 +1,8 @@
|
||||
async function testIt() {
|
||||
const device = await navigator.bluetooth.requestDevice({
|
||||
acceptAllDevices: true
|
||||
})
|
||||
document.getElementById('device-name').innerHTML = device.name || `ID: ${device.id}`
|
||||
}
|
||||
|
||||
document.getElementById('clickme').addEventListener('click',testIt)
|
||||
21
docs/fiddles/features/web-hid/index.html
Normal file
21
docs/fiddles/features/web-hid/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
||||
<title>WebHID API</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebHID API</h1>
|
||||
|
||||
<button id="clickme">Test WebHID</button>
|
||||
|
||||
<h3>HID devices automatically granted access via <i>setDevicePermissionHandler</i></h3>
|
||||
<div id="granted-devices"></div>
|
||||
|
||||
<h3>HID devices automatically granted access via <i>select-hid-device</i></h3>
|
||||
<div id="granted-devices2"></div>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
50
docs/fiddles/features/web-hid/main.js
Normal file
50
docs/fiddles/features/web-hid/main.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
function createWindow () {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
event.preventDefault()
|
||||
if (details.deviceList && details.deviceList.length > 0) {
|
||||
callback(details.deviceList[0].deviceId)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('hid-device-added', (event, device) => {
|
||||
console.log('hid-device-added FIRED WITH', device)
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('hid-device-removed', (event, device) => {
|
||||
console.log('hid-device-removed FIRED WITH', device)
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
if (permission === 'hid' && details.securityOrigin === 'file:///') {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (details.deviceType === 'hid' && details.origin === 'file://') {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadFile('index.html')
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
19
docs/fiddles/features/web-hid/renderer.js
Normal file
19
docs/fiddles/features/web-hid/renderer.js
Normal file
@@ -0,0 +1,19 @@
|
||||
async function testIt() {
|
||||
const grantedDevices = await navigator.hid.getDevices()
|
||||
let grantedDeviceList = ''
|
||||
grantedDevices.forEach(device => {
|
||||
grantedDeviceList += `<hr>${device.productName}</hr>`
|
||||
})
|
||||
document.getElementById('granted-devices').innerHTML = grantedDeviceList
|
||||
const grantedDevices2 = await navigator.hid.requestDevice({
|
||||
filters: []
|
||||
})
|
||||
|
||||
grantedDeviceList = ''
|
||||
grantedDevices2.forEach(device => {
|
||||
grantedDeviceList += `<hr>${device.productName}</hr>`
|
||||
})
|
||||
document.getElementById('granted-devices2').innerHTML = grantedDeviceList
|
||||
}
|
||||
|
||||
document.getElementById('clickme').addEventListener('click',testIt)
|
||||
16
docs/fiddles/features/web-serial/index.html
Normal file
16
docs/fiddles/features/web-serial/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
||||
<title>Web Serial API</title>
|
||||
<body>
|
||||
<h1>Web Serial API</h1>
|
||||
|
||||
<button id="clickme">Test Web Serial API</button>
|
||||
|
||||
<p>Matching Arduino Uno device: <strong id="device-name""></strong></p>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
docs/fiddles/features/web-serial/main.js
Normal file
54
docs/fiddles/features/web-serial/main.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
function createWindow () {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
|
||||
event.preventDefault()
|
||||
if (portList && portList.length > 0) {
|
||||
callback(portList[0].portId)
|
||||
} else {
|
||||
callback('') //Could not find any matching devices
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('serial-port-added', (event, port) => {
|
||||
console.log('serial-port-added FIRED WITH', port)
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('serial-port-removed', (event, port) => {
|
||||
console.log('serial-port-removed FIRED WITH', port)
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
if (permission === 'serial' && details.securityOrigin === 'file:///') {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (details.deviceType === 'serial' && details.origin === 'file://') {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadFile('index.html')
|
||||
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
19
docs/fiddles/features/web-serial/renderer.js
Normal file
19
docs/fiddles/features/web-serial/renderer.js
Normal file
@@ -0,0 +1,19 @@
|
||||
async function testIt() {
|
||||
const filters = [
|
||||
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
|
||||
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
|
||||
];
|
||||
try {
|
||||
const port = await navigator.serial.requestPort({filters});
|
||||
const portInfo = port.getInfo();
|
||||
document.getElementById('device-name').innerHTML = `vendorId: ${portInfo.usbVendorId} | productId: ${portInfo.usbProductId} `
|
||||
} catch (ex) {
|
||||
if (ex.name === 'NotFoundError') {
|
||||
document.getElementById('device-name').innerHTML = 'Device NOT found'
|
||||
} else {
|
||||
document.getElementById('device-name').innerHTML = ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('clickme').addEventListener('click',testIt)
|
||||
99
docs/tutorial/devices.md
Normal file
99
docs/tutorial/devices.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Device Access
|
||||
|
||||
Like Chromium based browsers, Electron provides access to device hardware
|
||||
through web APIs. For the most part these APIs work like they do in a browser,
|
||||
but there are some differences that need to be taken into account. The primary
|
||||
difference between Electron and browsers is what happens when device access is
|
||||
requested. In a browser, users are presented with a popup where they can grant
|
||||
access to an individual device. In Electron APIs are provided which can be
|
||||
used by a developer to either automatically pick a device or prompt users to
|
||||
pick a device via a developer created interface.
|
||||
|
||||
## Web Bluetooth API
|
||||
|
||||
The [Web Bluetooth API](https://web.dev/bluetooth/) can be used to communicate
|
||||
with bluetooth devices. In order to use this API in Electron, developers will
|
||||
need to handle the [`select-bluetooth-device` event on the webContents](../api/web-contents.md#event-select-bluetooth-device)
|
||||
associated with the device request.
|
||||
|
||||
### Example
|
||||
|
||||
This example demonstrates an Electron application that automatically selects
|
||||
the first available bluetooth device when the `Test Bluetooth` button is
|
||||
clicked.
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/web-bluetooth'
|
||||
|
||||
```
|
||||
|
||||
## WebHID API
|
||||
|
||||
The [WebHID API](https://web.dev/hid/) can be used to access HID devices such
|
||||
as keyboards and gamepads. Electron provides several APIs for working with
|
||||
the WebHID API:
|
||||
|
||||
* The [`select-hid-device` event on the Session](../api/session.md#event-select-hid-device)
|
||||
can be used to select a HID device when a call to
|
||||
`navigator.hid.requestDevice` is made. Additionally the [`hid-device-added`](../api/session.md#event-hid-device-added)
|
||||
and [`hid-device-removed`](../api/session.md#event-hid-device-removed) events
|
||||
on the Session can be used to handle devices being plugged in or unplugged during the
|
||||
`navigator.hid.requestDevice` process.
|
||||
* [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
|
||||
can be used to provide default permissioning to devices without first calling
|
||||
for permission to devices via `navigator.hid.requestDevice`. Additionally,
|
||||
the default behavior of Electron is to store granted device permision through
|
||||
the lifetime of the corresponding WebContents. If longer term storage is
|
||||
needed, a developer can store granted device permissions (eg when handling
|
||||
the `select-hid-device` event) and then read from that storage with
|
||||
`setDevicePermissionHandler`.
|
||||
* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
|
||||
can be used to disable HID access for specific origins.
|
||||
|
||||
### Blocklist
|
||||
|
||||
By default Electron employs the same [blocklist](https://github.com/WICG/webhid/blob/main/blocklist.txt)
|
||||
used by Chromium. If you wish to override this behavior, you can do so by
|
||||
setting the `disable-hid-blocklist` flag:
|
||||
|
||||
```javascript
|
||||
app.commandLine.appendSwitch('disable-hid-blocklist')
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
This example demonstrates an Electron application that automatically selects
|
||||
HID devices through [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
|
||||
and through [`select-hid-device` event on the Session](../api/session.md#event-select-hid-device)
|
||||
when the `Test WebHID` button is clicked.
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/web-hid'
|
||||
|
||||
```
|
||||
|
||||
## Web Serial API
|
||||
|
||||
The [Web Serial API](https://web.dev/serial/) can be used to access serial
|
||||
devices that are connected via serial port, USB, or Bluetooth. In order to use
|
||||
this API in Electron, developers will need to handle the
|
||||
[`select-serial-port` event on the Session](../api/session.md#event-select-serial-port)
|
||||
associated with the serial port request.
|
||||
|
||||
There are several additional APIs for working with the Web Serial API:
|
||||
|
||||
* The [`serial-port-added`](../api/session.md#event-serial-port-added)
|
||||
and [`serial-port-removed`](../api/session.md#event-serial-port-removed) events
|
||||
on the Session can be used to handle devices being plugged in or unplugged during the
|
||||
`navigator.serial.requestPort` process.
|
||||
* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
|
||||
can be used to disable serial access for specific origins.
|
||||
|
||||
### Example
|
||||
|
||||
This example demonstrates an Electron application that automatically selects
|
||||
the first available Arduino Uno serial device (if connected) through
|
||||
[`select-serial-port` event on the Session](../api/session.md#event-select-serial-port)
|
||||
when the `Test Web Serial` button is clicked.
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/web-serial'
|
||||
|
||||
```
|
||||
@@ -70,9 +70,9 @@ until the maintainers feel the maintenance burden is too high to continue doing
|
||||
|
||||
### Currently supported versions
|
||||
|
||||
* 17.x.y
|
||||
* 16.x.y
|
||||
* 15.x.y
|
||||
* 14.x.y
|
||||
* 13
|
||||
|
||||
### End-of-life
|
||||
|
||||
@@ -125,5 +125,7 @@
|
||||
</message>
|
||||
<message name="IDS_BADGE_UNREAD_NOTIFICATIONS" desc="The accessibility text which will be read by a screen reader when there are notifcatications">
|
||||
{UNREAD_NOTIFICATIONS, plural, =1 {1 Unread Notification} other {# Unread Notifications}}
|
||||
</message>
|
||||
</message>
|
||||
<message name="IDS_HID_CHOOSER_ITEM_WITHOUT_NAME" desc="User option displaying the device IDs for a Human Interface Device (HID) without a device name.">
|
||||
Unknown Device (<ph name="DEVICE_ID">$1<ex>1234:abcd</ex></ph>) </message>
|
||||
</grit-part>
|
||||
|
||||
@@ -84,6 +84,7 @@ auto_filenames = {
|
||||
"docs/api/structures/file-filter.md",
|
||||
"docs/api/structures/file-path-with-headers.md",
|
||||
"docs/api/structures/gpu-feature-status.md",
|
||||
"docs/api/structures/hid-device.md",
|
||||
"docs/api/structures/input-event.md",
|
||||
"docs/api/structures/io-counters.md",
|
||||
"docs/api/structures/ipc-main-event.md",
|
||||
|
||||
@@ -387,6 +387,14 @@ filenames = {
|
||||
"shell/browser/font/electron_font_access_delegate.h",
|
||||
"shell/browser/font_defaults.cc",
|
||||
"shell/browser/font_defaults.h",
|
||||
"shell/browser/hid/electron_hid_delegate.cc",
|
||||
"shell/browser/hid/electron_hid_delegate.h",
|
||||
"shell/browser/hid/hid_chooser_context.cc",
|
||||
"shell/browser/hid/hid_chooser_context.h",
|
||||
"shell/browser/hid/hid_chooser_context_factory.cc",
|
||||
"shell/browser/hid/hid_chooser_context_factory.h",
|
||||
"shell/browser/hid/hid_chooser_controller.cc",
|
||||
"shell/browser/hid/hid_chooser_controller.h",
|
||||
"shell/browser/javascript_environment.cc",
|
||||
"shell/browser/javascript_environment.h",
|
||||
"shell/browser/lib/bluetooth_chooser.cc",
|
||||
|
||||
@@ -168,6 +168,7 @@ const messageBox = (sync: boolean, window: BrowserWindow | null, options?: Messa
|
||||
defaultId = -1,
|
||||
detail = '',
|
||||
icon = null,
|
||||
textWidth = 0,
|
||||
noLink = false,
|
||||
message = '',
|
||||
title = '',
|
||||
@@ -225,7 +226,8 @@ const messageBox = (sync: boolean, window: BrowserWindow | null, options?: Messa
|
||||
detail,
|
||||
checkboxLabel,
|
||||
checkboxChecked,
|
||||
icon
|
||||
icon,
|
||||
textWidth
|
||||
};
|
||||
|
||||
if (sync) {
|
||||
|
||||
@@ -666,9 +666,9 @@ WebContents.prototype._init = function () {
|
||||
postBody
|
||||
};
|
||||
windowOpenOverriddenOptions = this._callWindowOpenHandler(event, details);
|
||||
// if attempting to use this API with the deprecated window.open event,
|
||||
// if attempting to use this API with the deprecated new-window event,
|
||||
// windowOpenOverriddenOptions will always return null. This ensures
|
||||
// short-term backwards compatibility until window.open is removed.
|
||||
// short-term backwards compatibility until new-window is removed.
|
||||
const parsedFeatures = parseFeatures(rawFeatures);
|
||||
const overriddenFeatures: BrowserWindowConstructorOptions = {
|
||||
...parsedFeatures.options,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "electron",
|
||||
"version": "16.0.0-nightly.20210922",
|
||||
"version": "17.0.0-nightly.20210924",
|
||||
"repository": "https://github.com/electron/electron",
|
||||
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -100,7 +100,6 @@ build_do_not_depend_on_packed_resource_integrity.patch
|
||||
refactor_restore_base_adaptcallbackforrepeating.patch
|
||||
hack_to_allow_gclient_sync_with_host_os_mac_on_linux_in_ci.patch
|
||||
don_t_run_pcscan_notifythreadcreated_if_pcscan_is_disabled.patch
|
||||
add_gin_wrappable_crash_key.patch
|
||||
logging_win32_only_create_a_console_if_logging_to_stderr.patch
|
||||
fix_media_key_usage_with_globalshortcuts.patch
|
||||
feat_expose_raw_response_headers_from_urlloader.patch
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: VerteDinde <khammond@slack-corp.com>
|
||||
Date: Thu, 15 Jul 2021 12:16:50 -0700
|
||||
Subject: chore: add gin::wrappable wrapperinfo crash key
|
||||
|
||||
This patch adds an additional crash key for gin::Wrappable, to help
|
||||
debug a crash that is occurring during garbage collection in SecondWeakCallback.
|
||||
|
||||
The crash seems to be due to a class that is holding a reference to
|
||||
gin::Wrappable even after being deleted. This added crash key compares
|
||||
the soon-to-be-deleted WrapperInfo with known WrapperInfo components to
|
||||
help determine where the crash originated from.
|
||||
|
||||
This patch should not be upstreamed, and can be removed in Electron 15 and
|
||||
beyond once we identify the cause of the crash.
|
||||
|
||||
diff --git a/gin/BUILD.gn b/gin/BUILD.gn
|
||||
index c6059fdb0e0f74ee3ef78c5517634ed5a36f1b10..e16b2ec43b98c3b8724fd85085a33fe52a1e1979 100644
|
||||
--- a/gin/BUILD.gn
|
||||
+++ b/gin/BUILD.gn
|
||||
@@ -88,6 +88,10 @@ component("gin") {
|
||||
frameworks = [ "CoreFoundation.framework" ]
|
||||
}
|
||||
|
||||
+ if (!is_mas_build) {
|
||||
+ public_deps += [ "//components/crash/core/common:crash_key" ]
|
||||
+ }
|
||||
+
|
||||
configs += [
|
||||
"//tools/v8_context_snapshot:use_v8_context_snapshot",
|
||||
"//v8:external_startup_data",
|
||||
diff --git a/gin/wrappable.cc b/gin/wrappable.cc
|
||||
index fe07eb94a8e679859bba6d76ff0d6ee86bd0c67e..ecb0aa2c4ec57e1814f4c94194e775440f4e35ee 100644
|
||||
--- a/gin/wrappable.cc
|
||||
+++ b/gin/wrappable.cc
|
||||
@@ -8,6 +8,11 @@
|
||||
#include "gin/object_template_builder.h"
|
||||
#include "gin/per_isolate_data.h"
|
||||
|
||||
+#if !defined(MAS_BUILD)
|
||||
+#include "components/crash/core/common/crash_key.h"
|
||||
+#include "electron/shell/common/crash_keys.h"
|
||||
+#endif
|
||||
+
|
||||
namespace gin {
|
||||
|
||||
WrappableBase::WrappableBase() = default;
|
||||
@@ -36,6 +41,15 @@ void WrappableBase::FirstWeakCallback(
|
||||
void WrappableBase::SecondWeakCallback(
|
||||
const v8::WeakCallbackInfo<WrappableBase>& data) {
|
||||
WrappableBase* wrappable = data.GetParameter();
|
||||
+
|
||||
+#if !defined(MAS_BUILD)
|
||||
+ WrapperInfo* wrapperInfo = static_cast<WrapperInfo*>(data.GetInternalField(0));
|
||||
+ std::string location = electron::crash_keys::GetCrashValueForGinWrappable(wrapperInfo);
|
||||
+
|
||||
+ static crash_reporter::CrashKeyString<32> crash_key("gin-wrappable-fatal.location");
|
||||
+ crash_reporter::ScopedCrashKeyString auto_clear(&crash_key, location);
|
||||
+#endif
|
||||
+
|
||||
delete wrappable;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,26 @@ This should be upstreamed in some form, though it may need to be tweaked
|
||||
before it's acceptable to upstream, as this patch comments out a couple
|
||||
of tests that upstream probably cares about.
|
||||
|
||||
diff --git a/test/parallel/test-crypto-async-sign-verify.js b/test/parallel/test-crypto-async-sign-verify.js
|
||||
index 4e3c32fdcd23fbe3e74bd5e624b739d224689f33..19d65aae7fa8ec9f9b907733ead17a208ed47909 100644
|
||||
--- a/test/parallel/test-crypto-async-sign-verify.js
|
||||
+++ b/test/parallel/test-crypto-async-sign-verify.js
|
||||
@@ -88,6 +88,7 @@ test('rsa_public.pem', 'rsa_private.pem', 'sha256', false,
|
||||
// ED25519
|
||||
test('ed25519_public.pem', 'ed25519_private.pem', undefined, true);
|
||||
// ED448
|
||||
+/*
|
||||
test('ed448_public.pem', 'ed448_private.pem', undefined, true);
|
||||
|
||||
// ECDSA w/ der signature encoding
|
||||
@@ -109,6 +110,7 @@ test('dsa_public.pem', 'dsa_private.pem', 'sha256',
|
||||
// DSA w/ ieee-p1363 signature encoding
|
||||
test('dsa_public.pem', 'dsa_private.pem', 'sha256', false,
|
||||
{ dsaEncoding: 'ieee-p1363' });
|
||||
+*/
|
||||
|
||||
// Test Parallel Execution w/ KeyObject is threadsafe in openssl3
|
||||
{
|
||||
diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js
|
||||
index 21c5af6cfe3e5eef64fc2d4dcc63c55b1d79ad51..b21eb4b97ad778304b3a4e8d549e109614350dfb 100644
|
||||
--- a/test/parallel/test-crypto-authenticated.js
|
||||
|
||||
@@ -250,19 +250,6 @@ index 1bbf9a1753e4e2d82c55c4187489c22867d1d9bb..585af1674e129dc4d1c918d29fe9915b
|
||||
}
|
||||
|
||||
if (target
|
||||
diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc
|
||||
index 7846df17ffbe8b5ea3a685c46f73b5d28ad64b1f..2bf12b8b4a7e16adf9c1f58d72ae4f59a0b2b2a4 100644
|
||||
--- a/src/crypto/crypto_sig.cc
|
||||
+++ b/src/crypto/crypto_sig.cc
|
||||
@@ -110,7 +110,7 @@ unsigned int GetBytesOfRS(const ManagedEVPPKey& pkey) {
|
||||
if (base_id == EVP_PKEY_DSA) {
|
||||
const DSA* dsa_key = EVP_PKEY_get0_DSA(pkey.get());
|
||||
// Both r and s are computed mod q, so their width is limited by that of q.
|
||||
- bits = BN_num_bits(DSA_get0_q(dsa_key));
|
||||
+ bits = BN_num_bits(dsa_key->q);
|
||||
} else if (base_id == EVP_PKEY_EC) {
|
||||
const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(pkey.get());
|
||||
const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key);
|
||||
diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc
|
||||
index f18304cd655842e999a39659315c4eb3ce1c0c6e..1aed0e7e88460cea63950f71dac502829d662cff 100644
|
||||
--- a/src/crypto/crypto_util.cc
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
"abort/test-abort-backtrace",
|
||||
"async-hooks/test-crypto-pbkdf2",
|
||||
"async-hooks/test-crypto-randomBytes",
|
||||
"message/source_map_throw_catch",
|
||||
"message/source_map_throw_first_tick",
|
||||
"message/source_map_throw_set_immediate",
|
||||
"message/v8_warning",
|
||||
"parallel/test-assert",
|
||||
"parallel/test-bootstrap-modules",
|
||||
"parallel/test-buffer-backing-arraybuffer",
|
||||
"parallel/test-buffer-constructor-node-modules-paths",
|
||||
@@ -18,7 +13,6 @@
|
||||
"parallel/test-cluster-shared-handle-bind-privileged-port",
|
||||
"parallel/test-code-cache",
|
||||
"parallel/test-crypto-aes-wrap",
|
||||
"parallel/test-crypto-async-sign-verify",
|
||||
"parallel/test-crypto-authenticated-stream",
|
||||
"parallel/test-crypto-certificate",
|
||||
"parallel/test-crypto-des3-wrap",
|
||||
@@ -28,35 +22,18 @@
|
||||
"parallel/test-crypto-fips",
|
||||
"parallel/test-crypto-hkdf.js",
|
||||
"parallel/test-crypto-secure-heap",
|
||||
"parallel/test-debug-args",
|
||||
"parallel/test-debug-usage",
|
||||
"parallel/test-debugger-pid",
|
||||
"parallel/test-domain-abort-on-uncaught",
|
||||
"parallel/test-domain-with-abort-on-uncaught-exception",
|
||||
"parallel/test-finalization-group-error",
|
||||
"parallel/test-freeze-intrinsics",
|
||||
"parallel/test-fs-utimes-y2K38",
|
||||
"parallel/test-gc-tls-external-memory",
|
||||
"parallel/test-http2-clean-output",
|
||||
"parallel/test-http2-reset-flood",
|
||||
"parallel/test-https-agent-session-reuse",
|
||||
"parallel/test-https-options-boolean-check",
|
||||
"parallel/test-inspector-inspect-brk-node",
|
||||
"parallel/test-inspector-multisession-ws",
|
||||
"parallel/test-inspector-port-zero-cluster",
|
||||
"parallel/test-inspector-tracing-domain",
|
||||
"parallel/test-inspector-vm-global-accessors-getter-sideeffect",
|
||||
"parallel/test-inspector-vm-global-accessors-sideeffects",
|
||||
"parallel/test-module-loading-globalpaths",
|
||||
"parallel/test-module-run-main-monkey-patch",
|
||||
"parallel/test-module-version",
|
||||
"parallel/test-openssl-ca-options",
|
||||
"parallel/test-policy-integrity",
|
||||
"parallel/test-process-env-allowed-flags-are-documented",
|
||||
"parallel/test-process-external-stdio-close",
|
||||
"parallel/test-process-external-stdio-close-spawn",
|
||||
"parallel/test-process-versions",
|
||||
"parallel/test-readline-interface",
|
||||
"parallel/test-repl",
|
||||
"parallel/test-repl-harmony",
|
||||
"parallel/test-repl-pretty-custom-stack",
|
||||
@@ -134,14 +111,8 @@
|
||||
"parallel/test-trace-events-v8",
|
||||
"parallel/test-trace-events-vm",
|
||||
"parallel/test-trace-events-worker-metadata",
|
||||
"parallel/test-util-inspect",
|
||||
"parallel/test-v8-coverage",
|
||||
"parallel/test-v8-flags",
|
||||
"parallel/test-v8-untrusted-code-mitigations",
|
||||
"parallel/test-vm-module-basic",
|
||||
"parallel/test-vm-parse-abort-on-uncaught-exception",
|
||||
"parallel/test-vm-sigint-existing-handler",
|
||||
"parallel/test-vm-timeout",
|
||||
"parallel/test-webcrypto-derivebits-hkdf",
|
||||
"parallel/test-webcrypto-derivebits-node-dh",
|
||||
"parallel/test-webcrypto-ed25519-ed448",
|
||||
@@ -153,17 +124,8 @@
|
||||
"parallel/test-webcrypto-sign-verify-node-dsa",
|
||||
"parallel/test-webcrypto-x25519-x448",
|
||||
"parallel/test-whatwg-encoding-custom-textdecoder",
|
||||
"parallel/test-worker-message-channel",
|
||||
"parallel/test-worker-message-port",
|
||||
"parallel/test-worker-message-port-transfer-duplicate",
|
||||
"parallel/test-worker-message-port-transfer-target",
|
||||
"parallel/test-worker-resource-limits",
|
||||
"parallel/test-worker-sharedarraybuffer-from-worker-thread",
|
||||
"parallel/test-worker-stdio",
|
||||
"parallel/test-worker-workerdata-messageport",
|
||||
"parallel/test-zlib-unused-weak",
|
||||
"pseudo-tty/test-set-raw-mode-reset-process-exit",
|
||||
"pseudo-tty/test-set-raw-mode-reset-signal",
|
||||
"report/test-report-fatal-error",
|
||||
"report/test-report-getreport",
|
||||
"report/test-report-signal",
|
||||
@@ -172,13 +134,7 @@
|
||||
"report/test-report-uv-handles",
|
||||
"report/test-report-worker",
|
||||
"report/test-report-writereport",
|
||||
"sequential/test-inspector-contexts",
|
||||
"sequential/test-inspector-port-zero",
|
||||
"sequential/test-inspector-resource-name-to-url",
|
||||
"sequential/test-inspector-stress-http",
|
||||
"sequential/test-perf-hooks",
|
||||
"sequential/test-tls-connect",
|
||||
"sequential/test-vm-timeout-rethrow",
|
||||
"sequential/test-worker-prof",
|
||||
"wpt/test-encoding",
|
||||
"wpt/test-webcrypto"
|
||||
|
||||
@@ -644,6 +644,18 @@ void Session::SetPermissionCheckHandler(v8::Local<v8::Value> val,
|
||||
permission_manager->SetPermissionCheckHandler(handler);
|
||||
}
|
||||
|
||||
void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
|
||||
gin::Arguments* args) {
|
||||
ElectronPermissionManager::DeviceCheckHandler handler;
|
||||
if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &handler))) {
|
||||
args->ThrowTypeError("Must pass null or function");
|
||||
return;
|
||||
}
|
||||
auto* permission_manager = static_cast<ElectronPermissionManager*>(
|
||||
browser_context()->GetPermissionControllerDelegate());
|
||||
permission_manager->SetDevicePermissionHandler(handler);
|
||||
}
|
||||
|
||||
v8::Local<v8::Promise> Session::ClearHostResolverCache(gin::Arguments* args) {
|
||||
v8::Isolate* isolate = args->isolate();
|
||||
gin_helper::Promise<void> promise(isolate);
|
||||
@@ -1148,6 +1160,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
|
||||
&Session::SetPermissionRequestHandler)
|
||||
.SetMethod("setPermissionCheckHandler",
|
||||
&Session::SetPermissionCheckHandler)
|
||||
.SetMethod("setDevicePermissionHandler",
|
||||
&Session::SetDevicePermissionHandler)
|
||||
.SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
|
||||
.SetMethod("clearAuthCache", &Session::ClearAuthCache)
|
||||
.SetMethod("allowNTLMCredentialsForDomains",
|
||||
|
||||
@@ -104,6 +104,8 @@ class Session : public gin::Wrappable<Session>,
|
||||
gin::Arguments* args);
|
||||
void SetPermissionCheckHandler(v8::Local<v8::Value> val,
|
||||
gin::Arguments* args);
|
||||
void SetDevicePermissionHandler(v8::Local<v8::Value> val,
|
||||
gin::Arguments* args);
|
||||
v8::Local<v8::Promise> ClearHostResolverCache(gin::Arguments* args);
|
||||
v8::Local<v8::Promise> ClearAuthCache();
|
||||
void AllowNTLMCredentialsForDomains(const std::string& domains);
|
||||
|
||||
@@ -918,6 +918,12 @@ void WebContents::InitWithWebContents(
|
||||
}
|
||||
|
||||
WebContents::~WebContents() {
|
||||
// clear out objects that have been granted permissions so that when
|
||||
// WebContents::RenderFrameDeleted is called as a result of WebContents
|
||||
// destruction it doesn't try to clear out a granted_devices_
|
||||
// on a destructed object.
|
||||
granted_devices_.clear();
|
||||
|
||||
if (!inspectable_web_contents_) {
|
||||
WebContentsDestroyed();
|
||||
return;
|
||||
@@ -1427,6 +1433,12 @@ void WebContents::RenderFrameDeleted(
|
||||
// - Cross-origin navigation creates a new RFH in a separate process which
|
||||
// is swapped by content::RenderFrameHostManager.
|
||||
//
|
||||
|
||||
// clear out objects that have been granted permissions
|
||||
if (!granted_devices_.empty()) {
|
||||
granted_devices_.erase(render_frame_host->GetFrameTreeNodeId());
|
||||
}
|
||||
|
||||
// WebFrameMain::FromRenderFrameHost(rfh) will use the RFH's FrameTreeNode ID
|
||||
// to find an existing instance of WebFrameMain. During a cross-origin
|
||||
// navigation, the deleted RFH will be the old host which was swapped out. In
|
||||
@@ -3237,6 +3249,42 @@ v8::Local<v8::Promise> WebContents::TakeHeapSnapshot(
|
||||
return handle;
|
||||
}
|
||||
|
||||
void WebContents::GrantDevicePermission(
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::PermissionType permissionType,
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
granted_devices_[render_frame_host->GetFrameTreeNodeId()][permissionType]
|
||||
[origin]
|
||||
.push_back(
|
||||
std::make_unique<base::Value>(device->Clone()));
|
||||
}
|
||||
|
||||
std::vector<base::Value> WebContents::GetGrantedDevices(
|
||||
const url::Origin& origin,
|
||||
content::PermissionType permissionType,
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
const auto& devices_for_frame_host_it =
|
||||
granted_devices_.find(render_frame_host->GetFrameTreeNodeId());
|
||||
if (devices_for_frame_host_it == granted_devices_.end())
|
||||
return {};
|
||||
|
||||
const auto& current_devices_it =
|
||||
devices_for_frame_host_it->second.find(permissionType);
|
||||
if (current_devices_it == devices_for_frame_host_it->second.end())
|
||||
return {};
|
||||
|
||||
const auto& origin_devices_it = current_devices_it->second.find(origin);
|
||||
if (origin_devices_it == current_devices_it->second.end())
|
||||
return {};
|
||||
|
||||
std::vector<base::Value> results;
|
||||
for (const auto& object : origin_devices_it->second)
|
||||
results.push_back(object->Clone());
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void WebContents::UpdatePreferredSize(content::WebContents* web_contents,
|
||||
const gfx::Size& pref_size) {
|
||||
Emit("preferred-size-changed", pref_size);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "content/common/frame.mojom.h"
|
||||
#include "content/public/browser/devtools_agent_host.h"
|
||||
#include "content/public/browser/keyboard_event_processing_result.h"
|
||||
#include "content/public/browser/permission_type.h"
|
||||
#include "content/public/browser/render_widget_host.h"
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "content/public/browser/web_contents_delegate.h"
|
||||
@@ -91,6 +92,11 @@ class OffScreenWebContentsView;
|
||||
|
||||
namespace api {
|
||||
|
||||
using DevicePermissionMap = std::map<
|
||||
int,
|
||||
std::map<content::PermissionType,
|
||||
std::map<url::Origin, std::vector<std::unique_ptr<base::Value>>>>>;
|
||||
|
||||
// Wrapper around the content::WebContents.
|
||||
class WebContents : public gin::Wrappable<WebContents>,
|
||||
public gin_helper::EventEmitterMixin<WebContents>,
|
||||
@@ -419,6 +425,21 @@ class WebContents : public gin::Wrappable<WebContents>,
|
||||
electron::mojom::ElectronBrowser::DoGetZoomLevelCallback callback);
|
||||
void SetImageAnimationPolicy(const std::string& new_policy);
|
||||
|
||||
// Grants |origin| access to |device|.
|
||||
// To be used in place of ObjectPermissionContextBase::GrantObjectPermission.
|
||||
void GrantDevicePermission(const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::PermissionType permissionType,
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
|
||||
// Returns the list of devices that |origin| has been granted permission to
|
||||
// access. To be used in place of
|
||||
// ObjectPermissionContextBase::GetGrantedObjects.
|
||||
std::vector<base::Value> GetGrantedDevices(
|
||||
const url::Origin& origin,
|
||||
content::PermissionType permissionType,
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
|
||||
private:
|
||||
// Does not manage lifetime of |web_contents|.
|
||||
WebContents(v8::Isolate* isolate, content::WebContents* web_contents);
|
||||
@@ -762,6 +783,9 @@ class WebContents : public gin::Wrappable<WebContents>,
|
||||
// Stores the frame thats currently in fullscreen, nullptr if there is none.
|
||||
content::RenderFrameHost* fullscreen_frame_ = nullptr;
|
||||
|
||||
// In-memory cache that holds objects that have been granted permissions.
|
||||
DevicePermissionMap granted_devices_;
|
||||
|
||||
base::WeakPtrFactory<WebContents> weak_factory_{this};
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(WebContents);
|
||||
|
||||
@@ -1705,4 +1705,10 @@ device::GeolocationManager* ElectronBrowserClient::GetGeolocationManager() {
|
||||
#endif
|
||||
}
|
||||
|
||||
content::HidDelegate* ElectronBrowserClient::GetHidDelegate() {
|
||||
if (!hid_delegate_)
|
||||
hid_delegate_ = std::make_unique<ElectronHidDelegate>();
|
||||
return hid_delegate_.get();
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "services/metrics/public/cpp/ukm_source_id.h"
|
||||
#include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
|
||||
#include "shell/browser/font/electron_font_access_delegate.h"
|
||||
#include "shell/browser/hid/electron_hid_delegate.h"
|
||||
#include "shell/browser/serial/electron_serial_delegate.h"
|
||||
#include "third_party/blink/public/mojom/badging/badging.mojom-forward.h"
|
||||
|
||||
@@ -96,6 +97,8 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
||||
|
||||
content::BluetoothDelegate* GetBluetoothDelegate() override;
|
||||
|
||||
content::HidDelegate* GetHidDelegate() override;
|
||||
|
||||
device::GeolocationManager* GetGeolocationManager() override;
|
||||
|
||||
protected:
|
||||
@@ -309,6 +312,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
||||
std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
|
||||
std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
|
||||
std::unique_ptr<ElectronFontAccessDelegate> font_access_delegate_;
|
||||
std::unique_ptr<ElectronHidDelegate> hid_delegate_;
|
||||
|
||||
#if defined(OS_MAC)
|
||||
ElectronBrowserMainParts* browser_main_parts_ = nullptr;
|
||||
|
||||
@@ -17,9 +17,17 @@
|
||||
#include "content/public/browser/render_process_host.h"
|
||||
#include "content/public/browser/render_view_host.h"
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "gin/data_object_builder.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/electron_browser_client.h"
|
||||
#include "shell/browser/electron_browser_main_parts.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
#include "shell/browser/web_contents_permission_helper.h"
|
||||
#include "shell/browser/web_contents_preferences.h"
|
||||
#include "shell/common/gin_converters/content_converter.h"
|
||||
#include "shell/common/gin_converters/frame_converter.h"
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
#include "shell/common/gin_helper/event_emitter_caller.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
@@ -117,6 +125,11 @@ void ElectronPermissionManager::SetPermissionCheckHandler(
|
||||
check_handler_ = handler;
|
||||
}
|
||||
|
||||
void ElectronPermissionManager::SetDevicePermissionHandler(
|
||||
const DeviceCheckHandler& handler) {
|
||||
device_permission_handler_ = handler;
|
||||
}
|
||||
|
||||
void ElectronPermissionManager::RequestPermission(
|
||||
content::PermissionType permission,
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
@@ -282,6 +295,71 @@ bool ElectronPermissionManager::CheckPermissionWithDetails(
|
||||
mutable_details);
|
||||
}
|
||||
|
||||
bool ElectronPermissionManager::CheckDevicePermission(
|
||||
content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
api::WebContents* api_web_contents = api::WebContents::From(web_contents);
|
||||
if (device_permission_handler_.is_null()) {
|
||||
if (api_web_contents) {
|
||||
std::vector<base::Value> granted_devices =
|
||||
api_web_contents->GetGrantedDevices(origin, permission,
|
||||
render_frame_host);
|
||||
|
||||
for (const auto& granted_device : granted_devices) {
|
||||
if (permission ==
|
||||
static_cast<content::PermissionType>(
|
||||
WebContentsPermissionHelper::PermissionType::HID)) {
|
||||
if (device->FindIntKey(kHidVendorIdKey) !=
|
||||
granted_device.FindIntKey(kHidVendorIdKey) ||
|
||||
device->FindIntKey(kHidProductIdKey) !=
|
||||
granted_device.FindIntKey(kHidProductIdKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* serial_number =
|
||||
granted_device.FindStringKey(kHidSerialNumberKey);
|
||||
const auto* device_serial_number =
|
||||
device->FindStringKey(kHidSerialNumberKey);
|
||||
|
||||
if (serial_number && device_serial_number &&
|
||||
*device_serial_number == *serial_number)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
|
||||
.Set("deviceType", permission)
|
||||
.Set("origin", origin.Serialize())
|
||||
.Set("device", device->Clone())
|
||||
.Set("frame", render_frame_host)
|
||||
.Build();
|
||||
return device_permission_handler_.Run(details);
|
||||
}
|
||||
}
|
||||
|
||||
void ElectronPermissionManager::GrantDevicePermission(
|
||||
content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
if (device_permission_handler_.is_null()) {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
api::WebContents* api_web_contents = api::WebContents::From(web_contents);
|
||||
if (api_web_contents)
|
||||
api_web_contents->GrantDevicePermission(origin, device, permission,
|
||||
render_frame_host);
|
||||
}
|
||||
}
|
||||
|
||||
blink::mojom::PermissionStatus
|
||||
ElectronPermissionManager::GetPermissionStatusForFrame(
|
||||
content::PermissionType permission,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "base/callback.h"
|
||||
#include "base/containers/id_map.h"
|
||||
#include "content/public/browser/permission_controller_delegate.h"
|
||||
#include "gin/dictionary.h"
|
||||
|
||||
namespace base {
|
||||
class DictionaryValue;
|
||||
@@ -42,9 +43,13 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
|
||||
const GURL& requesting_origin,
|
||||
const base::Value&)>;
|
||||
|
||||
using DeviceCheckHandler =
|
||||
base::RepeatingCallback<bool(const v8::Local<v8::Object>&)>;
|
||||
|
||||
// Handler to dispatch permission requests in JS.
|
||||
void SetPermissionRequestHandler(const RequestHandler& handler);
|
||||
void SetPermissionCheckHandler(const CheckHandler& handler);
|
||||
void SetDevicePermissionHandler(const DeviceCheckHandler& handler);
|
||||
|
||||
// content::PermissionControllerDelegate:
|
||||
void RequestPermission(content::PermissionType permission,
|
||||
@@ -81,6 +86,16 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
|
||||
const GURL& requesting_origin,
|
||||
const base::DictionaryValue* details) const;
|
||||
|
||||
bool CheckDevicePermission(content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* object,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
void GrantDevicePermission(content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* object,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
protected:
|
||||
void OnPermissionResponse(int request_id,
|
||||
int permission_id,
|
||||
@@ -108,6 +123,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
|
||||
|
||||
RequestHandler request_handler_;
|
||||
CheckHandler check_handler_;
|
||||
DeviceCheckHandler device_permission_handler_;
|
||||
|
||||
PendingRequestsMap pending_requests_;
|
||||
|
||||
|
||||
163
shell/browser/hid/electron_hid_delegate.cc
Normal file
163
shell/browser/hid/electron_hid_delegate.cc
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2021 Microsoft, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/hid/electron_hid_delegate.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
#include "shell/browser/hid/hid_chooser_context_factory.h"
|
||||
#include "shell/browser/hid/hid_chooser_controller.h"
|
||||
#include "shell/browser/web_contents_permission_helper.h"
|
||||
|
||||
namespace {
|
||||
|
||||
electron::HidChooserContext* GetChooserContext(
|
||||
content::RenderFrameHost* frame) {
|
||||
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
|
||||
auto* browser_context = web_contents->GetBrowserContext();
|
||||
return electron::HidChooserContextFactory::GetForBrowserContext(
|
||||
browser_context);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace electron {
|
||||
|
||||
ElectronHidDelegate::ElectronHidDelegate() = default;
|
||||
|
||||
ElectronHidDelegate::~ElectronHidDelegate() = default;
|
||||
|
||||
std::unique_ptr<content::HidChooser> ElectronHidDelegate::RunChooser(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback) {
|
||||
electron::HidChooserContext* chooser_context =
|
||||
GetChooserContext(render_frame_host);
|
||||
if (!device_observation_.IsObserving())
|
||||
device_observation_.Observe(chooser_context);
|
||||
|
||||
HidChooserController* controller = ControllerForFrame(render_frame_host);
|
||||
if (controller) {
|
||||
DeleteControllerForFrame(render_frame_host);
|
||||
}
|
||||
AddControllerForFrame(render_frame_host, std::move(filters),
|
||||
std::move(callback));
|
||||
|
||||
// Return a nullptr because the return value isn't used for anything, eg
|
||||
// there is no mechanism to cancel navigator.hid.requestDevice(). The return
|
||||
// value is simply used in Chromium to cleanup the chooser UI once the serial
|
||||
// service is destroyed.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ElectronHidDelegate::CanRequestDevicePermission(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto* permission_helper =
|
||||
WebContentsPermissionHelper::FromWebContents(web_contents);
|
||||
return permission_helper->CheckHIDAccessPermission(
|
||||
web_contents->GetMainFrame()->GetLastCommittedOrigin());
|
||||
}
|
||||
|
||||
bool ElectronHidDelegate::HasDevicePermission(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto* chooser_context = GetChooserContext(render_frame_host);
|
||||
const auto& origin =
|
||||
render_frame_host->GetMainFrame()->GetLastCommittedOrigin();
|
||||
return chooser_context->HasDevicePermission(origin, device,
|
||||
render_frame_host);
|
||||
}
|
||||
|
||||
device::mojom::HidManager* ElectronHidDelegate::GetHidManager(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
auto* chooser_context = GetChooserContext(render_frame_host);
|
||||
return chooser_context->GetHidManager();
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::AddObserver(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
Observer* observer) {
|
||||
observer_list_.AddObserver(observer);
|
||||
auto* chooser_context = GetChooserContext(render_frame_host);
|
||||
if (!device_observation_.IsObserving())
|
||||
device_observation_.Observe(chooser_context);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::RemoveObserver(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
content::HidDelegate::Observer* observer) {
|
||||
observer_list_.RemoveObserver(observer);
|
||||
}
|
||||
|
||||
const device::mojom::HidDeviceInfo* ElectronHidDelegate::GetDeviceInfo(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
const std::string& guid) {
|
||||
auto* chooser_context = GetChooserContext(render_frame_host);
|
||||
return chooser_context->GetDeviceInfo(guid);
|
||||
}
|
||||
|
||||
bool ElectronHidDelegate::IsFidoAllowedForOrigin(const url::Origin& origin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnDeviceAdded(
|
||||
const device::mojom::HidDeviceInfo& device_info) {
|
||||
for (auto& observer : observer_list_)
|
||||
observer.OnDeviceAdded(device_info);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnDeviceRemoved(
|
||||
const device::mojom::HidDeviceInfo& device_info) {
|
||||
for (auto& observer : observer_list_)
|
||||
observer.OnDeviceRemoved(device_info);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnDeviceChanged(
|
||||
const device::mojom::HidDeviceInfo& device_info) {
|
||||
for (auto& observer : observer_list_)
|
||||
observer.OnDeviceChanged(device_info);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnHidManagerConnectionError() {
|
||||
device_observation_.Reset();
|
||||
|
||||
for (auto& observer : observer_list_)
|
||||
observer.OnHidManagerConnectionError();
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnHidChooserContextShutdown() {
|
||||
device_observation_.Reset();
|
||||
}
|
||||
|
||||
HidChooserController* ElectronHidDelegate::ControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
auto mapping = controller_map_.find(render_frame_host);
|
||||
return mapping == controller_map_.end() ? nullptr : mapping->second.get();
|
||||
}
|
||||
|
||||
HidChooserController* ElectronHidDelegate::AddControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback) {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto controller = std::make_unique<HidChooserController>(
|
||||
render_frame_host, std::move(filters), std::move(callback), web_contents,
|
||||
weak_factory_.GetWeakPtr());
|
||||
controller_map_.insert(
|
||||
std::make_pair(render_frame_host, std::move(controller)));
|
||||
return ControllerForFrame(render_frame_host);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::DeleteControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
controller_map_.erase(render_frame_host);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
84
shell/browser/hid/electron_hid_delegate.h
Normal file
84
shell/browser/hid/electron_hid_delegate.h
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
|
||||
#define SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "base/observer_list.h"
|
||||
#include "base/scoped_observation.h"
|
||||
#include "content/public/browser/hid_delegate.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
class HidChooserController;
|
||||
|
||||
class ElectronHidDelegate : public content::HidDelegate,
|
||||
public HidChooserContext::DeviceObserver {
|
||||
public:
|
||||
ElectronHidDelegate();
|
||||
ElectronHidDelegate(ElectronHidDelegate&) = delete;
|
||||
ElectronHidDelegate& operator=(ElectronHidDelegate&) = delete;
|
||||
~ElectronHidDelegate() override;
|
||||
|
||||
// content::HidDelegate:
|
||||
std::unique_ptr<content::HidChooser> RunChooser(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback) override;
|
||||
bool CanRequestDevicePermission(
|
||||
content::RenderFrameHost* render_frame_host) override;
|
||||
bool HasDevicePermission(content::RenderFrameHost* render_frame_host,
|
||||
const device::mojom::HidDeviceInfo& device) override;
|
||||
device::mojom::HidManager* GetHidManager(
|
||||
content::RenderFrameHost* render_frame_host) override;
|
||||
void AddObserver(content::RenderFrameHost* render_frame_host,
|
||||
content::HidDelegate::Observer* observer) override;
|
||||
void RemoveObserver(content::RenderFrameHost* render_frame_host,
|
||||
content::HidDelegate::Observer* observer) override;
|
||||
const device::mojom::HidDeviceInfo* GetDeviceInfo(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
const std::string& guid) override;
|
||||
bool IsFidoAllowedForOrigin(const url::Origin& origin) override;
|
||||
|
||||
// HidChooserContext::DeviceObserver:
|
||||
void OnDeviceAdded(const device::mojom::HidDeviceInfo&) override;
|
||||
void OnDeviceRemoved(const device::mojom::HidDeviceInfo&) override;
|
||||
void OnDeviceChanged(const device::mojom::HidDeviceInfo&) override;
|
||||
void OnHidManagerConnectionError() override;
|
||||
void OnHidChooserContextShutdown() override;
|
||||
|
||||
void DeleteControllerForFrame(content::RenderFrameHost* render_frame_host);
|
||||
|
||||
private:
|
||||
HidChooserController* ControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
|
||||
HidChooserController* AddControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback);
|
||||
|
||||
base::ScopedObservation<HidChooserContext,
|
||||
HidChooserContext::DeviceObserver,
|
||||
&HidChooserContext::AddDeviceObserver,
|
||||
&HidChooserContext::RemoveDeviceObserver>
|
||||
device_observation_{this};
|
||||
base::ObserverList<content::HidDelegate::Observer> observer_list_;
|
||||
|
||||
std::unordered_map<content::RenderFrameHost*,
|
||||
std::unique_ptr<HidChooserController>>
|
||||
controller_map_;
|
||||
|
||||
base::WeakPtrFactory<ElectronHidDelegate> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
|
||||
272
shell/browser/hid/hid_chooser_context.cc
Normal file
272
shell/browser/hid/hid_chooser_context.cc
Normal file
@@ -0,0 +1,272 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "base/command_line.h"
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/strings/stringprintf.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
#include "base/values.h"
|
||||
#include "components/content_settings/core/common/content_settings_types.h"
|
||||
#include "components/prefs/pref_service.h"
|
||||
#include "content/public/browser/device_service.h"
|
||||
#include "electron/grit/electron_resources.h"
|
||||
#include "services/device/public/cpp/hid/hid_blocklist.h"
|
||||
#include "services/device/public/cpp/hid/hid_switches.h"
|
||||
#include "shell/browser/web_contents_permission_helper.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
const char kHidDeviceNameKey[] = "name";
|
||||
const char kHidGuidKey[] = "guid";
|
||||
const char kHidVendorIdKey[] = "vendorId";
|
||||
const char kHidProductIdKey[] = "productId";
|
||||
const char kHidSerialNumberKey[] = "serialNumber";
|
||||
|
||||
HidChooserContext::HidChooserContext(ElectronBrowserContext* context)
|
||||
: browser_context_(context) {}
|
||||
|
||||
HidChooserContext::~HidChooserContext() {
|
||||
// Notify observers that the chooser context is about to be destroyed.
|
||||
// Observers must remove themselves from the observer lists.
|
||||
for (auto& observer : device_observer_list_) {
|
||||
observer.OnHidChooserContextShutdown();
|
||||
DCHECK(!device_observer_list_.HasObserver(&observer));
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
std::u16string HidChooserContext::DisplayNameFromDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
if (device.product_name.empty()) {
|
||||
auto device_id_string = base::ASCIIToUTF16(
|
||||
base::StringPrintf("%04X:%04X", device.vendor_id, device.product_id));
|
||||
return l10n_util::GetStringFUTF16(IDS_HID_CHOOSER_ITEM_WITHOUT_NAME,
|
||||
device_id_string);
|
||||
}
|
||||
return base::UTF8ToUTF16(device.product_name);
|
||||
}
|
||||
|
||||
// static
|
||||
bool HidChooserContext::CanStorePersistentEntry(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
return !device.serial_number.empty() && !device.product_name.empty();
|
||||
}
|
||||
|
||||
// static
|
||||
base::Value HidChooserContext::DeviceInfoToValue(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
base::Value value(base::Value::Type::DICTIONARY);
|
||||
value.SetStringKey(
|
||||
kHidDeviceNameKey,
|
||||
base::UTF16ToUTF8(HidChooserContext::DisplayNameFromDeviceInfo(device)));
|
||||
value.SetIntKey(kHidVendorIdKey, device.vendor_id);
|
||||
value.SetIntKey(kHidProductIdKey, device.product_id);
|
||||
if (HidChooserContext::CanStorePersistentEntry(device)) {
|
||||
// Use the USB serial number as a persistent identifier. If it is
|
||||
// unavailable, only ephemeral permissions may be granted.
|
||||
value.SetStringKey(kHidSerialNumberKey, device.serial_number);
|
||||
} else {
|
||||
// The GUID is a temporary ID created on connection that remains valid until
|
||||
// the device is disconnected. Ephemeral permissions are keyed by this ID
|
||||
// and must be granted again each time the device is connected.
|
||||
value.SetStringKey(kHidGuidKey, device.guid);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void HidChooserContext::GrantDevicePermission(
|
||||
const url::Origin& origin,
|
||||
const device::mojom::HidDeviceInfo& device,
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
DCHECK(base::Contains(devices_, device.guid));
|
||||
if (CanStorePersistentEntry(device)) {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto* permission_helper =
|
||||
WebContentsPermissionHelper::FromWebContents(web_contents);
|
||||
permission_helper->GrantHIDDevicePermission(
|
||||
origin, DeviceInfoToValue(device), render_frame_host);
|
||||
} else {
|
||||
ephemeral_devices_[origin].insert(device.guid);
|
||||
}
|
||||
}
|
||||
|
||||
bool HidChooserContext::HasDevicePermission(
|
||||
const url::Origin& origin,
|
||||
const device::mojom::HidDeviceInfo& device,
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
|
||||
switches::kDisableHidBlocklist) &&
|
||||
device::HidBlocklist::IsDeviceExcluded(device))
|
||||
return false;
|
||||
|
||||
auto it = ephemeral_devices_.find(origin);
|
||||
if (it != ephemeral_devices_.end() &&
|
||||
base::Contains(it->second, device.guid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto* permission_helper =
|
||||
WebContentsPermissionHelper::FromWebContents(web_contents);
|
||||
return permission_helper->CheckHIDDevicePermission(
|
||||
origin, DeviceInfoToValue(device), render_frame_host);
|
||||
}
|
||||
|
||||
void HidChooserContext::AddDeviceObserver(DeviceObserver* observer) {
|
||||
EnsureHidManagerConnection();
|
||||
device_observer_list_.AddObserver(observer);
|
||||
}
|
||||
|
||||
void HidChooserContext::RemoveDeviceObserver(DeviceObserver* observer) {
|
||||
device_observer_list_.RemoveObserver(observer);
|
||||
}
|
||||
|
||||
void HidChooserContext::GetDevices(
|
||||
device::mojom::HidManager::GetDevicesCallback callback) {
|
||||
if (!is_initialized_) {
|
||||
EnsureHidManagerConnection();
|
||||
pending_get_devices_requests_.push(std::move(callback));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> device_list;
|
||||
device_list.reserve(devices_.size());
|
||||
for (const auto& pair : devices_)
|
||||
device_list.push_back(pair.second->Clone());
|
||||
base::SequencedTaskRunnerHandle::Get()->PostTask(
|
||||
FROM_HERE, base::BindOnce(std::move(callback), std::move(device_list)));
|
||||
}
|
||||
|
||||
const device::mojom::HidDeviceInfo* HidChooserContext::GetDeviceInfo(
|
||||
const std::string& guid) {
|
||||
DCHECK(is_initialized_);
|
||||
auto it = devices_.find(guid);
|
||||
return it == devices_.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
device::mojom::HidManager* HidChooserContext::GetHidManager() {
|
||||
EnsureHidManagerConnection();
|
||||
return hid_manager_.get();
|
||||
}
|
||||
|
||||
base::WeakPtr<HidChooserContext> HidChooserContext::AsWeakPtr() {
|
||||
return weak_factory_.GetWeakPtr();
|
||||
}
|
||||
|
||||
void HidChooserContext::DeviceAdded(device::mojom::HidDeviceInfoPtr device) {
|
||||
DCHECK(device);
|
||||
|
||||
// Update the device list.
|
||||
if (!base::Contains(devices_, device->guid))
|
||||
devices_.insert({device->guid, device->Clone()});
|
||||
|
||||
// Notify all observers.
|
||||
for (auto& observer : device_observer_list_)
|
||||
observer.OnDeviceAdded(*device);
|
||||
}
|
||||
|
||||
void HidChooserContext::DeviceRemoved(device::mojom::HidDeviceInfoPtr device) {
|
||||
DCHECK(device);
|
||||
DCHECK(base::Contains(devices_, device->guid));
|
||||
|
||||
// Update the device list.
|
||||
devices_.erase(device->guid);
|
||||
|
||||
// Notify all device observers.
|
||||
for (auto& observer : device_observer_list_)
|
||||
observer.OnDeviceRemoved(*device);
|
||||
|
||||
// Next we'll notify observers for revoked permissions. If the device does not
|
||||
// support persistent permissions then device permissions are revoked on
|
||||
// disconnect.
|
||||
if (CanStorePersistentEntry(*device))
|
||||
return;
|
||||
|
||||
std::vector<url::Origin> revoked_origins;
|
||||
for (auto& map_entry : ephemeral_devices_) {
|
||||
if (map_entry.second.erase(device->guid) > 0)
|
||||
revoked_origins.push_back(map_entry.first);
|
||||
}
|
||||
if (revoked_origins.empty())
|
||||
return;
|
||||
}
|
||||
|
||||
void HidChooserContext::DeviceChanged(device::mojom::HidDeviceInfoPtr device) {
|
||||
DCHECK(device);
|
||||
DCHECK(base::Contains(devices_, device->guid));
|
||||
|
||||
// Update the device list.
|
||||
devices_[device->guid] = device->Clone();
|
||||
|
||||
// Notify all observers.
|
||||
for (auto& observer : device_observer_list_)
|
||||
observer.OnDeviceChanged(*device);
|
||||
}
|
||||
|
||||
void HidChooserContext::EnsureHidManagerConnection() {
|
||||
if (hid_manager_)
|
||||
return;
|
||||
|
||||
mojo::PendingRemote<device::mojom::HidManager> manager;
|
||||
content::GetDeviceService().BindHidManager(
|
||||
manager.InitWithNewPipeAndPassReceiver());
|
||||
SetUpHidManagerConnection(std::move(manager));
|
||||
}
|
||||
|
||||
void HidChooserContext::SetUpHidManagerConnection(
|
||||
mojo::PendingRemote<device::mojom::HidManager> manager) {
|
||||
hid_manager_.Bind(std::move(manager));
|
||||
hid_manager_.set_disconnect_handler(base::BindOnce(
|
||||
&HidChooserContext::OnHidManagerConnectionError, base::Unretained(this)));
|
||||
|
||||
hid_manager_->GetDevicesAndSetClient(
|
||||
client_receiver_.BindNewEndpointAndPassRemote(),
|
||||
base::BindOnce(&HidChooserContext::InitDeviceList,
|
||||
weak_factory_.GetWeakPtr()));
|
||||
}
|
||||
|
||||
void HidChooserContext::InitDeviceList(
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
|
||||
for (auto& device : devices)
|
||||
devices_.insert({device->guid, std::move(device)});
|
||||
|
||||
is_initialized_ = true;
|
||||
|
||||
while (!pending_get_devices_requests_.empty()) {
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> device_list;
|
||||
device_list.reserve(devices.size());
|
||||
for (const auto& entry : devices_)
|
||||
device_list.push_back(entry.second->Clone());
|
||||
std::move(pending_get_devices_requests_.front())
|
||||
.Run(std::move(device_list));
|
||||
pending_get_devices_requests_.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void HidChooserContext::OnHidManagerConnectionError() {
|
||||
hid_manager_.reset();
|
||||
client_receiver_.reset();
|
||||
devices_.clear();
|
||||
|
||||
std::vector<url::Origin> revoked_origins;
|
||||
revoked_origins.reserve(ephemeral_devices_.size());
|
||||
for (const auto& map_entry : ephemeral_devices_)
|
||||
revoked_origins.push_back(map_entry.first);
|
||||
ephemeral_devices_.clear();
|
||||
|
||||
// Notify all device observers.
|
||||
for (auto& observer : device_observer_list_)
|
||||
observer.OnHidManagerConnectionError();
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
136
shell/browser/hid/hid_chooser_context.h
Normal file
136
shell/browser/hid/hid_chooser_context.h
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2021 Microsoft, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
|
||||
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/containers/queue.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/observer_list.h"
|
||||
#include "base/unguessable_token.h"
|
||||
#include "content/public/browser/render_frame_host.h"
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "mojo/public/cpp/bindings/associated_receiver.h"
|
||||
#include "mojo/public/cpp/bindings/pending_remote.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "services/device/public/mojom/hid.mojom.h"
|
||||
#include "shell/browser/electron_browser_context.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
namespace base {
|
||||
class Value;
|
||||
}
|
||||
|
||||
namespace electron {
|
||||
|
||||
extern const char kHidDeviceNameKey[];
|
||||
extern const char kHidGuidKey[];
|
||||
extern const char kHidVendorIdKey[];
|
||||
extern const char kHidProductIdKey[];
|
||||
extern const char kHidSerialNumberKey[];
|
||||
|
||||
// Manages the internal state and connection to the device service for the
|
||||
// Human Interface Device (HID) chooser UI.
|
||||
class HidChooserContext : public KeyedService,
|
||||
public device::mojom::HidManagerClient {
|
||||
public:
|
||||
// This observer can be used to be notified when HID devices are connected or
|
||||
// disconnected.
|
||||
class DeviceObserver : public base::CheckedObserver {
|
||||
public:
|
||||
virtual void OnDeviceAdded(const device::mojom::HidDeviceInfo&) = 0;
|
||||
virtual void OnDeviceRemoved(const device::mojom::HidDeviceInfo&) = 0;
|
||||
virtual void OnDeviceChanged(const device::mojom::HidDeviceInfo&) = 0;
|
||||
virtual void OnHidManagerConnectionError() = 0;
|
||||
|
||||
// Called when the HidChooserContext is shutting down. Observers must remove
|
||||
// themselves before returning.
|
||||
virtual void OnHidChooserContextShutdown() = 0;
|
||||
};
|
||||
|
||||
explicit HidChooserContext(ElectronBrowserContext* context);
|
||||
HidChooserContext(const HidChooserContext&) = delete;
|
||||
HidChooserContext& operator=(const HidChooserContext&) = delete;
|
||||
~HidChooserContext() override;
|
||||
|
||||
// Returns a human-readable string identifier for |device|.
|
||||
static std::u16string DisplayNameFromDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device);
|
||||
|
||||
// Returns true if a persistent permission can be granted for |device|.
|
||||
static bool CanStorePersistentEntry(
|
||||
const device::mojom::HidDeviceInfo& device);
|
||||
|
||||
static base::Value DeviceInfoToValue(
|
||||
const device::mojom::HidDeviceInfo& device);
|
||||
|
||||
// HID-specific interface for granting and checking permissions.
|
||||
void GrantDevicePermission(const url::Origin& origin,
|
||||
const device::mojom::HidDeviceInfo& device,
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
bool HasDevicePermission(const url::Origin& origin,
|
||||
const device::mojom::HidDeviceInfo& device,
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
|
||||
// For ScopedObserver.
|
||||
void AddDeviceObserver(DeviceObserver* observer);
|
||||
void RemoveDeviceObserver(DeviceObserver* observer);
|
||||
|
||||
// Forward HidManager::GetDevices.
|
||||
void GetDevices(device::mojom::HidManager::GetDevicesCallback callback);
|
||||
|
||||
// Only call this if you're sure |devices_| has been initialized before-hand.
|
||||
// The returned raw pointer is owned by |devices_| and will be destroyed when
|
||||
// the device is removed.
|
||||
const device::mojom::HidDeviceInfo* GetDeviceInfo(const std::string& guid);
|
||||
|
||||
device::mojom::HidManager* GetHidManager();
|
||||
|
||||
base::WeakPtr<HidChooserContext> AsWeakPtr();
|
||||
|
||||
private:
|
||||
// device::mojom::HidManagerClient implementation:
|
||||
void DeviceAdded(device::mojom::HidDeviceInfoPtr device_info) override;
|
||||
void DeviceRemoved(device::mojom::HidDeviceInfoPtr device_info) override;
|
||||
void DeviceChanged(device::mojom::HidDeviceInfoPtr device_info) override;
|
||||
|
||||
void EnsureHidManagerConnection();
|
||||
void SetUpHidManagerConnection(
|
||||
mojo::PendingRemote<device::mojom::HidManager> manager);
|
||||
void InitDeviceList(std::vector<device::mojom::HidDeviceInfoPtr> devices);
|
||||
void OnHidManagerInitializedForTesting(
|
||||
device::mojom::HidManager::GetDevicesCallback callback,
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices);
|
||||
void OnHidManagerConnectionError();
|
||||
|
||||
ElectronBrowserContext* browser_context_;
|
||||
|
||||
bool is_initialized_ = false;
|
||||
base::queue<device::mojom::HidManager::GetDevicesCallback>
|
||||
pending_get_devices_requests_;
|
||||
|
||||
// Tracks the set of devices to which an origin has access to.
|
||||
std::map<url::Origin, std::set<std::string>> ephemeral_devices_;
|
||||
|
||||
// Map from device GUID to device info.
|
||||
std::map<std::string, device::mojom::HidDeviceInfoPtr> devices_;
|
||||
|
||||
mojo::Remote<device::mojom::HidManager> hid_manager_;
|
||||
mojo::AssociatedReceiver<device::mojom::HidManagerClient> client_receiver_{
|
||||
this};
|
||||
base::ObserverList<DeviceObserver> device_observer_list_;
|
||||
|
||||
base::WeakPtrFactory<HidChooserContext> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
|
||||
55
shell/browser/hid/hid_chooser_context_factory.cc
Normal file
55
shell/browser/hid/hid_chooser_context_factory.cc
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/hid/hid_chooser_context_factory.h"
|
||||
|
||||
#include "components/keyed_service/content/browser_context_dependency_manager.h"
|
||||
#include "shell/browser/electron_browser_context.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
// static
|
||||
HidChooserContextFactory* HidChooserContextFactory::GetInstance() {
|
||||
static base::NoDestructor<HidChooserContextFactory> factory;
|
||||
return factory.get();
|
||||
}
|
||||
|
||||
// static
|
||||
HidChooserContext* HidChooserContextFactory::GetForBrowserContext(
|
||||
content::BrowserContext* context) {
|
||||
return static_cast<HidChooserContext*>(
|
||||
GetInstance()->GetServiceForBrowserContext(context, true));
|
||||
}
|
||||
|
||||
// static
|
||||
HidChooserContext* HidChooserContextFactory::GetForBrowserContextIfExists(
|
||||
content::BrowserContext* context) {
|
||||
return static_cast<HidChooserContext*>(
|
||||
GetInstance()->GetServiceForBrowserContext(context, /*create=*/false));
|
||||
}
|
||||
|
||||
HidChooserContextFactory::HidChooserContextFactory()
|
||||
: BrowserContextKeyedServiceFactory(
|
||||
"HidChooserContext",
|
||||
BrowserContextDependencyManager::GetInstance()) {}
|
||||
|
||||
HidChooserContextFactory::~HidChooserContextFactory() = default;
|
||||
|
||||
KeyedService* HidChooserContextFactory::BuildServiceInstanceFor(
|
||||
content::BrowserContext* context) const {
|
||||
auto* browser_context =
|
||||
static_cast<electron::ElectronBrowserContext*>(context);
|
||||
return new HidChooserContext(browser_context);
|
||||
}
|
||||
|
||||
content::BrowserContext* HidChooserContextFactory::GetBrowserContextToUse(
|
||||
content::BrowserContext* context) const {
|
||||
return context;
|
||||
}
|
||||
|
||||
void HidChooserContextFactory::BrowserContextShutdown(
|
||||
content::BrowserContext* context) {}
|
||||
|
||||
} // namespace electron
|
||||
42
shell/browser/hid/hid_chooser_context_factory.h
Normal file
42
shell/browser/hid/hid_chooser_context_factory.h
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
|
||||
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
|
||||
|
||||
#include "base/macros.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
class HidChooserContext;
|
||||
|
||||
class HidChooserContextFactory : public BrowserContextKeyedServiceFactory {
|
||||
public:
|
||||
static HidChooserContext* GetForBrowserContext(
|
||||
content::BrowserContext* context);
|
||||
static HidChooserContext* GetForBrowserContextIfExists(
|
||||
content::BrowserContext* context);
|
||||
static HidChooserContextFactory* GetInstance();
|
||||
|
||||
private:
|
||||
friend base::NoDestructor<HidChooserContextFactory>;
|
||||
|
||||
HidChooserContextFactory();
|
||||
~HidChooserContextFactory() override;
|
||||
|
||||
// BrowserContextKeyedBaseFactory:
|
||||
KeyedService* BuildServiceInstanceFor(
|
||||
content::BrowserContext* profile) const override;
|
||||
content::BrowserContext* GetBrowserContextToUse(
|
||||
content::BrowserContext* context) const override;
|
||||
void BrowserContextShutdown(content::BrowserContext* context) override;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(HidChooserContextFactory);
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
|
||||
366
shell/browser/hid/hid_chooser_controller.cc
Normal file
366
shell/browser/hid/hid_chooser_controller.cc
Normal file
@@ -0,0 +1,366 @@
|
||||
// Copyright (c) 2021 Microsoft, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/hid/hid_chooser_controller.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "base/bind.h"
|
||||
#include "base/command_line.h"
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/ranges/algorithm.h"
|
||||
#include "base/stl_util.h"
|
||||
#include "gin/data_object_builder.h"
|
||||
#include "services/device/public/cpp/hid/hid_blocklist.h"
|
||||
#include "services/device/public/cpp/hid/hid_switches.h"
|
||||
#include "shell/browser/api/electron_api_session.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
#include "shell/browser/hid/hid_chooser_context_factory.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/common/gin_converters/callback_converter.h"
|
||||
#include "shell/common/gin_converters/content_converter.h"
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "shell/common/process_util.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::string PhysicalDeviceIdFromDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
// A single physical device may expose multiple HID interfaces, each
|
||||
// represented by a HidDeviceInfo object. When a device exposes multiple
|
||||
// HID interfaces, the HidDeviceInfo objects will share a common
|
||||
// |physical_device_id|. Group these devices so that a single chooser item
|
||||
// is shown for each physical device. If a device's physical device ID is
|
||||
// empty, use its GUID instead.
|
||||
return device.physical_device_id.empty() ? device.guid
|
||||
: device.physical_device_id;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace gin {
|
||||
|
||||
template <>
|
||||
struct Converter<device::mojom::HidDeviceInfoPtr> {
|
||||
static v8::Local<v8::Value> ToV8(
|
||||
v8::Isolate* isolate,
|
||||
const device::mojom::HidDeviceInfoPtr& device) {
|
||||
base::Value value = electron::HidChooserContext::DeviceInfoToValue(*device);
|
||||
value.SetStringKey("deviceId", PhysicalDeviceIdFromDeviceInfo(*device));
|
||||
return gin::ConvertToV8(isolate, value);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace gin
|
||||
|
||||
namespace electron {
|
||||
|
||||
HidChooserController::HidChooserController(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback,
|
||||
content::WebContents* web_contents,
|
||||
base::WeakPtr<ElectronHidDelegate> hid_delegate)
|
||||
: WebContentsObserver(web_contents),
|
||||
filters_(std::move(filters)),
|
||||
callback_(std::move(callback)),
|
||||
origin_(content::WebContents::FromRenderFrameHost(render_frame_host)
|
||||
->GetMainFrame()
|
||||
->GetLastCommittedOrigin()),
|
||||
frame_tree_node_id_(render_frame_host->GetFrameTreeNodeId()),
|
||||
hid_delegate_(hid_delegate),
|
||||
render_frame_host_id_(render_frame_host->GetGlobalId()) {
|
||||
chooser_context_ = HidChooserContextFactory::GetForBrowserContext(
|
||||
web_contents->GetBrowserContext())
|
||||
->AsWeakPtr();
|
||||
DCHECK(chooser_context_);
|
||||
|
||||
chooser_context_->GetHidManager()->GetDevices(base::BindOnce(
|
||||
&HidChooserController::OnGotDevices, weak_factory_.GetWeakPtr()));
|
||||
}
|
||||
|
||||
HidChooserController::~HidChooserController() {
|
||||
if (callback_)
|
||||
std::move(callback_).Run(std::vector<device::mojom::HidDeviceInfoPtr>());
|
||||
}
|
||||
|
||||
api::Session* HidChooserController::GetSession() {
|
||||
if (!web_contents()) {
|
||||
return nullptr;
|
||||
}
|
||||
return api::Session::FromBrowserContext(web_contents()->GetBrowserContext());
|
||||
}
|
||||
|
||||
void HidChooserController::OnDeviceAdded(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
if (!DisplayDevice(device))
|
||||
return;
|
||||
if (AddDeviceInfo(device)) {
|
||||
api::Session* session = GetSession();
|
||||
if (session) {
|
||||
auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
|
||||
.Set("device", device.Clone())
|
||||
.Set("frame", rfh)
|
||||
.Build();
|
||||
session->Emit("hid-device-added", details);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void HidChooserController::OnDeviceRemoved(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto id = PhysicalDeviceIdFromDeviceInfo(device);
|
||||
auto items_it = std::find(items_.begin(), items_.end(), id);
|
||||
if (items_it == items_.end())
|
||||
return;
|
||||
api::Session* session = GetSession();
|
||||
if (session) {
|
||||
auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
|
||||
.Set("device", device.Clone())
|
||||
.Set("frame", rfh)
|
||||
.Build();
|
||||
session->Emit("hid-device-removed", details);
|
||||
}
|
||||
RemoveDeviceInfo(device);
|
||||
}
|
||||
|
||||
void HidChooserController::OnDeviceChanged(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
bool has_chooser_item =
|
||||
base::Contains(items_, PhysicalDeviceIdFromDeviceInfo(device));
|
||||
if (!DisplayDevice(device)) {
|
||||
if (has_chooser_item)
|
||||
OnDeviceRemoved(device);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!has_chooser_item) {
|
||||
OnDeviceAdded(device);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the item to replace the old device info with |device|.
|
||||
UpdateDeviceInfo(device);
|
||||
}
|
||||
|
||||
void HidChooserController::OnDeviceChosen(gin::Arguments* args) {
|
||||
std::string device_id;
|
||||
if (!args->GetNext(&device_id) || device_id.empty()) {
|
||||
RunCallback({});
|
||||
} else {
|
||||
auto find_it = device_map_.find(device_id);
|
||||
if (find_it != device_map_.end()) {
|
||||
auto& device_infos = find_it->second;
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices;
|
||||
devices.reserve(device_infos.size());
|
||||
for (auto& device : device_infos) {
|
||||
chooser_context_->GrantDevicePermission(origin_, *device,
|
||||
web_contents()->GetMainFrame());
|
||||
devices.push_back(device->Clone());
|
||||
}
|
||||
RunCallback(std::move(devices));
|
||||
} else {
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
node::Environment* env = node::Environment::GetCurrent(isolate);
|
||||
EmitWarning(env, "The device id " + device_id + " was not found.",
|
||||
"UnknownHIDDeviceId");
|
||||
RunCallback({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HidChooserController::OnHidManagerConnectionError() {
|
||||
observation_.Reset();
|
||||
}
|
||||
|
||||
void HidChooserController::OnHidChooserContextShutdown() {
|
||||
observation_.Reset();
|
||||
}
|
||||
|
||||
void HidChooserController::OnGotDevices(
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devicesToDisplay;
|
||||
devicesToDisplay.reserve(devices.size());
|
||||
|
||||
for (auto& device : devices) {
|
||||
if (DisplayDevice(*device)) {
|
||||
if (AddDeviceInfo(*device)) {
|
||||
devicesToDisplay.push_back(device->Clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to HidChooserContext for OnDeviceAdded/Removed events after the
|
||||
// enumeration.
|
||||
if (chooser_context_)
|
||||
observation_.Observe(chooser_context_.get());
|
||||
bool prevent_default = false;
|
||||
api::Session* session = GetSession();
|
||||
if (session) {
|
||||
auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
|
||||
.Set("deviceList", devicesToDisplay)
|
||||
.Set("frame", rfh)
|
||||
.Build();
|
||||
prevent_default =
|
||||
session->Emit("select-hid-device", details,
|
||||
base::AdaptCallbackForRepeating(
|
||||
base::BindOnce(&HidChooserController::OnDeviceChosen,
|
||||
weak_factory_.GetWeakPtr())));
|
||||
}
|
||||
if (!prevent_default) {
|
||||
RunCallback({});
|
||||
}
|
||||
}
|
||||
|
||||
bool HidChooserController::DisplayDevice(
|
||||
const device::mojom::HidDeviceInfo& device) const {
|
||||
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
|
||||
switches::kDisableHidBlocklist)) {
|
||||
// Do not pass the device to the chooser if it is excluded by the blocklist.
|
||||
if (device::HidBlocklist::IsDeviceExcluded(device))
|
||||
return false;
|
||||
|
||||
// Do not pass the device to the chooser if it has a top-level collection
|
||||
// with the FIDO usage page.
|
||||
//
|
||||
// Note: The HID blocklist also blocks top-level collections with the FIDO
|
||||
// usage page, but will not block the device if it has other (non-FIDO)
|
||||
// collections. The check below will exclude the device from the chooser
|
||||
// if it has any top-level FIDO collection.
|
||||
auto find_it =
|
||||
std::find_if(device.collections.begin(), device.collections.end(),
|
||||
[](const device::mojom::HidCollectionInfoPtr& c) {
|
||||
return c->usage->usage_page == device::mojom::kPageFido;
|
||||
});
|
||||
if (find_it != device.collections.end())
|
||||
return false;
|
||||
}
|
||||
|
||||
return FilterMatchesAny(device);
|
||||
}
|
||||
|
||||
bool HidChooserController::FilterMatchesAny(
|
||||
const device::mojom::HidDeviceInfo& device) const {
|
||||
if (filters_.empty())
|
||||
return true;
|
||||
|
||||
for (const auto& filter : filters_) {
|
||||
if (filter->device_ids) {
|
||||
if (filter->device_ids->is_vendor()) {
|
||||
if (filter->device_ids->get_vendor() != device.vendor_id)
|
||||
continue;
|
||||
} else if (filter->device_ids->is_vendor_and_product()) {
|
||||
const auto& vendor_and_product =
|
||||
filter->device_ids->get_vendor_and_product();
|
||||
if (vendor_and_product->vendor != device.vendor_id)
|
||||
continue;
|
||||
if (vendor_and_product->product != device.product_id)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter->usage) {
|
||||
if (filter->usage->is_page()) {
|
||||
const uint16_t usage_page = filter->usage->get_page();
|
||||
auto find_it =
|
||||
std::find_if(device.collections.begin(), device.collections.end(),
|
||||
[=](const device::mojom::HidCollectionInfoPtr& c) {
|
||||
return usage_page == c->usage->usage_page;
|
||||
});
|
||||
if (find_it == device.collections.end())
|
||||
continue;
|
||||
} else if (filter->usage->is_usage_and_page()) {
|
||||
const auto& usage_and_page = filter->usage->get_usage_and_page();
|
||||
auto find_it = std::find_if(
|
||||
device.collections.begin(), device.collections.end(),
|
||||
[&usage_and_page](const device::mojom::HidCollectionInfoPtr& c) {
|
||||
return usage_and_page->usage_page == c->usage->usage_page &&
|
||||
usage_and_page->usage == c->usage->usage;
|
||||
});
|
||||
if (find_it == device.collections.end())
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HidChooserController::AddDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto id = PhysicalDeviceIdFromDeviceInfo(device);
|
||||
auto find_it = device_map_.find(id);
|
||||
if (find_it != device_map_.end()) {
|
||||
find_it->second.push_back(device.Clone());
|
||||
return false;
|
||||
}
|
||||
// A new device was connected. Append it to the end of the chooser list.
|
||||
device_map_[id].push_back(device.Clone());
|
||||
items_.push_back(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HidChooserController::RemoveDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto id = PhysicalDeviceIdFromDeviceInfo(device);
|
||||
auto find_it = device_map_.find(id);
|
||||
DCHECK(find_it != device_map_.end());
|
||||
auto& device_infos = find_it->second;
|
||||
base::EraseIf(device_infos,
|
||||
[&device](const device::mojom::HidDeviceInfoPtr& d) {
|
||||
return d->guid == device.guid;
|
||||
});
|
||||
if (!device_infos.empty())
|
||||
return false;
|
||||
// A device was disconnected. Remove it from the chooser list.
|
||||
device_map_.erase(find_it);
|
||||
base::Erase(items_, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HidChooserController::UpdateDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto id = PhysicalDeviceIdFromDeviceInfo(device);
|
||||
auto physical_device_it = device_map_.find(id);
|
||||
DCHECK(physical_device_it != device_map_.end());
|
||||
auto& device_infos = physical_device_it->second;
|
||||
auto device_it = base::ranges::find_if(
|
||||
device_infos, [&device](const device::mojom::HidDeviceInfoPtr& d) {
|
||||
return d->guid == device.guid;
|
||||
});
|
||||
DCHECK(device_it != device_infos.end());
|
||||
*device_it = device.Clone();
|
||||
}
|
||||
|
||||
void HidChooserController::RunCallback(
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
|
||||
if (callback_) {
|
||||
std::move(callback_).Run(std::move(devices));
|
||||
}
|
||||
}
|
||||
|
||||
void HidChooserController::RenderFrameDeleted(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
if (hid_delegate_) {
|
||||
hid_delegate_->DeleteControllerForFrame(render_frame_host);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
126
shell/browser/hid/hid_chooser_controller.h
Normal file
126
shell/browser/hid/hid_chooser_controller.h
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2021 Microsoft, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
|
||||
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/macros.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "content/public/browser/global_routing_id.h"
|
||||
#include "content/public/browser/hid_chooser.h"
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "content/public/browser/web_contents_observer.h"
|
||||
#include "services/device/public/mojom/hid.mojom-forward.h"
|
||||
#include "shell/browser/api/electron_api_session.h"
|
||||
#include "shell/browser/hid/electron_hid_delegate.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
#include "shell/common/gin_converters/frame_converter.h"
|
||||
#include "third_party/blink/public/mojom/hid/hid.mojom.h"
|
||||
|
||||
namespace content {
|
||||
class RenderFrameHost;
|
||||
} // namespace content
|
||||
|
||||
namespace electron {
|
||||
|
||||
class ElectronHidDelegate;
|
||||
|
||||
class HidChooserContext;
|
||||
|
||||
// HidChooserController provides data for the WebHID API permission prompt.
|
||||
class HidChooserController
|
||||
: public content::WebContentsObserver,
|
||||
public electron::HidChooserContext::DeviceObserver {
|
||||
public:
|
||||
// Construct a chooser controller for Human Interface Devices (HID).
|
||||
// |render_frame_host| is used to initialize the chooser strings and to access
|
||||
// the requesting and embedding origins. |callback| is called when the chooser
|
||||
// is closed, either by selecting an item or by dismissing the chooser dialog.
|
||||
// The callback is called with the selected device, or nullptr if no device is
|
||||
// selected.
|
||||
HidChooserController(content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback,
|
||||
content::WebContents* web_contents,
|
||||
base::WeakPtr<ElectronHidDelegate> hid_delegate);
|
||||
HidChooserController(HidChooserController&) = delete;
|
||||
HidChooserController& operator=(HidChooserController&) = delete;
|
||||
~HidChooserController() override;
|
||||
|
||||
// HidChooserContext::DeviceObserver:
|
||||
void OnDeviceAdded(const device::mojom::HidDeviceInfo& device_info) override;
|
||||
void OnDeviceRemoved(
|
||||
const device::mojom::HidDeviceInfo& device_info) override;
|
||||
void OnDeviceChanged(
|
||||
const device::mojom::HidDeviceInfo& device_info) override;
|
||||
void OnHidManagerConnectionError() override;
|
||||
void OnHidChooserContextShutdown() override;
|
||||
|
||||
// content::WebContentsObserver:
|
||||
void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override;
|
||||
|
||||
private:
|
||||
api::Session* GetSession();
|
||||
void OnGotDevices(std::vector<device::mojom::HidDeviceInfoPtr> devices);
|
||||
bool DisplayDevice(const device::mojom::HidDeviceInfo& device) const;
|
||||
bool FilterMatchesAny(const device::mojom::HidDeviceInfo& device) const;
|
||||
|
||||
// Add |device_info| to |device_map_|. The device is added to the chooser item
|
||||
// representing the physical device. If the chooser item does not yet exist, a
|
||||
// new item is appended. Returns true if an item was appended.
|
||||
bool AddDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
|
||||
|
||||
// Remove |device_info| from |device_map_|. The device info is removed from
|
||||
// the chooser item representing the physical device. If this would cause the
|
||||
// item to be empty, the chooser item is removed. Does nothing if the device
|
||||
// is not in the chooser item. Returns true if an item was removed.
|
||||
bool RemoveDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
|
||||
|
||||
// Update the information for the device described by |device_info| in the
|
||||
// |device_map_|.
|
||||
void UpdateDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
|
||||
|
||||
void RunCallback(std::vector<device::mojom::HidDeviceInfoPtr> devices);
|
||||
void OnDeviceChosen(gin::Arguments* args);
|
||||
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters_;
|
||||
content::HidChooser::Callback callback_;
|
||||
const url::Origin origin_;
|
||||
const int frame_tree_node_id_;
|
||||
|
||||
// The lifetime of the chooser context is tied to the browser context used to
|
||||
// create it, and may be destroyed while the chooser is still active.
|
||||
base::WeakPtr<HidChooserContext> chooser_context_;
|
||||
|
||||
// Information about connected devices and their HID interfaces. A single
|
||||
// physical device may expose multiple HID interfaces. Keys are physical
|
||||
// device IDs, values are collections of HidDeviceInfo objects representing
|
||||
// the HID interfaces hosted by the physical device.
|
||||
std::map<std::string, std::vector<device::mojom::HidDeviceInfoPtr>>
|
||||
device_map_;
|
||||
|
||||
// An ordered list of physical device IDs that determines the order of items
|
||||
// in the chooser.
|
||||
std::vector<std::string> items_;
|
||||
|
||||
base::ScopedObservation<HidChooserContext,
|
||||
HidChooserContext::DeviceObserver,
|
||||
&HidChooserContext::AddDeviceObserver,
|
||||
&HidChooserContext::RemoveDeviceObserver>
|
||||
observation_{this};
|
||||
|
||||
base::WeakPtr<ElectronHidDelegate> hid_delegate_;
|
||||
|
||||
content::GlobalRenderFrameHostId render_frame_host_id_;
|
||||
|
||||
base::WeakPtrFactory<HidChooserController> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
|
||||
@@ -62,6 +62,10 @@ const NSAutoresizingMaskOptions kDefaultAutoResizingMask =
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstMouse:(NSEvent*)event {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)shouldIgnoreMouseEvent {
|
||||
NSEventType type = [[NSApp currentEvent] type];
|
||||
return type != NSEventTypeLeftMouseDragged &&
|
||||
|
||||
@@ -111,6 +111,11 @@ void FlipWindowStyle(HWND handle, bool on, DWORD flag) {
|
||||
else
|
||||
style &= ~flag;
|
||||
::SetWindowLong(handle, GWL_STYLE, style);
|
||||
// Window's frame styles are cached so we need to call SetWindowPos
|
||||
// with the SWP_FRAMECHANGED flag to update cache properly.
|
||||
::SetWindowPos(handle, 0, 0, 0, 0, 0, // ignored
|
||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
|
||||
SWP_NOACTIVATE | SWP_NOOWNERZORDER);
|
||||
}
|
||||
|
||||
gfx::Rect DIPToScreenRect(HWND hwnd, const gfx::Rect& pixel_bounds) {
|
||||
|
||||
@@ -50,8 +50,8 @@ END
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 16,0,0,20210922
|
||||
PRODUCTVERSION 16,0,0,20210922
|
||||
FILEVERSION 17,0,0,20210924
|
||||
PRODUCTVERSION 17,0,0,20210924
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -68,12 +68,12 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "GitHub, Inc."
|
||||
VALUE "FileDescription", "Electron"
|
||||
VALUE "FileVersion", "16.0.0"
|
||||
VALUE "FileVersion", "17.0.0"
|
||||
VALUE "InternalName", "electron.exe"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2015 GitHub, Inc. All rights reserved."
|
||||
VALUE "OriginalFilename", "electron.exe"
|
||||
VALUE "ProductName", "Electron"
|
||||
VALUE "ProductVersion", "16.0.0"
|
||||
VALUE "ProductVersion", "17.0.0"
|
||||
VALUE "SquirrelAwareVersion", "1"
|
||||
END
|
||||
END
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <gmodule.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "base/callback.h"
|
||||
#include "base/files/file_util.h"
|
||||
@@ -131,24 +132,25 @@ class FileChooserDialog {
|
||||
: parent_(
|
||||
static_cast<electron::NativeWindowViews*>(settings.parent_window)),
|
||||
filters_(settings.filters) {
|
||||
const char* confirm_text = gtk_util::kOkLabel;
|
||||
|
||||
if (!settings.button_label.empty())
|
||||
confirm_text = settings.button_label.c_str();
|
||||
else if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
|
||||
confirm_text = gtk_util::kSaveLabel;
|
||||
else if (action == GTK_FILE_CHOOSER_ACTION_OPEN)
|
||||
confirm_text = gtk_util::kOpenLabel;
|
||||
|
||||
InitGtkFileChooserNativeSupport();
|
||||
|
||||
auto label = settings.button_label;
|
||||
|
||||
if (*supports_gtk_file_chooser_native) {
|
||||
dialog_ = GTK_FILE_CHOOSER(
|
||||
dl_gtk_file_chooser_native_new(settings.title.c_str(), NULL, action,
|
||||
confirm_text, gtk_util::kCancelLabel));
|
||||
dialog_ = GTK_FILE_CHOOSER(dl_gtk_file_chooser_native_new(
|
||||
settings.title.c_str(), NULL, action,
|
||||
label.empty() ? nullptr : label.c_str(), nullptr));
|
||||
} else {
|
||||
const char* confirm_text = gtk_util::GetOkLabel();
|
||||
if (!label.empty())
|
||||
confirm_text = label.c_str();
|
||||
else if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
|
||||
confirm_text = gtk_util::GetSaveLabel();
|
||||
else if (action == GTK_FILE_CHOOSER_ACTION_OPEN)
|
||||
confirm_text = gtk_util::GetOpenLabel();
|
||||
|
||||
dialog_ = GTK_FILE_CHOOSER(gtk_file_chooser_dialog_new(
|
||||
settings.title.c_str(), NULL, action, gtk_util::kCancelLabel,
|
||||
settings.title.c_str(), NULL, action, gtk_util::GetCancelLabel(),
|
||||
GTK_RESPONSE_CANCEL, confirm_text, GTK_RESPONSE_ACCEPT, NULL));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,36 +8,71 @@
|
||||
#include <gtk/gtk.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "third_party/skia/include/core/SkBitmap.h"
|
||||
#include "third_party/skia/include/core/SkColor.h"
|
||||
#include "third_party/skia/include/core/SkUnPreMultiply.h"
|
||||
#include "ui/gtk/gtk_compat.h" // nogncheck
|
||||
|
||||
namespace gtk_util {
|
||||
|
||||
// Copied from L40-L55 in
|
||||
// https://cs.chromium.org/chromium/src/chrome/browser/ui/libgtkui/select_file_dialog_impl_gtk.cc
|
||||
#if GTK_CHECK_VERSION(3, 90, 0)
|
||||
// GTK stock items have been deprecated. The docs say to switch to using the
|
||||
// strings "_Open", etc. However this breaks i18n. We could supply our own
|
||||
// internationalized strings, but the "_" in these strings is significant: it's
|
||||
// the keyboard shortcut to select these actions. TODO: Provide
|
||||
// internationalized strings when GTK provides support for it.
|
||||
const char* const kCancelLabel = "_Cancel";
|
||||
const char* const kNoLabel = "_No";
|
||||
const char* const kOkLabel = "_OK";
|
||||
const char* const kOpenLabel = "_Open";
|
||||
const char* const kSaveLabel = "_Save";
|
||||
const char* const kYesLabel = "_Yes";
|
||||
#else
|
||||
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
||||
const char* const kCancelLabel = GTK_STOCK_CANCEL;
|
||||
const char* const kNoLabel = GTK_STOCK_NO;
|
||||
const char* const kOkLabel = GTK_STOCK_OK;
|
||||
const char* const kOpenLabel = GTK_STOCK_OPEN;
|
||||
const char* const kSaveLabel = GTK_STOCK_SAVE;
|
||||
const char* const kYesLabel = GTK_STOCK_YES;
|
||||
G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
#endif
|
||||
// The following utilities are pulled from
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/gtk/select_file_dialog_impl_gtk.cc;l=43-74
|
||||
|
||||
const char* GettextPackage() {
|
||||
static base::NoDestructor<std::string> gettext_package(
|
||||
"gtk" + base::NumberToString(gtk::GtkVersion().components()[0]) + "0");
|
||||
return gettext_package->c_str();
|
||||
}
|
||||
|
||||
const char* GtkGettext(const char* str) {
|
||||
return g_dgettext(GettextPackage(), str);
|
||||
}
|
||||
|
||||
const char* GetCancelLabel() {
|
||||
if (!gtk::GtkCheckVersion(4))
|
||||
return "gtk-cancel"; // In GTK3, this is GTK_STOCK_CANCEL.
|
||||
static const char* cancel = GtkGettext("_Cancel");
|
||||
return cancel;
|
||||
}
|
||||
|
||||
const char* GetOpenLabel() {
|
||||
if (!gtk::GtkCheckVersion(4))
|
||||
return "gtk-open"; // In GTK3, this is GTK_STOCK_OPEN.
|
||||
static const char* open = GtkGettext("_Open");
|
||||
return open;
|
||||
}
|
||||
|
||||
const char* GetSaveLabel() {
|
||||
if (!gtk::GtkCheckVersion(4))
|
||||
return "gtk-save"; // In GTK3, this is GTK_STOCK_SAVE.
|
||||
static const char* save = GtkGettext("_Save");
|
||||
return save;
|
||||
}
|
||||
|
||||
const char* GetOkLabel() {
|
||||
if (!gtk::GtkCheckVersion(4))
|
||||
return "gtk-ok"; // In GTK3, this is GTK_STOCK_OK.
|
||||
static const char* ok = GtkGettext("_Ok");
|
||||
return ok;
|
||||
}
|
||||
|
||||
const char* GetNoLabel() {
|
||||
if (!gtk::GtkCheckVersion(4))
|
||||
return "gtk-no"; // In GTK3, this is GTK_STOCK_NO.
|
||||
static const char* no = GtkGettext("_No");
|
||||
return no;
|
||||
}
|
||||
|
||||
const char* GetYesLabel() {
|
||||
if (!gtk::GtkCheckVersion(4))
|
||||
return "gtk-yes"; // In GTK3, this is GTK_STOCK_YES.
|
||||
static const char* yes = GtkGettext("_Yes");
|
||||
return yes;
|
||||
}
|
||||
|
||||
GdkPixbuf* GdkPixbufFromSkBitmap(const SkBitmap& bitmap) {
|
||||
if (bitmap.isNull())
|
||||
|
||||
@@ -11,14 +11,15 @@ class SkBitmap;
|
||||
|
||||
namespace gtk_util {
|
||||
|
||||
/* These are `const char*` rather than the project-preferred `const char[]`
|
||||
because they must fit the type of an external dependency */
|
||||
extern const char* const kCancelLabel;
|
||||
extern const char* const kNoLabel;
|
||||
extern const char* const kOkLabel;
|
||||
extern const char* const kOpenLabel;
|
||||
extern const char* const kSaveLabel;
|
||||
extern const char* const kYesLabel;
|
||||
const char* GettextPackage();
|
||||
const char* GtkGettext(const char* str);
|
||||
|
||||
const char* GetCancelLabel();
|
||||
const char* GetOpenLabel();
|
||||
const char* GetSaveLabel();
|
||||
const char* GetOkLabel();
|
||||
const char* GetNoLabel();
|
||||
const char* GetYesLabel();
|
||||
|
||||
// Convert and copy a SkBitmap to a GdkPixbuf. NOTE: this uses BGRAToRGBA, so
|
||||
// it is an expensive operation. The returned GdkPixbuf will have a refcount of
|
||||
|
||||
@@ -38,6 +38,7 @@ struct MessageBoxSettings {
|
||||
std::string checkbox_label;
|
||||
bool checkbox_checked = false;
|
||||
gfx::ImageSkia icon;
|
||||
int text_width = 0;
|
||||
|
||||
MessageBoxSettings();
|
||||
MessageBoxSettings(const MessageBoxSettings&);
|
||||
|
||||
@@ -145,13 +145,13 @@ class GtkMessageBox : public NativeWindowObserver {
|
||||
const char* TranslateToStock(int id, const std::string& text) {
|
||||
const std::string lower = base::ToLowerASCII(text);
|
||||
if (lower == "cancel")
|
||||
return gtk_util::kCancelLabel;
|
||||
return gtk_util::GetCancelLabel();
|
||||
if (lower == "no")
|
||||
return gtk_util::kNoLabel;
|
||||
return gtk_util::GetNoLabel();
|
||||
if (lower == "ok")
|
||||
return gtk_util::kOkLabel;
|
||||
return gtk_util::GetOkLabel();
|
||||
if (lower == "yes")
|
||||
return gtk_util::kYesLabel;
|
||||
return gtk_util::GetYesLabel();
|
||||
return text.c_str();
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,12 @@ NSAlert* CreateNSAlert(const MessageBoxSettings& settings) {
|
||||
[alert setIcon:image];
|
||||
}
|
||||
|
||||
if (settings.text_width > 0) {
|
||||
NSRect rect = NSMakeRect(0, 0, settings.text_width, 0);
|
||||
NSView* accessoryView = [[NSView alloc] initWithFrame:rect];
|
||||
[alert setAccessoryView:[accessoryView autorelease]];
|
||||
}
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,28 @@ bool WebContentsPermissionHelper::CheckPermission(
|
||||
details);
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckDevicePermission(
|
||||
content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
auto* permission_manager = static_cast<ElectronPermissionManager*>(
|
||||
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
|
||||
return permission_manager->CheckDevicePermission(permission, origin, device,
|
||||
render_frame_host);
|
||||
}
|
||||
|
||||
void WebContentsPermissionHelper::GrantDevicePermission(
|
||||
content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
auto* permission_manager = static_cast<ElectronPermissionManager*>(
|
||||
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
|
||||
permission_manager->GrantDevicePermission(permission, origin, device,
|
||||
render_frame_host);
|
||||
}
|
||||
|
||||
void WebContentsPermissionHelper::RequestFullscreenPermission(
|
||||
base::OnceCallback<void(bool)> callback) {
|
||||
RequestPermission(
|
||||
@@ -168,6 +190,32 @@ bool WebContentsPermissionHelper::CheckSerialAccessPermission(
|
||||
static_cast<content::PermissionType>(PermissionType::SERIAL), &details);
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckHIDAccessPermission(
|
||||
const url::Origin& embedding_origin) const {
|
||||
base::DictionaryValue details;
|
||||
details.SetString("securityOrigin", embedding_origin.GetURL().spec());
|
||||
return CheckPermission(
|
||||
static_cast<content::PermissionType>(PermissionType::HID), &details);
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckHIDDevicePermission(
|
||||
const url::Origin& origin,
|
||||
base::Value device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
return CheckDevicePermission(
|
||||
static_cast<content::PermissionType>(PermissionType::HID), origin,
|
||||
&device, render_frame_host);
|
||||
}
|
||||
|
||||
void WebContentsPermissionHelper::GrantHIDDevicePermission(
|
||||
const url::Origin& origin,
|
||||
base::Value device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
return GrantDevicePermission(
|
||||
static_cast<content::PermissionType>(PermissionType::HID), origin,
|
||||
&device, render_frame_host);
|
||||
}
|
||||
|
||||
WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPermissionHelper)
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -23,7 +23,8 @@ class WebContentsPermissionHelper
|
||||
POINTER_LOCK = static_cast<int>(content::PermissionType::NUM) + 1,
|
||||
FULLSCREEN,
|
||||
OPEN_EXTERNAL,
|
||||
SERIAL
|
||||
SERIAL,
|
||||
HID
|
||||
};
|
||||
|
||||
// Asynchronous Requests
|
||||
@@ -41,6 +42,15 @@ class WebContentsPermissionHelper
|
||||
bool CheckMediaAccessPermission(const GURL& security_origin,
|
||||
blink::mojom::MediaStreamType type) const;
|
||||
bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const;
|
||||
bool CheckHIDAccessPermission(const url::Origin& embedding_origin) const;
|
||||
bool CheckHIDDevicePermission(
|
||||
const url::Origin& origin,
|
||||
base::Value device,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
void GrantHIDDevicePermission(
|
||||
const url::Origin& origin,
|
||||
base::Value device,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
private:
|
||||
explicit WebContentsPermissionHelper(content::WebContents* web_contents);
|
||||
@@ -54,6 +64,16 @@ class WebContentsPermissionHelper
|
||||
bool CheckPermission(content::PermissionType permission,
|
||||
const base::DictionaryValue* details) const;
|
||||
|
||||
bool CheckDevicePermission(content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
void GrantDevicePermission(content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
content::WebContents* web_contents_;
|
||||
|
||||
WEB_CONTENTS_USER_DATA_KEY_DECL();
|
||||
|
||||
@@ -16,41 +16,13 @@
|
||||
#include "content/public/common/content_switches.h"
|
||||
#include "electron/buildflags/buildflags.h"
|
||||
#include "electron/fuses.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/common/electron_constants.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "shell/common/options_switches.h"
|
||||
#include "shell/common/process_util.h"
|
||||
#include "third_party/crashpad/crashpad/client/annotation.h"
|
||||
|
||||
#include "gin/wrappable.h"
|
||||
#include "shell/browser/api/electron_api_app.h"
|
||||
#include "shell/browser/api/electron_api_auto_updater.h"
|
||||
#include "shell/browser/api/electron_api_browser_view.h"
|
||||
#include "shell/browser/api/electron_api_cookies.h"
|
||||
#include "shell/browser/api/electron_api_data_pipe_holder.h"
|
||||
#include "shell/browser/api/electron_api_debugger.h"
|
||||
#include "shell/browser/api/electron_api_desktop_capturer.h"
|
||||
#include "shell/browser/api/electron_api_download_item.h"
|
||||
#include "shell/browser/api/electron_api_global_shortcut.h"
|
||||
#include "shell/browser/api/electron_api_in_app_purchase.h"
|
||||
#include "shell/browser/api/electron_api_menu.h"
|
||||
#include "shell/browser/api/electron_api_native_theme.h"
|
||||
#include "shell/browser/api/electron_api_net_log.h"
|
||||
#include "shell/browser/api/electron_api_notification.h"
|
||||
#include "shell/browser/api/electron_api_power_monitor.h"
|
||||
#include "shell/browser/api/electron_api_power_save_blocker.h"
|
||||
#include "shell/browser/api/electron_api_protocol.h"
|
||||
#include "shell/browser/api/electron_api_service_worker_context.h"
|
||||
#include "shell/browser/api/electron_api_session.h"
|
||||
#include "shell/browser/api/electron_api_system_preferences.h"
|
||||
#include "shell/browser/api/electron_api_tray.h"
|
||||
#include "shell/browser/api/electron_api_url_loader.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/api/electron_api_web_frame_main.h"
|
||||
#include "shell/browser/api/electron_api_web_request.h"
|
||||
#include "shell/browser/api/event.h"
|
||||
#include "shell/common/api/electron_api_native_image.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace crash_keys {
|
||||
@@ -195,78 +167,6 @@ void SetPlatformCrashKey() {
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string GetCrashValueForGinWrappable(gin::WrapperInfo* info) {
|
||||
std::string crash_location;
|
||||
|
||||
// Adds a breadcrumb for crashes within gin::WrappableBase::SecondWeakCallback
|
||||
// (see patch: add_gin_wrappable_crash_key.patch)
|
||||
// Compares the pointers for the kWrapperInfo within SecondWeakCallback
|
||||
// with the wrapper info from classes that use gin::Wrappable and
|
||||
// could potentially retain a reference after deletion.
|
||||
if (info == &electron::api::WebContents::kWrapperInfo)
|
||||
crash_location = "WebContents";
|
||||
else if (info == &electron::api::BrowserView::kWrapperInfo)
|
||||
crash_location = "BrowserView";
|
||||
else if (info == &electron::api::Notification::kWrapperInfo)
|
||||
crash_location = "Notification";
|
||||
else if (info == &electron::api::Cookies::kWrapperInfo)
|
||||
crash_location = "Cookies";
|
||||
#if BUILDFLAG(ENABLE_DESKTOP_CAPTURER)
|
||||
else if (info == &electron::api::DesktopCapturer::kWrapperInfo)
|
||||
crash_location = "DesktopCapturer";
|
||||
#endif
|
||||
else if (info == &electron::api::Tray::kWrapperInfo)
|
||||
crash_location = "Tray";
|
||||
else if (info == &electron::api::NetLog::kWrapperInfo)
|
||||
crash_location = "NetLog";
|
||||
else if (info == &electron::api::NativeImage::kWrapperInfo)
|
||||
crash_location = "NativeImage";
|
||||
else if (info == &electron::api::Menu::kWrapperInfo)
|
||||
crash_location = "Menu";
|
||||
else if (info == &electron::api::PowerMonitor::kWrapperInfo)
|
||||
crash_location = "PowerMonitor";
|
||||
else if (info == &electron::api::Protocol::kWrapperInfo)
|
||||
crash_location = "Protocol";
|
||||
else if (info == &electron::api::ServiceWorkerContext::kWrapperInfo)
|
||||
crash_location = "ServiceWorkerContext";
|
||||
else if (info == &electron::api::WebFrameMain::kWrapperInfo)
|
||||
crash_location = "WebFrameMain";
|
||||
else if (info == &electron::api::WebRequest::kWrapperInfo)
|
||||
crash_location = "WebRequest";
|
||||
else if (info == &electron::api::SystemPreferences::kWrapperInfo)
|
||||
crash_location = "SystemPreferences";
|
||||
else if (info == &electron::api::Session::kWrapperInfo)
|
||||
crash_location = "Session";
|
||||
else if (info == &electron::api::DownloadItem::kWrapperInfo)
|
||||
crash_location = "DownloadItem";
|
||||
else if (info == &electron::api::NativeTheme::kWrapperInfo)
|
||||
crash_location = "NativeTheme";
|
||||
else if (info == &electron::api::Debugger::kWrapperInfo)
|
||||
crash_location = "Debugger";
|
||||
else if (info == &electron::api::GlobalShortcut::kWrapperInfo)
|
||||
crash_location = "GlobalShortcut";
|
||||
else if (info == &electron::api::InAppPurchase::kWrapperInfo)
|
||||
crash_location = "InAppPurchase";
|
||||
else if (info == &electron::api::DataPipeHolder::kWrapperInfo)
|
||||
crash_location = "DataPipeHolder";
|
||||
else if (info == &electron::api::AutoUpdater::kWrapperInfo)
|
||||
crash_location = "AutoUpdater";
|
||||
else if (info == &electron::api::SimpleURLLoaderWrapper::kWrapperInfo)
|
||||
crash_location = "SimpleURLLoaderWrapper";
|
||||
else if (info == &gin_helper::Event::kWrapperInfo)
|
||||
crash_location = "Event";
|
||||
else if (info == &electron::api::PowerSaveBlocker::kWrapperInfo)
|
||||
crash_location = "PowerSaveBlocker";
|
||||
else if (info == &electron::api::App::kWrapperInfo)
|
||||
crash_location = "App";
|
||||
else
|
||||
crash_location =
|
||||
"Deleted kWrapperInfo does not match listed component. Please review "
|
||||
"listed crash keys.";
|
||||
|
||||
return crash_location;
|
||||
}
|
||||
|
||||
} // namespace crash_keys
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "gin/wrappable.h"
|
||||
|
||||
namespace base {
|
||||
class CommandLine;
|
||||
}
|
||||
@@ -25,8 +23,6 @@ void GetCrashKeys(std::map<std::string, std::string>* keys);
|
||||
void SetCrashKeysFromCommandLine(const base::CommandLine& command_line);
|
||||
void SetPlatformCrashKey();
|
||||
|
||||
std::string GetCrashValueForGinWrappable(gin::WrapperInfo* info);
|
||||
|
||||
} // namespace crash_keys
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -209,6 +209,8 @@ v8::Local<v8::Value> Converter<content::PermissionType>::ToV8(
|
||||
return StringToV8(isolate, "openExternal");
|
||||
case PermissionType::SERIAL:
|
||||
return StringToV8(isolate, "serial");
|
||||
case PermissionType::HID:
|
||||
return StringToV8(isolate, "hid");
|
||||
default:
|
||||
return StringToV8(isolate, "unknown");
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ bool Converter<electron::MessageBoxSettings>::FromV8(
|
||||
dict.Get("noLink", &out->no_link);
|
||||
dict.Get("checkboxChecked", &out->checkbox_checked);
|
||||
dict.Get("icon", &out->icon);
|
||||
dict.Get("textWidth", &out->text_width);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from 'chai';
|
||||
import { BrowserWindow, WebContents, session, ipcMain, app, protocol, webContents } from 'electron/main';
|
||||
import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents } from 'electron/main';
|
||||
import { emittedOnce } from './events-helpers';
|
||||
import { closeAllWindows } from './window-helpers';
|
||||
import * as https from 'https';
|
||||
@@ -1878,3 +1878,127 @@ describe('navigator.bluetooth', () => {
|
||||
expect(bluetooth).to.be.oneOf(['Found a device!', 'Bluetooth adapter not available.', 'User cancelled the requestDevice() chooser.']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigator.hid', () => {
|
||||
let w: BrowserWindow;
|
||||
let server: http.Server;
|
||||
let serverUrl: string;
|
||||
before(async () => {
|
||||
w = new BrowserWindow({
|
||||
show: false
|
||||
});
|
||||
await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
|
||||
server = http.createServer((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('<body>');
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
serverUrl = `http://localhost:${(server.address() as any).port}`;
|
||||
});
|
||||
|
||||
const getDevices: any = () => {
|
||||
return w.webContents.executeJavaScript(`
|
||||
navigator.hid.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString());
|
||||
`, true);
|
||||
};
|
||||
|
||||
after(() => {
|
||||
server.close();
|
||||
closeAllWindows();
|
||||
});
|
||||
afterEach(() => {
|
||||
session.defaultSession.setPermissionCheckHandler(null);
|
||||
session.defaultSession.setDevicePermissionHandler(null);
|
||||
session.defaultSession.removeAllListeners('select-hid-device');
|
||||
});
|
||||
|
||||
it('does not return a device if select-hid-device event is not defined', async () => {
|
||||
w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
|
||||
const device = await getDevices();
|
||||
expect(device).to.equal('');
|
||||
});
|
||||
|
||||
it('does not return a device when permission denied', async () => {
|
||||
let selectFired = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
selectFired = true;
|
||||
callback();
|
||||
});
|
||||
session.defaultSession.setPermissionCheckHandler(() => false);
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.false();
|
||||
expect(device).to.equal('');
|
||||
});
|
||||
|
||||
it('returns a device when select-hid-device event is defined', async () => {
|
||||
let haveDevices = false;
|
||||
let selectFired = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
expect(details.frame).to.have.ownProperty('frameTreeNodeId').that.is.a('number');
|
||||
selectFired = true;
|
||||
if (details.deviceList.length > 0) {
|
||||
haveDevices = true;
|
||||
callback(details.deviceList[0].deviceId);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.true();
|
||||
if (haveDevices) {
|
||||
expect(device).to.contain('[object HIDDevice]');
|
||||
} else {
|
||||
expect(device).to.equal('');
|
||||
}
|
||||
if (process.arch === 'arm64' || process.arch === 'arm') {
|
||||
// arm CI returns HID devices - this block may need to change if CI hardware changes.
|
||||
expect(haveDevices).to.be.true();
|
||||
// Verify that navigation will clear device permissions
|
||||
const grantedDevices = await w.webContents.executeJavaScript('navigator.hid.getDevices()');
|
||||
expect(grantedDevices).to.not.be.empty();
|
||||
w.loadURL(serverUrl);
|
||||
const [,,,,, frameProcessId, frameRoutingId] = await emittedOnce(w.webContents, 'did-frame-navigate');
|
||||
const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
|
||||
expect(frame).to.not.be.empty();
|
||||
if (frame) {
|
||||
const grantedDevicesOnNewPage = await frame.executeJavaScript('navigator.hid.getDevices()');
|
||||
expect(grantedDevicesOnNewPage).to.be.empty();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns a device when DevicePermissionHandler is defined', async () => {
|
||||
let haveDevices = false;
|
||||
let selectFired = false;
|
||||
let gotDevicePerms = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
selectFired = true;
|
||||
if (details.deviceList.length > 0) {
|
||||
const foundDevice = details.deviceList.find((device) => {
|
||||
if (device.name && device.name !== '' && device.serialNumber && device.serialNumber !== '') {
|
||||
haveDevices = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (foundDevice) {
|
||||
callback(foundDevice.deviceId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
session.defaultSession.setDevicePermissionHandler(() => {
|
||||
gotDevicePerms = true;
|
||||
return true;
|
||||
});
|
||||
await w.webContents.executeJavaScript('navigator.hid.getDevices();', true);
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.true();
|
||||
if (haveDevices) {
|
||||
expect(device).to.contain('[object HIDDevice]');
|
||||
expect(gotDevicePerms).to.be.true();
|
||||
} else {
|
||||
expect(device).to.equal('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user