@@ -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,77 @@ const createWindow = ({search = null, url = 'index.html', ...browserWindowOption
47
73
search,
48
74
slashes : true
49
75
}
50
- ) ;
76
+ ) ) ;
77
+
78
+ const handlePermissionRequest = async ( webContents , permission , callback , details ) => {
79
+ if ( webContents !== _windows . main . webContents ) {
80
+ // deny: request came from somewhere other than the main window's web contents
81
+ return callback ( false ) ;
82
+ }
83
+ if ( ! details . isMainFrame ) {
84
+ // deny: request came from a subframe of the main window, not the main frame
85
+ return callback ( false ) ;
86
+ }
87
+ if ( permission !== 'media' ) {
88
+ // deny: request is for some other kind of access like notifications or pointerLock
89
+ return callback ( false ) ;
90
+ }
91
+ const requiredBase = makeFullUrl ( '/' ) ;
92
+ if ( details . requestingUrl . indexOf ( requiredBase ) !== 0 ) {
93
+ // deny: request came from a URL outside of our "sandbox"
94
+ return callback ( false ) ;
95
+ }
96
+ let askForMicrophone = false ;
97
+ let askForCamera = false ;
98
+ for ( const mediaType of details . mediaTypes ) {
99
+ switch ( mediaType ) {
100
+ case 'audio' :
101
+ askForMicrophone = true ;
102
+ break ;
103
+ case 'video' :
104
+ askForCamera = true ;
105
+ break ;
106
+ default :
107
+ // deny: unhandled media type
108
+ return callback ( false ) ;
109
+ }
110
+ }
111
+ const parentWindow = _windows . main ; // if we ever allow media in non-main windows we'll also need to change this
112
+ if ( askForMicrophone ) {
113
+ const microphoneResult = await systemPreferences . askForMediaAccess ( 'microphone' ) ;
114
+ if ( ! microphoneResult ) {
115
+ displayPermissionDeniedWarning ( parentWindow , 'microphone' ) ;
116
+ return callback ( false ) ;
117
+ }
118
+ }
119
+ if ( askForCamera ) {
120
+ const cameraResult = await systemPreferences . askForMediaAccess ( 'camera' ) ;
121
+ if ( ! cameraResult ) {
122
+ displayPermissionDeniedWarning ( parentWindow , 'camera' ) ;
123
+ return callback ( false ) ;
124
+ }
125
+ }
126
+ return callback ( true ) ;
127
+ } ;
128
+
129
+ const createWindow = ( { search = null , url = 'index.html' , ...browserWindowOptions } ) => {
130
+ const window = new BrowserWindow ( {
131
+ useContentSize : true ,
132
+ show : false ,
133
+ webPreferences : {
134
+ nodeIntegration : true
135
+ } ,
136
+ ...browserWindowOptions
137
+ } ) ;
138
+ const webContents = window . webContents ;
139
+
140
+ webContents . session . setPermissionRequestHandler ( handlePermissionRequest ) ;
141
+
142
+ if ( isDevelopment ) {
143
+ webContents . openDevTools ( { mode : 'detach' , activate : true } ) ;
144
+ }
145
+
146
+ const fullUrl = makeFullUrl ( url , search ) ;
51
147
window . loadURL ( fullUrl ) ;
52
148
53
149
return window ;
@@ -140,10 +236,6 @@ const createMainWindow = () => {
140
236
if ( process . platform === 'darwin' ) {
141
237
const osxMenu = Menu . buildFromTemplate ( MacOSMenu ( app ) ) ;
142
238
Menu . setApplicationMenu ( osxMenu ) ;
143
- ( async ( ) => {
144
- await systemPreferences . askForMediaAccess ( 'microphone' ) ;
145
- await systemPreferences . askForMediaAccess ( 'camera' ) ;
146
- } ) ( ) ;
147
239
} else {
148
240
// disable menu for other platforms
149
241
Menu . setApplicationMenu ( null ) ;
0 commit comments