1
- import { BrowserWindow , Menu , app , dialog , ipcMain } from 'electron' ;
1
+ import { BrowserWindow , Menu , app , dialog , ipcMain , systemPreferences } from 'electron' ;
2
2
import fs from 'fs' ;
3
3
import path from 'path' ;
4
- import { format as formatUrl } from 'url' ;
4
+ import { URL } from 'url' ;
5
5
6
6
import { getFilterForExtension } from './FileFilters' ;
7
7
import MacOSMenu from './MacOSMenu' ;
@@ -16,6 +16,126 @@ const isDevelopment = process.env.NODE_ENV !== 'production';
16
16
// global window references prevent them from being garbage-collected
17
17
const _windows = { } ;
18
18
19
+ const displayPermissionDeniedWarning = ( browserWindow , permissionType ) => {
20
+ let title ;
21
+ let message ;
22
+ switch ( permissionType ) {
23
+ case 'camera' :
24
+ title = 'Camera Permission Denied' ;
25
+ message = 'Permission to use the camera has been denied. ' +
26
+ 'Scratch will not be able to take a photo or use video sensing blocks.' ;
27
+ break ;
28
+ case 'microphone' :
29
+ title = 'Microphone Permission Denied' ;
30
+ message = 'Permission to use the microphone has been denied. ' +
31
+ 'Scratch will not be able to record sounds or detect loudness.' ;
32
+ break ;
33
+ default : // shouldn't ever happen...
34
+ title = 'Permission Denied' ;
35
+ message = 'A permission has been denied.' ;
36
+ }
37
+
38
+ let instructions ;
39
+ switch ( process . platform ) {
40
+ case 'darwin' :
41
+ instructions = 'To change Scratch permissions, please check "Security & Privacy" in System Preferences.' ;
42
+ break ;
43
+ default :
44
+ instructions = 'To change Scratch permissions, please check your system settings and restart Scratch.' ;
45
+ break ;
46
+ }
47
+ message = `${ message } \n\n${ instructions } ` ;
48
+
49
+ dialog . showMessageBox ( browserWindow , { type : 'warning' , title, message} ) ;
50
+ } ;
51
+
52
+ /**
53
+ * Build an absolute URL from a relative one, optionally adding search query parameters.
54
+ * The base of the URL will depend on whether or not the application is running in development mode.
55
+ * @param {string } url - the relative URL, like 'index.html'
56
+ * @param {* } search - the optional "search" parameters (the part of the URL after '?'), like "route=about"
57
+ * @returns {string } - an absolute URL as a string
58
+ */
59
+ const makeFullUrl = ( url , search = null ) => {
60
+ const baseUrl = ( isDevelopment ?
61
+ `http://localhost:${ process . env . ELECTRON_WEBPACK_WDS_PORT } /` :
62
+ `file://${ __dirname } /`
63
+ ) ;
64
+ const fullUrl = new URL ( url , baseUrl ) ;
65
+ if ( search ) {
66
+ fullUrl . search = search ; // automatically percent-encodes anything that needs it
67
+ }
68
+ return fullUrl . toString ( ) ;
69
+ } ;
70
+
71
+ /**
72
+ * Prompt in a platform-specific way for permission to access the microphone or camera, if Electron supports doing so.
73
+ * Any application-level checks, such as whether or not a particular frame or document should be allowed to ask,
74
+ * should be done before calling this function.
75
+ *
76
+ * @param {string } mediaType - one of Electron's media types, like 'microphone' or 'camera'
77
+ * @returns {boolean } - true if permission granted, false otherwise.
78
+ */
79
+ const askForMediaAccess = async mediaType => {
80
+ if ( systemPreferences . askForMediaAccess ) {
81
+ // Electron currently only implements this on macOS
82
+ return systemPreferences . askForMediaAccess ( mediaType ) ;
83
+ }
84
+ // For other platforms we can't reasonably do anything other than assume we have access.
85
+ return true ;
86
+ } ;
87
+
88
+ const handlePermissionRequest = async ( webContents , permission , callback , details ) => {
89
+ if ( webContents !== _windows . main . webContents ) {
90
+ // deny: request came from somewhere other than the main window's web contents
91
+ return callback ( false ) ;
92
+ }
93
+ if ( ! details . isMainFrame ) {
94
+ // deny: request came from a subframe of the main window, not the main frame
95
+ return callback ( false ) ;
96
+ }
97
+ if ( permission !== 'media' ) {
98
+ // deny: request is for some other kind of access like notifications or pointerLock
99
+ return callback ( false ) ;
100
+ }
101
+ const requiredBase = makeFullUrl ( '' ) ;
102
+ if ( details . requestingUrl . indexOf ( requiredBase ) !== 0 ) {
103
+ // deny: request came from a URL outside of our "sandbox"
104
+ return callback ( false ) ;
105
+ }
106
+ let askForMicrophone = false ;
107
+ let askForCamera = false ;
108
+ for ( const mediaType of details . mediaTypes ) {
109
+ switch ( mediaType ) {
110
+ case 'audio' :
111
+ askForMicrophone = true ;
112
+ break ;
113
+ case 'video' :
114
+ askForCamera = true ;
115
+ break ;
116
+ default :
117
+ // deny: unhandled media type
118
+ return callback ( false ) ;
119
+ }
120
+ }
121
+ const parentWindow = _windows . main ; // if we ever allow media in non-main windows we'll also need to change this
122
+ if ( askForMicrophone ) {
123
+ const microphoneResult = await askForMediaAccess ( 'microphone' ) ;
124
+ if ( ! microphoneResult ) {
125
+ displayPermissionDeniedWarning ( parentWindow , 'microphone' ) ;
126
+ return callback ( false ) ;
127
+ }
128
+ }
129
+ if ( askForCamera ) {
130
+ const cameraResult = await askForMediaAccess ( 'camera' ) ;
131
+ if ( ! cameraResult ) {
132
+ displayPermissionDeniedWarning ( parentWindow , 'camera' ) ;
133
+ return callback ( false ) ;
134
+ }
135
+ }
136
+ return callback ( true ) ;
137
+ } ;
138
+
19
139
const createWindow = ( { search = null , url = 'index.html' , ...browserWindowOptions } ) => {
20
140
const window = new BrowserWindow ( {
21
141
useContentSize : true ,
@@ -28,25 +148,13 @@ const createWindow = ({search = null, url = 'index.html', ...browserWindowOption
28
148
} ) ;
29
149
const webContents = window . webContents ;
30
150
151
+ webContents . session . setPermissionRequestHandler ( handlePermissionRequest ) ;
152
+
31
153
if ( isDevelopment ) {
32
154
webContents . openDevTools ( { mode : 'detach' , activate : true } ) ;
33
155
}
34
156
35
- const fullUrl = formatUrl ( isDevelopment ?
36
- { // Webpack Dev Server
37
- hostname : 'localhost' ,
38
- pathname : url ,
39
- port : process . env . ELECTRON_WEBPACK_WDS_PORT ,
40
- protocol : 'http' ,
41
- search,
42
- slashes : true
43
- } : { // production / bundled
44
- pathname : path . join ( __dirname , url ) ,
45
- protocol : 'file' ,
46
- search,
47
- slashes : true
48
- }
49
- ) ;
157
+ const fullUrl = makeFullUrl ( url , search ) ;
50
158
window . loadURL ( fullUrl ) ;
51
159
52
160
return window ;
0 commit comments