diff --git a/atom/browser/api/atom_api_auto_updater.cc b/atom/browser/api/atom_api_auto_updater.cc index ea3024191e9..c23e488f64f 100644 --- a/atom/browser/api/atom_api_auto_updater.cc +++ b/atom/browser/api/atom_api_auto_updater.cc @@ -7,6 +7,7 @@ #include "atom/browser/browser.h" #include "atom/browser/native_window.h" #include "atom/browser/window_list.h" +#include "atom/common/api/event_emitter_caller.h" #include "atom/common/native_mate_converters/callback.h" #include "atom/common/node_includes.h" #include "base/time/time.h" @@ -47,7 +48,9 @@ void AutoUpdater::OnError(const std::string& message) { v8::Locker locker(isolate()); v8::HandleScope handle_scope(isolate()); auto error = v8::Exception::Error(mate::StringToV8(isolate(), message)); - EmitCustomEvent( + mate::EmitEvent( + isolate(), + GetWrapper(), "error", error->ToObject(isolate()->GetCurrentContext()).ToLocalChecked(), // Message is also emitted to keep compatibility with old code. diff --git a/atom/browser/api/atom_api_window.cc b/atom/browser/api/atom_api_window.cc index 4cf2fee82f7..72b8e33ef7b 100644 --- a/atom/browser/api/atom_api_window.cc +++ b/atom/browser/api/atom_api_window.cc @@ -191,6 +191,10 @@ void Window::OnWindowClosed() { FROM_HERE, GetDestroyClosure()); } +void Window::OnWindowEndSession() { + Emit("session-end"); +} + void Window::OnWindowBlur() { Emit("blur"); } diff --git a/atom/browser/api/atom_api_window.h b/atom/browser/api/atom_api_window.h index 1388b63de75..75f0328ba64 100644 --- a/atom/browser/api/atom_api_window.h +++ b/atom/browser/api/atom_api_window.h @@ -63,6 +63,7 @@ class Window : public mate::TrackableObject, void WillCloseWindow(bool* prevent_default) override; void WillDestroyNativeObject() override; void OnWindowClosed() override; + void OnWindowEndSession() override; void OnWindowBlur() override; void OnWindowFocus() override; void OnWindowShow() override; diff --git a/atom/browser/native_window.cc b/atom/browser/native_window.cc index 93bfc76c51e..9e2c11aec4a 100644 --- a/atom/browser/native_window.cc +++ b/atom/browser/native_window.cc @@ -474,6 +474,11 @@ void NativeWindow::NotifyWindowClosed() { observer.OnWindowClosed(); } +void NativeWindow::NotifyWindowEndSession() { + for (NativeWindowObserver& observer : observers_) + observer.OnWindowEndSession(); +} + void NativeWindow::NotifyWindowBlur() { for (NativeWindowObserver& observer : observers_) observer.OnWindowBlur(); diff --git a/atom/browser/native_window.h b/atom/browser/native_window.h index 48b56e8b47e..d3f18d8fb95 100644 --- a/atom/browser/native_window.h +++ b/atom/browser/native_window.h @@ -218,6 +218,7 @@ class NativeWindow : public base::SupportsUserData, // Public API used by platform-dependent delegates and observers to send UI // related notifications. void NotifyWindowClosed(); + void NotifyWindowEndSession(); void NotifyWindowBlur(); void NotifyWindowFocus(); void NotifyWindowShow(); diff --git a/atom/browser/native_window_observer.h b/atom/browser/native_window_observer.h index 191f1bcab49..8c908dc8237 100644 --- a/atom/browser/native_window_observer.h +++ b/atom/browser/native_window_observer.h @@ -40,6 +40,9 @@ class NativeWindowObserver { // Called when the window is closed. virtual void OnWindowClosed() {} + // Called when Windows sends WM_ENDSESSION message + virtual void OnWindowEndSession() {} + // Called when window loses focus. virtual void OnWindowBlur() {} diff --git a/atom/browser/native_window_views_win.cc b/atom/browser/native_window_views_win.cc index 1b523e90b80..abda0d0b026 100644 --- a/atom/browser/native_window_views_win.cc +++ b/atom/browser/native_window_views_win.cc @@ -147,6 +147,11 @@ bool NativeWindowViews::PreHandleMSG( } return false; } + case WM_ENDSESSION: { + if (w_param) { + NotifyWindowEndSession(); + } + } default: return false; } diff --git a/default_app/index.html b/default_app/index.html index 3f29908a563..41b5396360d 100644 --- a/default_app/index.html +++ b/default_app/index.html @@ -113,24 +113,6 @@ - -
- +
Docs @@ -162,25 +144,15 @@ Console (or Terminal):

