Skip to content

Commit

Permalink
Enable context isolation and disable nodejs support. Fixes #2018
Browse files Browse the repository at this point in the history
All access to electron API is now done through an API exposed via a
preload script. Access to the electron API (including electron-remote)
and nodejs API is no longer possible.
Theia extensions can contribute to the preload script
via a `theiaExtensions` module declaration in their package.json

Contributed on behalf or STMicroelectronics

Signed-off-by: Thomas Mäder <[email protected]>
  • Loading branch information
tsmaeder committed Mar 15, 2023
1 parent 60209ec commit 0fab8d5
Show file tree
Hide file tree
Showing 50 changed files with 1,147 additions and 558 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<a name="breaking_changes_1.36.0">[Breaking Changes:](#breaking_changes_1.36.0)</a>

- [plugin] renamed `TreeViewExtImpl#toTreeItem()` to `TreeViewExtImpl#toTreeElement()`
- [electron] enabled context isolation and disabled node integration in Electron renderer (https://github.com/eclipse-theia/theia/issues/2018)

## v1.35.0 - 02/23/2023

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class FrontendGenerator extends AbstractGenerator {
const frontendModules = this.pck.targetFrontendModules;
await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(frontendModules));
await this.write(this.pck.frontend('index.js'), this.compileIndexJs(frontendModules));
await this.write(this.pck.frontend('preload.js'), this.compilePreloadJs());
await this.write(this.pck.frontend('secondary-window.html'), this.compileSecondaryWindowHtml());
await this.write(this.pck.frontend('secondary-index.js'), this.compileSecondaryIndexJs(this.pck.secondaryWindowModules));
if (this.pck.isElectron()) {
Expand Down Expand Up @@ -133,7 +134,6 @@ module.exports = preloader.preload().then(() => {
return `// @ts-check
require('reflect-metadata');
require('@theia/electron/shared/@electron/remote/main').initialize();
// Useful for Electron/NW.js apps as GUI apps on macOS doesn't inherit the \`$PATH\` define
// in your dotfiles (.bashrc/.bash_profile/.zshrc/etc).
Expand Down Expand Up @@ -266,6 +266,17 @@ module.exports = Promise.resolve().then(() => {
container.load(frontendApplicationModule);
${compiledModuleImports}
});
`;
}

compilePreloadJs(): string {
const lines = Array.from(this.pck.preloadModules)
.map(([moduleName, path]) => `require('${path}').preload();`);
const imports = '\n' + lines.join('\n');

return `\
// @ts-check
${imports}
`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ module.exports = [{
devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]',
globalObject: 'self'
},
target: '${this.ifBrowser('web', 'electron-renderer')}',
target: 'web',
cache: staticCompression,
module: {
rules: [
Expand Down Expand Up @@ -252,7 +252,7 @@ module.exports = [{
devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]',
globalObject: 'self'
},
target: 'electron-renderer',
target: 'web',
cache: staticCompression,
module: {
rules: [
Expand All @@ -278,6 +278,24 @@ module.exports = [{
warnings: true,
children: true
}
}, {
mode,
devtool: 'source-map',
entry: {
"preload": path.resolve(__dirname, 'src-gen/frontend/preload.js'),
},
output: {
filename: '[name].js',
path: outputPath,
devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]',
globalObject: 'self'
},
target: 'electron-preload',
cache: staticCompression,
stats: {
warnings: true,
children: true
}
}];`;
}

Expand Down
8 changes: 8 additions & 0 deletions dev-packages/application-package/src/application-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export class ApplicationPackage {
protected _backendModules: Map<string, string> | undefined;
protected _backendElectronModules: Map<string, string> | undefined;
protected _electronMainModules: Map<string, string> | undefined;
protected _preloadModules: Map<string, string> | undefined;
protected _extensionPackages: ReadonlyArray<ExtensionPackage> | undefined;

/**
Expand Down Expand Up @@ -176,6 +177,13 @@ export class ApplicationPackage {
return this._electronMainModules;
}

get preloadModules(): Map<string, string> {
if (!this._preloadModules) {
this._preloadModules = this.computeModules('preload');
}
return this._preloadModules;
}

protected computeModules<P extends keyof Extension, S extends keyof Extension = P>(primary: P, secondary?: S): Map<string, string> {
const result = new Map<string, string>();
let moduleIndex = 1;
Expand Down
1 change: 1 addition & 0 deletions dev-packages/application-package/src/extension-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface Extension {
backend?: string;
backendElectron?: string;
electronMain?: string;
preload?: string;
}

export interface ExtensionPackageOptions {
Expand Down
27 changes: 27 additions & 0 deletions doc/Migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,33 @@ For example:
}
```


### v1.36.0

#### Disabled node integration and added context isolation flag in Electron renderer

This also means that `electron-remote` can no longer be used in components in `electron-frontend` or `electron-common`. In order to use electron-related functionality from the browser, you need to expose an API via a preload script (see https://www.electronjs.org/docs/latest/tutorial/context-isolation). to achieve this from a Theia extension, you need to follow these steps:
1. Define the API interface and declare an api variable on the global `window` variable. See `packages/filesystem/electron-common/electron-api.ts` for an example
2. Write a preload script module that implements the API on the renderer ("browser") side and exposes the API via `exposeInMainWorld`. You'll need to expose the API in an exported function called `preload()`. See `packages/filesystem/electron-browser/preload.ts` for an example.
3. Declare a `theiaExtensions` entry pointing to the preload script like so:
```
"theiaExtensions": [
{
"preload": "lib/electron-browser/preload",
```
See `/packages/filesystem/package.json` for an example

4. Implement the API on the electron-main side by contributing a `ElectronMainApplicationContribution`. See `packages/filesystem/electron-main/electron-api-main.ts` for an example. If you don't have a module contributing to the electron-main application, you may have to declare it in your package.json.
```
"theiaExtensions": [
{
"preload": "lib/electron-browser/preload",
"electronMain": "lib/electron-main/electron-main-module"
}
```

If you are using nodejs API in your electron browser-side code you will also have to move the code outside of the renderer process, for exmaple
by setting up an API like described above, or, for example, by using a back-end service.
### v1.35.0

#### Drop support for `Node 14`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,26 @@
// *****************************************************************************

import { injectable, ContainerModule } from '@theia/core/shared/inversify';
import { CompoundMenuNode, MenuNode } from '@theia/core/lib/common/menu';
import { MenuNode } from '@theia/core/lib/common/menu';
import { ElectronMainMenuFactory, ElectronMenuOptions } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import { PlaceholderMenuNode } from '../../browser/menu/sample-menu-contribution';
import { MenuDto } from '@theia/core/lib/electron-common/electron-api';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(ElectronMainMenuFactory).to(SampleElectronMainMenuFactory).inSingletonScope();
});

@injectable()
class SampleElectronMainMenuFactory extends ElectronMainMenuFactory {
protected override fillMenuTemplate(
parentItems: Electron.MenuItemConstructorOptions[], menuModel: MenuNode & CompoundMenuNode, args: unknown[] = [], options: ElectronMenuOptions
): Electron.MenuItemConstructorOptions[] {
if (menuModel instanceof PlaceholderMenuNode) {
parentItems.push({ label: menuModel.label, enabled: false, visible: true });
protected override fillMenuTemplate(parentItems: MenuDto[],
menu: MenuNode,
args: unknown[] = [],
options: ElectronMenuOptions
): MenuDto[] {
if (menu instanceof PlaceholderMenuNode) {
parentItems.push({ label: menu.label, enabled: false, visible: true });
} else {
super.fillMenuTemplate(parentItems, menuModel, args, options);
super.fillMenuTemplate(parentItems, menu, args, options);
}
return parentItems;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************

import * as electronRemote from '@theia/core/electron-shared/@electron/remote';
import { Menu, BrowserWindow } from '@theia/core/electron-shared/electron';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { isOSX } from '@theia/core/lib/common/os';
import { CommonMenus } from '@theia/core/lib/browser';
import {
Emitter,
Expand Down Expand Up @@ -91,12 +88,8 @@ export class ElectronMenuUpdater {
this.setMenu();
}

private setMenu(menu: Menu | null = this.factory.createElectronMenuBar(), electronWindow: BrowserWindow = electronRemote.getCurrentWindow()): void {
if (isOSX) {
electronRemote.Menu.setApplicationMenu(menu);
} else {
electronWindow.setMenu(menu);
}
private setMenu(): void {
window.electronTheiaCore.setMenu(this.factory.createElectronMenuBar());
}

}
Expand Down
2 changes: 0 additions & 2 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ export class SomeClass {
## Re-Exports

- `@theia/core/electron-shared/...`
- `@electron/remote` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote))
- `@electron/remote/main` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote))
- `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap))
- `electron` (from [`electron@^15.3.5`](https://www.npmjs.com/package/electron))
- `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store))
Expand Down
1 change: 0 additions & 1 deletion packages/core/electron-shared/@electron/remote/index.d.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/core/electron-shared/@electron/remote/index.js

This file was deleted.

This file was deleted.

This file was deleted.

3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@
}
},
"theiaExtensions": [
{
"preload": "lib/electron-browser/preload"
},
{
"frontend": "lib/browser/i18n/i18n-frontend-module",
"backend": "lib/node/i18n/i18n-backend-module"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@
// *****************************************************************************

// eslint-disable-next-line import/no-extraneous-dependencies
import { clipboard } from 'electron';
import { injectable } from 'inversify';
import { ClipboardService } from '../browser/clipboard-service';

@injectable()
export class ElectronClipboardService implements ClipboardService {

readText(): string {
return clipboard.readText();
return window.electronTheiaCore.readClipboard();
}

writeText(value: string): void {
clipboard.writeText(value);
window.electronTheiaCore.writeClipboard(value);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************

import { ipcRenderer } from '@theia/electron/shared/electron';
import { postConstruct, injectable } from 'inversify';
import { KeyboardLayoutChangeNotifier, NativeKeyboardLayout } from '../../common/keyboard/keyboard-layout-provider';
import { Emitter, Event } from '../../common/event';
Expand All @@ -34,7 +33,7 @@ export class ElectronKeyboardLayoutChangeNotifier implements KeyboardLayoutChang

@postConstruct()
protected initialize(): void {
ipcRenderer.on('keyboardLayoutChanged', (event: Electron.IpcRendererEvent, newLayout: NativeKeyboardLayout) => this.nativeLayoutChanged.fire(newLayout));
window.electronTheiaCore.onKeyboardLayoutChanged((newLayout: NativeKeyboardLayout) => this.nativeLayoutChanged.fire(newLayout));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

/* eslint-disable @typescript-eslint/no-explicit-any */

import * as electron from '../../../electron-shared/electron';
import { inject, injectable, postConstruct } from 'inversify';
import {
ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands, coordinateFromAnchor, PreferenceService
Expand All @@ -25,12 +24,11 @@ import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { ContextMenuContext } from '../../browser/menu/context-menu-context';
import { MenuPath, MenuContribution, MenuModelRegistry } from '../../common';
import { BrowserContextMenuRenderer } from '../../browser/menu/browser-context-menu-renderer';
import { RequestTitleBarStyle, TitleBarStyleAtStartup } from '../../electron-common/messaging/electron-messages';

export class ElectronContextMenuAccess extends ContextMenuAccess {
constructor(readonly menu: electron.Menu) {
constructor(readonly menuHandle: Promise<number>) {
super({
dispose: () => menu.closePopup()
dispose: () => menuHandle.then(handle => window.electronTheiaCore.closePopup(handle))
});
}
}
Expand Down Expand Up @@ -93,28 +91,23 @@ export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer {

@postConstruct()
protected async init(): Promise<void> {
electron.ipcRenderer.on(TitleBarStyleAtStartup, (_event, style: string) => {
this.useNativeStyle = style === 'native';
});
electron.ipcRenderer.send(RequestTitleBarStyle);
this.useNativeStyle = await window.electronTheiaCore.getTitleBarStyleAtStartup() === 'native';
}

protected override doRender(options: RenderContextMenuOptions): ContextMenuAccess {
if (this.useNativeStyle) {
const { menuPath, anchor, args, onHide, context, contextKeyService } = options;
const menu = this.electronMenuFactory.createElectronContextMenu(menuPath, args, context, contextKeyService);
const { x, y } = coordinateFromAnchor(anchor);
const zoom = electron.webFrame.getZoomFactor();
// TODO: Remove the offset once Electron fixes https://github.com/electron/electron/issues/31641
const offset = process.platform === 'win32' ? 0 : 2;
// x and y values must be Ints or else there is a conversion error
menu.popup({ x: Math.round(x * zoom) + offset, y: Math.round(y * zoom) + offset });

const menuHandle = window.electronTheiaCore.popup(menu, x, y, () => {
if (onHide) {
onHide();
}
});
// native context menu stops the event loop, so there is no keyboard events
this.context.resetAltPressed();
if (onHide) {
menu.once('menu-will-close', () => onHide());
}
return new ElectronContextMenuAccess(menu);
return new ElectronContextMenuAccess(menuHandle);
} else {
return super.doRender(options);
}
Expand Down
Loading

0 comments on commit 0fab8d5

Please sign in to comment.