|
| 1 | +const { |
| 2 | + app, |
| 3 | + protocol, |
| 4 | + dialog, |
| 5 | + BrowserWindow, |
| 6 | + session, |
| 7 | + ipcMain, |
| 8 | + Menu |
| 9 | +} = require('electron'); |
| 10 | +// The splash screen is what appears while the app is loading |
| 11 | +const { initSplashScreen, OfficeTemplate } = require('electron-splashscreen'); |
| 12 | +const { resolve } = require('app-root-path'); |
| 13 | +const { |
| 14 | + default: installExtension, |
| 15 | + REACT_DEVELOPER_TOOLS |
| 16 | +} = require('electron-devtools-installer'); |
| 17 | +const debug = require('electron-debug'); |
| 18 | + |
| 19 | +const Protocol = require('./protocol'); |
| 20 | +// menu from another file to modularize the code |
| 21 | +const MenuBuilder = require('./menu'); |
| 22 | + |
| 23 | +const path = require('path'); |
| 24 | +// const fs = require('fs'); |
| 25 | + |
| 26 | +console.log('NODE ENV is ', process.env.NODE_ENV); |
| 27 | +const isDev = process.env.NODE_ENV === 'development'; |
| 28 | +const port = 8080; |
| 29 | +const selfHost = `http://localhost:${port}`; |
| 30 | + |
| 31 | +// Keep a global reference of the window object, if you don't, the window will |
| 32 | +// be closed automatically when the JavaScript object is garbage collected. |
| 33 | +let win; |
| 34 | +let menuBuilder; |
| 35 | + |
| 36 | +async function createWindow() { |
| 37 | + if (isDev) { |
| 38 | + await installExtension([REACT_DEVELOPER_TOOLS]) |
| 39 | + .then(name => console.log(`Added Extension: ${name}`)) |
| 40 | + .catch(err => console.log('An error occurred: ', err)); |
| 41 | + } else { |
| 42 | + // Needs to happen before creating/loading the browser window; |
| 43 | + // not necessarily instead of extensions, just using this code block |
| 44 | + // so I don't have to write another 'if' statement |
| 45 | + protocol.registerBufferProtocol(Protocol.scheme, Protocol.requestHandler); |
| 46 | + } |
| 47 | + |
| 48 | + // Create the browser window. |
| 49 | + win = new BrowserWindow({ |
| 50 | + // full screen |
| 51 | + width: 1920, |
| 52 | + height: 1080, |
| 53 | + // window title |
| 54 | + title: `ReacType`, |
| 55 | + show: false, |
| 56 | + icon: path.join(__dirname, '../src/public/icons/png/256x256.png'), |
| 57 | + win: { |
| 58 | + icon: path.join(__dirname, '../src/public/icons/win/icon.ico'), |
| 59 | + target: ['portable'] |
| 60 | + }, |
| 61 | + webPreferences: { |
| 62 | + zoomFactor: 0.7, |
| 63 | + // enable devtools |
| 64 | + devTools: isDev, |
| 65 | + // crucial security feature - blocks rendering process from having access to node moduels |
| 66 | + nodeIntegration: false, |
| 67 | + // web workers will not have access to node |
| 68 | + nodeIntegrationInWorker: false, |
| 69 | + // disallow experimental feature to allow node.js suppport in subframes (iframes/child windows) |
| 70 | + nodeIntegrationInSubFrames: false, |
| 71 | + // runs electron apis and preload script in a seperate JS context |
| 72 | + // sepearate context has access to document/window but has it's own built-ins and is isolate from chagnes to gloval environment by locaded page |
| 73 | + // Electron API only available from preload, not loaded page |
| 74 | + contextIsolation: true, |
| 75 | + // disables remote module. critical for ensuring that rendering process doesn't have access to node functionality |
| 76 | + enableRemoteModule: false, |
| 77 | + // path of preload script. preload is how the renderer page will have access to electron functionality |
| 78 | + preload: path.join(__dirname, 'preload.js') |
| 79 | + } |
| 80 | + }); |
| 81 | + |
| 82 | + console.log('PATH is ', resolve('/')); |
| 83 | + |
| 84 | + //splash screen deets |
| 85 | + // TODO: splash screen logo/icon aren't loading in dev mode |
| 86 | + const hideSplashscreen = initSplashScreen({ |
| 87 | + mainWindow: win, |
| 88 | + icon: resolve('app/src/public/icons/png/64x64.png'), |
| 89 | + url: OfficeTemplate, |
| 90 | + width: 500, |
| 91 | + height: 300, |
| 92 | + brand: 'OS Labs', |
| 93 | + productName: 'ReacType', |
| 94 | + logo: resolve('app/src/public/icons/png/64x64.png'), |
| 95 | + color: '#3BBCAF', |
| 96 | + website: 'www.reactype.io', |
| 97 | + text: 'Initializing ...' |
| 98 | + }); |
| 99 | + |
| 100 | + // Load app |
| 101 | + if (isDev) { |
| 102 | + // load app from webdev server |
| 103 | + win.loadURL(selfHost); |
| 104 | + } else { |
| 105 | + // load local file if in production |
| 106 | + win.loadURL(`${Protocol.scheme}://rse/index-prod.html`); |
| 107 | + } |
| 108 | + |
| 109 | + // load page once window is loaded |
| 110 | + win.once('ready-to-show', () => { |
| 111 | + win.show(); |
| 112 | + hideSplashscreen(); |
| 113 | + }); |
| 114 | + |
| 115 | + // Only do these things when in development |
| 116 | + if (isDev) { |
| 117 | + // automatically open DevTools when opening the app |
| 118 | + win.webContents.once('dom-ready', () => { |
| 119 | + debug(); |
| 120 | + win.webContents.openDevTools(); |
| 121 | + }); |
| 122 | + } |
| 123 | + |
| 124 | + // Emitted when the window is closed. |
| 125 | + win.on('closed', () => { |
| 126 | + // Dereference the window object, usually you would store windows |
| 127 | + // in an array if your app supports multi windows, this is the time |
| 128 | + // when you should delete the corresponding element. |
| 129 | + win = null; |
| 130 | + }); |
| 131 | + |
| 132 | + // https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content |
| 133 | + // TODO: is this the same type of sessions that have in react type |
| 134 | + // Could potentially remove this session capability - it appears to be more focused on approving requests from 3rd party notifications |
| 135 | + const ses = session; |
| 136 | + const partition = 'default'; |
| 137 | + ses |
| 138 | + .fromPartition(partition) |
| 139 | + .setPermissionRequestHandler((webContents, permission, callback) => { |
| 140 | + let allowedPermissions = []; // Full list here: https://developer.chrome.com/extensions/declare_permissions#manifest |
| 141 | + |
| 142 | + if (allowedPermissions.includes(permission)) { |
| 143 | + callback(true); // Approve permission request |
| 144 | + } else { |
| 145 | + console.error( |
| 146 | + `The application tried to request permission for '${permission}'. This permission was not whitelisted and has been blocked.` |
| 147 | + ); |
| 148 | + |
| 149 | + callback(false); // Deny |
| 150 | + } |
| 151 | + }); |
| 152 | + |
| 153 | + // https://electronjs.org/docs/tutorial/security#1-only-load-secure-content; |
| 154 | + // The below code can only run when a scheme and host are defined, I thought |
| 155 | + // we could use this over _all_ urls |
| 156 | + ses |
| 157 | + .fromPartition(partition) |
| 158 | + .webRequest.onBeforeRequest({ urls: ['http://localhost./*'] }, listener => { |
| 159 | + if (listener.url.indexOf('http://') >= 0) { |
| 160 | + listener.callback({ |
| 161 | + cancel: true |
| 162 | + }); |
| 163 | + } |
| 164 | + }); |
| 165 | + |
| 166 | + menuBuilder = MenuBuilder(win, 'ReacType'); |
| 167 | + menuBuilder.buildMenu(); |
| 168 | +} |
| 169 | + |
| 170 | +// TODO: unclear of whether this is necsssary or not. Looks like a security best practice but will likely introduce complications |
| 171 | +// Needs to be called before app is ready; |
| 172 | +// gives our scheme access to load relative files, |
| 173 | +// as well as local storage, cookies, etc. |
| 174 | +// https://electronjs.org/docs/api/protocol#protocolregisterschemesasprivilegedcustomschemes |
| 175 | +protocol.registerSchemesAsPrivileged([ |
| 176 | + { |
| 177 | + scheme: Protocol.scheme, |
| 178 | + privileges: { |
| 179 | + standard: true, |
| 180 | + secure: true |
| 181 | + } |
| 182 | + } |
| 183 | +]); |
| 184 | + |
| 185 | +// This method will be called when Electron has finished |
| 186 | +// initialization and is ready to create browser windows. |
| 187 | +// Some APIs can only be used after this event occurs. |
| 188 | +app.on('ready', createWindow); |
| 189 | + |
| 190 | +// Quit when all windows are closed. |
| 191 | +app.on('window-all-closed', () => { |
| 192 | + // On macOS it is common for applications and their menu bar |
| 193 | + // to stay active until the user quits explicitly with Cmd + Q |
| 194 | + if (process.platform !== 'darwin') { |
| 195 | + app.quit(); |
| 196 | + } else { |
| 197 | + // TODO: remove i18nextbackend |
| 198 | + // i18nextBackend.clearMainBindings(ipcMain); |
| 199 | + ContextMenu.clearMainBindings(ipcMain); |
| 200 | + } |
| 201 | +}); |
| 202 | + |
| 203 | +app.on('activate', () => { |
| 204 | + // On macOS it's common to re-create a window in the app when the |
| 205 | + // dock icon is clicked and there are no other windows open. |
| 206 | + if (win === null) { |
| 207 | + createWindow(); |
| 208 | + } |
| 209 | +}); |
| 210 | + |
| 211 | +// https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation |
| 212 | +// limits navigation within the app to a whitelisted array |
| 213 | +// redirects are a common attack vector |
| 214 | +// TODO: add github to the validOrigins whitelist array |
| 215 | + |
| 216 | +// after the contents of the webpage are rendered, set up event listeners on the webContents |
| 217 | +app.on('web-contents-created', (event, contents) => { |
| 218 | + contents.on('will-navigate', (event, navigationUrl) => { |
| 219 | + const parsedUrl = new URL(navigationUrl); |
| 220 | + const validOrigins = [ |
| 221 | + selfHost, |
| 222 | + 'https://localhost:8080', |
| 223 | + 'https://github.com/' |
| 224 | + ]; |
| 225 | + // Log and prevent the app from navigating to a new page if that page's origin is not whitelisted |
| 226 | + if (!validOrigins.includes(parsedUrl.origin)) { |
| 227 | + console.error( |
| 228 | + `The application tried to redirect to the following address: '${parsedUrl}'. This origin is not whitelisted and the attempt to navigate was blocked.` |
| 229 | + ); |
| 230 | + // if the requested URL is not in the whitelisted array, then don't navigate there |
| 231 | + event.preventDefault(); |
| 232 | + return; |
| 233 | + } |
| 234 | + }); |
| 235 | + |
| 236 | + contents.on('will-redirect', (event, navigationUrl) => { |
| 237 | + const parsedUrl = new URL(navigationUrl); |
| 238 | + const validOrigins = [ |
| 239 | + selfHost, |
| 240 | + 'https://localhost:8080', |
| 241 | + 'https://github.com/' |
| 242 | + ]; |
| 243 | + |
| 244 | + // Log and prevent the app from redirecting to a new page |
| 245 | + if (!validOrigins.includes(parsedUrl.origin)) { |
| 246 | + console.error( |
| 247 | + `The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.` |
| 248 | + ); |
| 249 | + |
| 250 | + event.preventDefault(); |
| 251 | + return; |
| 252 | + } |
| 253 | + }); |
| 254 | + |
| 255 | + // https://electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation |
| 256 | + // The web-view is used to embed guest content in a page |
| 257 | + // This event listener deletes webviews if they happen to occur in the app |
| 258 | + // https://www.electronjs.org/docs/api/web-contents#event-will-attach-webview |
| 259 | + contents.on('will-attach-webview', (event, webPreferences, params) => { |
| 260 | + // Strip away preload scripts if unused or verify their location is legitimate |
| 261 | + delete webPreferences.preload; |
| 262 | + delete webPreferences.preloadURL; |
| 263 | + |
| 264 | + // Disable Node.js integration |
| 265 | + webPreferences.nodeIntegration = false; |
| 266 | + }); |
| 267 | + |
| 268 | + // https://electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows |
| 269 | + contents.on('new-window', async (event, navigationUrl) => { |
| 270 | + // Log and prevent opening up a new window |
| 271 | + console.error( |
| 272 | + `The application tried to open a new window at the following address: '${navigationUrl}'. This attempt was blocked.` |
| 273 | + ); |
| 274 | + |
| 275 | + event.preventDefault(); |
| 276 | + return; |
| 277 | + }); |
| 278 | +}); |
| 279 | + |
| 280 | +// Filter loading any module via remote; |
| 281 | +// you shouldn't be using remote at all, though |
| 282 | +// https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module |
| 283 | +app.on('remote-require', (event, webContents, moduleName) => { |
| 284 | + event.preventDefault(); |
| 285 | +}); |
| 286 | + |
| 287 | +// built-ins are modules such as "app" |
| 288 | +app.on('remote-get-builtin', (event, webContents, moduleName) => { |
| 289 | + event.preventDefault(); |
| 290 | +}); |
| 291 | + |
| 292 | +app.on('remote-get-global', (event, webContents, globalName) => { |
| 293 | + event.preventDefault(); |
| 294 | +}); |
| 295 | + |
| 296 | +app.on('remote-get-current-window', (event, webContents) => { |
| 297 | + event.preventDefault(); |
| 298 | +}); |
| 299 | + |
| 300 | +app.on('remote-get-current-web-contents', (event, webContents) => { |
| 301 | + event.preventDefault(); |
| 302 | +}); |
| 303 | + |
| 304 | +// When a user selects "Export project", a function (chooseAppDir loaded via preload.js) |
| 305 | +// is triggered that sends a "choose_app_dir" message to the main process |
| 306 | +// when the "choose_app_dir" message is received it triggers this event listener |
| 307 | +ipcMain.on('choose_app_dir', event => { |
| 308 | + // dialog displays the native system's dialogue for selecting files |
| 309 | + // once a directory is chosen send a message back to the renderer with the path of the directory |
| 310 | + dialog |
| 311 | + .showOpenDialog(win, { |
| 312 | + properties: ['openDirectory'], |
| 313 | + buttonLabel: 'Export' |
| 314 | + }) |
| 315 | + .then(directory => { |
| 316 | + if (!directory) return; |
| 317 | + event.sender.send('app_dir_selected', directory.filePaths[0]); |
| 318 | + }) |
| 319 | + .catch(err => console.log('ERROR on "choose_app_dir" event: ', err)); |
| 320 | +}); |
0 commit comments