- +

 
     

The path-to-your-app should be the path to your own Electron app.

-

You can read the - - guide in Electron's - +

You can read the quick start + guide in Electron's docs to learn how to write one.

@@ -195,25 +167,7 @@
diff --git a/default_app/renderer.js b/default_app/renderer.js new file mode 100644 index 00000000000..6196195eab9 --- /dev/null +++ b/default_app/renderer.js @@ -0,0 +1,45 @@ +const {remote, shell} = require('electron') +const {execFile} = require('child_process') + +const {execPath} = remote.process + +document.onclick = function (e) { + e.preventDefault() + if (e.target.tagName === 'A') { + shell.openExternal(e.target.href) + } + return false +} + +document.ondragover = document.ondrop = function (e) { + e.preventDefault() + return false +} + +const holder = document.getElementById('holder') +holder.ondragover = function () { + this.className = 'hover' + return false +} + +holder.ondragleave = holder.ondragend = function () { + this.className = '' + return false +} + +holder.ondrop = function (e) { + this.className = '' + e.preventDefault() + + const file = e.dataTransfer.files[0] + execFile(execPath, [file.path], { + detached: true, stdio: 'ignore' + }).unref() + return false +} + +const version = process.versions.electron +document.querySelector('.header-version').innerText = version +document.querySelector('.command-example').innerText = `${execPath} path-to-your-app` +document.querySelector('.quick-start-link').href = `https://github.com/electron/electron/blob/v${version}/docs/tutorial/quick-start.md` +document.querySelector('.docs-link').href = `https://github.com/electron/electron/tree/v${version}/docs#readme` diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index cd55ba0bbda..8fcc3f8213d 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -376,6 +376,11 @@ window.onbeforeunload = (e) => { Emitted when the window is closed. After you have received this event you should remove the reference to the window and avoid using it any more. +#### Event: 'session-end' _Windows_ + +Emitted when window session is going to end due to force shutdown or machine restart +or session log off. + #### Event: 'unresponsive' Emitted when the web page becomes unresponsive. diff --git a/docs/api/download-item.md b/docs/api/download-item.md index 6603b619aaf..c07bf98a869 100644 --- a/docs/api/download-item.md +++ b/docs/api/download-item.md @@ -100,6 +100,8 @@ Returns `Boolean` - Whether the download is paused. Resumes the download that has been paused. +**Note:** To enable resumable downloads the server you are downloading from must support range requests and provide both `Last-Modified` and `ETag` header values. Otherwise `resume()` will dismiss previously received bytes and restart the download from the beginning. + #### `downloadItem.canResume()` Resumes `Boolean` - Whether the download can resume. diff --git a/docs/api/menu-item.md b/docs/api/menu-item.md index 494bdb74f19..a7486f3003c 100644 --- a/docs/api/menu-item.md +++ b/docs/api/menu-item.md @@ -70,7 +70,7 @@ The `role` property can have following values: * `editMenu` - Whole default "Edit" menu (Undo, Copy, etc.) * `windowMenu` - Whole default "Window" menu (Minimize, Close, etc.) -The following additional roles are avaiable on macOS: +The following additional roles are available on macOS: * `about` - Map to the `orderFrontStandardAboutPanel` action * `hide` - Map to the `hide` action @@ -120,4 +120,4 @@ A String representing the menu items visible label #### `menuItem.click` -A Function that is fired when the MenuItem recieves a click event +A Function that is fired when the MenuItem receives a click event diff --git a/docs/api/window-open.md b/docs/api/window-open.md index 56216f551a0..41332aa6cfd 100644 --- a/docs/api/window-open.md +++ b/docs/api/window-open.md @@ -30,6 +30,10 @@ has to be a field of `BrowserWindow`'s options. * Node integration will always be disabled in the opened `window` if it is disabled on the parent window. +* Context isolation will always be enabled in the opened `window` if it is + enabled on the parent window. +* JavaScript will always be disabled in the opened `window` if it is disabled on + the parent window. * Non-standard features (that are not handled by Chromium or Electron) given in `features` will be passed to any registered `webContent`'s `new-window` event handler in the `additionalFeatures` argument. diff --git a/docs/faq.md b/docs/faq.md index 0079d43f4e2..abeac23f76e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -148,7 +148,7 @@ npm uninstall electron npm uninstall -g electron ``` -However if your are using the built-in module but still getting this error, it +However if you are using the built-in module but still getting this error, it is very likely you are using the module in the wrong process. For example `electron.app` can only be used in the main process, while `electron.webFrame` is only available in renderer processes. diff --git a/docs/tutorial/desktop-environment-integration.md b/docs/tutorial/desktop-environment-integration.md index 412341fe700..1c82e360e8e 100644 --- a/docs/tutorial/desktop-environment-integration.md +++ b/docs/tutorial/desktop-environment-integration.md @@ -36,8 +36,8 @@ are fine differences. * On Windows 8.1 and Windows 8, a shortcut to your app, with a [Application User Model ID][app-user-model-id], must be installed to the Start screen. Note, however, that it does not need to be pinned to the Start screen. -* On Windows 7, notifications are not supported. You can however send -"balloon notifications" using the [Tray API][tray-balloon]. +* On Windows 7, notifications work via a custom implemetation which visually +resembles the native one on newer systems. Furthermore, the maximum length for the notification body is 250 characters, with the Windows team recommending that notifications should be kept to 200 diff --git a/filenames.gypi b/filenames.gypi index 720209d663e..8408b273e66 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -89,6 +89,7 @@ 'default_app/index.html', 'default_app/main.js', 'default_app/package.json', + 'default_app/renderer.js', ], 'lib_sources': [ 'atom/app/atom_content_client.cc', diff --git a/lib/browser/guest-window-manager.js b/lib/browser/guest-window-manager.js index e5bfa741238..e668a3114a5 100644 --- a/lib/browser/guest-window-manager.js +++ b/lib/browser/guest-window-manager.js @@ -5,7 +5,7 @@ const {isSameOrigin} = process.atomBinding('v8_util') const parseFeaturesString = require('../common/parse-features-string') const hasProp = {}.hasOwnProperty -const frameToGuest = {} +const frameToGuest = new Map() // Copy attribute of |parent| to |child| if it is not defined in |child|. const mergeOptions = function (child, parent, visited) { @@ -48,11 +48,16 @@ const mergeBrowserWindowOptions = function (embedder, options) { options.webPreferences.nodeIntegration = false } - // Enable context isolation on child window if enable on parent window + // Enable context isolation on child window if enabled on parent window if (embedder.getWebPreferences().contextIsolation === true) { options.webPreferences.contextIsolation = true } + // Disable JavaScript on child window if disabled on parent window + if (embedder.getWebPreferences().javascript === false) { + options.webPreferences.javascript = false + } + // Sets correct openerId here to give correct options to 'new-window' event handler options.webPreferences.openerId = embedder.id @@ -87,10 +92,10 @@ const setupGuest = function (embedder, frameName, guest, options) { guest.once('closed', closedByUser) } if (frameName) { - frameToGuest[frameName] = guest + frameToGuest.set(frameName, guest) guest.frameName = frameName guest.once('closed', function () { - delete frameToGuest[frameName] + frameToGuest.delete(frameName) }) } return guestId @@ -98,7 +103,7 @@ const setupGuest = function (embedder, frameName, guest, options) { // Create a new guest created by |embedder| with |options|. const createGuest = function (embedder, url, frameName, options, postData) { - let guest = frameToGuest[frameName] + let guest = frameToGuest.get(frameName) if (frameName && (guest != null)) { guest.loadURL(url) return guest.webContents.id @@ -186,7 +191,7 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, frameName, const options = {} const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor'] - const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload'] + const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload', 'javascript', 'contextIsolation'] const disposition = 'new-window' // Used to store additional features @@ -197,6 +202,10 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, frameName, if (value === undefined) { additionalFeatures.push(key) } else { + // Don't allow webPreferences to be set since it must be an object + // that cannot be directly overridden + if (key === 'webPreferences') return + if (webPreferences.includes(key)) { if (options.webPreferences == null) { options.webPreferences = {} diff --git a/lib/renderer/init.js b/lib/renderer/init.js index 0306463bc48..9b7ea9dabdb 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -78,7 +78,7 @@ for (let arg of process.argv) { if (window.location.protocol === 'chrome-devtools:') { // Override some inspector APIs. require('./inspector') - nodeIntegration = 'true' + nodeIntegration = 'false' } else if (window.location.protocol === 'chrome-extension:') { // Add implementations of chrome API. require('./chrome-api').injectTo(window.location.hostname, isBackgroundPage, window) diff --git a/lib/renderer/window-setup.js b/lib/renderer/window-setup.js index 21f0741a22a..99f7aef5423 100644 --- a/lib/renderer/window-setup.js +++ b/lib/renderer/window-setup.js @@ -32,6 +32,13 @@ const resolveURL = function (url) { return a.href } +// Use this method to ensure values expected as strings in the main process +// are convertible to strings in the renderer process. This ensures exceptions +// converting values to strings are thrown in this process. +const toString = (value) => { + return value != null ? `${value}` : value +} + const windowProxies = {} const getOrCreateProxy = (ipcRenderer, guestId) => { @@ -82,7 +89,7 @@ function BrowserWindowProxy (ipcRenderer, guestId) { } this.postMessage = (message, targetOrigin) => { - ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', guestId, message, targetOrigin, window.location.origin) + ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', guestId, message, toString(targetOrigin), window.location.origin) } this.eval = (...args) => { @@ -112,7 +119,7 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage) => { if (url != null && url !== '') { url = resolveURL(url) } - const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, frameName, features) + const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features)) if (guestId != null) { return getOrCreateProxy(ipcRenderer, guestId) } else { @@ -121,11 +128,11 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage) => { } window.alert = function (message, title) { - ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_ALERT', message, title) + ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_ALERT', toString(message), toString(title)) } window.confirm = function (message, title) { - return ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CONFIRM', message, title) + return ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CONFIRM', toString(message), toString(title)) } // But we do not support prompt(). @@ -157,7 +164,7 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage) => { } window.history.go = function (offset) { - sendHistoryOperation(ipcRenderer, 'goToOffset', offset) + sendHistoryOperation(ipcRenderer, 'goToOffset', +offset) } defineProperty(window.history, 'length', { diff --git a/spec/api-auto-updater-spec.js b/spec/api-auto-updater-spec.js index 0716c2245df..df77822e456 100644 --- a/spec/api-auto-updater-spec.js +++ b/spec/api-auto-updater-spec.js @@ -1,6 +1,6 @@ const assert = require('assert') -const autoUpdater = require('electron').remote.autoUpdater -const ipcRenderer = require('electron').ipcRenderer +const {autoUpdater} = require('electron').remote +const {ipcRenderer} = require('electron') // Skip autoUpdater tests in MAS build. if (!process.mas) { @@ -64,5 +64,25 @@ if (!process.mas) { autoUpdater.quitAndInstall() }) }) + + describe('error event', function () { + it('serializes correctly over the remote module', function (done) { + if (process.platform === 'linux') { + return done() + } + + autoUpdater.once('error', function (error) { + assert.equal(error instanceof Error, true) + assert.deepEqual(Object.getOwnPropertyNames(error), ['stack', 'message', 'name']) + done() + }) + + autoUpdater.setFeedURL('') + + if (process.platform === 'win32') { + autoUpdater.checkForUpdates() + } + }) + }) }) } diff --git a/spec/chromium-spec.js b/spec/chromium-spec.js index 6a5937bbce2..d5d1b3f4782 100644 --- a/spec/chromium-spec.js +++ b/spec/chromium-spec.js @@ -229,6 +229,45 @@ describe('chromium feature', function () { b = window.open(windowUrl, '', 'nodeIntegration=no,show=no') }) + it('disables node integration when it is disabled on the parent window for chrome devtools URLs', function (done) { + var b + app.once('web-contents-created', (event, contents) => { + contents.once('did-finish-load', () => { + contents.executeJavaScript('typeof process').then((typeofProcessGlobal) => { + assert.equal(typeofProcessGlobal, 'undefined') + b.close() + done() + }).catch(done) + }) + }) + b = window.open('chrome-devtools://devtools/bundled/inspector.html', '', 'nodeIntegration=no,show=no') + }) + + it('disables JavaScript when it is disabled on the parent window', function (done) { + var b + app.once('web-contents-created', (event, contents) => { + contents.once('did-finish-load', () => { + app.once('browser-window-created', (event, window) => { + const preferences = window.webContents.getWebPreferences() + assert.equal(preferences.javascript, false) + window.destroy() + b.close() + done() + }) + // Click link on page + contents.sendInputEvent({type: 'mouseDown', clickCount: 1, x: 1, y: 1}) + contents.sendInputEvent({type: 'mouseUp', clickCount: 1, x: 1, y: 1}) + }) + }) + + var windowUrl = require('url').format({ + pathname: `${fixtures}/pages/window-no-javascript.html`, + protocol: 'file', + slashes: true + }) + b = window.open(windowUrl, '', 'javascript=no,show=no') + }) + it('does not override child options', function (done) { var b, size size = { @@ -322,6 +361,44 @@ describe('chromium feature', function () { }) b = window.open() }) + + it('throws an exception when the arguments cannot be converted to strings', function () { + assert.throws(function () { + window.open('', {toString: null}) + }, /Cannot convert object to primitive value/) + + assert.throws(function () { + window.open('', '', {toString: 3}) + }, /Cannot convert object to primitive value/) + }) + + it('sets the window title to the specified frameName', function (done) { + let b + app.once('browser-window-created', (event, createdWindow) => { + assert.equal(createdWindow.getTitle(), 'hello') + b.close() + done() + }) + b = window.open('', 'hello') + }) + + it('does not throw an exception when the frameName is a built-in object property', function (done) { + let b + app.once('browser-window-created', (event, createdWindow) => { + assert.equal(createdWindow.getTitle(), '__proto__') + b.close() + done() + }) + b = window.open('', '__proto__') + }) + + it('does not throw an exception when the features include webPreferences', function () { + let b + assert.doesNotThrow(function () { + b = window.open('', '', 'webPreferences=') + }) + b.close() + }) }) describe('window.opener', function () { @@ -499,6 +576,14 @@ describe('chromium feature', function () { }) b = window.open('file://' + fixtures + '/pages/window-open-postMessage.html', '', 'show=no') }) + + it('throws an exception when the targetOrigin cannot be converted to a string', function () { + var b = window.open('') + assert.throws(function () { + b.postMessage('test', {toString: null}) + }, /Cannot convert object to primitive value/) + b.close() + }) }) describe('window.opener.postMessage', function () { @@ -880,4 +965,36 @@ describe('chromium feature', function () { }) }) }) + + describe('window.alert(message, title)', function () { + it('throws an exception when the arguments cannot be converted to strings', function () { + assert.throws(function () { + window.alert({toString: null}) + }, /Cannot convert object to primitive value/) + + assert.throws(function () { + window.alert('message', {toString: 3}) + }, /Cannot convert object to primitive value/) + }) + }) + + describe('window.confirm(message, title)', function () { + it('throws an exception when the arguments cannot be converted to strings', function () { + assert.throws(function () { + window.confirm({toString: null}, 'title') + }, /Cannot convert object to primitive value/) + + assert.throws(function () { + window.confirm('message', {toString: 3}) + }, /Cannot convert object to primitive value/) + }) + }) + + describe('window.history.go(offset)', function () { + it('throws an exception when the argumnet cannot be converted to a string', function () { + assert.throws(function () { + window.history.go({toString: null}) + }, /Cannot convert object to primitive value/) + }) + }) }) diff --git a/spec/fixtures/pages/window-no-javascript.html b/spec/fixtures/pages/window-no-javascript.html new file mode 100644 index 00000000000..9c38c9e0bba --- /dev/null +++ b/spec/fixtures/pages/window-no-javascript.html @@ -0,0 +1,12 @@ + + + + +CLICK + + diff --git a/spec/static/main.js b/spec/static/main.js index 033a0c5546c..7e56da623bf 100644 --- a/spec/static/main.js +++ b/spec/static/main.js @@ -2,20 +2,15 @@ process.throwDeprecation = true const electron = require('electron') -const app = electron.app -const crashReporter = electron.crashReporter -const ipcMain = electron.ipcMain -const dialog = electron.dialog -const BrowserWindow = electron.BrowserWindow -const protocol = electron.protocol -const webContents = electron.webContents -const v8 = require('v8') +const {app, BrowserWindow, crashReporter, dialog, ipcMain, protocol, webContents} = electron + +const {Coverage} = require('electabul') -const Coverage = require('electabul').Coverage const fs = require('fs') const path = require('path') const url = require('url') const util = require('util') +const v8 = require('v8') var argv = require('yargs') .boolean('ci') @@ -103,6 +98,12 @@ app.on('window-all-closed', function () { app.quit() }) +app.on('web-contents-created', (event, contents) => { + contents.on('crashed', (event, killed) => { + console.log(`webContents ${contents.id} crashed: ${contents.getURL()} (killed=${killed})`) + }) +}) + app.on('ready', function () { // Test if using protocol module would crash. electron.protocol.registerStringProtocol('test-if-crashes', function () {})