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