Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable context isolation and disable nodejs support. Fixes #2018 #12299

Merged
merged 5 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
- Interface `ScmInlineActions` removes `commands: CommandRegistry`
- Interface `ScmTreeWidget.Props` removes `commands: CommandRegistry`
- [terminal] removed `openTerminalFromProfile` method from `TerminalFrontendContribution` [#12322](https://github.com/eclipse-theia/theia/pull/12322)
- [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: 1 addition & 1 deletion examples/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@
},
"devDependencies": {
"@theia/cli": "1.36.0",
"electron": "^15.3.5"
"electron": "^22.3.2"
}
}
4 changes: 1 addition & 3 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,8 @@ 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` (from [`electron@^22.3.2`](https://www.npmjs.com/package/electron))
- `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store))
- `fix-path` (from [`fix-path@^3.0.0`](https://www.npmjs.com/package/fix-path))
- `@theia/core/shared/...`
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